From 5ced119ed0d4ae93eb56ac8d6b6bdf4e6a1835ab Mon Sep 17 00:00:00 2001 From: Chris Hughes Date: Wed, 3 Jul 2019 14:45:24 +0100 Subject: [PATCH 001/125] Did initial reformatting (with black) and a little cleanup. Also added LRU caching to DistributionTruncated.ppf for a speed boost. --- calibration_conditions.py | 134 ++- calibrationscore.py | 33 +- catbond.py | 385 +++++--- compute_profits_losses_from_cash.py | 8 +- condition_aux.py | 132 ++- distribution_wrapper_test.py | 16 +- distributionreinsurance.py | 64 +- distributiontruncated.py | 53 +- ensemble.py | 198 ++-- genericagent.py | 9 +- genericagentabce.py | 1 + insurancecontract.py | 45 +- insurancefirm.py | 332 ++++--- insurancesimulation.py | 842 ++++++++++++------ isleconfig.py | 163 ++-- listify.py | 16 +- logger.py | 125 +-- metainsurancecontract.py | 110 ++- metainsuranceorg.py | 736 ++++++++++----- metaplotter.py | 272 ++++-- metaplotter_pl_timescale.py | 261 ++++-- ...lotter_pl_timescale_additional_measures.py | 313 +++++-- plotter.py | 42 +- plotter_pl_timescale.py | 43 +- reinsurancecontract.py | 86 +- reinsurancefirm.py | 4 +- resume.py | 165 ++-- riskmodel.py | 265 ++++-- setup.py | 97 +- start.py | 278 ++++-- visualisation.py | 379 ++++++-- visualization_network.py | 88 +- 32 files changed, 3980 insertions(+), 1715 deletions(-) diff --git a/calibration_conditions.py b/calibration_conditions.py index 33bc6f7..94a8980 100644 --- a/calibration_conditions.py +++ b/calibration_conditions.py @@ -31,100 +31,158 @@ import condition_aux import isleconfig + def condition_stationary_state_cash(logobj): """Stationarity test for total cash""" - return condition_aux.condition_stationary_state(logobj.history_logs['total_cash']) - + return condition_aux.condition_stationary_state(logobj.history_logs["total_cash"]) + + def condition_stationary_state_excess_capital(logobj): """Stationarity test for total excess capital""" - return condition_aux.condition_stationary_state(logobj.history_logs['total_excess_capital']) + return condition_aux.condition_stationary_state( + logobj.history_logs["total_excess_capital"] + ) + def condition_stationary_state_profits_losses(logobj): """Stationarity test for total profits and losses""" - return condition_aux.condition_stationary_state(logobj.history_logs['total_profitslosses']) + return condition_aux.condition_stationary_state( + logobj.history_logs["total_profitslosses"] + ) + def condition_stationary_state_contracts(logobj): """Stationarity test for total number of contracts""" - return condition_aux.condition_stationary_state(logobj.history_logs['total_contracts']) + return condition_aux.condition_stationary_state( + logobj.history_logs["total_contracts"] + ) + def condition_stationary_state_rein_cash(logobj): """Stationarity test for total cash (reinsurers)""" - return condition_aux.condition_stationary_state(logobj.history_logs['total_reincash']) + return condition_aux.condition_stationary_state( + logobj.history_logs["total_reincash"] + ) + def condition_stationary_state_rein_excess_capital(logobj): """Stationarity test for total excess capital (reinsurers)""" - return condition_aux.condition_stationary_state(logobj.history_logs['total_reinexcess_capital']) + return condition_aux.condition_stationary_state( + logobj.history_logs["total_reinexcess_capital"] + ) + def condition_stationary_state_rein_profits_losses(logobj): """Stationarity test for total profits and losses (reinsurers)""" - return condition_aux.condition_stationary_state(logobj.history_logs['total_reinprofitslosses']) + return condition_aux.condition_stationary_state( + logobj.history_logs["total_reinprofitslosses"] + ) + def condition_stationary_state_rein_contracts(logobj): """Stationarity test for total number of reinsured contracts""" - return condition_aux.condition_stationary_state(logobj.history_logs['total_reincontracts']) + return condition_aux.condition_stationary_state( + logobj.history_logs["total_reincontracts"] + ) + def condition_stationary_state_market_premium(logobj): """Stationarity test for insurance market premium""" - return condition_aux.condition_stationary_state(logobj.history_logs['market_premium']) + return condition_aux.condition_stationary_state( + logobj.history_logs["market_premium"] + ) + def condition_stationary_state_rein_market_premium(logobj): """Stationarity test for reinsurance market premium""" - return condition_aux.condition_stationary_state(logobj.history_logs['market_reinpremium']) + return condition_aux.condition_stationary_state( + logobj.history_logs["market_reinpremium"] + ) -def condition_defaults_insurance(logobj): # TODO: develop this into a non-binary measure + +def condition_defaults_insurance( + logobj +): # TODO: develop this into a non-binary measure """Test for number of insurance bankruptcies (non zero, not all insurers)""" - #series = logobj.history_logs['total_operational'] - #if series[-1] != 0 and any(series[i]-series[i-1] < 0 for i in range(1,len(series))): - opseries = [logobj.history_logs["insurance_firms_cash"][-1][i][2] for i in \ - range(len(logobj.history_logs["insurance_firms_cash"][-1]))] + # series = logobj.history_logs['total_operational'] + # if series[-1] != 0 and any(series[i]-series[i-1] < 0 for i in range(1,len(series))): + opseries = [ + logobj.history_logs["insurance_firms_cash"][-1][i][2] + for i in range(len(logobj.history_logs["insurance_firms_cash"][-1])) + ] if any(opseries) and not all(opseries): return 1 else: return 0 -def condition_defaults_reinsurance(logobj): # TODO: develop this into a non-binary measure + +def condition_defaults_reinsurance( + logobj +): # TODO: develop this into a non-binary measure """Test for number of reinsurance bankruptcies (non zero, not all reinsurers)""" - #series = logobj.history_logs['total_reinoperational'] - #if series[-1] != 0 and any(series[i]-series[i-1] < 0 for i in range(1,len(series))): - opseries = [logobj.history_logs["reinsurance_firms_cash"][-1][i][2] for i in \ - range(len(logobj.history_logs["reinsurance_firms_cash"][-1]))] + # series = logobj.history_logs['total_reinoperational'] + # if series[-1] != 0 and any(series[i]-series[i-1] < 0 for i in range(1,len(series))): + opseries = [ + logobj.history_logs["reinsurance_firms_cash"][-1][i][2] + for i in range(len(logobj.history_logs["reinsurance_firms_cash"][-1])) + ] if any(opseries) and not all(opseries): return 1 else: return 0 + def condition_insurance_coverage(logobj): """Test for insurance coverage close to 100%""" - return logobj.history_logs['total_contracts'][-1] * 1. / isleconfig.simulation_parameters["no_risks"] + return ( + logobj.history_logs["total_contracts"][-1] + * 1.0 + / isleconfig.simulation_parameters["no_risks"] + ) + def condition_reinsurance_coverage(logobj, minimum=0.6): """Test for reinsurance coverage close to some minimum that may be less than 100% (default 60%)""" - score = logobj.history_logs['total_reincontracts'][-1] * 1. / (minimum * logobj.history_logs['total_contracts'][-1]) - score = 1 if score>1 else score + score = ( + logobj.history_logs["total_reincontracts"][-1] + * 1.0 + / (minimum * logobj.history_logs["total_contracts"][-1]) + ) + score = 1 if score > 1 else score return score -def condition_insurance_firm_dist(logobj): + +def condition_insurance_firm_dist(logobj): """Empirical calibration test for insurance firm size (total assets; cash)""" """filter operational firms""" - #dist = [logobj.history_logs["insurance_firms_cash"][-1][i][0] for i in range(len(logobj.history_logs["insurance_firms_cash"])) if \ + # dist = [logobj.history_logs["insurance_firms_cash"][-1][i][0] for i in range(len(logobj.history_logs["insurance_firms_cash"])) if \ # logobj.history_logs["insurance_firms_cash"][-1][i][0] > isleconfig.simulation_parameters["cash_permanency_limit"]] - dist = [logobj.history_logs["insurance_firms_cash"][-1][i][0] for i in \ - range(len(logobj.history_logs["insurance_firms_cash"][-1])) if \ - logobj.history_logs["insurance_firms_cash"][-1][i][2]] + dist = [ + logobj.history_logs["insurance_firms_cash"][-1][i][0] + for i in range(len(logobj.history_logs["insurance_firms_cash"][-1])) + if logobj.history_logs["insurance_firms_cash"][-1][i][2] + ] """run two-sided KS test""" - KS_statistic, p_value = stats.ks_2samp(condition_aux.scaler(condition_aux.insurance_firm_sizes_empirical_2017), - condition_aux.scaler(dist)) + KS_statistic, p_value = stats.ks_2samp( + condition_aux.scaler(condition_aux.insurance_firm_sizes_empirical_2017), + condition_aux.scaler(dist), + ) return p_value -def condition_reinsurance_firm_dist(logobj): + +def condition_reinsurance_firm_dist(logobj): """Empirical calibration test for reinsurance firm size (total assets; cash)""" """filter operational firms""" - #dist = [logobj.history_logs["reinsurance_firms_cash"][-1][i][0] for i in range(len(logobj.history_logs["reinsurance_firms_cash"])) if \ + # dist = [logobj.history_logs["reinsurance_firms_cash"][-1][i][0] for i in range(len(logobj.history_logs["reinsurance_firms_cash"])) if \ # logobj.history_logs["reinsurance_firms_cash"][-1][i][0] > isleconfig.simulation_parameters["cash_permanency_limit"]] - dist = [logobj.history_logs["reinsurance_firms_cash"][-1][i][0] for i in \ - range(len(logobj.history_logs["reinsurance_firms_cash"][-1])) if - logobj.history_logs["reinsurance_firms_cash"][-1][i][2]] + dist = [ + logobj.history_logs["reinsurance_firms_cash"][-1][i][0] + for i in range(len(logobj.history_logs["reinsurance_firms_cash"][-1])) + if logobj.history_logs["reinsurance_firms_cash"][-1][i][2] + ] """run two-sided KS test""" - KS_statistic, p_value = stats.ks_2samp(condition_aux.scaler(condition_aux.reinsurance_firm_sizes_empirical_2017), - condition_aux.scaler(dist)) + KS_statistic, p_value = stats.ks_2samp( + condition_aux.scaler(condition_aux.reinsurance_firm_sizes_empirical_2017), + condition_aux.scaler(dist), + ) return p_value diff --git a/calibrationscore.py b/calibrationscore.py index 32d870c..b0a86a6 100644 --- a/calibrationscore.py +++ b/calibrationscore.py @@ -5,9 +5,10 @@ from inspect import getmembers, isfunction import numpy as np -import calibration_conditions # Test functions +import calibration_conditions # Test functions -class CalibrationScore(): + +class CalibrationScore: def __init__(self, L): """Constructor method. Arguments: @@ -17,31 +18,41 @@ def __init__(self, L): """Assert sanity of log and save log.""" assert isinstance(L, logger.Logger) self.logger = L - + """Prepare list of calibration tests from calibration_conditions.py""" - self.conditions = [f for f in getmembers(calibration_conditions) if isfunction(f[1])] - + self.conditions = [ + f for f in getmembers(calibration_conditions) if isfunction(f[1]) + ] + """Prepare calibration score variable.""" self.calibration_score = None - + def test_all(self): """Method to test all calibration tests. No arguments. Returns combined calibration score as float \in [0,1].""" - + """Compute score components""" - scores = {condition[0]: condition[1](self.logger) for condition in self.conditions} + scores = { + condition[0]: condition[1](self.logger) for condition in self.conditions + } """Print components""" print("\n") for cond_name, score in scores.items(): print("{0:47s}: {1:8f}".format(cond_name, score)) """Compute combined score""" - self.calibration_score = self.combine_scores(np.array([*scores.values()], dtype=object)) + self.calibration_score = self.combine_scores( + np.array([*scores.values()], dtype=object) + ) """Print combined score""" - print("\n Total calibration score: {0:8f}".format(self.calibration_score)) + print( + "\n Total calibration score: {0:8f}".format( + self.calibration_score + ) + ) """Return""" return self.calibration_score - + def combine_scores(self, slist): """Method to combine calibration score components. Combination is additive (mean). Change the function for other combination methods (multiplicative or minimum). diff --git a/catbond.py b/catbond.py index 1445011..9f7ee68 100644 --- a/catbond.py +++ b/catbond.py @@ -1,4 +1,3 @@ - import isleconfig import numpy as np import scipy.stats @@ -9,8 +8,11 @@ import sys, pdb import uuid + class CatBond(MetaInsuranceOrg): - def init(self, simulation, per_period_premium, owner, interest_rate = 0): # do we need simulation parameters + def init( + self, simulation, per_period_premium, owner, interest_rate=0 + ): # do we need simulation parameters self.simulation = simulation self.id = 0 self.underwritten_contracts = [] @@ -20,61 +22,89 @@ def init(self, simulation, per_period_premium, owner, interest_rate = 0): # do self.operational = True self.owner = owner self.per_period_dividend = per_period_premium - self.interest_rate = interest_rate # TODO: shift obtain_yield method to insurancesimulation, thereby making it unnecessary to drag parameters like self.interest_rate from instance to instance and from class to class - #self.simulation_no_risk_categories = self.simulation.simulation_parameters["no_categories"] - + self.interest_rate = ( + interest_rate + ) # TODO: shift obtain_yield method to insurancesimulation, thereby making it unnecessary to drag parameters like self.interest_rate from instance to instance and from class to class + # self.simulation_no_risk_categories = self.simulation.simulation_parameters["no_categories"] + # TODO: change start and InsuranceSimulation so that it iterates CatBonds - #old parent class init, cat bond class should be much smaller + # old parent class init, cat bond class should be much smaller def parent_init(self, simulation_parameters, agent_parameters): - #def init(self, simulation_parameters, agent_parameters): - self.simulation = simulation_parameters['simulation'] + # def init(self, simulation_parameters, agent_parameters): + self.simulation = simulation_parameters["simulation"] self.simulation_parameters = simulation_parameters - self.contract_runtime_dist = scipy.stats.randint(simulation_parameters["mean_contract_runtime"] - \ - simulation_parameters["contract_runtime_halfspread"], simulation_parameters["mean_contract_runtime"] \ - + simulation_parameters["contract_runtime_halfspread"] + 1) - self.default_contract_payment_period = simulation_parameters["default_contract_payment_period"] - self.id = agent_parameters['id'] - self.cash = agent_parameters['initial_cash'] + self.contract_runtime_dist = scipy.stats.randint( + simulation_parameters["mean_contract_runtime"] + - simulation_parameters["contract_runtime_halfspread"], + simulation_parameters["mean_contract_runtime"] + + simulation_parameters["contract_runtime_halfspread"] + + 1, + ) + self.default_contract_payment_period = simulation_parameters[ + "default_contract_payment_period" + ] + self.id = agent_parameters["id"] + self.cash = agent_parameters["initial_cash"] self.premium = agent_parameters["norm_premium"] - self.profit_target = agent_parameters['profit_target'] - self.acceptance_threshold = agent_parameters['initial_acceptance_threshold'] # 0.5 - self.acceptance_threshold_friction = agent_parameters['acceptance_threshold_friction'] # 0.9 #1.0 to switch off + self.profit_target = agent_parameters["profit_target"] + self.acceptance_threshold = agent_parameters[ + "initial_acceptance_threshold" + ] # 0.5 + self.acceptance_threshold_friction = agent_parameters[ + "acceptance_threshold_friction" + ] # 0.9 #1.0 to switch off self.interest_rate = agent_parameters["interest_rate"] self.reinsurance_limit = agent_parameters["reinsurance_limit"] self.simulation_no_risk_categories = simulation_parameters["no_categories"] - self.simulation_reinsurance_type = simulation_parameters["simulation_reinsurance_type"] - - rm_config = agent_parameters['riskmodel_config'] - self.riskmodel = RiskModel(damage_distribution=rm_config["damage_distribution"], \ - expire_immediately=rm_config["expire_immediately"], \ - cat_separation_distribution=rm_config["cat_separation_distribution"], \ - norm_premium=rm_config["norm_premium"], \ - category_number=rm_config["no_categories"], \ - init_average_exposure=rm_config["risk_value_mean"], \ - init_average_risk_factor=rm_config["risk_factor_mean"], \ - init_profit_estimate=rm_config["norm_profit_markup"], \ - margin_of_safety=rm_config["margin_of_safety"], \ - var_tail_prob=rm_config["var_tail_prob"], \ - inaccuracy=rm_config["inaccuracy_by_categ"]) - - self.category_reinsurance = [None for i in range(self.simulation_no_risk_categories)] - if self.simulation_reinsurance_type == 'non-proportional': - self.np_reinsurance_deductible_fraction = simulation_parameters["default_non-proportional_reinsurance_deductible"] - self.np_reinsurance_excess_fraction = simulation_parameters["default_non-proportional_reinsurance_excess"] - self.np_reinsurance_premium_share = simulation_parameters["default_non-proportional_reinsurance_premium_share"] + self.simulation_reinsurance_type = simulation_parameters[ + "simulation_reinsurance_type" + ] + + rm_config = agent_parameters["riskmodel_config"] + self.riskmodel = RiskModel( + damage_distribution=rm_config["damage_distribution"], + expire_immediately=rm_config["expire_immediately"], + cat_separation_distribution=rm_config["cat_separation_distribution"], + norm_premium=rm_config["norm_premium"], + category_number=rm_config["no_categories"], + init_average_exposure=rm_config["risk_value_mean"], + init_average_risk_factor=rm_config["risk_factor_mean"], + init_profit_estimate=rm_config["norm_profit_markup"], + margin_of_safety=rm_config["margin_of_safety"], + var_tail_prob=rm_config["var_tail_prob"], + inaccuracy=rm_config["inaccuracy_by_categ"], + ) + + self.category_reinsurance = [ + None for i in range(self.simulation_no_risk_categories) + ] + if self.simulation_reinsurance_type == "non-proportional": + self.np_reinsurance_deductible_fraction = simulation_parameters[ + "default_non-proportional_reinsurance_deductible" + ] + self.np_reinsurance_excess_fraction = simulation_parameters[ + "default_non-proportional_reinsurance_excess" + ] + self.np_reinsurance_premium_share = simulation_parameters[ + "default_non-proportional_reinsurance_premium_share" + ] self.obligations = [] self.underwritten_contracts = [] - #self.reinsurance_contracts = [] + # self.reinsurance_contracts = [] self.operational = True self.is_insurer = True self.is_reinsurer = False - + """set up risk value estimate variables""" - self.var_counter = 0 # sum over risk model inaccuracies for all contracts - self.var_counter_per_risk = 0 # average risk model inaccuracy across contracts - self.var_sum = 0 # sum over initial VaR for all contracts - self.counter_category = np.zeros(self.simulation_no_risk_categories) # var_counter disaggregated by category - self.var_category = np.zeros(self.simulation_no_risk_categories) # var_sum disaggregated by category + self.var_counter = 0 # sum over risk model inaccuracies for all contracts + self.var_counter_per_risk = 0 # average risk model inaccuracy across contracts + self.var_sum = 0 # sum over initial VaR for all contracts + self.counter_category = np.zeros( + self.simulation_no_risk_categories + ) # var_counter disaggregated by category + self.var_category = np.zeros( + self.simulation_no_risk_categories + ) # var_sum disaggregated by category def iterate(self, time): """obtain investments yield""" @@ -83,11 +113,22 @@ def iterate(self, time): """realize due payments""" self.effect_payments(time) if isleconfig.verbose: - print(time, ":", self.id, len(self.underwritten_contracts), self.cash, self.operational) - + print( + time, + ":", + self.id, + len(self.underwritten_contracts), + self.cash, + self.operational, + ) + """mature contracts""" print("Number of underwritten contracts ", len(self.underwritten_contracts)) - maturing = [contract for contract in self.underwritten_contracts if contract.expiration <= time] + maturing = [ + contract + for contract in self.underwritten_contracts + if contract.expiration <= time + ] for contract in maturing: self.underwritten_contracts.remove(contract) contract.mature(time) @@ -95,32 +136,45 @@ def iterate(self, time): """effect payments from contracts""" [contract.check_payment_due(time) for contract in self.underwritten_contracts] - + if self.underwritten_contracts == []: - self.mature_bond() #TODO: mature_bond method should check if operational - - else: #TODO: dividend should only be payed according to pre-arranged schedule, and only if no risk events have materialized so far + self.mature_bond() # TODO: mature_bond method should check if operational + + else: # TODO: dividend should only be payed according to pre-arranged schedule, and only if no risk events have materialized so far if self.operational: self.pay_dividends(time) - #self.estimated_var() # cannot compute VaR for catbond as catbond does not have a riskmodel - - #old parent class iterate, cat bond class should be much smaller - def parent_iterate(self, time): # TODO: split function so that only the sequence of events remains here and everything else is in separate methods + # self.estimated_var() # cannot compute VaR for catbond as catbond does not have a riskmodel + + # old parent class iterate, cat bond class should be much smaller + def parent_iterate( + self, time + ): # TODO: split function so that only the sequence of events remains here and everything else is in separate methods """obtain investments yield""" self.obtain_yield(time) """realize due payments""" self.effect_payments(time) if isleconfig.verbose: - print(time, ":", self.id, len(self.underwritten_contracts), self.cash, self.operational) + print( + time, + ":", + self.id, + len(self.underwritten_contracts), + self.cash, + self.operational, + ) self.make_reinsurance_claims(time) """mature contracts""" if isleconfig.verbose: print("Number of underwritten contracts ", len(self.underwritten_contracts)) - maturing = [contract for contract in self.underwritten_contracts if contract.expiration <= time] + maturing = [ + contract + for contract in self.underwritten_contracts + if contract.expiration <= time + ] for contract in maturing: self.underwritten_contracts.remove(contract) contract.mature(time) @@ -134,89 +188,183 @@ def parent_iterate(self, time): # TODO: split function so that only the s """request risks to be considered for underwriting in the next period and collect those for this period""" new_risks = [] if self.is_insurer: - new_risks += self.simulation.solicit_insurance_requests(self.id, self.cash) + new_risks += self.simulation.solicit_insurance_requests( + self.id, self.cash + ) if self.is_reinsurer: - new_risks += self.simulation.solicit_reinsurance_requests(self.id, self.cash) + new_risks += self.simulation.solicit_reinsurance_requests( + self.id, self.cash + ) contracts_offered = len(new_risks) try: assert contracts_offered > 2 * contracts_dissolved except: - print("Something wrong; agent {0:d} receives too few new contracts {1:d} <= {2:d}".format(self.id, contracts_offered, 2*contracts_dissolved), file=sys.stderr) - #print(self.id, " has ", len(self.underwritten_contracts), " & receives ", contracts_offered, " & lost ", contracts_dissolved) - - new_nonproportional_risks = [risk for risk in new_risks if risk.get("insurancetype")=='excess-of-loss' and risk["owner"] is not self] - new_risks = [risk for risk in new_risks if risk.get("insurancetype") in ['proportional', None] and risk["owner"] is not self] - - underwritten_risks = [{"value": contract.value, "category": contract.category, \ - "risk_factor": contract.risk_factor, "deductible": contract.deductible, \ - "excess": contract.excess, "insurancetype": contract.insurancetype, \ - "runtime": contract.runtime} for contract in self.underwritten_contracts if contract.reinsurance_share != 1.0] - + print( + "Something wrong; agent {0:d} receives too few new contracts {1:d} <= {2:d}".format( + self.id, contracts_offered, 2 * contracts_dissolved + ), + file=sys.stderr, + ) + # print(self.id, " has ", len(self.underwritten_contracts), " & receives ", contracts_offered, " & lost ", contracts_dissolved) + + new_nonproportional_risks = [ + risk + for risk in new_risks + if risk.get("insurancetype") == "excess-of-loss" + and risk["owner"] is not self + ] + new_risks = [ + risk + for risk in new_risks + if risk.get("insurancetype") in ["proportional", None] + and risk["owner"] is not self + ] + + underwritten_risks = [ + { + "value": contract.value, + "category": contract.category, + "risk_factor": contract.risk_factor, + "deductible": contract.deductible, + "excess": contract.excess, + "insurancetype": contract.insurancetype, + "runtime": contract.runtime, + } + for contract in self.underwritten_contracts + if contract.reinsurance_share != 1.0 + ] + """deal with non-proportional risks first as they must evaluate each request separatly, then with proportional ones""" for risk in new_nonproportional_risks: - accept, var_this_risk = self.riskmodel.evaluate(underwritten_risks, self.cash, risk) # TODO: change riskmodel.evaluate() to accept new risk to be evaluated and to account for existing non-proportional risks correctly -> DONE. + accept, var_this_risk = self.riskmodel.evaluate( + underwritten_risks, self.cash, risk + ) # TODO: change riskmodel.evaluate() to accept new risk to be evaluated and to account for existing non-proportional risks correctly -> DONE. if accept: - per_value_reinsurance_premium = self.np_reinsurance_premium_share * risk["periodized_total_premium"] * risk["runtime"] / risk["value"] #TODO: rename this to per_value_premium in insurancecontract.py to avoid confusion - contract = ReinsuranceContract(self, risk, time, per_value_reinsurance_premium, risk["runtime"], \ - self.default_contract_payment_period, \ - expire_immediately=self.simulation_parameters["expire_immediately"], \ - initial_VaR=var_this_risk, \ - insurancetype=risk["insurancetype"]) # TODO: implement excess of loss for reinsurance contracts + per_value_reinsurance_premium = ( + self.np_reinsurance_premium_share + * risk["periodized_total_premium"] + * risk["runtime"] + / risk["value"] + ) # TODO: rename this to per_value_premium in insurancecontract.py to avoid confusion + contract = ReinsuranceContract( + self, + risk, + time, + per_value_reinsurance_premium, + risk["runtime"], + self.default_contract_payment_period, + expire_immediately=self.simulation_parameters[ + "expire_immediately" + ], + initial_VaR=var_this_risk, + insurancetype=risk["insurancetype"], + ) # TODO: implement excess of loss for reinsurance contracts self.underwritten_contracts.append(contract) - #pass # TODO: write this nonproportional risk acceptance decision section based on commented code in the lines above this -> DONE. - + # pass # TODO: write this nonproportional risk acceptance decision section based on commented code in the lines above this -> DONE. + """make underwriting decisions, category-wise""" # TODO: Enable reinsurance shares other tan 0.0 and 1.0 - expected_profit, acceptable_by_category, var_per_risk_per_categ = self.riskmodel.evaluate(underwritten_risks, self.cash) + expected_profit, acceptable_by_category, var_per_risk_per_categ = self.riskmodel.evaluate( + underwritten_risks, self.cash + ) - #if expected_profit * 1./self.cash < self.profit_target: + # if expected_profit * 1./self.cash < self.profit_target: # self.acceptance_threshold = ((self.acceptance_threshold - .4) * 5. * self.acceptance_threshold_friction) / 5. + .4 - #else: + # else: # self.acceptance_threshold = (1 - self.acceptance_threshold_friction * (1 - (self.acceptance_threshold - .4) * 5.)) / 5. + .4 - growth_limit = max(50, 2 * len(self.underwritten_contracts) + contracts_dissolved) + growth_limit = max( + 50, 2 * len(self.underwritten_contracts) + contracts_dissolved + ) if sum(acceptable_by_category) > growth_limit: acceptable_by_category = np.asarray(acceptable_by_category) - acceptable_by_category = acceptable_by_category * growth_limit / sum(acceptable_by_category) + acceptable_by_category = ( + acceptable_by_category * growth_limit / sum(acceptable_by_category) + ) acceptable_by_category = np.int64(np.round(acceptable_by_category)) not_accepted_risks = [] for categ_id in range(len(acceptable_by_category)): - categ_risks = [risk for risk in new_risks if risk["category"] == categ_id] + categ_risks = [ + risk for risk in new_risks if risk["category"] == categ_id + ] new_risks = [risk for risk in new_risks if risk["category"] != categ_id] - categ_risks = sorted(categ_risks, key = lambda risk: risk["risk_factor"]) + categ_risks = sorted(categ_risks, key=lambda risk: risk["risk_factor"]) i = 0 if isleconfig.verbose: - print("InsuranceFirm underwrote: ", len(self.underwritten_contracts), " will accept: ", acceptable_by_category[categ_id], " out of ", len(categ_risks), "acceptance threshold: ", self.acceptance_threshold) - while (acceptable_by_category[categ_id] > 0 and len(categ_risks) > i): #\ - #and categ_risks[i]["risk_factor"] < self.acceptance_threshold): - if categ_risks[i].get("contract") is not None: #categ_risks[i]["reinsurance"]: - if categ_risks[i]["contract"].expiration > time: # required to rule out contracts that have exploded in the meantime - #print("ACCEPTING", categ_risks[i]["contract"].expiration, categ_risks[i]["expiration"], categ_risks[i]["identifier"], categ_risks[i].get("contract").terminating) - contract = ReinsuranceContract(self, categ_risks[i], time, \ - self.simulation.get_market_premium(), categ_risks[i]["expiration"] - time, \ - self.default_contract_payment_period, \ - expire_immediately=self.simulation_parameters["expire_immediately"], ) + print( + "InsuranceFirm underwrote: ", + len(self.underwritten_contracts), + " will accept: ", + acceptable_by_category[categ_id], + " out of ", + len(categ_risks), + "acceptance threshold: ", + self.acceptance_threshold, + ) + while ( + acceptable_by_category[categ_id] > 0 and len(categ_risks) > i + ): # \ + # and categ_risks[i]["risk_factor"] < self.acceptance_threshold): + if ( + categ_risks[i].get("contract") is not None + ): # categ_risks[i]["reinsurance"]: + if ( + categ_risks[i]["contract"].expiration > time + ): # required to rule out contracts that have exploded in the meantime + # print("ACCEPTING", categ_risks[i]["contract"].expiration, categ_risks[i]["expiration"], categ_risks[i]["identifier"], categ_risks[i].get("contract").terminating) + contract = ReinsuranceContract( + self, + categ_risks[i], + time, + self.simulation.get_market_premium(), + categ_risks[i]["expiration"] - time, + self.default_contract_payment_period, + expire_immediately=self.simulation_parameters[ + "expire_immediately" + ], + ) self.underwritten_contracts.append(contract) - #categ_risks[i]["contract"].reincontract = contract + # categ_risks[i]["contract"].reincontract = contract # TODO: move this to insurancecontract (ca. line 14) -> DONE # TODO: do not write into other object's properties, use setter -> DONE - assert categ_risks[i]["contract"].expiration >= contract.expiration, "Reinsurancecontract lasts longer than insurancecontract: {0:d}>{1:d} (EXPIRATION2: {2:d} Time: {3:d})".format(contract.expiration, categ_risks[i]["contract"].expiration, categ_risks[i]["expiration"], time) - #else: + assert ( + categ_risks[i]["contract"].expiration + >= contract.expiration + ), "Reinsurancecontract lasts longer than insurancecontract: {0:d}>{1:d} (EXPIRATION2: {2:d} Time: {3:d})".format( + contract.expiration, + categ_risks[i]["contract"].expiration, + categ_risks[i]["expiration"], + time, + ) + # else: # pass else: - contract = InsuranceContract(self, categ_risks[i], time, self.simulation.get_market_premium(), \ - self.contract_runtime_dist.rvs(), \ - self.default_contract_payment_period, \ - expire_immediately=self.simulation_parameters["expire_immediately"], \ - initial_VaR = var_per_risk_per_categ[categ_id]) + contract = InsuranceContract( + self, + categ_risks[i], + time, + self.simulation.get_market_premium(), + self.contract_runtime_dist.rvs(), + self.default_contract_payment_period, + expire_immediately=self.simulation_parameters[ + "expire_immediately" + ], + initial_VaR=var_per_risk_per_categ[categ_id], + ) self.underwritten_contracts.append(contract) - acceptable_by_category[categ_id] -= 1 # TODO: allow different values per risk (i.e. sum over value (and reinsurance_share) or exposure instead of counting) + acceptable_by_category[ + categ_id + ] -= ( + 1 + ) # TODO: allow different values per risk (i.e. sum over value (and reinsurance_share) or exposure instead of counting) i += 1 not_accepted_risks += categ_risks[i:] - not_accepted_risks = [risk for risk in not_accepted_risks if risk.get("contract") is None] + not_accepted_risks = [ + risk for risk in not_accepted_risks if risk.get("contract") is None + ] # seek reinsurance if self.is_insurer: @@ -224,28 +372,31 @@ def parent_iterate(self, time): # TODO: split function so that only the s self.ask_reinsurance(time) # return unacceptables - #print(self.id, " now has ", len(self.underwritten_contracts), " & returns ", len(not_accepted_risks)) + # print(self.id, " now has ", len(self.underwritten_contracts), " & returns ", len(not_accepted_risks)) self.simulation.return_risks(not_accepted_risks) - #not implemented - #"""adjust liquidity, borrow or invest""" - #pass + # not implemented + # """adjust liquidity, borrow or invest""" + # pass self.estimated_var() - + def set_owner(self, owner): self.owner = owner if isleconfig.verbose: print("SOLD") - #pdb.set_trace() - + # pdb.set_trace() + def set_contract(self, contract): self.underwritten_contracts.append(contract) - + def mature_bond(self): - obligation = {"amount": self.cash, "recipient": self.simulation, "due_time": 1, "purpose": 'mature'} + obligation = { + "amount": self.cash, + "recipient": self.simulation, + "due_time": 1, + "purpose": "mature", + } self.pay(obligation) self.simulation.delete_agents("catbond", [self]) self.operational = False - - diff --git a/compute_profits_losses_from_cash.py b/compute_profits_losses_from_cash.py index ed6691a..865f749 100644 --- a/compute_profits_losses_from_cash.py +++ b/compute_profits_losses_from_cash.py @@ -9,11 +9,9 @@ infile.close() filename = "data/" + r + "_" + ft + "profitslosses.dat" outfile = open(filename, "w") - + for series in data: - outputdata = [series[i]-series[i-1] for i in range(1, len(series))] + outputdata = [series[i] - series[i - 1] for i in range(1, len(series))] outfile.write(str(outputdata) + "\n") - - outfile.close() - + outfile.close() diff --git a/condition_aux.py b/condition_aux.py index 9ffab5b..206bf13 100644 --- a/condition_aux.py +++ b/condition_aux.py @@ -2,24 +2,101 @@ import numpy as np - """Data""" """Bloomberg size data for US firms""" -insurance_firm_sizes_empirical_2017 = [42.4701, 108.0418, 110.2641, 114.437, 130.2988, 133.674, 146.438, 152.3354, - 239.032, 337.689, 375.914, 376.988, 395.859, 436.191, 482.503, 585.824, 667.849, - 842.264, 894.848, 896.227, 904.873, 1231.126, 1357.016, 1454.999, 1518.236, - 1665.859, 1681.94, 1737.9198, 1771.21, 1807.279, 1989.742, 2059.921, 2385.485, - 2756.695, 2947.244, 3014.3, 3659.2, 3840.1, 4183.431, 4929.197, 5101.323, - 5224.622, 5900.881, 7686.431, 8376.2, 8439.743, 8764.0, 9095.0, 11198.34, - 14433.0, 15469.6, 19403.5, 21843.0, 23192.374, 24299.917, 25218.63, 31843.0, - 32051.658, 32805.016, 38701.2, 56567.0, 60658.0, 79586.0, 103483.0, 112422.0, - 167022.0, 225260.0, 498301.0, 702095.0] -reinsurance_firm_sizes_empirical_2017 = [396.898, 627.808, 6644.189, 15226.131, 25384.317, 23591.792, 3357.393, - 13606.422, 4671.794, 614.121, 60514.818, 24760.177, 2001.669, 182.2, 12906.4] +insurance_firm_sizes_empirical_2017 = [ + 42.4701, + 108.0418, + 110.2641, + 114.437, + 130.2988, + 133.674, + 146.438, + 152.3354, + 239.032, + 337.689, + 375.914, + 376.988, + 395.859, + 436.191, + 482.503, + 585.824, + 667.849, + 842.264, + 894.848, + 896.227, + 904.873, + 1231.126, + 1357.016, + 1454.999, + 1518.236, + 1665.859, + 1681.94, + 1737.9198, + 1771.21, + 1807.279, + 1989.742, + 2059.921, + 2385.485, + 2756.695, + 2947.244, + 3014.3, + 3659.2, + 3840.1, + 4183.431, + 4929.197, + 5101.323, + 5224.622, + 5900.881, + 7686.431, + 8376.2, + 8439.743, + 8764.0, + 9095.0, + 11198.34, + 14433.0, + 15469.6, + 19403.5, + 21843.0, + 23192.374, + 24299.917, + 25218.63, + 31843.0, + 32051.658, + 32805.016, + 38701.2, + 56567.0, + 60658.0, + 79586.0, + 103483.0, + 112422.0, + 167022.0, + 225260.0, + 498301.0, + 702095.0, +] +reinsurance_firm_sizes_empirical_2017 = [ + 396.898, + 627.808, + 6644.189, + 15226.131, + 25384.317, + 23591.792, + 3357.393, + 13606.422, + 4671.794, + 614.121, + 60514.818, + 24760.177, + 2001.669, + 182.2, + 12906.4, +] """Functions""" + def condition_stationary_state(series): """Stationarity test function for time series. Tests if the mean of the last 25% of the time series is within 1-2 standard deviation of the mean of the middle section (between 25% and 75% of the time series). The first @@ -29,24 +106,29 @@ def condition_stationary_state(series): Returns: Calibration score between 0 and 1. Is 1 if last 25% are within one standard deviation, between 0 and 1 if they are between 1 and 2 standard deviations, 0 otherwise.""" - + """Compute means and standard deviation""" - mean_reference = np.mean(series[int(len(series)*.25):int(len(series)*.75)]) - std_reference = np.std(series[int(len(series)*.25):int(len(series)*.75)]) - mean_test = np.mean(series[int(len(series)*.75):int(len(series)*1.)]) - + mean_reference = np.mean(series[int(len(series) * 0.25) : int(len(series) * 0.75)]) + std_reference = np.std(series[int(len(series) * 0.25) : int(len(series) * 0.75)]) + mean_test = np.mean(series[int(len(series) * 0.75) : int(len(series) * 1.0)]) + """Compute score""" score = 1 + (np.abs(mean_test - mean_reference) - std_reference) / std_reference - score = 1 if score>1 else score - score = 0 if score<0 else score - + score = 1 if score > 1 else score + score = 0 if score < 0 else score + """Set score to one if standard deviation is zero""" - if score == np.nan and np.std(series[int(len(series)*.25):int(len(series)*.75)]) == 0: + if ( + score == np.nan + and np.std(series[int(len(series) * 0.25) : int(len(series) * 0.75)]) == 0 + ): score = 1 return score - -def scaler(series): # TODO: find a better way to scale heavy-tailed distributions than to use standard score scaling on logs + +def scaler( + series +): # TODO: find a better way to scale heavy-tailed distributions than to use standard score scaling on logs """Function to do a standard score scaling of the log of a heavy-tailed distribution. This is used to calibrate distributions where the unit is not important (distributions of sizes of firms e.g.). This would be perfectly appropriate for lognormal distributions, but should work reasonably well for calibration of other heavy-tailed @@ -56,10 +138,10 @@ def scaler(series): # TODO: find a better way to scale heavy-tailed distribution Returns: Calibratied series.""" series = np.asarray(series) - assert (series>1).all() + assert (series > 1).all() logseries = np.log(series) mean = np.mean(logseries) std = np.std(logseries) - z = (logseries - mean)/std + z = (logseries - mean) / std newseries = np.exp(z) return newseries diff --git a/distribution_wrapper_test.py b/distribution_wrapper_test.py index b4bfa97..81734f9 100644 --- a/distribution_wrapper_test.py +++ b/distribution_wrapper_test.py @@ -5,14 +5,20 @@ import pdb non_truncated_dist = scipy.stats.pareto(b=2, loc=0, scale=0.5) -truncated_dist = TruncatedDistWrapper(lower_bound=0.6, upper_bound=1., dist=non_truncated_dist) -reinsurance_dist = ReinsuranceDistWrapper(lower_bound=0.85, upper_bound=0.95, dist=truncated_dist) +truncated_dist = TruncatedDistWrapper( + lower_bound=0.6, upper_bound=1.0, dist=non_truncated_dist +) +reinsurance_dist = ReinsuranceDistWrapper( + lower_bound=0.85, upper_bound=0.95, dist=truncated_dist +) x1 = np.linspace(non_truncated_dist.ppf(0.01), non_truncated_dist.ppf(0.99), 100) -x2 = np.linspace(truncated_dist.ppf(0.01), truncated_dist.ppf(1.), 100) -x3 = np.linspace(reinsurance_dist.ppf(0.01), reinsurance_dist.ppf(1.), 100) +x2 = np.linspace(truncated_dist.ppf(0.01), truncated_dist.ppf(1.0), 100) +x3 = np.linspace(reinsurance_dist.ppf(0.01), reinsurance_dist.ppf(1.0), 100) x_val_1 = reinsurance_dist.lower_bound -x_val_2 = truncated_dist.upper_bound - (reinsurance_dist.upper_bound - reinsurance_dist.lower_bound) +x_val_2 = truncated_dist.upper_bound - ( + reinsurance_dist.upper_bound - reinsurance_dist.lower_bound +) x_val_3 = reinsurance_dist.upper_bound x_val_4 = truncated_dist.upper_bound diff --git a/distributionreinsurance.py b/distributionreinsurance.py index fd85eb3..8d9dcb2 100644 --- a/distributionreinsurance.py +++ b/distributionreinsurance.py @@ -4,7 +4,8 @@ import scipy import pdb -class ReinsuranceDistWrapper(): + +class ReinsuranceDistWrapper: def __init__(self, dist, lower_bound=None, upper_bound=None): assert lower_bound is not None or upper_bound is not None self.dist = dist @@ -17,57 +18,72 @@ def __init__(self, dist, lower_bound=None, upper_bound=None): assert self.upper_bound > self.lower_bound self.redistributed_share = dist.cdf(upper_bound) - dist.cdf(lower_bound) - def pdf(self, x): x = np.array(x, ndmin=1) - r = map(lambda Y: self.dist.pdf(Y) if Y < self.lower_bound \ - else np.inf if Y==self.lower_bound \ - else self.dist.pdf(Y + self.upper_bound - self.lower_bound), x) + r = map( + lambda Y: self.dist.pdf(Y) + if Y < self.lower_bound + else np.inf + if Y == self.lower_bound + else self.dist.pdf(Y + self.upper_bound - self.lower_bound), + x, + ) r = np.array(list(r)) if len(r.flatten()) == 1: r = float(r) return r - + def cdf(self, x): x = np.array(x, ndmin=1) - r = map(lambda Y: self.dist.cdf(Y) if Y < self.lower_bound \ - else self.dist.cdf(Y + self.upper_bound - self.lower_bound), x) + r = map( + lambda Y: self.dist.cdf(Y) + if Y < self.lower_bound + else self.dist.cdf(Y + self.upper_bound - self.lower_bound), + x, + ) r = np.array(list(r)) if len(r.flatten()) == 1: r = float(r) return r - + def ppf(self, x): x = np.array(x, ndmin=1) assert (x >= 0).all() and (x <= 1).all() - r = map(lambda Y: self.dist.ppf(Y) if Y <= self.dist.cdf(self.lower_bound) \ - else self.dist.ppf(self.dist.cdf(self.lower_bound)) if Y <= self.dist.cdf(self.upper_bound) \ - else self.dist.ppf(Y) - self.upper_bound + self.lower_bound, x) + r = map( + lambda Y: self.dist.ppf(Y) + if Y <= self.dist.cdf(self.lower_bound) + else self.dist.ppf(self.dist.cdf(self.lower_bound)) + if Y <= self.dist.cdf(self.upper_bound) + else self.dist.ppf(Y) - self.upper_bound + self.lower_bound, + x, + ) r = np.array(list(r)) if len(r.flatten()) == 1: r = float(r) return r - + def rvs(self, size=1): sample = self.dist.rvs(size=size) - sample1 = sample[sample<=self.lower_bound] - sample2 = sample[sample>self.lower_bound] - sample3 = sample2[sample2>=self.upper_bound] - sample2 = sample2[sample2 self.lower_bound] + sample3 = sample2[sample2 >= self.upper_bound] + sample2 = sample2[sample2 < self.upper_bound] + sample2 = np.ones(len(sample2)) * self.lower_bound - sample3 = sample3 -self.upper_bound + self.lower_bound - - sample = np.append(np.append(sample1,sample2),sample3) + sample3 = sample3 - self.upper_bound + self.lower_bound + + sample = np.append(np.append(sample1, sample2), sample3) return sample[:size] if __name__ == "__main__": non_truncated = scipy.stats.pareto(b=2, loc=0, scale=0.5) - #truncated = ReinsuranceDistWrapper(lower_bound=0, upper_bound=1, dist=non_truncated) - truncated = ReinsuranceDistWrapper(lower_bound=0.9, upper_bound=1.1, dist=non_truncated) + # truncated = ReinsuranceDistWrapper(lower_bound=0, upper_bound=1, dist=non_truncated) + truncated = ReinsuranceDistWrapper( + lower_bound=0.9, upper_bound=1.1, dist=non_truncated + ) x = np.linspace(non_truncated.ppf(0.01), non_truncated.ppf(0.99), 100) x2 = np.linspace(truncated.ppf(0.01), truncated.ppf(0.99), 100) - #pdb.set_trace() + # pdb.set_trace() diff --git a/distributiontruncated.py b/distributiontruncated.py index d862d8d..c50387b 100644 --- a/distributiontruncated.py +++ b/distributiontruncated.py @@ -2,56 +2,77 @@ import numpy as np from math import ceil import scipy.integrate +import functools -class TruncatedDistWrapper(): + +class TruncatedDistWrapper: def __init__(self, dist, lower_bound=0, upper_bound=1): self.dist = dist self.normalizing_factor = dist.cdf(upper_bound) - dist.cdf(lower_bound) self.lower_bound = lower_bound self.upper_bound = upper_bound assert self.upper_bound > self.lower_bound - + def pdf(self, x): x = np.array(x, ndmin=1) - r = map(lambda Y: self.dist.pdf(Y) / self.normalizing_factor \ - if (Y >= self.lower_bound and Y <= self.upper_bound) else 0, x) + r = map( + lambda Y: self.dist.pdf(Y) / self.normalizing_factor + if (self.lower_bound <= Y <= self.upper_bound) + else 0, + x, + ) r = np.array(list(r)) if len(r.flatten()) == 1: r = float(r) return r - + def cdf(self, x): x = np.array(x, ndmin=1) - r = map(lambda Y: 0 if Y < self.lower_bound else 1 if Y > self.upper_bound \ - else (self.dist.cdf(Y) - self.dist.cdf(self.lower_bound))/ self.normalizing_factor, x) + r = map( + lambda Y: 0 + if Y < self.lower_bound + else 1 + if Y > self.upper_bound + else (self.dist.cdf(Y) - self.dist.cdf(self.lower_bound)) + / self.normalizing_factor, + x, + ) r = np.array(list(r)) if len(r.flatten()) == 1: r = float(r) return r - + + @functools.lru_cache(maxsize=512) def ppf(self, x): x = np.array(x, ndmin=1) assert (x >= 0).all() and (x <= 1).all() - return self.dist.ppf(x * self.normalizing_factor + self.dist.cdf(self.lower_bound)) + return self.dist.ppf( + x * self.normalizing_factor + self.dist.cdf(self.lower_bound) + ) def rvs(self, size=1): init_sample_size = int(ceil(size / self.normalizing_factor * 1.1)) sample = self.dist.rvs(size=init_sample_size) - sample = sample[sample>=self.lower_bound] - sample = sample[sample<=self.upper_bound] + sample = sample[sample >= self.lower_bound] + sample = sample[sample <= self.upper_bound] while len(sample) < size: sample = np.append(sample, self.rvs(size - len(sample))) - return sample[:size] - + return sample[:size] + def mean(self): - mean_estimate, mean_error = scipy.integrate.quad(lambda Y: Y*self.pdf(Y), self.lower_bound, self.upper_bound) + mean_estimate, mean_error = scipy.integrate.quad( + lambda Y: Y * self.pdf(Y), self.lower_bound, self.upper_bound + ) return mean_estimate + if __name__ == "__main__": non_truncated = scipy.stats.pareto(b=2, loc=0, scale=0.5) - truncated = TruncatedDistWrapper(lower_bound=0.55, upper_bound=1., dist=non_truncated) + truncated = TruncatedDistWrapper( + lower_bound=0.55, upper_bound=1.0, dist=non_truncated + ) x = np.linspace(non_truncated.ppf(0.01), non_truncated.ppf(0.99), 100) x2 = np.linspace(truncated.ppf(0.01), truncated.ppf(0.99), 100) - + print(truncated.mean()) diff --git a/ensemble.py b/ensemble.py index 92c3d49..b35ed7e 100644 --- a/ensemble.py +++ b/ensemble.py @@ -1,6 +1,6 @@ -#This script allows to launch an ensemble of simulations for different number of risks models. -#It can be run locally if no argument is passed when called from the terminal. -#It can be run in the cloud if it is passed as argument the server that will be used. +# This script allows to launch an ensemble of simulations for different number of risks models. +# It can be run locally if no argument is passed when called from the terminal. +# It can be run in the cloud if it is passed as argument the server that will be used. import sys import random import os @@ -16,76 +16,78 @@ from sandman2.api import operation, Session - @operation def agg(*outputs): # do nothing - return outputs + return outputs def rake(hostname): - jobs = [] """Configuration of the ensemble""" - replications = 70 #Number of replications to be carried out for each configuration. Usually one risk model, two risk models, three risk models, four risk models. + replications = ( + 70 + ) # Number of replications to be carried out for each configuration. Usually one risk model, two risk models, three risk models, four risk models. model = start.main - m = operation(model, include_modules = True) + m = operation(model, include_modules=True) - riskmodels = [1,2,3,4] #The number of risk models that will be used. + riskmodels = [1, 2, 3, 4] # The number of risk models that will be used. parameters = isleconfig.simulation_parameters - nums = {'1': 'one', - '2': 'two', - '3': 'three', - '4': 'four', - '5': 'five', - '6': 'six', - '7': 'seven', - '8': 'eight', - '9': 'nine'} + nums = { + "1": "one", + "2": "two", + "3": "three", + "4": "four", + "5": "five", + "6": "six", + "7": "seven", + "8": "eight", + "9": "nine", + } """Configure the return values and corresponding file suffixes where they should be saved""" - requested_logs = {'total_cash': '_cash.dat', - 'total_excess_capital': '_excess_capital.dat', - 'total_profitslosses': '_profitslosses.dat', - 'total_contracts': '_contracts.dat', - 'total_operational': '_operational.dat', - 'total_reincash': '_reincash.dat', - 'total_reinexcess_capital': '_reinexcess_capital.dat', - 'total_reinprofitslosses': '_reinprofitslosses.dat', - 'total_reincontracts': '_reincontracts.dat', - 'total_reinoperational': '_reinoperational.dat', - 'total_catbondsoperational': '_total_catbondsoperational.dat', - 'market_premium': '_premium.dat', - 'market_reinpremium': '_reinpremium.dat', - 'cumulative_bankruptcies': '_cumulative_bankruptcies.dat', - 'cumulative_market_exits': '_cumulative_market_exits', # TODO: correct filename - 'cumulative_unrecovered_claims': '_cumulative_unrecovered_claims.dat', - 'cumulative_claims': '_cumulative_claims.dat', - 'insurance_firms_cash': '_insurance_firms_cash.dat', - 'reinsurance_firms_cash': '_reinsurance_firms_cash.dat', - 'market_diffvar': '_market_diffvar.dat', - 'rc_event_schedule_initial': '_rc_event_schedule.dat', - 'rc_event_damage_initial': '_rc_event_damage.dat', - 'number_riskmodels': '_number_riskmodels.dat' - } - + requested_logs = { + "total_cash": "_cash.dat", + "total_excess_capital": "_excess_capital.dat", + "total_profitslosses": "_profitslosses.dat", + "total_contracts": "_contracts.dat", + "total_operational": "_operational.dat", + "total_reincash": "_reincash.dat", + "total_reinexcess_capital": "_reinexcess_capital.dat", + "total_reinprofitslosses": "_reinprofitslosses.dat", + "total_reincontracts": "_reincontracts.dat", + "total_reinoperational": "_reinoperational.dat", + "total_catbondsoperational": "_total_catbondsoperational.dat", + "market_premium": "_premium.dat", + "market_reinpremium": "_reinpremium.dat", + "cumulative_bankruptcies": "_cumulative_bankruptcies.dat", + "cumulative_market_exits": "_cumulative_market_exits", # TODO: correct filename + "cumulative_unrecovered_claims": "_cumulative_unrecovered_claims.dat", + "cumulative_claims": "_cumulative_claims.dat", + "insurance_firms_cash": "_insurance_firms_cash.dat", + "reinsurance_firms_cash": "_reinsurance_firms_cash.dat", + "market_diffvar": "_market_diffvar.dat", + "rc_event_schedule_initial": "_rc_event_schedule.dat", + "rc_event_damage_initial": "_rc_event_damage.dat", + "number_riskmodels": "_number_riskmodels.dat", + } + if isleconfig.slim_log: - for name in ['insurance_firms_cash', 'reinsurance_firms_cash']: + for name in ["insurance_firms_cash", "reinsurance_firms_cash"]: del requested_logs[name] - + assert "number_riskmodels" in requested_logs - """Configure log directory and ensure that the directory exists""" dir_prefix = "/data/" directory = os.getcwd() + dir_prefix - try: #Here it is checked whether the directory to collect the results exists or not. If not it is created. + try: # Here it is checked whether the directory to collect the results exists or not. If not it is created. os.stat(directory) except: os.mkdir(directory) @@ -95,78 +97,124 @@ def rake(hostname): filename = os.getcwd() + dir_prefix + nums[str(i)] + "_history_logs.dat" if os.path.exists(filename): os.remove(filename) - """Setup of the simulations""" - setup = SetupSim() #Here the setup for the simulation is done. - [general_rc_event_schedule, general_rc_event_damage, np_seeds, random_seeds] = setup.obtain_ensemble(replications) #Since this script is used to carry out simulations in the cloud will usually have more than 1 replication.. - save_iter = isleconfig.simulation_parameters["max_time"] + 2 # never save simulation state in ensemble runs (resuming is impossible anyway) - - for i in riskmodels: #In this loop the parameters, schedules and random seeds for every run are prepared. Different risk models will be run with the same schedule, damage size and random seed for a fair comparison. - - simulation_parameters = copy.copy(parameters) #Here the parameters used for the simulation are loaded. Clone is needed otherwise all the runs will be carried out with the last number of thee loop. - simulation_parameters["no_riskmodels"] = i #Since we want to obtain ensembles for different number of risk models, we vary here the number of risks models. - job = [m(simulation_parameters, general_rc_event_schedule[x], general_rc_event_damage[x], np_seeds[x], random_seeds[x], save_iter, list(requested_logs.keys())) for x in range(replications)] #Here is assembled each job with the corresponding: simulation parameters, time events, damage events, seeds, simulation state save interval (never, i.e. longer than max_time), and list of requested logs. - jobs.append(job) #All jobs are collected in the jobs list. + setup = SetupSim() # Here the setup for the simulation is done. + [ + general_rc_event_schedule, + general_rc_event_damage, + np_seeds, + random_seeds, + ] = setup.obtain_ensemble( + replications + ) # Since this script is used to carry out simulations in the cloud will usually have more than 1 replication.. + save_iter = ( + isleconfig.simulation_parameters["max_time"] + 2 + ) # never save simulation state in ensemble runs (resuming is impossible anyway) + + for ( + i + ) in ( + riskmodels + ): # In this loop the parameters, schedules and random seeds for every run are prepared. Different risk models will be run with the same schedule, damage size and random seed for a fair comparison. + + simulation_parameters = copy.copy( + parameters + ) # Here the parameters used for the simulation are loaded. Clone is needed otherwise all the runs will be carried out with the last number of thee loop. + simulation_parameters[ + "no_riskmodels" + ] = ( + i + ) # Since we want to obtain ensembles for different number of risk models, we vary here the number of risks models. + job = [ + m( + simulation_parameters, + general_rc_event_schedule[x], + general_rc_event_damage[x], + np_seeds[x], + random_seeds[x], + save_iter, + list(requested_logs.keys()), + ) + for x in range(replications) + ] # Here is assembled each job with the corresponding: simulation parameters, time events, damage events, seeds, simulation state save interval (never, i.e. longer than max_time), and list of requested logs. + jobs.append(job) # All jobs are collected in the jobs list. """Here the jobs are submitted""" with Session(host=hostname, default_cb_to_stdout=True) as sess: - for job in jobs: #If there are 4 risk models jobs will be a list with 4 elements. - + for ( + job + ) in jobs: # If there are 4 risk models jobs will be a list with 4 elements. + """Run simulation and obtain result""" result = sess.submit(job) - - + """find number of riskmodels from log""" delistified_result = [listify.delistify(list(res)) for res in result] - #nrmidx = result[0][-1].index("number_riskmodels") - #nrm = result[0][nrmidx] + # nrmidx = result[0][-1].index("number_riskmodels") + # nrm = result[0][nrmidx] nrm = delistified_result[0]["number_riskmodels"] """These are the files created to collect the results""" wfiles_dict = {} logfile_dict = {} - + for name in requested_logs.keys(): if "rc_event" in name or "number_riskmodels" in name: - logfile_dict[name] = os.getcwd() + dir_prefix + "check_" + str(nums[str(nrm)]) + requested_logs[name] + logfile_dict[name] = ( + os.getcwd() + + dir_prefix + + "check_" + + str(nums[str(nrm)]) + + requested_logs[name] + ) elif "firms_cash" in name: - logfile_dict[name] = os.getcwd() + dir_prefix + "record_" + str(nums[str(nrm)]) + requested_logs[name] + logfile_dict[name] = ( + os.getcwd() + + dir_prefix + + "record_" + + str(nums[str(nrm)]) + + requested_logs[name] + ) else: - logfile_dict[name] = os.getcwd() + dir_prefix + str(nums[str(nrm)]) + requested_logs[name] + logfile_dict[name] = ( + os.getcwd() + + dir_prefix + + str(nums[str(nrm)]) + + requested_logs[name] + ) for name in logfile_dict: wfiles_dict[name] = open(logfile_dict[name], "w") - """Recreate logger object locally and save logs""" - + """Create local object""" L = logger.Logger() for i in range(len(job)): """Populate logger object with logs obtained from remote simulation run""" L.restore_logger_object(list(result[i])) - + """Save logs as dict (to _history_logs.dat)""" L.save_log(True) - + """Save logs as indivitual files""" for name in logfile_dict: wfiles_dict[name].write(str(delistified_result[i][name]) + "\n") - + """Once the data is stored in disk the files are closed""" for name in logfile_dict: wfiles_dict[name].close() del wfiles_dict[name] -if __name__ == '__main__': +if __name__ == "__main__": host = None if len(sys.argv) > 1: - host = sys.argv[1] #The server is passed as an argument. + host = sys.argv[1] # The server is passed as an argument. rake(host) diff --git a/genericagent.py b/genericagent.py index 8e58efe..4985372 100644 --- a/genericagent.py +++ b/genericagent.py @@ -1,7 +1,8 @@ - -class GenericAgent(): +class GenericAgent: def __init__(self, *args, **kwargs): self.init(*args, **kwargs) - + def init(*args, **kwargs): - assert False, "Error: GenericAgent init method should have been overridden but was not." + assert ( + False + ), "Error: GenericAgent init method should have been overridden but was not." diff --git a/genericagentabce.py b/genericagentabce.py index c0eb884..28d9531 100644 --- a/genericagentabce.py +++ b/genericagentabce.py @@ -1,4 +1,5 @@ import abce + class GenericAgent(abce.Agent): pass diff --git a/insurancecontract.py b/insurancecontract.py index d331a8d..55a8fc3 100644 --- a/insurancecontract.py +++ b/insurancecontract.py @@ -11,11 +11,35 @@ class InsuranceContract(MetaInsuranceContract): The signature of this class' constructor is the same as that of the InsuranceContract constructor. The class has two methods (explode, mature) that overwrite methods in InsuranceContract.""" - def __init__(self, insurer, properties, time, premium, runtime, payment_period, expire_immediately, initial_VaR=0.,\ - insurancetype="proportional", deductible_fraction=None, excess_fraction=None, reinsurance=0): - super(InsuranceContract, self).__init__(insurer, properties, time, premium, runtime, payment_period, \ - expire_immediately, initial_VaR, insurancetype, deductible_fraction, - excess_fraction, reinsurance) + def __init__( + self, + insurer, + properties, + time, + premium, + runtime, + payment_period, + expire_immediately, + initial_VaR=0.0, + insurancetype="proportional", + deductible_fraction=None, + excess_fraction=None, + reinsurance=0, + ): + super(InsuranceContract, self).__init__( + insurer, + properties, + time, + premium, + runtime, + payment_period, + expire_immediately, + initial_VaR, + insurancetype, + deductible_fraction, + excess_fraction, + reinsurance, + ) self.risk_data = properties @@ -34,10 +58,14 @@ def explode(self, time, uniform_value, damage_extent): if uniform_value < self.risk_factor: # if True: claim = min(self.excess, damage_extent * self.value) - self.deductible - self.insurer.register_claim(claim) #Every insurance claim made is immediately registered. + self.insurer.register_claim( + claim + ) # Every insurance claim made is immediately registered. self.current_claim += claim - self.insurer.receive_obligation(claim, self.property_holder, time + 2, 'claim') + self.insurer.receive_obligation( + claim, self.property_holder, time + 2, "claim" + ) # Insurer pays one time step after reinsurer to avoid bankruptcy. # TODO: Is this realistic? Change this? if self.expire_immediately: @@ -51,10 +79,9 @@ def mature(self, time): No return value. Returns risk to simulation as contract terminates. Calls terminate_reinsurance to dissolve any reinsurance contracts.""" - #self.terminating = True + # self.terminating = True self.terminate_reinsurance(time) if not self.roll_over_flag: self.property_holder.return_risks([self.risk_data]) - diff --git a/insurancefirm.py b/insurancefirm.py index 61d5746..554b0d0 100644 --- a/insurancefirm.py +++ b/insurancefirm.py @@ -4,9 +4,11 @@ from reinsurancecontract import ReinsuranceContract import isleconfig + class InsuranceFirm(MetaInsuranceOrg): """ReinsuranceFirm class. Inherits from InsuranceFirm.""" + def init(self, simulation_parameters, agent_parameters): """Constructor method. Accepts arguments @@ -18,78 +20,134 @@ def init(self, simulation_parameters, agent_parameters): self.is_reinsurer = False def adjust_dividends(self, time, actual_capacity): - #TODO: Implement algorithm from flowchart + # TODO: Implement algorithm from flowchart profits = self.get_profitslosses() - self.per_period_dividend = max(0, self.dividend_share_of_profits * profits) # max function ensures that no negative dividends are paid - #if profits < 0: # no dividends when losses are written + self.per_period_dividend = max( + 0, self.dividend_share_of_profits * profits + ) # max function ensures that no negative dividends are paid + # if profits < 0: # no dividends when losses are written # self.per_period_dividend = 0 - if actual_capacity < self.capacity_target: # no dividends if firm misses capital target + if ( + actual_capacity < self.capacity_target + ): # no dividends if firm misses capital target self.per_period_dividend = 0 def get_reinsurance_VaR_estimate(self, max_var): - reinsurance_factor_estimate = (sum([ 1 for categ_id in range(self.simulation_no_risk_categories) \ - if (self.category_reinsurance[categ_id] is None)]) \ - * 1. / self.simulation_no_risk_categories) \ - * (1. - self.np_reinsurance_deductible_fraction) - reinsurance_VaR_estimate = max_var * (1. + reinsurance_factor_estimate) + reinsurance_factor_estimate = ( + sum( + [ + 1 + for categ_id in range(self.simulation_no_risk_categories) + if (self.category_reinsurance[categ_id] is None) + ] + ) + * 1.0 + / self.simulation_no_risk_categories + ) * (1.0 - self.np_reinsurance_deductible_fraction) + reinsurance_VaR_estimate = max_var * (1.0 + reinsurance_factor_estimate) return reinsurance_VaR_estimate - + def adjust_capacity_target(self, max_var): reinsurance_VaR_estimate = self.get_reinsurance_VaR_estimate(max_var) - capacity_target_var_ratio_estimate = (self.capacity_target + reinsurance_VaR_estimate) * 1. / (max_var + reinsurance_VaR_estimate) - if capacity_target_var_ratio_estimate > self.capacity_target_increment_threshold: + try: + capacity_target_var_ratio_estimate = ( + (self.capacity_target + reinsurance_VaR_estimate) + * 1.0 + / (max_var + reinsurance_VaR_estimate) + ) + except RuntimeError: + pass + if ( + capacity_target_var_ratio_estimate + > self.capacity_target_increment_threshold + ): self.capacity_target *= self.capacity_target_increment_factor - elif capacity_target_var_ratio_estimate < self.capacity_target_decrement_threshold: + elif ( + capacity_target_var_ratio_estimate + < self.capacity_target_decrement_threshold + ): self.capacity_target *= self.capacity_target_decrement_factor - return + return def get_capacity(self, max_var): - if max_var < self.cash: # ensure presence of sufficiently much cash to cover VaR + if ( + max_var < self.cash + ): # ensure presence of sufficiently much cash to cover VaR reinsurance_VaR_estimate = self.get_reinsurance_VaR_estimate(max_var) return self.cash + reinsurance_VaR_estimate # else: # (This point is only reached when insurer is in severe financial difficulty. Ensure insurer recovers complete coverage.) return self.cash def increase_capacity(self, time, max_var): - '''This is implemented for non-proportional reinsurance only. Otherwise the price comparison is not meaningful. Assert non-proportional mode.''' - assert self.simulation_reinsurance_type == 'non-proportional' - '''get prices''' - reinsurance_price = self.simulation.get_reinsurance_premium(self.np_reinsurance_deductible_fraction) - cat_bond_price = self.simulation.get_cat_bond_price(self.np_reinsurance_deductible_fraction) + """This is implemented for non-proportional reinsurance only. Otherwise the price comparison is not meaningful. Assert non-proportional mode.""" + assert self.simulation_reinsurance_type == "non-proportional" + """get prices""" + reinsurance_price = self.simulation.get_reinsurance_premium( + self.np_reinsurance_deductible_fraction + ) + cat_bond_price = self.simulation.get_cat_bond_price( + self.np_reinsurance_deductible_fraction + ) capacity = None - if not reinsurance_price == cat_bond_price == float('inf'): - categ_ids = [ categ_id for categ_id in range(self.simulation_no_risk_categories) if (self.category_reinsurance[categ_id] is None)] + if not reinsurance_price == cat_bond_price == float("inf"): + categ_ids = [ + categ_id + for categ_id in range(self.simulation_no_risk_categories) + if (self.category_reinsurance[categ_id] is None) + ] if len(categ_ids) > 1: np.random.shuffle(categ_ids) - while len(categ_ids) >= 1: + while len(categ_ids) >= 1: categ_id = categ_ids.pop() capacity = self.get_capacity(max_var) - if self.capacity_target < capacity: # just one per iteration, unless capital target is unmatched - if self.increase_capacity_by_category(time, categ_id, reinsurance_price=reinsurance_price, cat_bond_price=cat_bond_price, force=False): + if ( + self.capacity_target < capacity + ): # just one per iteration, unless capital target is unmatched + if self.increase_capacity_by_category( + time, + categ_id, + reinsurance_price=reinsurance_price, + cat_bond_price=cat_bond_price, + force=False, + ): categ_ids = [] else: - self.increase_capacity_by_category(time, categ_id, reinsurance_price=reinsurance_price, cat_bond_price=cat_bond_price, force=True) + self.increase_capacity_by_category( + time, + categ_id, + reinsurance_price=reinsurance_price, + cat_bond_price=cat_bond_price, + force=True, + ) # capacity is returned in order not to recompute more often than necessary - if capacity is None: + if capacity is None: capacity = self.get_capacity(max_var) - return capacity + return capacity - def increase_capacity_by_category(self, time, categ_id, reinsurance_price, cat_bond_price, force=False): + def increase_capacity_by_category( + self, time, categ_id, reinsurance_price, cat_bond_price, force=False + ): if isleconfig.verbose: - print("IF {0:d} increasing capacity in period {1:d}, cat bond price: {2:f}, reinsurance premium {3:f}".format(self.id, time, cat_bond_price, reinsurance_price)) + print( + "IF {0:d} increasing capacity in period {1:d}, cat bond price: {2:f}, reinsurance premium {3:f}".format( + self.id, time, cat_bond_price, reinsurance_price + ) + ) if not force: actual_premium = self.get_average_premium(categ_id) possible_premium = self.simulation.get_market_premium() if actual_premium >= possible_premium: return False - '''on the basis of prices decide for obtaining reinsurance or for issuing cat bond''' + """on the basis of prices decide for obtaining reinsurance or for issuing cat bond""" if reinsurance_price > cat_bond_price: if isleconfig.verbose: print("IF {0:d} issuing Cat bond in period {1:d}".format(self.id, time)) self.issue_cat_bond(time, categ_id) else: if isleconfig.verbose: - print("IF {0:d} getting reinsurance in period {1:d}".format(self.id, time)) + print( + "IF {0:d} getting reinsurance in period {1:d}".format(self.id, time) + ) self.ask_reinsurance_non_proportional_by_category(time, categ_id) return True @@ -102,13 +160,13 @@ def get_average_premium(self, categ_id): contract_premium = contract.periodized_premium * contract.runtime weighted_premium_sum += contract_premium if total_weight == 0: - return 0 # will prevent any attempt to reinsure empty categories + return 0 # will prevent any attempt to reinsure empty categories return weighted_premium_sum * 1.0 / total_weight - + def ask_reinsurance(self, time): - if self.simulation_reinsurance_type == 'proportional': + if self.simulation_reinsurance_type == "proportional": self.ask_reinsurance_proportional() - elif self.simulation_reinsurance_type == 'non-proportional': + elif self.simulation_reinsurance_type == "non-proportional": self.ask_reinsurance_non_proportional(time) else: assert False, "Undefined reinsurance type" @@ -125,7 +183,7 @@ def ask_reinsurance_non_proportional(self, time): """Evaluate by risk category""" for categ_id in range(self.simulation_no_risk_categories): """Seek reinsurance only with probability 10% if not already reinsured""" # TODO: find a more generic way to decide whether to request reinsurance for category in this period - if (self.category_reinsurance[categ_id] is None): + if self.category_reinsurance[categ_id] is None: self.ask_reinsurance_non_proportional_by_category(time, categ_id) def characterize_underwritten_risks_by_category(self, time, categ_id): @@ -139,22 +197,30 @@ def characterize_underwritten_risks_by_category(self, time, categ_id): avg_risk_factor += contract.risk_factor number_risks += 1 periodized_total_premium += contract.periodized_premium - if number_risks > 0: + if number_risks > 0: avg_risk_factor /= number_risks return total_value, avg_risk_factor, number_risks, periodized_total_premium - def ask_reinsurance_non_proportional_by_category(self, time, categ_id): """Proceed with creation of reinsurance risk only if category is not empty.""" - total_value, avg_risk_factor, number_risks, periodized_total_premium = self.characterize_underwritten_risks_by_category(time, categ_id) - if number_risks > 0: - risk = {"value": total_value, "category": categ_id, "owner": self, - #"identifier": uuid.uuid1(), - "insurancetype": 'excess-of-loss', "number_risks": number_risks, - "deductible_fraction": self.np_reinsurance_deductible_fraction, - "excess_fraction": self.np_reinsurance_excess_fraction, - "periodized_total_premium": periodized_total_premium, "runtime": 12, - "expiration": time + 12, "risk_factor": avg_risk_factor} # TODO: make runtime into a parameter + total_value, avg_risk_factor, number_risks, periodized_total_premium = self.characterize_underwritten_risks_by_category( + time, categ_id + ) + if number_risks > 0: + risk = { + "value": total_value, + "category": categ_id, + "owner": self, + # "identifier": uuid.uuid1(), + "insurancetype": "excess-of-loss", + "number_risks": number_risks, + "deductible_fraction": self.np_reinsurance_deductible_fraction, + "excess_fraction": self.np_reinsurance_excess_fraction, + "periodized_total_premium": periodized_total_premium, + "runtime": 12, + "expiration": time + 12, + "risk_factor": avg_risk_factor, + } # TODO: make runtime into a parameter self.simulation.append_reinrisks(risk) @@ -164,83 +230,125 @@ def ask_reinsurance_proportional(self): if contract.reincontract == None: nonreinsured.append(contract) - #nonreinsured_b = [contract + # nonreinsured_b = [contract # for contract in self.underwritten_contracts # if contract.reincontract == None] # - #try: + # try: # assert nonreinsured == nonreinsured_b - #except: + # except: # pdb.set_trace() nonreinsured.reverse() - if len(nonreinsured) >= (1 - self.reinsurance_limit) * len(self.underwritten_contracts): + if len(nonreinsured) >= (1 - self.reinsurance_limit) * len( + self.underwritten_contracts + ): counter = 0 - limitrein = len(nonreinsured) - (1 - self.reinsurance_limit) * len(self.underwritten_contracts) + limitrein = len(nonreinsured) - (1 - self.reinsurance_limit) * len( + self.underwritten_contracts + ) for contract in nonreinsured: if counter < limitrein: - risk = {"value": contract.value, "category": contract.category, "owner": self, - #"identifier": uuid.uuid1(), - "reinsurance_share": 1., - "expiration": contract.expiration, "contract": contract, - "risk_factor": contract.risk_factor} + risk = { + "value": contract.value, + "category": contract.category, + "owner": self, + # "identifier": uuid.uuid1(), + "reinsurance_share": 1.0, + "expiration": contract.expiration, + "contract": contract, + "risk_factor": contract.risk_factor, + } - #print("CREATING", risk["expiration"], contract.expiration, risk["contract"].expiration, risk["identifier"]) + # print("CREATING", risk["expiration"], contract.expiration, risk["contract"].expiration, risk["identifier"]) self.simulation.append_reinrisks(risk) counter += 1 else: break def add_reinsurance(self, category, excess_fraction, deductible_fraction, contract): - self.riskmodel.add_reinsurance(category, excess_fraction, deductible_fraction, contract) + self.riskmodel.add_reinsurance( + category, excess_fraction, deductible_fraction, contract + ) self.category_reinsurance[category] = contract - #pass + # pass - def delete_reinsurance(self, category, excess_fraction, deductible_fraction, contract): - self.riskmodel.delete_reinsurance(category, excess_fraction, deductible_fraction, contract) + def delete_reinsurance( + self, category, excess_fraction, deductible_fraction, contract + ): + self.riskmodel.delete_reinsurance( + category, excess_fraction, deductible_fraction, contract + ) self.category_reinsurance[category] = None - #pass - - def issue_cat_bond(self, time, categ_id, per_value_per_period_premium = 0): + # pass + + def issue_cat_bond(self, time, categ_id, per_value_per_period_premium=0): # premium is for usual reinsurance contracts paid using per value market premium # for the quasi-contract for the cat bond, nothing is paid, everything is already paid at the beginning. - #per_value_reinsurance_premium = self.np_reinsurance_premium_share * risk["periodized_total_premium"] * risk["runtime"] / risk["value"] #TODO: rename this to per_value_premium in insurancecontract.py to avoid confusion + # per_value_reinsurance_premium = self.np_reinsurance_premium_share * risk["periodized_total_premium"] * risk["runtime"] / risk["value"] #TODO: rename this to per_value_premium in insurancecontract.py to avoid confusion """ create catbond """ - total_value, avg_risk_factor, number_risks, periodized_total_premium = self.characterize_underwritten_risks_by_category(time, categ_id) + total_value, avg_risk_factor, number_risks, periodized_total_premium = self.characterize_underwritten_risks_by_category( + time, categ_id + ) if number_risks > 0: - risk = {"value": total_value, "category": categ_id, "owner": self, - #"identifier": uuid.uuid1(), - "insurancetype": 'excess-of-loss', "number_risks": number_risks, - "deductible_fraction": self.np_reinsurance_deductible_fraction, - "excess_fraction": self.np_reinsurance_excess_fraction, - "periodized_total_premium": 0, "runtime": 12, - "expiration": time + 12, "risk_factor": avg_risk_factor} # TODO: make runtime into a parameter + risk = { + "value": total_value, + "category": categ_id, + "owner": self, + # "identifier": uuid.uuid1(), + "insurancetype": "excess-of-loss", + "number_risks": number_risks, + "deductible_fraction": self.np_reinsurance_deductible_fraction, + "excess_fraction": self.np_reinsurance_excess_fraction, + "periodized_total_premium": 0, + "runtime": 12, + "expiration": time + 12, + "risk_factor": avg_risk_factor, + } # TODO: make runtime into a parameter _, _, var_this_risk, _ = self.riskmodel.evaluate([], self.cash, risk) per_period_premium = per_value_per_period_premium * risk["value"] - total_premium = sum([per_period_premium * ((1/(1+self.interest_rate))**i) for i in range(risk["runtime"])]) # TODO: or is it range(1, risk["runtime"]+1)? - #catbond = CatBond(self.simulation, per_period_premium) - catbond = CatBond(self.simulation, per_period_premium, self.interest_rate) # TODO: shift obtain_yield method to insurancesimulation, thereby making it unnecessary to drag parameters like self.interest_rate from instance to instance and from class to class + total_premium = sum( + [ + per_period_premium * ((1 / (1 + self.interest_rate)) ** i) + for i in range(risk["runtime"]) + ] + ) # TODO: or is it range(1, risk["runtime"]+1)? + # catbond = CatBond(self.simulation, per_period_premium) + catbond = CatBond( + self.simulation, per_period_premium, self.interest_rate + ) # TODO: shift obtain_yield method to insurancesimulation, thereby making it unnecessary to drag parameters like self.interest_rate from instance to instance and from class to class """add contract; contract is a quasi-reinsurance contract""" - contract = ReinsuranceContract(catbond, risk, time, 0, risk["runtime"], \ - self.default_contract_payment_period, \ - expire_immediately=self.simulation_parameters["expire_immediately"], \ - initial_VaR=var_this_risk, \ - insurancetype=risk["insurancetype"]) + contract = ReinsuranceContract( + catbond, + risk, + time, + 0, + risk["runtime"], + self.default_contract_payment_period, + expire_immediately=self.simulation_parameters["expire_immediately"], + initial_VaR=var_this_risk, + insurancetype=risk["insurancetype"], + ) # per_value_reinsurance_premium = 0 because the insurance firm does not continue to make payments to the cat bond. Only once. catbond.set_contract(contract) """sell cat bond (to self.simulation)""" - self.simulation.receive_obligation(var_this_risk, self, time, 'bond') + self.simulation.receive_obligation(var_this_risk, self, time, "bond") catbond.set_owner(self.simulation) """hand cash over to cat bond such that var_this_risk is covered""" - obligation = {"amount": var_this_risk + total_premium, "recipient": catbond, "due_time": time, "purpose": 'bond'} - self.pay(obligation) #TODO: is var_this_risk the correct amount? + obligation = { + "amount": var_this_risk + total_premium, + "recipient": catbond, + "due_time": time, + "purpose": "bond", + } + self.pay(obligation) # TODO: is var_this_risk the correct amount? """register catbond""" self.simulation.accept_agents("catbond", [catbond], time=time) - def make_reinsurance_claims(self,time): + def make_reinsurance_claims(self, time): """collect and effect reinsurance claims""" # TODO: reorganize this with risk category ledgers # TODO: Put facultative insurance claims here @@ -249,35 +357,53 @@ def make_reinsurance_claims(self,time): categ_id, claims, is_proportional = contract.get_and_reset_current_claim() if is_proportional: claims_this_turn[categ_id] += claims - if (contract.reincontract != None): + if contract.reincontract != None: contract.reincontract.explode(time, claims) for categ_id in range(self.simulation_no_risk_categories): - if claims_this_turn[categ_id] > 0 and self.category_reinsurance[categ_id] is not None: - self.category_reinsurance[categ_id].explode(time, claims_this_turn[categ_id]) + if ( + claims_this_turn[categ_id] > 0 + and self.category_reinsurance[categ_id] is not None + ): + self.category_reinsurance[categ_id].explode( + time, claims_this_turn[categ_id] + ) def get_excess_of_loss_reinsurance(self): reinsurance = [] for categ_id in range(self.simulation_no_risk_categories): if self.category_reinsurance[categ_id] is not None: - reinsurance_contract = {} - reinsurance_contract["reinsurer"] = self.category_reinsurance[categ_id].insurer - reinsurance_contract["value"] = self.category_reinsurance[categ_id].value - reinsurance_contract["category"] = categ_id - reinsurance.append(reinsurance_contract) + reinsurance_contract = {} + reinsurance_contract["reinsurer"] = self.category_reinsurance[ + categ_id + ].insurer + reinsurance_contract["value"] = self.category_reinsurance[ + categ_id + ].value + reinsurance_contract["category"] = categ_id + reinsurance.append(reinsurance_contract) return reinsurance def create_reinrisk(self, time, categ_id): """Proceed with creation of reinsurance risk only if category is not empty.""" - total_value, avg_risk_factor, number_risks, periodized_total_premium = self.characterize_underwritten_risks_by_category(time, categ_id) + total_value, avg_risk_factor, number_risks, periodized_total_premium = self.characterize_underwritten_risks_by_category( + time, categ_id + ) if number_risks > 0: - risk = {"value": total_value, "category": categ_id, "owner": self, - #"identifier": uuid.uuid1(), - "insurancetype": 'excess-of-loss', "number_risks": number_risks, - "deductible_fraction": self.np_reinsurance_deductible_fraction, - "excess_fraction": self.np_reinsurance_excess_fraction, - "periodized_total_premium": periodized_total_premium, "runtime": 12, - "expiration": time + 12, "risk_factor": avg_risk_factor} # TODO: make runtime into a parameter + risk = { + "value": total_value, + "category": categ_id, + "owner": self, + # "identifier": uuid.uuid1(), + "insurancetype": "excess-of-loss", + "number_risks": number_risks, + "deductible_fraction": self.np_reinsurance_deductible_fraction, + "excess_fraction": self.np_reinsurance_excess_fraction, + "periodized_total_premium": periodized_total_premium, + "runtime": 12, + "expiration": time + 12, + "risk_factor": avg_risk_factor, + } # TODO: make runtime into a parameter return risk else: return None diff --git a/insurancesimulation.py b/insurancesimulation.py index f7383d8..8389b0f 100644 --- a/insurancesimulation.py +++ b/insurancesimulation.py @@ -1,5 +1,6 @@ from insurancefirm import InsuranceFirm -#from riskmodel import RiskModel + +# from riskmodel import RiskModel from reinsurancefirm import ReinsuranceFirm from distributiontruncated import TruncatedDistWrapper import numpy as np @@ -11,70 +12,98 @@ import copy import logger -if isleconfig.show_network: - import visualization_network if isleconfig.use_abce: import abce - #print("abce imported") -#else: -# print("abce not imported") + # print("abce imported") -class InsuranceSimulation(): - def __init__(self, override_no_riskmodels, replic_ID, simulation_parameters, rc_event_schedule, rc_event_damage): +# else: +# print("abce not imported") + + +class InsuranceSimulation: + def __init__( + self, + override_no_riskmodels, + replic_ID, + simulation_parameters, + rc_event_schedule, + rc_event_damage, + ): # override one-riskmodel case (this is to ensure all other parameters are truly identical for comparison runs) if override_no_riskmodels: simulation_parameters["no_riskmodels"] = override_no_riskmodels self.number_riskmodels = simulation_parameters["no_riskmodels"] - + # save parameters if (replic_ID is None) or (isleconfig.force_foreground): - self.background_run = False + self.background_run = False else: self.background_run = True self.replic_ID = replic_ID self.simulation_parameters = simulation_parameters # unpack parameters, set up environment (distributions etc.) - + # damage distribution # TODO: control damage distribution via parameters, not directly - #self.damage_distribution = scipy.stats.uniform(loc=0, scale=1) + # self.damage_distribution = scipy.stats.uniform(loc=0, scale=1) non_truncated = scipy.stats.pareto(b=2, loc=0, scale=0.25) - self.damage_distribution = TruncatedDistWrapper(lower_bound=0.25, upper_bound=1., dist=non_truncated) - + self.damage_distribution = TruncatedDistWrapper( + lower_bound=0.25, upper_bound=1.0, dist=non_truncated + ) + # remaining parameters self.catbonds_off = simulation_parameters["catbonds_off"] self.reinsurance_off = simulation_parameters["reinsurance_off"] - self.cat_separation_distribution = scipy.stats.expon(0, simulation_parameters["event_time_mean_separation"]) + self.cat_separation_distribution = scipy.stats.expon( + 0, simulation_parameters["event_time_mean_separation"] + ) self.risk_factor_lower_bound = simulation_parameters["risk_factor_lower_bound"] - self.risk_factor_spread = simulation_parameters["risk_factor_upper_bound"] - simulation_parameters["risk_factor_lower_bound"] - self.risk_factor_distribution = scipy.stats.uniform(loc=self.risk_factor_lower_bound, scale=self.risk_factor_spread) + self.risk_factor_spread = ( + simulation_parameters["risk_factor_upper_bound"] + - simulation_parameters["risk_factor_lower_bound"] + ) + self.risk_factor_distribution = scipy.stats.uniform( + loc=self.risk_factor_lower_bound, scale=self.risk_factor_spread + ) if not simulation_parameters["risk_factors_present"]: self.risk_factor_distribution = scipy.stats.uniform(loc=1.0, scale=0) - #self.risk_value_distribution = scipy.stats.uniform(loc=100, scale=9900) + # self.risk_value_distribution = scipy.stats.uniform(loc=100, scale=9900) self.risk_value_distribution = scipy.stats.uniform(loc=1000, scale=0) risk_factor_mean = self.risk_factor_distribution.mean() - if np.isnan(risk_factor_mean): # unfortunately scipy.stats.mean is not well-defined if scale = 0 + if np.isnan( + risk_factor_mean + ): # unfortunately scipy.stats.mean is not well-defined if scale = 0 risk_factor_mean = self.risk_factor_distribution.rvs() # set initial market price (normalized, i.e. must be multiplied by value or excess-deductible) if self.simulation_parameters["expire_immediately"]: assert self.cat_separation_distribution.dist.name == "expon" - expected_damage_frequency = 1 - scipy.stats.poisson(1 / self.simulation_parameters["event_time_mean_separation"] * \ - self.simulation_parameters["mean_contract_runtime"]).pmf(0) + expected_damage_frequency = 1 - scipy.stats.poisson( + 1 + / self.simulation_parameters["event_time_mean_separation"] + * self.simulation_parameters["mean_contract_runtime"] + ).pmf(0) else: - expected_damage_frequency = self.simulation_parameters["mean_contract_runtime"] / \ - self.cat_separation_distribution.mean() - self.norm_premium = expected_damage_frequency * self.damage_distribution.mean() * \ - risk_factor_mean * \ - (1 + self.simulation_parameters["norm_profit_markup"]) + expected_damage_frequency = ( + self.simulation_parameters["mean_contract_runtime"] + / self.cat_separation_distribution.mean() + ) + self.norm_premium = ( + expected_damage_frequency + * self.damage_distribution.mean() + * risk_factor_mean + * (1 + self.simulation_parameters["norm_profit_markup"]) + ) self.market_premium = self.norm_premium - self.reinsurance_market_premium = self.market_premium # TODO: is this problematic as initial value? (later it is recomputed in every iteration) + self.reinsurance_market_premium = ( + self.market_premium + ) # TODO: is this problematic as initial value? (later it is recomputed in every iteration) self.total_no_risks = simulation_parameters["no_risks"] # set up monetary system (should instead be with the customers, if customers are modeled explicitly) @@ -85,142 +114,237 @@ def __init__(self, override_no_riskmodels, replic_ID, simulation_parameters, rc_ self.riskcategories = list(range(self.simulation_parameters["no_categories"])) self.rc_event_schedule = [] self.rc_event_damage = [] - self.rc_event_schedule_initial = [] #For debugging (cloud debugging) purposes is good to store the initial schedule of catastrophes - self.rc_event_damage_initial = [] #and damages that will be use in a single run of the model. - - if rc_event_schedule is not None and rc_event_damage is not None: #If we have schedules pass as arguments we used them. + self.rc_event_schedule_initial = ( + [] + ) # For debugging (cloud debugging) purposes is good to store the initial schedule of catastrophes + self.rc_event_damage_initial = ( + [] + ) # and damages that will be use in a single run of the model. + + if ( + rc_event_schedule is not None and rc_event_damage is not None + ): # If we have schedules pass as arguments we used them. self.rc_event_schedule = copy.copy(rc_event_schedule) self.rc_event_schedule_initial = copy.copy(rc_event_schedule) self.rc_event_damage = copy.copy(rc_event_damage) self.rc_event_damage_initial = copy.copy(rc_event_damage) - else: #Otherwise the schedules and damages are generated. + else: # Otherwise the schedules and damages are generated. self.setup_risk_categories_caller() - # set up risks risk_value_mean = self.risk_value_distribution.mean() - if np.isnan(risk_value_mean): # unfortunately scipy.stats.mean is not well-defined if scale = 0 + if np.isnan( + risk_value_mean + ): # unfortunately scipy.stats.mean is not well-defined if scale = 0 risk_value_mean = self.risk_value_distribution.rvs() - rrisk_factors = self.risk_factor_distribution.rvs(size=self.simulation_parameters["no_risks"]) - rvalues = self.risk_value_distribution.rvs(size=self.simulation_parameters["no_risks"]) - rcategories = np.random.randint(0, self.simulation_parameters["no_categories"], size=self.simulation_parameters["no_risks"]) - self.risks = [{"risk_factor": rrisk_factors[i], "value": rvalues[i], "category": rcategories[i], "owner": self} for i in range(self.simulation_parameters["no_risks"])] - - self.risks_counter = [0,0,0,0] + rrisk_factors = self.risk_factor_distribution.rvs( + size=self.simulation_parameters["no_risks"] + ) + rvalues = self.risk_value_distribution.rvs( + size=self.simulation_parameters["no_risks"] + ) + rcategories = np.random.randint( + 0, + self.simulation_parameters["no_categories"], + size=self.simulation_parameters["no_risks"], + ) + self.risks = [ + { + "risk_factor": rrisk_factors[i], + "value": rvalues[i], + "category": rcategories[i], + "owner": self, + } + for i in range(self.simulation_parameters["no_risks"]) + ] + + self.risks_counter = [0, 0, 0, 0] for item in self.risks: - self.risks_counter[item["category"]] = self.risks_counter[item["category"]] + 1 - + self.risks_counter[item["category"]] = ( + self.risks_counter[item["category"]] + 1 + ) # set up risk models - #inaccuracy = [[(1./self.simulation_parameters["riskmodel_inaccuracy_parameter"] if (i + j) % 2 == 0 \ + # inaccuracy = [[(1./self.simulation_parameters["riskmodel_inaccuracy_parameter"] if (i + j) % 2 == 0 \ # else self.simulation_parameters["riskmodel_inaccuracy_parameter"]) \ # for i in range(self.simulation_parameters["no_categories"])] \ # for j in range(self.simulation_parameters["no_riskmodels"])] - self.inaccuracy = self.get_all_riskmodel_combinations(self.simulation_parameters["no_categories"], self.simulation_parameters["riskmodel_inaccuracy_parameter"]) - - self.inaccuracy = random.sample(self.inaccuracy, self.simulation_parameters["no_riskmodels"]) - - risk_model_configurations = [{"damage_distribution": self.damage_distribution, - "expire_immediately": self.simulation_parameters["expire_immediately"], - "cat_separation_distribution": self.cat_separation_distribution, - "norm_premium": self.norm_premium, - "no_categories": self.simulation_parameters["no_categories"], - "risk_value_mean": risk_value_mean, - "risk_factor_mean": risk_factor_mean, - "norm_profit_markup": self.simulation_parameters["norm_profit_markup"], - "margin_of_safety": self.simulation_parameters["riskmodel_margin_of_safety"], - "var_tail_prob": self.simulation_parameters["value_at_risk_tail_probability"], - "inaccuracy_by_categ": self.inaccuracy[i]} \ - for i in range(self.simulation_parameters["no_riskmodels"])] - + self.inaccuracy = self.get_all_riskmodel_combinations( + self.simulation_parameters["no_categories"], + self.simulation_parameters["riskmodel_inaccuracy_parameter"], + ) + + self.inaccuracy = random.sample( + self.inaccuracy, self.simulation_parameters["no_riskmodels"] + ) + + risk_model_configurations = [ + { + "damage_distribution": self.damage_distribution, + "expire_immediately": self.simulation_parameters["expire_immediately"], + "cat_separation_distribution": self.cat_separation_distribution, + "norm_premium": self.norm_premium, + "no_categories": self.simulation_parameters["no_categories"], + "risk_value_mean": risk_value_mean, + "risk_factor_mean": risk_factor_mean, + "norm_profit_markup": self.simulation_parameters["norm_profit_markup"], + "margin_of_safety": self.simulation_parameters[ + "riskmodel_margin_of_safety" + ], + "var_tail_prob": self.simulation_parameters[ + "value_at_risk_tail_probability" + ], + "inaccuracy_by_categ": self.inaccuracy[i], + } + for i in range(self.simulation_parameters["no_riskmodels"]) + ] + # prepare setting up agents (to be done from start.py) - self.agent_parameters = {"insurancefirm": [], "reinsurance": []} # TODO: rename reinsurance -> reinsurancefirm (also in start.py and below in method accept_agents + self.agent_parameters = { + "insurancefirm": [], + "reinsurance": [], + } # TODO: rename reinsurance -> reinsurancefirm (also in start.py and below in method accept_agents self.insurer_id_counter = 0 # TODO: collapse the following two loops into one generic one? for i in range(simulation_parameters["no_insurancefirms"]): - if simulation_parameters['static_non-proportional_reinsurance_levels']: - insurance_reinsurance_level = simulation_parameters["default_non-proportional_reinsurance_deductible"] + if simulation_parameters["static_non-proportional_reinsurance_levels"]: + insurance_reinsurance_level = simulation_parameters[ + "default_non-proportional_reinsurance_deductible" + ] else: - insurance_reinsurance_level = np.random.uniform(simulation_parameters["insurance_reinsurance_levels_lower_bound"], simulation_parameters["insurance_reinsurance_levels_upper_bound"]) - - riskmodel_config = risk_model_configurations[i % len(risk_model_configurations)] - self.agent_parameters["insurancefirm"].append({'id': self.get_unique_insurer_id(), 'initial_cash': simulation_parameters["initial_agent_cash"], - 'riskmodel_config': riskmodel_config, 'norm_premium': self.norm_premium, - 'profit_target': simulation_parameters["norm_profit_markup"], - 'initial_acceptance_threshold': simulation_parameters["initial_acceptance_threshold"], - 'acceptance_threshold_friction': simulation_parameters["acceptance_threshold_friction"], - 'reinsurance_limit': simulation_parameters["reinsurance_limit"], - 'non-proportional_reinsurance_level': insurance_reinsurance_level, - 'capacity_target_decrement_threshold': simulation_parameters['capacity_target_decrement_threshold'], - 'capacity_target_increment_threshold': simulation_parameters['capacity_target_increment_threshold'], - 'capacity_target_decrement_factor': simulation_parameters['capacity_target_decrement_factor'], - 'capacity_target_increment_factor': simulation_parameters['capacity_target_increment_factor'], - 'interest_rate': simulation_parameters["interest_rate"]}) + insurance_reinsurance_level = np.random.uniform( + simulation_parameters["insurance_reinsurance_levels_lower_bound"], + simulation_parameters["insurance_reinsurance_levels_upper_bound"], + ) + + riskmodel_config = risk_model_configurations[ + i % len(risk_model_configurations) + ] + self.agent_parameters["insurancefirm"].append( + { + "id": self.get_unique_insurer_id(), + "initial_cash": simulation_parameters["initial_agent_cash"], + "riskmodel_config": riskmodel_config, + "norm_premium": self.norm_premium, + "profit_target": simulation_parameters["norm_profit_markup"], + "initial_acceptance_threshold": simulation_parameters[ + "initial_acceptance_threshold" + ], + "acceptance_threshold_friction": simulation_parameters[ + "acceptance_threshold_friction" + ], + "reinsurance_limit": simulation_parameters["reinsurance_limit"], + "non-proportional_reinsurance_level": insurance_reinsurance_level, + "capacity_target_decrement_threshold": simulation_parameters[ + "capacity_target_decrement_threshold" + ], + "capacity_target_increment_threshold": simulation_parameters[ + "capacity_target_increment_threshold" + ], + "capacity_target_decrement_factor": simulation_parameters[ + "capacity_target_decrement_factor" + ], + "capacity_target_increment_factor": simulation_parameters[ + "capacity_target_increment_factor" + ], + "interest_rate": simulation_parameters["interest_rate"], + } + ) self.reinsurer_id_counter = 0 for i in range(simulation_parameters["no_reinsurancefirms"]): - if simulation_parameters['static_non-proportional_reinsurance_levels']: - reinsurance_reinsurance_level = simulation_parameters["default_non-proportional_reinsurance_deductible"] + if simulation_parameters["static_non-proportional_reinsurance_levels"]: + reinsurance_reinsurance_level = simulation_parameters[ + "default_non-proportional_reinsurance_deductible" + ] else: - reinsurance_reinsurance_level = np.random.uniform(simulation_parameters["reinsurance_reinsurance_levels_lower_bound"], simulation_parameters["reinsurance_reinsurance_levels_upper_bound"]) - - riskmodel_config = risk_model_configurations[i % len(risk_model_configurations)] - self.agent_parameters["reinsurance"].append({'id': self.get_unique_reinsurer_id(), 'initial_cash': simulation_parameters["initial_reinagent_cash"], - 'riskmodel_config': riskmodel_config, 'norm_premium': self.norm_premium, - 'profit_target': simulation_parameters["norm_profit_markup"], - 'initial_acceptance_threshold': simulation_parameters["initial_acceptance_threshold"], - 'acceptance_threshold_friction': simulation_parameters["acceptance_threshold_friction"], - 'reinsurance_limit': simulation_parameters["reinsurance_limit"], - 'non-proportional_reinsurance_level': reinsurance_reinsurance_level, - 'capacity_target_decrement_threshold': simulation_parameters['capacity_target_decrement_threshold'], - 'capacity_target_increment_threshold': simulation_parameters['capacity_target_increment_threshold'], - 'capacity_target_decrement_factor': simulation_parameters['capacity_target_decrement_factor'], - 'capacity_target_increment_factor': simulation_parameters['capacity_target_increment_factor'], - 'interest_rate': simulation_parameters["interest_rate"]}) - + reinsurance_reinsurance_level = np.random.uniform( + simulation_parameters["reinsurance_reinsurance_levels_lower_bound"], + simulation_parameters["reinsurance_reinsurance_levels_upper_bound"], + ) + + riskmodel_config = risk_model_configurations[ + i % len(risk_model_configurations) + ] + self.agent_parameters["reinsurance"].append( + { + "id": self.get_unique_reinsurer_id(), + "initial_cash": simulation_parameters["initial_reinagent_cash"], + "riskmodel_config": riskmodel_config, + "norm_premium": self.norm_premium, + "profit_target": simulation_parameters["norm_profit_markup"], + "initial_acceptance_threshold": simulation_parameters[ + "initial_acceptance_threshold" + ], + "acceptance_threshold_friction": simulation_parameters[ + "acceptance_threshold_friction" + ], + "reinsurance_limit": simulation_parameters["reinsurance_limit"], + "non-proportional_reinsurance_level": reinsurance_reinsurance_level, + "capacity_target_decrement_threshold": simulation_parameters[ + "capacity_target_decrement_threshold" + ], + "capacity_target_increment_threshold": simulation_parameters[ + "capacity_target_increment_threshold" + ], + "capacity_target_decrement_factor": simulation_parameters[ + "capacity_target_decrement_factor" + ], + "capacity_target_increment_factor": simulation_parameters[ + "capacity_target_increment_factor" + ], + "interest_rate": simulation_parameters["interest_rate"], + } + ) + # set up remaining list variables - + # agent lists self.reinsurancefirms = [] self.insurancefirms = [] self.catbonds = [] - + # lists of agent weights self.insurers_weights = {} self.reinsurers_weights = {} - # list of reinsurance risks offered for underwriting self.reinrisks = [] self.not_accepted_reinrisks = [] - + # cumulative variables for history and logging self.cumulative_bankruptcies = 0 self.cumulative_market_exits = 0 self.cumulative_unrecovered_claims = 0.0 self.cumulative_claims = 0.0 - + # lists for logging history - self.logger = logger.Logger(no_riskmodels=simulation_parameters["no_riskmodels"], - rc_event_schedule_initial=self.rc_event_schedule_initial, - rc_event_damage_initial=self.rc_event_damage_initial) - - self.insurance_models_counter = np.zeros(self.simulation_parameters["no_categories"]) - self.reinsurance_models_counter = np.zeros(self.simulation_parameters["no_categories"]) - - - - def build_agents(self, agent_class, agent_class_string, parameters, agent_parameters): - #assert agent_parameters == self.agent_parameters[agent_class_string] #assert fits only the initial creation of agents, not later additions # TODO: fix + self.logger = logger.Logger( + no_riskmodels=simulation_parameters["no_riskmodels"], + rc_event_schedule_initial=self.rc_event_schedule_initial, + rc_event_damage_initial=self.rc_event_damage_initial, + ) + + self.insurance_models_counter = np.zeros( + self.simulation_parameters["no_categories"] + ) + self.reinsurance_models_counter = np.zeros( + self.simulation_parameters["no_categories"] + ) + + def build_agents( + self, agent_class, agent_class_string, parameters, agent_parameters + ): + # assert agent_parameters == self.agent_parameters[agent_class_string] #assert fits only the initial creation of agents, not later additions # TODO: fix agents = [] for ap in agent_parameters: agents.append(agent_class(parameters, ap)) return agents - + def accept_agents(self, agent_class_string, agents, agent_group=None, time=0): # TODO: fix agent id's for late entrants (both firms and catbonds) if agent_class_string == "insurancefirm": @@ -251,17 +375,21 @@ def accept_agents(self, agent_class_string, agents, agent_group=None, time=0): self.catbonds += agents except: print(sys.exc_info()) - pdb.set_trace() + pdb.set_trace() else: - assert False, "Error: Unexpected agent class used {0:s}".format(agent_class_string) + assert False, "Error: Unexpected agent class used {0:s}".format( + agent_class_string + ) def delete_agents(self, agent_class_string, agents): if agent_class_string == "catbond": for agent in agents: self.catbonds.remove(agent) else: - assert False, "Trying to remove unremovable agent, type: {0:s}".format(agent_class_string) - + assert False, "Trying to remove unremovable agent, type: {0:s}".format( + agent_class_string + ) + def iterate(self, t): if isleconfig.verbose: @@ -272,16 +400,19 @@ def iterate(self, t): self.reset_pls() - # adjust market premiums - sum_capital = sum([agent.get_cash() for agent in self.insurancefirms]) #TODO: include reinsurancefirms + sum_capital = sum( + [agent.get_cash() for agent in self.insurancefirms] + ) # TODO: include reinsurancefirms self.adjust_market_premium(capital=sum_capital) - sum_capital = sum([agent.get_cash() for agent in self.reinsurancefirms]) # TODO: include reinsurancefirms + sum_capital = sum( + [agent.get_cash() for agent in self.reinsurancefirms] + ) # TODO: include reinsurancefirms self.adjust_reinsurance_market_premium(capital=sum_capital) # pay obligations self.effect_payments(t) - + # identify perils and effect claims for categ_id in range(len(self.rc_event_schedule)): try: @@ -289,53 +420,62 @@ def iterate(self, t): assert self.rc_event_schedule[categ_id][0] >= t except: print("Something wrong; past events not deleted", file=sys.stderr) - if len(self.rc_event_schedule[categ_id]) > 0 and self.rc_event_schedule[categ_id][0] == t: + if ( + len(self.rc_event_schedule[categ_id]) > 0 + and self.rc_event_schedule[categ_id][0] == t + ): self.rc_event_schedule[categ_id] = self.rc_event_schedule[categ_id][1:] - damage_extent = copy.copy(self.rc_event_damage[categ_id][0]) #Schedules of catastrophes and damages must me generated at the same time. - self.inflict_peril(categ_id=categ_id, damage=damage_extent, t=t)# TODO: consider splitting the following lines from this method and running it with nb.jit + damage_extent = copy.copy( + self.rc_event_damage[categ_id][0] + ) # Schedules of catastrophes and damages must me generated at the same time. + self.inflict_peril( + categ_id=categ_id, damage=damage_extent, t=t + ) # TODO: consider splitting the following lines from this method and running it with nb.jit self.rc_event_damage[categ_id] = self.rc_event_damage[categ_id][1:] else: if isleconfig.verbose: print("Next peril ", self.rc_event_schedule[categ_id]) - + # shuffle risks (insurance and reinsurance risks) self.shuffle_risks() # reset reinweights self.reset_reinsurance_weights() - + # iterate reinsurnace firm agents for reinagent in self.reinsurancefirms: reinagent.iterate(t) # TODO: is the following necessary for abce to work (log) properly? - #if isleconfig.use_abce: + # if isleconfig.use_abce: # self.reinsurancefirms_group.iterate(time=t) - #else: + # else: # for reinagent in self.reinsurancefirms: # reinagent.iterate(t) - + # remove all non-accepted reinsurance risks self.reinrisks = [] # reset weights self.reset_insurance_weights() - + # iterate insurance firm agents for agent in self.insurancefirms: agent.iterate(t) # TODO: is the following necessary for abce to work (log) properly? - #if isleconfig.use_abce: + # if isleconfig.use_abce: # self.insurancefirms_group.iterate(time=t) - #else: + # else: # for agent in self.insurancefirms: # agent.iterate(t) - - # iterate catbonds + + # iterate catbonds for agent in self.catbonds: agent.iterate(t) - self.insurance_models_counter = np.zeros(self.simulation_parameters["no_categories"]) + self.insurance_models_counter = np.zeros( + self.simulation_parameters["no_categories"] + ) for insurer in self.insurancefirms: for i in range(len(self.inaccuracy)): @@ -343,84 +483,139 @@ def iterate(self, t): if insurer.riskmodel.inaccuracy == self.inaccuracy[i]: self.insurance_models_counter[i] += 1 - self.reinsurance_models_counter = np.zeros(self.simulation_parameters["no_categories"]) + self.reinsurance_models_counter = np.zeros( + self.simulation_parameters["no_categories"] + ) for reinsurer in self.reinsurancefirms: for i in range(len(self.inaccuracy)): if reinsurer.operational: if reinsurer.riskmodel.inaccuracy == self.inaccuracy[i]: self.reinsurance_models_counter[i] += 1 - - #print(isleconfig.show_network) + + # print(isleconfig.show_network) # TODO: use network representation in a more generic way, perhaps only once at the end to characterize the network and use for calibration(?) if isleconfig.show_network and t % 40 == 0 and t > 0: - RN = visualization_network.ReinsuranceNetwork(self.insurancefirms, self.reinsurancefirms, self.catbonds) + import visualization_network + + RN = visualization_network.ReinsuranceNetwork( + self.insurancefirms, self.reinsurancefirms, self.catbonds + ) RN.compute_measures() RN.visualize() - - + def save_data(self): """Method to collect statistics about the current state of the simulation. Will pass these to the Logger object (self.logger) to be recorded. No arguments. Returns None.""" - + """ collect data """ - total_cash_no = sum([insurancefirm.cash for insurancefirm in self.insurancefirms]) - total_excess_capital = sum([insurancefirm.get_excess_capital() for insurancefirm in self.insurancefirms]) - total_profitslosses = sum([insurancefirm.get_profitslosses() for insurancefirm in self.insurancefirms]) - total_contracts_no = sum([len(insurancefirm.underwritten_contracts) for insurancefirm in self.insurancefirms]) - total_reincash_no = sum([reinsurancefirm.cash for reinsurancefirm in self.reinsurancefirms]) - total_reinexcess_capital = sum([reinsurancefirm.get_excess_capital() for reinsurancefirm in self.reinsurancefirms]) - total_reinprofitslosses = sum([reinsurancefirm.get_profitslosses() for reinsurancefirm in self.reinsurancefirms]) - total_reincontracts_no = sum([len(reinsurancefirm.underwritten_contracts) for reinsurancefirm in self.reinsurancefirms]) - operational_no = sum([insurancefirm.operational for insurancefirm in self.insurancefirms]) - reinoperational_no = sum([reinsurancefirm.operational for reinsurancefirm in self.reinsurancefirms]) + total_cash_no = sum( + [insurancefirm.cash for insurancefirm in self.insurancefirms] + ) + total_excess_capital = sum( + [ + insurancefirm.get_excess_capital() + for insurancefirm in self.insurancefirms + ] + ) + total_profitslosses = sum( + [insurancefirm.get_profitslosses() for insurancefirm in self.insurancefirms] + ) + total_contracts_no = sum( + [ + len(insurancefirm.underwritten_contracts) + for insurancefirm in self.insurancefirms + ] + ) + total_reincash_no = sum( + [reinsurancefirm.cash for reinsurancefirm in self.reinsurancefirms] + ) + total_reinexcess_capital = sum( + [ + reinsurancefirm.get_excess_capital() + for reinsurancefirm in self.reinsurancefirms + ] + ) + total_reinprofitslosses = sum( + [ + reinsurancefirm.get_profitslosses() + for reinsurancefirm in self.reinsurancefirms + ] + ) + total_reincontracts_no = sum( + [ + len(reinsurancefirm.underwritten_contracts) + for reinsurancefirm in self.reinsurancefirms + ] + ) + operational_no = sum( + [insurancefirm.operational for insurancefirm in self.insurancefirms] + ) + reinoperational_no = sum( + [reinsurancefirm.operational for reinsurancefirm in self.reinsurancefirms] + ) catbondsoperational_no = sum([catbond.operational for catbond in self.catbonds]) - + """ collect agent-level data """ - insurance_firms = [(insurancefirm.cash,insurancefirm.id,insurancefirm.operational) for insurancefirm in self.insurancefirms] - reinsurance_firms = [(reinsurancefirm.cash,reinsurancefirm.id,reinsurancefirm.operational) for reinsurancefirm in self.reinsurancefirms] - + insurance_firms = [ + (insurancefirm.cash, insurancefirm.id, insurancefirm.operational) + for insurancefirm in self.insurancefirms + ] + reinsurance_firms = [ + (reinsurancefirm.cash, reinsurancefirm.id, reinsurancefirm.operational) + for reinsurancefirm in self.reinsurancefirms + ] + """ prepare dict """ current_log = {} - current_log['total_cash'] = total_cash_no - current_log['total_excess_capital'] = total_excess_capital - current_log['total_profitslosses'] = total_profitslosses - current_log['total_contracts'] = total_contracts_no - current_log['total_operational'] = operational_no - current_log['total_reincash'] = total_reincash_no - current_log['total_reinexcess_capital'] = total_reinexcess_capital - current_log['total_reinprofitslosses'] = total_reinprofitslosses - current_log['total_reincontracts'] = total_reincontracts_no - current_log['total_reinoperational'] = reinoperational_no - current_log['total_catbondsoperational'] = catbondsoperational_no - current_log['market_premium'] = self.market_premium - current_log['market_reinpremium'] = self.reinsurance_market_premium - current_log['cumulative_bankruptcies'] = self.cumulative_bankruptcies - current_log['cumulative_market_exits'] = self.cumulative_market_exits - current_log['cumulative_unrecovered_claims'] = self.cumulative_unrecovered_claims - current_log['cumulative_claims'] = self.cumulative_claims #Log the cumulative claims received so far. - - """ add agent-level data to dict""" - current_log['insurance_firms_cash'] = insurance_firms - current_log['reinsurance_firms_cash'] = reinsurance_firms - current_log['market_diffvar'] = self.compute_market_diffvar() - - current_log['individual_contracts'] = [] - individual_contracts_no = [len(insurancefirm.underwritten_contracts) for insurancefirm in self.insurancefirms] + current_log["total_cash"] = total_cash_no + current_log["total_excess_capital"] = total_excess_capital + current_log["total_profitslosses"] = total_profitslosses + current_log["total_contracts"] = total_contracts_no + current_log["total_operational"] = operational_no + current_log["total_reincash"] = total_reincash_no + current_log["total_reinexcess_capital"] = total_reinexcess_capital + current_log["total_reinprofitslosses"] = total_reinprofitslosses + current_log["total_reincontracts"] = total_reincontracts_no + current_log["total_reinoperational"] = reinoperational_no + current_log["total_catbondsoperational"] = catbondsoperational_no + current_log["market_premium"] = self.market_premium + current_log["market_reinpremium"] = self.reinsurance_market_premium + current_log["cumulative_bankruptcies"] = self.cumulative_bankruptcies + current_log["cumulative_market_exits"] = self.cumulative_market_exits + current_log[ + "cumulative_unrecovered_claims" + ] = self.cumulative_unrecovered_claims + current_log[ + "cumulative_claims" + ] = self.cumulative_claims # Log the cumulative claims received so far. + + """ add agent-level data to dict""" + current_log["insurance_firms_cash"] = insurance_firms + current_log["reinsurance_firms_cash"] = reinsurance_firms + current_log["market_diffvar"] = self.compute_market_diffvar() + + current_log["individual_contracts"] = [] + individual_contracts_no = [ + len(insurancefirm.underwritten_contracts) + for insurancefirm in self.insurancefirms + ] for i in range(len(individual_contracts_no)): - current_log['individual_contracts'].append(individual_contracts_no[i]) + current_log["individual_contracts"].append(individual_contracts_no[i]) """ call to Logger object """ self.logger.record_data(current_log) - - def obtain_log(self, requested_logs=None): #This function allows to return in a list all the data generated by the model. There is no other way to transfer it back from the cloud. + + def obtain_log( + self, requested_logs=None + ): # This function allows to return in a list all the data generated by the model. There is no other way to transfer it back from the cloud. return self.logger.obtain_log(requested_logs) - + def advance_round(self, *args): pass - + def finalize(self, *args): """Function to handle oberations after the end of the simulation run. Currently empty. @@ -431,21 +626,38 @@ def finalize(self, *args): pass def inflict_peril(self, categ_id, damage, t): - affected_contracts = [contract for insurer in self.insurancefirms for contract in insurer.underwritten_contracts if contract.category == categ_id] + affected_contracts = [ + contract + for insurer in self.insurancefirms + for contract in insurer.underwritten_contracts + if contract.category == categ_id + ] if isleconfig.verbose: print("**** PERIL ", damage) - damagevalues = np.random.beta(1, 1./damage -1, size=self.risks_counter[categ_id]) + damagevalues = np.random.beta( + 1, 1.0 / damage - 1, size=self.risks_counter[categ_id] + ) uniformvalues = np.random.uniform(0, 1, size=self.risks_counter[categ_id]) - [contract.explode(t, uniformvalues[i], damagevalues[i]) for i, contract in enumerate(affected_contracts)] - + [ + contract.explode(t, uniformvalues[i], damagevalues[i]) + for i, contract in enumerate(affected_contracts) + ] + def receive_obligation(self, amount, recipient, due_time, purpose): - obligation = {"amount": amount, "recipient": recipient, "due_time": due_time, "purpose": purpose} + obligation = { + "amount": amount, + "recipient": recipient, + "due_time": due_time, + "purpose": purpose, + } self.obligations.append(obligation) def effect_payments(self, time): - due = [item for item in self.obligations if item["due_time"]<=time] - #print("SIMULATION obligations: ", len(self.obligations), " of which are due: ", len(due)) - self.obligations = [item for item in self.obligations if item["due_time"]>time] + due = [item for item in self.obligations if item["due_time"] <= time] + # print("SIMULATION obligations: ", len(self.obligations), " of which are due: ", len(due)) + self.obligations = [ + item for item in self.obligations if item["due_time"] > time + ] sum_due = sum([item["amount"] for item in due]) for obligation in due: self.pay(obligation) @@ -475,7 +687,11 @@ def reset_reinsurance_weights(self): self.not_accepted_reinrisks = [] - operational_reinfirms = [reinsurancefirm for reinsurancefirm in self.reinsurancefirms if reinsurancefirm.operational] + operational_reinfirms = [ + reinsurancefirm + for reinsurancefirm in self.reinsurancefirms + if reinsurancefirm.operational + ] operational_no = len(operational_reinfirms) @@ -488,8 +704,8 @@ def reset_reinsurance_weights(self): if operational_no > 0: - if reinrisks_no/operational_no > 1: - weights = reinrisks_no/operational_no + if reinrisks_no / operational_no > 1: + weights = reinrisks_no / operational_no for reinsurer in self.reinsurancefirms: self.reinsurers_weights[reinsurer.id] = math.floor(weights) else: @@ -501,9 +717,15 @@ def reset_reinsurance_weights(self): def reset_insurance_weights(self): - operational_no = sum([insurancefirm.operational for insurancefirm in self.insurancefirms]) + operational_no = sum( + [insurancefirm.operational for insurancefirm in self.insurancefirms] + ) - operational_firms = [insurancefirm for insurancefirm in self.insurancefirms if insurancefirm.operational] + operational_firms = [ + insurancefirm + for insurancefirm in self.insurancefirms + if insurancefirm.operational + ] risks_no = len(self.risks) @@ -514,8 +736,8 @@ def reset_insurance_weights(self): if operational_no > 0: - if risks_no/operational_no > 1: - weights = risks_no/operational_no + if risks_no / operational_no > 1: + weights = risks_no / operational_no for insurer in self.insurancefirms: self.insurers_weights[insurer.id] = math.floor(weights) else: @@ -536,10 +758,24 @@ def adjust_market_premium(self, capital): with the capital available in the insurance market and viceversa. The premium reduces until it reaches a minimum below which no insurer is willing to reduce further the price. This method is only called in the self.iterate() method of this class.""" - self.market_premium = self.norm_premium * (self.simulation_parameters["upper_price_limit"] - self.simulation_parameters["premium_sensitivity"] * capital / (self.simulation_parameters["initial_agent_cash"] * self.damage_distribution.mean() * self.simulation_parameters["no_risks"])) - if self.market_premium < self.norm_premium * self.simulation_parameters["lower_price_limit"]: - self.market_premium = self.norm_premium * self.simulation_parameters["lower_price_limit"] - + self.market_premium = self.norm_premium * ( + self.simulation_parameters["upper_price_limit"] + - self.simulation_parameters["premium_sensitivity"] + * capital + / ( + self.simulation_parameters["initial_agent_cash"] + * self.damage_distribution.mean() + * self.simulation_parameters["no_risks"] + ) + ) + if ( + self.market_premium + < self.norm_premium * self.simulation_parameters["lower_price_limit"] + ): + self.market_premium = ( + self.norm_premium * self.simulation_parameters["lower_price_limit"] + ) + def adjust_reinsurance_market_premium(self, capital): """Adjust_market_premium Method. Accepts arguments @@ -549,9 +785,23 @@ def adjust_reinsurance_market_premium(self, capital): with the capital available in the reinsurance market and viceversa. The premium reduces until it reaches a minimum below which no reinsurer is willing to reduce further the price. This method is only called in the self.iterate() method of this class.""" - self.reinsurance_market_premium = self.norm_premium * (self.simulation_parameters["upper_price_limit"] - self.simulation_parameters["reinpremium_sensitivity"] * capital / (self.simulation_parameters["initial_agent_cash"] * self.damage_distribution.mean() * self.simulation_parameters["no_risks"])) - if self.reinsurance_market_premium < self.norm_premium * self.simulation_parameters["lower_price_limit"]: - self.reinsurance_market_premium = self.norm_premium * self.simulation_parameters["lower_price_limit"] + self.reinsurance_market_premium = self.norm_premium * ( + self.simulation_parameters["upper_price_limit"] + - self.simulation_parameters["reinpremium_sensitivity"] + * capital + / ( + self.simulation_parameters["initial_agent_cash"] + * self.damage_distribution.mean() + * self.simulation_parameters["no_risks"] + ) + ) + if ( + self.reinsurance_market_premium + < self.norm_premium * self.simulation_parameters["lower_price_limit"] + ): + self.reinsurance_market_premium = ( + self.norm_premium * self.simulation_parameters["lower_price_limit"] + ) def get_market_premium(self): """Get_market_premium Method. @@ -574,25 +824,29 @@ def get_reinsurance_premium(self, np_reinsurance_deductible_fraction): # TODO: make premiums dependend on the deductible per value (np_reinsurance_deductible_fraction) -> DONE. # TODO: make max_reduction into simulation_parameter ? if self.reinsurance_off: - return float('inf') + return float("inf") max_reduction = 0.1 - return self.reinsurance_market_premium * (1. - max_reduction * np_reinsurance_deductible_fraction) - + return self.reinsurance_market_premium * ( + 1.0 - max_reduction * np_reinsurance_deductible_fraction + ) + def get_cat_bond_price(self, np_reinsurance_deductible_fraction): # TODO: implement function dependent on total capital in cat bonds and on deductible () # TODO: make max_reduction and max_CB_surcharge into simulation_parameters ? if self.catbonds_off: - return float('inf') + return float("inf") max_reduction = 0.9 - max_CB_surcharge = 0.5 - return self.reinsurance_market_premium * (1. + max_CB_surcharge - max_reduction * np_reinsurance_deductible_fraction) - + max_CB_surcharge = 0.5 + return self.reinsurance_market_premium * ( + 1.0 + max_CB_surcharge - max_reduction * np_reinsurance_deductible_fraction + ) + def append_reinrisks(self, item): - if(len(item) > 0): + if len(item) > 0: self.reinrisks.append(item) - def remove_reinrisks(self,risko): - if(risko != None): + def remove_reinrisks(self, risko): + if risko != None: self.reinrisks.remove(risko) def get_reinrisks(self): @@ -601,8 +855,8 @@ def get_reinrisks(self): def solicit_insurance_requests(self, id, cash, insurer): - risks_to_be_sent = self.risks[:int(self.insurers_weights[insurer.id])] - self.risks = self.risks[int(self.insurers_weights[insurer.id]):] + risks_to_be_sent = self.risks[: int(self.insurers_weights[insurer.id])] + self.risks = self.risks[int(self.insurers_weights[insurer.id]) :] for risk in insurer.risks_kept: risks_to_be_sent.append(risk) @@ -613,8 +867,10 @@ def solicit_insurance_requests(self, id, cash, insurer): return risks_to_be_sent def solicit_reinsurance_requests(self, id, cash, reinsurer): - reinrisks_to_be_sent = self.reinrisks[:int(self.reinsurers_weights[reinsurer.id])] - self.reinrisks = self.reinrisks[int(self.reinsurers_weights[reinsurer.id]):] + reinrisks_to_be_sent = self.reinrisks[ + : int(self.reinsurers_weights[reinsurer.id]) + ] + self.reinrisks = self.reinrisks[int(self.reinsurers_weights[reinsurer.id]) :] for reinrisk in reinsurer.reinrisks_kept: reinrisks_to_be_sent.append(reinrisk) @@ -634,8 +890,10 @@ def return_reinrisks(self, not_accepted_risks): def get_all_riskmodel_combinations(self, n, rm_factor): riskmodels = [] for i in range(self.simulation_parameters["no_categories"]): - riskmodel_combination = rm_factor * np.ones(self.simulation_parameters["no_categories"]) - riskmodel_combination[i] = 1/rm_factor + riskmodel_combination = rm_factor * np.ones( + self.simulation_parameters["no_categories"] + ) + riskmodel_combination[i] = 1 / rm_factor riskmodels.append(riskmodel_combination.tolist()) return riskmodels @@ -644,20 +902,26 @@ def setup_risk_categories(self): event_schedule = [] event_damage = [] total = 0 - while (total < self.simulation_parameters["max_time"]): + while total < self.simulation_parameters["max_time"]: separation_time = self.cat_separation_distribution.rvs() total += int(math.ceil(separation_time)) if total < self.simulation_parameters["max_time"]: event_schedule.append(total) - event_damage.append(self.damage_distribution.rvs()) #Schedules of catastrophes and damages must me generated at the same time. Reason: replication across different risk models. + event_damage.append( + self.damage_distribution.rvs() + ) # Schedules of catastrophes and damages must me generated at the same time. Reason: replication across different risk models. self.rc_event_schedule.append(event_schedule) self.rc_event_damage.append(event_damage) - self.rc_event_schedule_initial = copy.copy(self.rc_event_damage) #For debugging (cloud debugging) purposes is good to store the initial schedule of catastrophes - self.rc_event_damage_initial = copy.copy(self.rc_event_damage) #and damages that will be use in a single run of the model. + self.rc_event_schedule_initial = copy.copy( + self.rc_event_damage + ) # For debugging (cloud debugging) purposes is good to store the initial schedule of catastrophes + self.rc_event_damage_initial = copy.copy( + self.rc_event_damage + ) # and damages that will be use in a single run of the model. def setup_risk_categories_caller(self): - #if self.background_run: + # if self.background_run: if self.replic_ID is not None: if isleconfig.replicating: self.restore_state_and_risk_categories() @@ -670,44 +934,66 @@ def setup_risk_categories_caller(self): def save_state_and_risk_categories(self): # save numpy Mersenne Twister state mersennetwoster_randomseed = str(np.random.get_state()) - mersennetwoster_randomseed = mersennetwoster_randomseed.replace("\n","").replace("array", "np.array").replace("uint32", "np.uint32") - wfile = open("data/replication_randomseed.dat","a") - wfile.write(mersennetwoster_randomseed+"\n") + mersennetwoster_randomseed = ( + mersennetwoster_randomseed.replace("\n", "") + .replace("array", "np.array") + .replace("uint32", "np.uint32") + ) + wfile = open("data/replication_randomseed.dat", "a") + wfile.write(mersennetwoster_randomseed + "\n") wfile.close() # save event schedule - wfile = open("data/replication_rc_event_schedule.dat","a") - wfile.write(str(self.rc_event_schedule)+"\n") + wfile = open("data/replication_rc_event_schedule.dat", "a") + wfile.write(str(self.rc_event_schedule) + "\n") wfile.close() - + def restore_state_and_risk_categories(self): - rfile = open("data/replication_rc_event_schedule.dat","r") + rfile = open("data/replication_rc_event_schedule.dat", "r") found = False for i, line in enumerate(rfile): - #print(i, self.replic_ID) + # print(i, self.replic_ID) if i == self.replic_ID: self.rc_event_schedule = eval(line) found = True rfile.close() - assert found, "rc event schedule for current replication ID number {0:d} not found in data file. Exiting.".format(self.replic_ID) - rfile = open("data/replication_randomseed.dat","r") + assert ( + found + ), "rc event schedule for current replication ID number {0:d} not found in data file. Exiting.".format( + self.replic_ID + ) + rfile = open("data/replication_randomseed.dat", "r") found = False for i, line in enumerate(rfile): - #print(i, self.replic_ID) + # print(i, self.replic_ID) if i == self.replic_ID: mersennetwister_randomseed = eval(line) found = True rfile.close() np.random.set_state(mersennetwister_randomseed) - assert found, "mersennetwister randomseed for current replication ID number {0:d} not found in data file. Exiting.".format(self.replic_ID) - - def insurance_firm_market_entry(self, prob=-1, agent_type="InsuranceFirm"): # TODO: replace method name with a more descriptive one + assert ( + found + ), "mersennetwister randomseed for current replication ID number {0:d} not found in data file. Exiting.".format( + self.replic_ID + ) + + def insurance_firm_market_entry( + self, prob=-1, agent_type="InsuranceFirm" + ): # TODO: replace method name with a more descriptive one if prob == -1: if agent_type == "InsuranceFirm": - prob = self.simulation_parameters["insurance_firm_market_entry_probability"] + prob = self.simulation_parameters[ + "insurance_firm_market_entry_probability" + ] elif agent_type == "ReinsuranceFirm": - prob = self.simulation_parameters["reinsurance_firm_market_entry_probability"] + prob = self.simulation_parameters[ + "reinsurance_firm_market_entry_probability" + ] else: - assert False, "Unknown agent type. Simulation requested to create agent of type {0:s}".format(agent_type) + assert ( + False + ), "Unknown agent type. Simulation requested to create agent of type {0:s}".format( + agent_type + ) if np.random.random() < prob: return True else: @@ -732,12 +1018,14 @@ def record_market_exit(self): def record_unrecovered_claims(self, loss): self.cumulative_unrecovered_claims += loss - def record_claims(self, claims): #This method records every claim made to insurers and reinsurers. It is called from both insurers and reinsurers (metainsuranceorg.py). + def record_claims( + self, claims + ): # This method records every claim made to insurers and reinsurers. It is called from both insurers and reinsurers (metainsuranceorg.py). self.cumulative_claims += claims - + def log(self): self.logger.save_log(self.background_run) - + def compute_market_diffvar(self): varsfirms = [] @@ -765,20 +1053,27 @@ def compute_market_diffvar(self): totalreal = totalreal + sum(varsreinfirms) totaldiff = totalina - totalreal - + return totaldiff - #self.history_logs['market_diffvar'].append(totaldiff) + # self.history_logs['market_diffvar'].append(totaldiff) def count_underwritten_and_reinsured_risks_by_category(self): underwritten_risks = 0 reinsured_risks = 0 - underwritten_per_category = np.zeros(self.simulation_parameters["no_categories"]) + underwritten_per_category = np.zeros( + self.simulation_parameters["no_categories"] + ) reinsured_per_category = np.zeros(self.simulation_parameters["no_categories"]) for firm in self.insurancefirms: if firm.operational: underwritten_by_category += firm.counter_category - if self.simulation_parameters["simulation_reinsurance_type"] == "non-proportional": - reinsured_per_category += firm.counter_category * firm.category_reinsurance + if ( + self.simulation_parameters["simulation_reinsurance_type"] + == "non-proportional" + ): + reinsured_per_category += ( + firm.counter_category * firm.category_reinsurance + ) if self.simulation_parameters["simulation_reinsurance_type"] == "proportional": for firm in self.insurancefirms: if firm.operational: @@ -795,28 +1090,44 @@ def get_unique_reinsurer_id(self): return current_id def insurance_entry_index(self): - return self.insurance_models_counter[0:self.simulation_parameters["no_riskmodels"]].argmin() + return self.insurance_models_counter[ + 0 : self.simulation_parameters["no_riskmodels"] + ].argmin() def reinsurance_entry_index(self): - return self.reinsurance_models_counter[0:self.simulation_parameters["no_riskmodels"]].argmin() + return self.reinsurance_models_counter[ + 0 : self.simulation_parameters["no_riskmodels"] + ].argmin() def get_operational(self): return True - def reinsurance_capital_entry(self): #This method determines the capital market entry of reinsurers. It is only run in start.py. + def reinsurance_capital_entry( + self + ): # This method determines the capital market entry of reinsurers. It is only run in start.py. capital_per_non_re_cat = [] for reinrisk in self.not_accepted_reinrisks: - capital_per_non_re_cat.append(reinrisk["value"]) #It takes all the values of the reinsurance risks NOT REINSURED. - - if len(capital_per_non_re_cat) > 0: #We only perform this action if there are reinsurance contracts that has not been reinsured in the last period of time. - capital_per_non_re_cat = np.random.choice(capital_per_non_re_cat, 10) #Only 10 values sampled randomly are considered. (Too low?) - entry = max(capital_per_non_re_cat) #For market entry the maximum of the sample is considered. - entry = 2 * entry #The capital market entry of those values will be the double of the maximum. - else: #Otherwise the default reinsurance cash market entry is considered. + capital_per_non_re_cat.append( + reinrisk["value"] + ) # It takes all the values of the reinsurance risks NOT REINSURED. + + if ( + len(capital_per_non_re_cat) > 0 + ): # We only perform this action if there are reinsurance contracts that has not been reinsured in the last period of time. + capital_per_non_re_cat = np.random.choice( + capital_per_non_re_cat, 10 + ) # Only 10 values sampled randomly are considered. (Too low?) + entry = max( + capital_per_non_re_cat + ) # For market entry the maximum of the sample is considered. + entry = ( + 2 * entry + ) # The capital market entry of those values will be the double of the maximum. + else: # Otherwise the default reinsurance cash market entry is considered. entry = self.simulation_parameters["initial_reinagent_cash"] - return entry #The capital market entry is returned. + return entry # The capital market entry is returned. def reset_pls(self): """Reset_pls Method. @@ -831,4 +1142,3 @@ def reset_pls(self): for catbond in self.catbonds: catbond.reset_pl() - diff --git a/isleconfig.py b/isleconfig.py index 2885c21..6e5a1df 100644 --- a/isleconfig.py +++ b/isleconfig.py @@ -4,76 +4,95 @@ force_foreground = False verbose = False showprogress = False -show_network = False # Should network be visualized? This should be False by default, to be overridden by commandline arguments -slim_log = True # Should logs be small in ensemble runs (only aggregated level data)? - -simulation_parameters={"no_categories": 4, - "no_insurancefirms": 20, - "no_reinsurancefirms": 4, - "no_riskmodels": 2, - "riskmodel_inaccuracy_parameter": 2, # values >=1; inaccuracy higher with higher values - "riskmodel_margin_of_safety": 2, # values >=1; factor of additional liquidity beyond value at risk - "margin_increase": 0, # This parameter modifies the margin of safety depending on the number of risks models available in the market. When is 0 all risk models have the same margin of safety. - "value_at_risk_tail_probability": 0.005, # values <1, >0, usually close to 0; tail probability at which the value at risk is taken by the risk models - "norm_profit_markup": 0.15, - "rein_norm_profit_markup": 0.15, - "dividend_share_of_profits": 0.4, - "mean_contract_runtime": 12, - "contract_runtime_halfspread": 2, - "default_contract_payment_period": 3, - "max_time": 1000, - "money_supply": 2000000000, - "event_time_mean_separation": 100 / 3., - "expire_immediately": False, - "risk_factors_present": False, - "risk_factor_lower_bound": 0.4, - "risk_factor_upper_bound": 0.6, - "initial_acceptance_threshold": 0.5, - "acceptance_threshold_friction": 0.9, - "insurance_firm_market_entry_probability": 0.3, #0.02, - "reinsurance_firm_market_entry_probability": 0.05, #0.004, - "simulation_reinsurance_type": 'non-proportional', - "default_non-proportional_reinsurance_deductible": 0.3, - "default_non-proportional_reinsurance_excess": 1.0, - "default_non-proportional_reinsurance_premium_share": 0.3, - "static_non-proportional_reinsurance_levels": False, - "catbonds_off": True, - "reinsurance_off": False, - "capacity_target_decrement_threshold": 1.8, - "capacity_target_increment_threshold": 1.2, - "capacity_target_decrement_factor": 24/25., - "capacity_target_increment_factor": 25/24., - # Retention parameters - "insurance_retention": 0.85, # Ratio of insurance contracts retained every iteration. - "reinsurance_retention": 1, # Ratio of reinsurance contracts retained every iteration. - #Premium sensitivity parameters - "premium_sensitivity": 5, # This parameter represents how sensitive is the variation of the insurance premium with respect of the capital of the market. Higher means more sensitive. - "reinpremium_sensitivity": 6, # This parameter represents how sensitive is the variation of the reinsurance premium with respect of the capital of the market. Higher means more sensitive. - #Balanced portfolio parameters - "insurers_balance_ratio": 0.1, # This ratio represents how low we want to keep the standard deviation of the cash reserved below the mean for insurers. Lower means more balanced. - "reinsurers_balance_ratio": 20, # This ratio represents how low we want to keep the standard deviation of the cash reserved below the mean for reinsurers. Lower means more balanced. (Deactivated for the moment) - "insurers_recursion_limit": 50, # Intensity of the recursion algorithm to balance the portfolio of risks for insurers. - "reinsurers_recursion_limit": 10, # Intensity of the recursion algorithm to balance the portfolio of risks for reinsurers. - #Market permanency parameters - "market_permanency_off": False, # This parameter activates (deactivates) the following market permanency constraints. - "cash_permanency_limit": 100, # This parameter enforces the limit under which the firms leave the market because they cannot underwrite anything. - "insurance_permanency_contracts_limit": 4, # If insurers stay for too long under this limit of contracts they deccide to leave the market. - "insurance_permanency_ratio_limit": 0.6, # If insurers stay for too long under this limit they deccide to leave the market because they have too much capital. - "insurance_permanency_time_constraint": 24, # This parameter defines the period that the insurers wait if they have few capital or few contract before leaving the market. - "reinsurance_permanency_contracts_limit": 2, # If reinsurers stay for too long under this limit of contracts they deccide to leave the market. - "reinsurance_permanency_ratio_limit": 0.8, # If reinsurers stay for too long under this limit they deccide to leave the market because they have too much capital. - "reinsurance_permanency_time_constraint": 48, # This parameter defines the period that the reinsurers wait if they have few capital or few contract before leaving the market. - #Insurance and Reinsurance deductibles - "insurance_reinsurance_levels_lower_bound": 0.25, - "insurance_reinsurance_levels_upper_bound": 0.30, - "reinsurance_reinsurance_levels_lower_bound": 0.5, - "reinsurance_reinsurance_levels_upper_bound": 0.95, - "initial_agent_cash": 80000, - "initial_reinagent_cash": 2000000, - "interest_rate": 0.001, - "reinsurance_limit": 0.1, - "upper_price_limit": 1.2, - "lower_price_limit": 0.85, - "no_risks": 20000} - +show_network = ( + False +) # Should network be visualized? This should be False by default, to be overridden by commandline arguments +slim_log = True # Should logs be small in ensemble runs (only aggregated level data)? +simulation_parameters = { + "no_categories": 4, + "no_insurancefirms": 20, + "no_reinsurancefirms": 4, + "no_riskmodels": 2, + "riskmodel_inaccuracy_parameter": 2, # values >=1; inaccuracy higher with higher values + "riskmodel_margin_of_safety": 2, + # values >=1; factor of additional liquidity beyond value at risk + "margin_increase": 0, + # This parameter modifies the margin of safety depending on the number of risks models available in the market. When is 0 all risk models have the same margin of safety. + "value_at_risk_tail_probability": 0.005, + # values <1, >0, usually close to 0; tail probability at which the value at risk is taken by the risk models + "norm_profit_markup": 0.15, + "rein_norm_profit_markup": 0.15, + "dividend_share_of_profits": 0.4, + "mean_contract_runtime": 12, + "contract_runtime_halfspread": 2, + "default_contract_payment_period": 3, + "max_time": 100, + "money_supply": 2000000000, + "event_time_mean_separation": 100 / 3.0, + "expire_immediately": False, + "risk_factors_present": False, + "risk_factor_lower_bound": 0.4, + "risk_factor_upper_bound": 0.6, + "initial_acceptance_threshold": 0.5, + "acceptance_threshold_friction": 0.9, + "insurance_firm_market_entry_probability": 0.3, # 0.02, + "reinsurance_firm_market_entry_probability": 0.05, # 0.004, + "simulation_reinsurance_type": "non-proportional", + "default_non-proportional_reinsurance_deductible": 0.3, + "default_non-proportional_reinsurance_excess": 1.0, + "default_non-proportional_reinsurance_premium_share": 0.3, + "static_non-proportional_reinsurance_levels": False, + "catbonds_off": True, + "reinsurance_off": False, + "capacity_target_decrement_threshold": 1.8, + "capacity_target_increment_threshold": 1.2, + "capacity_target_decrement_factor": 24 / 25.0, + "capacity_target_increment_factor": 25 / 24.0, + # Retention parameters + "insurance_retention": 0.85, # Ratio of insurance contracts retained every iteration. + "reinsurance_retention": 1, # Ratio of reinsurance contracts retained every iteration. + # Premium sensitivity parameters + "premium_sensitivity": 5, + # This parameter represents how sensitive is the variation of the insurance premium with respect of the capital of the market. Higher means more sensitive. + "reinpremium_sensitivity": 6, + # This parameter represents how sensitive is the variation of the reinsurance premium with respect of the capital of the market. Higher means more sensitive. + # Balanced portfolio parameters + "insurers_balance_ratio": 0.1, + # This ratio represents how low we want to keep the standard deviation of the cash reserved below the mean for insurers. Lower means more balanced. + "reinsurers_balance_ratio": 20, + # This ratio represents how low we want to keep the standard deviation of the cash reserved below the mean for reinsurers. Lower means more balanced. (Deactivated for the moment) + "insurers_recursion_limit": 50, + # Intensity of the recursion algorithm to balance the portfolio of risks for insurers. + "reinsurers_recursion_limit": 10, + # Intensity of the recursion algorithm to balance the portfolio of risks for reinsurers. + # Market permanency parameters + "market_permanency_off": False, + # This parameter activates (deactivates) the following market permanency constraints. + "cash_permanency_limit": 100, + # This parameter enforces the limit under which the firms leave the market because they cannot underwrite anything. + "insurance_permanency_contracts_limit": 4, + # If insurers stay for too long under this limit of contracts they deccide to leave the market. + "insurance_permanency_ratio_limit": 0.6, + # If insurers stay for too long under this limit they deccide to leave the market because they have too much capital. + "insurance_permanency_time_constraint": 24, + # This parameter defines the period that the insurers wait if they have few capital or few contract before leaving the market. + "reinsurance_permanency_contracts_limit": 2, + # If reinsurers stay for too long under this limit of contracts they deccide to leave the market. + "reinsurance_permanency_ratio_limit": 0.8, + # If reinsurers stay for too long under this limit they deccide to leave the market because they have too much capital. + "reinsurance_permanency_time_constraint": 48, + # This parameter defines the period that the reinsurers wait if they have few capital or few contract before leaving the market. + # Insurance and Reinsurance deductibles + "insurance_reinsurance_levels_lower_bound": 0.25, + "insurance_reinsurance_levels_upper_bound": 0.30, + "reinsurance_reinsurance_levels_lower_bound": 0.5, + "reinsurance_reinsurance_levels_upper_bound": 0.95, + "initial_agent_cash": 80000, + "initial_reinagent_cash": 2000000, + "interest_rate": 0.001, + "reinsurance_limit": 0.1, + "upper_price_limit": 1.2, + "lower_price_limit": 0.85, + "no_risks": 20000, +} diff --git a/listify.py b/listify.py index 591e626..c410c21 100644 --- a/listify.py +++ b/listify.py @@ -1,6 +1,7 @@ """Auxiliary function to transform dicts into lists and back for transfer from cloud (sandman2) to local.""" + def listify(d): """Function to convert dict to list with keys in last list element. Arguments: @@ -8,16 +9,17 @@ def listify(d): Returns: list with dict values as elements [:-1] and dict keys as last element.""" - + """extract keys""" keys = list(d.keys()) - + """create list""" l = [d[key] for key in keys] l.append(keys) - + return l + def delistify(l): """Function to convert listified dict back to dict. Arguments: @@ -26,12 +28,12 @@ def delistify(l): dict keys as list in the last element. Returns: dict - The restored dict.""" - + """extract keys""" keys = l.pop() assert len(keys) == len(l) - + """create dict""" - d = {key: l[i] for i,key in enumerate(keys)} - + d = {key: l[i] for i, key in enumerate(keys)} + return d diff --git a/logger.py b/logger.py index 8fe928f..60f14d3 100644 --- a/logger.py +++ b/logger.py @@ -5,16 +5,22 @@ import listify LOG_DEFAULT = ( - 'total_cash total_excess_capital total_profitslosses total_contracts ' - 'total_operational total_reincash total_reinexcess_capital total_reinprofitslosses ' - 'total_reincontracts total_reinoperational total_catbondsoperational market_premium ' - 'market_reinpremium cumulative_bankruptcies cumulative_market_exits cumulative_unrecovered_claims ' - 'cumulative_claims insurance_firms_cash reinsurance_firms_cash market_diffvar ' - 'rc_event_schedule_initial rc_event_damage_initial number_riskmodels' -).split(' ') - -class Logger(): - def __init__(self, no_riskmodels=None, rc_event_schedule_initial=None, rc_event_damage_initial=None): + "total_cash total_excess_capital total_profitslosses total_contracts " + "total_operational total_reincash total_reinexcess_capital total_reinprofitslosses " + "total_reincontracts total_reinoperational total_catbondsoperational market_premium " + "market_reinpremium cumulative_bankruptcies cumulative_market_exits cumulative_unrecovered_claims " + "cumulative_claims insurance_firms_cash reinsurance_firms_cash market_diffvar " + "rc_event_schedule_initial rc_event_damage_initial number_riskmodels" +).split(" ") + + +class Logger: + def __init__( + self, + no_riskmodels=None, + rc_event_schedule_initial=None, + rc_event_damage_initial=None, + ): """Constructor. Prepares history_logs atribute as dict for the logs. Records initial event schedule of simulation run. Arguments @@ -22,82 +28,90 @@ def __init__(self, no_riskmodels=None, rc_event_schedule_initial=None, rc_event_ rc_event_schedule_initial: list of lists of int. Times of risk events by category rc_event_damage_initial: list of arrays (or lists) of float. Damage by peril for each category as share of total possible damage (maximum insured or excess). - Returns class instance.""" - + Returns class instance.""" + """Record number of riskmodels""" self.number_riskmodels = no_riskmodels - + """Record initial event schedule""" self.rc_event_schedule_initial = rc_event_schedule_initial self.rc_event_damage_initial = rc_event_damage_initial """Prepare history log dict""" self.history_logs = {} - + """Variables pertaining to insurance sector""" # TODO: should we not have `cumulative_bankruptcies` and # `cumulative_market_exits` for both insurance firms and reinsurance firms? # `cumulative_claims`: Here are stored the total cumulative claims received # by the whole insurance sector until a certain time. - insurance_sector = ('total_cash total_excess_capital total_profitslosses ' - 'total_contracts total_operational cumulative_bankruptcies ' - 'cumulative_market_exits cumulative_claims cumulative_unrecovered_claims').split(' ') + insurance_sector = ( + "total_cash total_excess_capital total_profitslosses " + "total_contracts total_operational cumulative_bankruptcies " + "cumulative_market_exits cumulative_claims cumulative_unrecovered_claims" + ).split(" ") for _v in insurance_sector: self.history_logs[_v] = [] - + """Variables pertaining to individual insurance firms""" - self.history_logs['individual_contracts'] = [] # TODO: Should there not be a similar record for reinsurance - self.history_logs['insurance_firms_cash'] = [] - + self.history_logs[ + "individual_contracts" + ] = [] # TODO: Should there not be a similar record for reinsurance + self.history_logs["insurance_firms_cash"] = [] + """Variables pertaining to reinsurance sector""" - self.history_logs['total_reincash'] = [] - self.history_logs['total_reinexcess_capital'] = [] - self.history_logs['total_reinprofitslosses'] = [] - self.history_logs['total_reincontracts'] = [] - self.history_logs['total_reinoperational'] = [] + self.history_logs["total_reincash"] = [] + self.history_logs["total_reinexcess_capital"] = [] + self.history_logs["total_reinprofitslosses"] = [] + self.history_logs["total_reincontracts"] = [] + self.history_logs["total_reinoperational"] = [] """Variables pertaining to individual reinsurance firms""" - self.history_logs['reinsurance_firms_cash'] = [] + self.history_logs["reinsurance_firms_cash"] = [] """Variables pertaining to cat bonds""" - self.history_logs['total_catbondsoperational'] = [] + self.history_logs["total_catbondsoperational"] = [] """Variables pertaining to premiums""" - self.history_logs['market_premium'] = [] - self.history_logs['market_reinpremium'] = [] - self.history_logs['market_diffvar'] = [] - + self.history_logs["market_premium"] = [] + self.history_logs["market_reinpremium"] = [] + self.history_logs["market_diffvar"] = [] + def record_data(self, data_dict): """Method to record data for one period Arguments data_dict: Type dict. Data with the same keys as are used in self.history_log(). - Returns None.""" + Returns None.""" for key in data_dict.keys(): if key != "individual_contracts": self.history_logs[key].append(data_dict[key]) else: for i in range(len(data_dict["individual_contracts"])): - self.history_logs['individual_contracts'][i].append(data_dict["individual_contracts"][i]) + self.history_logs["individual_contracts"][i].append( + data_dict["individual_contracts"][i] + ) - def obtain_log(self, requested_logs=LOG_DEFAULT): #This function allows to return in a list all the data generated by the model. There is no other way to transfer it back from the cloud. + def obtain_log( + self, requested_logs=LOG_DEFAULT + ): # This function allows to return in a list all the data generated by the model. There is no other way to transfer it back from the cloud. """Method to transfer entire log (self.history_log as well as risk event schedule). This is used to transfer the log to master core from work cores in ensemble runs in the cloud. No arguments. Returns list (listified dict).""" - + """Include environment variables (number of risk models and risk event schedule)""" self.history_logs["number_riskmodels"] = self.number_riskmodels self.history_logs["rc_event_damage_initial"] = self.rc_event_damage_initial self.history_logs["rc_event_schedule_initial"] = self.rc_event_schedule_initial - + """Parse logs to be returned""" if requested_logs == None: requested_logs = LOG_DEFAULT log = {name: self.history_logs[name] for name in requested_logs} - + """Convert to list and return""" return listify.listify(log) - + def restore_logger_object(self, log): """Method to restore logger object. A log can be restored later. It can also be restored on a different machine. This is useful in the case of ensemble runs to move the log to @@ -110,13 +124,17 @@ def restore_logger_object(self, log): """Restore dict""" log = listify.delistify(log) - + """Extract environment variables (number of risk models and risk event schedule)""" self.rc_event_schedule_initial = log["rc_event_schedule_initial"] self.rc_event_damage_initial = log["rc_event_damage_initial"] self.number_riskmodels = log["number_riskmodels"] - del log["rc_event_schedule_initial"], log["rc_event_damage_initial"], log["number_riskmodels"] - + del ( + log["rc_event_schedule_initial"], + log["rc_event_damage_initial"], + log["number_riskmodels"], + ) + """Restore history log""" self.history_logs = log @@ -126,18 +144,18 @@ def save_log(self, background_run): Arguments: background_run: Type bool. Is this an ensemble run (true) or not (false). Returns None.""" - + """Prepare writing tasks""" if background_run: to_log = self.replication_log_prepare() else: to_log = self.single_log_prepare() - + """Write to disk""" for filename, data, operation_character in to_log: with open(filename, operation_character) as wfile: wfile.write(str(data) + "\n") - + def replication_log_prepare(self): """Method to prepare writing tasks for ensemble run saving. No arguments @@ -150,7 +168,7 @@ def replication_log_prepare(self): to_log = [] to_log.append(("data/" + fpf + "_history_logs.dat", self.history_logs, "a")) return to_log - + def single_log_prepare(self): """Method to prepare writing tasks for single run saving. No arguments @@ -161,17 +179,18 @@ def single_log_prepare(self): to_log = [] to_log.append(("data/history_logs.dat", self.history_logs, "w")) return to_log - - def add_insurance_agent(self): + + def add_insurance_agent(self): """Method for adding an additional insurer agent to the history log. This is necessary to keep the number of individual insurance firm logs constant in time. No arguments. Returns None.""" - # TODO: should this not also be done for self.history_logs['insurance_firms_cash'] and + # TODO: should this not also be done for self.history_logs['insurance_firms_cash'] and # self.history_logs['reinsurance_firms_cash'] - if len(self.history_logs['individual_contracts']) > 0: - zeroes_to_append = list(np.zeros(len(self.history_logs['individual_contracts'][0]), dtype=int)) + if len(self.history_logs["individual_contracts"]) > 0: + zeroes_to_append = list( + np.zeros(len(self.history_logs["individual_contracts"][0]), dtype=int) + ) else: zeroes_to_append = [] - self.history_logs['individual_contracts'].append(zeroes_to_append) - + self.history_logs["individual_contracts"].append(zeroes_to_append) diff --git a/metainsurancecontract.py b/metainsurancecontract.py index 20d17bb..7df9cb7 100644 --- a/metainsurancecontract.py +++ b/metainsurancecontract.py @@ -1,9 +1,23 @@ import numpy as np import sys, pdb -class MetaInsuranceContract(): - def __init__(self, insurer, properties, time, premium, runtime, payment_period, expire_immediately, initial_VaR=0., \ - insurancetype="proportional", deductible_fraction=None, excess_fraction=None, reinsurance=0): + +class MetaInsuranceContract: + def __init__( + self, + insurer, + properties, + time, + premium, + runtime, + payment_period, + expire_immediately, + initial_VaR=0.0, + insurancetype="proportional", + deductible_fraction=None, + excess_fraction=None, + reinsurance=0, + ): """Constructor method. Accepts arguments insurer: Type InsuranceFirm. @@ -24,15 +38,19 @@ def __init__(self, insurer, properties, time, premium, runtime, payment_period, Creates InsuranceContract, saves parameters. Creates obligation for premium payment. Includes contract in reinsurance network if applicable (e.g. if this is a ReinsuranceContract).""" # TODO: argument reinsurance seems senseless; remove? - + # Save parameters self.insurer = insurer self.risk_factor = properties["risk_factor"] self.category = properties["category"] self.property_holder = properties["owner"] self.value = properties["value"] - self.contract = properties.get("contract") # will assign None if key does not exist - self.insurancetype = properties.get("insurancetype") if insurancetype is None else insurancetype + self.contract = properties.get( + "contract" + ) # will assign None if key does not exist + self.insurancetype = ( + properties.get("insurancetype") if insurancetype is None else insurancetype + ) self.runtime = runtime self.starttime = time self.expiration = runtime + time @@ -40,63 +58,83 @@ def __init__(self, insurer, properties, time, premium, runtime, payment_period, self.terminating = False self.current_claim = 0 self.initial_VaR = initial_VaR - - # set deductible from argument, risk property or default value, whichever first is not None + + # set deductible from argument, risk property or default value, whichever first is not None default_deductible_fraction = 0.0 - deductible_fraction_generator = (item for item in [deductible_fraction, properties.get("deductible_fraction"), \ - default_deductible_fraction] if item is not None) + deductible_fraction_generator = ( + item + for item in [ + deductible_fraction, + properties.get("deductible_fraction"), + default_deductible_fraction, + ] + if item is not None + ) self.deductible_fraction = next(deductible_fraction_generator) self.deductible = self.deductible_fraction * self.value - - # set excess from argument, risk property or default value, whichever first is not None + + # set excess from argument, risk property or default value, whichever first is not None default_excess_fraction = 1.0 - excess_fraction_generator = (item for item in [excess_fraction, properties.get("excess_fraction"), \ - default_excess_fraction] if item is not None) + excess_fraction_generator = ( + item + for item in [ + excess_fraction, + properties.get("excess_fraction"), + default_excess_fraction, + ] + if item is not None + ) self.excess_fraction = next(excess_fraction_generator) self.excess = self.excess_fraction * self.value - + self.reinsurance = reinsurance self.reinsurer = None self.reincontract = None self.reinsurance_share = None - #self.is_reinsurancecontract = False + # self.is_reinsurancecontract = False # setup payment schedule - #total_premium = premium * (self.excess - self.deductible) # TODO: excess and deductible should not be considered linearily in premium computation; this should be shifted to the (re)insurer who supplies the premium as argument to the contract's constructor method - total_premium = premium * self.value + # total_premium = premium * (self.excess - self.deductible) # TODO: excess and deductible should not be considered linearily in premium computation; this should be shifted to the (re)insurer who supplies the premium as argument to the contract's constructor method + total_premium = premium * self.value self.periodized_premium = total_premium / self.runtime - self.payment_times = [time + i for i in range(runtime) if i % payment_period == 0] - self.payment_values = total_premium * (np.ones(len(self.payment_times)) / len(self.payment_times)) - + self.payment_times = [ + time + i for i in range(runtime) if i % payment_period == 0 + ] + self.payment_values = total_premium * ( + np.ones(len(self.payment_times)) / len(self.payment_times) + ) + ## Create obligation for premium payment - #self.property_holder.receive_obligation(premium * (self.excess - self.deductible), self.insurer, time, 'premium') - + # self.property_holder.receive_obligation(premium * (self.excess - self.deductible), self.insurer, time, 'premium') + # Embed contract in reinsurance network, if applicable if self.contract is not None: - self.contract.reinsure(reinsurer=self.insurer, reinsurance_share=properties["reinsurance_share"], \ - reincontract=self) + self.contract.reinsure( + reinsurer=self.insurer, + reinsurance_share=properties["reinsurance_share"], + reincontract=self, + ) # This flag is set to 1, when the contract is about to expire and there is an attempt to roll it over. self.roll_over_flag = 0 - - def check_payment_due(self, time): if len(self.payment_times) > 0 and time >= self.payment_times[0]: # Create obligation for premium payment - #self.property_holder.receive_obligation(premium * (self.excess - self.deductible), self.insurer, time, 'premium') - self.property_holder.receive_obligation(self.payment_values[0], self.insurer, time, 'premium') - + # self.property_holder.receive_obligation(premium * (self.excess - self.deductible), self.insurer, time, 'premium') + self.property_holder.receive_obligation( + self.payment_values[0], self.insurer, time, "premium" + ) + # Remove current payment from payment schedule self.payment_times = self.payment_times[1:] self.payment_values = self.payment_values[1:] - + def get_and_reset_current_claim(self): current_claim = self.current_claim self.current_claim = 0 return self.category, current_claim, (self.insurancetype == "proportional") - def terminate_reinsurance(self, time): """Terminate reinsurance method. Accepts arguments @@ -105,7 +143,7 @@ def terminate_reinsurance(self, time): Causes any reinsurance contracts to be dissolved as the present contract terminates.""" if self.reincontract is not None: self.reincontract.dissolve(time) - + def dissolve(self, time): """Dissolve method. Accepts arguments @@ -126,9 +164,9 @@ def reinsure(self, reinsurer, reinsurance_share, reincontract): self.reinsurance = self.value * reinsurance_share self.reinsurance_share = reinsurance_share self.reincontract = reincontract - assert self.reinsurance_share in [None, 0.0, 1.0] - - def unreinsure(self): + assert self.reinsurance_share in [None, 0.0, 1.0] + + def unreinsure(self): """Unreinsurance Method. Accepts no arguments: No return value. diff --git a/metainsuranceorg.py b/metainsuranceorg.py index c0fa39e..f9ea1ec 100644 --- a/metainsuranceorg.py +++ b/metainsuranceorg.py @@ -1,4 +1,3 @@ - import isleconfig import numpy as np import scipy.stats @@ -11,99 +10,150 @@ if isleconfig.use_abce: from genericagentabce import GenericAgent - #print("abce imported") + + # print("abce imported") else: from genericagent import GenericAgent - #print("abce not imported") + + # print("abce not imported") + def get_mean(x): return sum(x) / len(x) + def get_mean_std(x): m = get_mean(x) variance = sum((val - m) ** 2 for val in x) return m, np.sqrt(variance / len(x)) + class MetaInsuranceOrg(GenericAgent): def init(self, simulation_parameters, agent_parameters): - self.simulation = simulation_parameters['simulation'] + self.simulation = simulation_parameters["simulation"] self.simulation_parameters = simulation_parameters - self.contract_runtime_dist = scipy.stats.randint(simulation_parameters["mean_contract_runtime"] - \ - simulation_parameters["contract_runtime_halfspread"], simulation_parameters["mean_contract_runtime"] \ - + simulation_parameters["contract_runtime_halfspread"] + 1) - self.default_contract_payment_period = simulation_parameters["default_contract_payment_period"] - self.id = agent_parameters['id'] - self.cash = agent_parameters['initial_cash'] + self.contract_runtime_dist = scipy.stats.randint( + simulation_parameters["mean_contract_runtime"] + - simulation_parameters["contract_runtime_halfspread"], + simulation_parameters["mean_contract_runtime"] + + simulation_parameters["contract_runtime_halfspread"] + + 1, + ) + self.default_contract_payment_period = simulation_parameters[ + "default_contract_payment_period" + ] + self.id = agent_parameters["id"] + self.cash = agent_parameters["initial_cash"] self.capacity_target = self.cash * 0.9 - self.capacity_target_decrement_threshold = agent_parameters['capacity_target_decrement_threshold'] - self.capacity_target_increment_threshold = agent_parameters['capacity_target_increment_threshold'] - self.capacity_target_decrement_factor = agent_parameters['capacity_target_decrement_factor'] - self.capacity_target_increment_factor = agent_parameters['capacity_target_increment_factor'] + self.capacity_target_decrement_threshold = agent_parameters[ + "capacity_target_decrement_threshold" + ] + self.capacity_target_increment_threshold = agent_parameters[ + "capacity_target_increment_threshold" + ] + self.capacity_target_decrement_factor = agent_parameters[ + "capacity_target_decrement_factor" + ] + self.capacity_target_increment_factor = agent_parameters[ + "capacity_target_increment_factor" + ] self.excess_capital = self.cash self.premium = agent_parameters["norm_premium"] - self.profit_target = agent_parameters['profit_target'] - self.acceptance_threshold = agent_parameters['initial_acceptance_threshold'] # 0.5 - self.acceptance_threshold_friction = agent_parameters['acceptance_threshold_friction'] # 0.9 #1.0 to switch off + self.profit_target = agent_parameters["profit_target"] + self.acceptance_threshold = agent_parameters[ + "initial_acceptance_threshold" + ] # 0.5 + self.acceptance_threshold_friction = agent_parameters[ + "acceptance_threshold_friction" + ] # 0.9 #1.0 to switch off self.interest_rate = agent_parameters["interest_rate"] self.reinsurance_limit = agent_parameters["reinsurance_limit"] self.simulation_no_risk_categories = simulation_parameters["no_categories"] - self.simulation_reinsurance_type = simulation_parameters["simulation_reinsurance_type"] - self.dividend_share_of_profits = simulation_parameters["dividend_share_of_profits"] - - self.owner = self.simulation # TODO: Make this into agent_parameter value? + self.simulation_reinsurance_type = simulation_parameters[ + "simulation_reinsurance_type" + ] + self.dividend_share_of_profits = simulation_parameters[ + "dividend_share_of_profits" + ] + + self.owner = self.simulation # TODO: Make this into agent_parameter value? self.per_period_dividend = 0 - self.cash_last_periods = list(np.zeros(4, dtype=int)*self.cash) - - rm_config = agent_parameters['riskmodel_config'] + self.cash_last_periods = list(np.zeros(4, dtype=int) * self.cash) + + rm_config = agent_parameters["riskmodel_config"] """Here we modify the margin of safety depending on the number of risks models available in the market. When is 0 all risk models have the same margin of safety. The reason for doing this is that with more risk models the firms tend to be closer to the max capacity""" - margin_of_safety_correction = (rm_config["margin_of_safety"] + (simulation_parameters["no_riskmodels"] - 1) * simulation_parameters["margin_increase"]) - - self.riskmodel = RiskModel(damage_distribution=rm_config["damage_distribution"], \ - expire_immediately=rm_config["expire_immediately"], \ - cat_separation_distribution=rm_config["cat_separation_distribution"], \ - norm_premium=rm_config["norm_premium"], \ - category_number=rm_config["no_categories"], \ - init_average_exposure=rm_config["risk_value_mean"], \ - init_average_risk_factor=rm_config["risk_factor_mean"], \ - init_profit_estimate=rm_config["norm_profit_markup"], \ - margin_of_safety=margin_of_safety_correction, \ - var_tail_prob=rm_config["var_tail_prob"], \ - inaccuracy=rm_config["inaccuracy_by_categ"]) - - self.category_reinsurance = [None for i in range(self.simulation_no_risk_categories)] - if self.simulation_reinsurance_type == 'non-proportional': - if agent_parameters['non-proportional_reinsurance_level'] is not None: - self.np_reinsurance_deductible_fraction = agent_parameters['non-proportional_reinsurance_level'] + margin_of_safety_correction = ( + rm_config["margin_of_safety"] + + (simulation_parameters["no_riskmodels"] - 1) + * simulation_parameters["margin_increase"] + ) + + self.riskmodel = RiskModel( + damage_distribution=rm_config["damage_distribution"], + expire_immediately=rm_config["expire_immediately"], + cat_separation_distribution=rm_config["cat_separation_distribution"], + norm_premium=rm_config["norm_premium"], + category_number=rm_config["no_categories"], + init_average_exposure=rm_config["risk_value_mean"], + init_average_risk_factor=rm_config["risk_factor_mean"], + init_profit_estimate=rm_config["norm_profit_markup"], + margin_of_safety=margin_of_safety_correction, + var_tail_prob=rm_config["var_tail_prob"], + inaccuracy=rm_config["inaccuracy_by_categ"], + ) + + self.category_reinsurance = [ + None for i in range(self.simulation_no_risk_categories) + ] + if self.simulation_reinsurance_type == "non-proportional": + if agent_parameters["non-proportional_reinsurance_level"] is not None: + self.np_reinsurance_deductible_fraction = agent_parameters[ + "non-proportional_reinsurance_level" + ] else: - self.np_reinsurance_deductible_fraction = simulation_parameters["default_non-proportional_reinsurance_deductible"] - self.np_reinsurance_excess_fraction = simulation_parameters["default_non-proportional_reinsurance_excess"] - self.np_reinsurance_premium_share = simulation_parameters["default_non-proportional_reinsurance_premium_share"] + self.np_reinsurance_deductible_fraction = simulation_parameters[ + "default_non-proportional_reinsurance_deductible" + ] + self.np_reinsurance_excess_fraction = simulation_parameters[ + "default_non-proportional_reinsurance_excess" + ] + self.np_reinsurance_premium_share = simulation_parameters[ + "default_non-proportional_reinsurance_premium_share" + ] self.obligations = [] self.underwritten_contracts = [] self.profits_losses = 0 - #self.reinsurance_contracts = [] + # self.reinsurance_contracts = [] self.operational = True self.is_insurer = True self.is_reinsurer = False - + """set up risk value estimate variables""" - self.var_counter = 0 # sum over risk model inaccuracies for all contracts - self.var_counter_per_risk = 0 # average risk model inaccuracy across contracts - self.var_sum = 0 # sum over initial VaR for all contracts - self.counter_category = np.zeros(self.simulation_no_risk_categories) # var_counter disaggregated by category - self.var_category = np.zeros(self.simulation_no_risk_categories) # var_sum disaggregated by category + self.var_counter = 0 # sum over risk model inaccuracies for all contracts + self.var_counter_per_risk = 0 # average risk model inaccuracy across contracts + self.var_sum = 0 # sum over initial VaR for all contracts + self.counter_category = np.zeros( + self.simulation_no_risk_categories + ) # var_counter disaggregated by category + self.var_category = np.zeros( + self.simulation_no_risk_categories + ) # var_sum disaggregated by category self.naccep = [] self.risks_kept = [] self.reinrisks_kept = [] - self.balance_ratio = simulation_parameters['insurers_balance_ratio'] - self.recursion_limit = simulation_parameters['insurers_recursion_limit'] - self.cash_left_by_categ = [self.cash for i in range(self.simulation_parameters["no_categories"])] + self.balance_ratio = simulation_parameters["insurers_balance_ratio"] + self.recursion_limit = simulation_parameters["insurers_recursion_limit"] + self.cash_left_by_categ = [ + self.cash for i in range(self.simulation_parameters["no_categories"]) + ] self.market_permanency_counter = 0 - def iterate(self, time): # TODO: split function so that only the sequence of events remains here and everything else is in separate methods + def iterate( + self, time + ): # TODO: split function so that only the sequence of events remains here and everything else is in separate methods """obtain investments yield""" self.obtain_yield(time) @@ -111,14 +161,25 @@ def iterate(self, time): # TODO: split function so that only the sequence """realize due payments""" self.effect_payments(time) if isleconfig.verbose: - print(time, ":", self.id, len(self.underwritten_contracts), self.cash, self.operational) + print( + time, + ":", + self.id, + len(self.underwritten_contracts), + self.cash, + self.operational, + ) self.make_reinsurance_claims(time) """mature contracts""" if isleconfig.verbose: print("Number of underwritten contracts ", len(self.underwritten_contracts)) - maturing = [contract for contract in self.underwritten_contracts if contract.expiration <= time] + maturing = [ + contract + for contract in self.underwritten_contracts + if contract.expiration <= time + ] for contract in maturing: self.underwritten_contracts.remove(contract) contract.mature(time) @@ -132,39 +193,80 @@ def iterate(self, time): # TODO: split function so that only the sequence """request risks to be considered for underwriting in the next period and collect those for this period""" new_risks = [] if self.is_insurer: - new_risks += self.simulation.solicit_insurance_requests(self.id, self.cash, self) + new_risks += self.simulation.solicit_insurance_requests( + self.id, self.cash, self + ) if self.is_reinsurer: - new_risks += self.simulation.solicit_reinsurance_requests(self.id, self.cash, self) + new_risks += self.simulation.solicit_reinsurance_requests( + self.id, self.cash, self + ) contracts_offered = len(new_risks) if isleconfig.verbose and contracts_offered < 2 * contracts_dissolved: - print("Something wrong; agent {0:d} receives too few new contracts {1:d} <= {2:d}".format( - self.id, contracts_offered, 2*contracts_dissolved)) - - new_nonproportional_risks = [risk for risk in new_risks if risk.get("insurancetype")=='excess-of-loss' and risk["owner"] is not self] - new_risks = [risk for risk in new_risks if risk.get("insurancetype") in ['proportional', None] and risk["owner"] is not self] - + print( + "Something wrong; agent {0:d} receives too few new contracts {1:d} <= {2:d}".format( + self.id, contracts_offered, 2 * contracts_dissolved + ) + ) + + new_nonproportional_risks = [ + risk + for risk in new_risks + if risk.get("insurancetype") == "excess-of-loss" + and risk["owner"] is not self + ] + new_risks = [ + risk + for risk in new_risks + if risk.get("insurancetype") in ["proportional", None] + and risk["owner"] is not self + ] + """deal with non-proportional risks first as they must evaluate each request separatly, then with proportional ones""" - [reinrisks_per_categ, number_reinrisks_categ] = self.risks_reinrisks_organizer(new_nonproportional_risks) #Here the new reinrisks are organized by category. + [ + reinrisks_per_categ, + number_reinrisks_categ, + ] = self.risks_reinrisks_organizer( + new_nonproportional_risks + ) # Here the new reinrisks are organized by category. - for repetition in range(self.recursion_limit): # TODO: find an efficient way to stop the recursion if there are no more risks to accept or if it is not accepting any more over several iterations. + for repetition in range( + self.recursion_limit + ): # TODO: find an efficient way to stop the recursion if there are no more risks to accept or if it is not accepting any more over several iterations. former_reinrisks_per_categ = copy.copy(reinrisks_per_categ) - [reinrisks_per_categ, not_accepted_reinrisks] = self.process_newrisks_reinsurer(reinrisks_per_categ, number_reinrisks_categ, time) #Here we process all the new reinrisks in order to keep the portfolio as balanced as possible. - if former_reinrisks_per_categ == reinrisks_per_categ: #Stop condition implemented. Might solve the previous TODO. + [ + reinrisks_per_categ, + not_accepted_reinrisks, + ] = self.process_newrisks_reinsurer( + reinrisks_per_categ, number_reinrisks_categ, time + ) # Here we process all the new reinrisks in order to keep the portfolio as balanced as possible. + if ( + former_reinrisks_per_categ == reinrisks_per_categ + ): # Stop condition implemented. Might solve the previous TODO. break self.simulation.return_reinrisks(not_accepted_reinrisks) - underwritten_risks = [{"value": contract.value, "category": contract.category, \ - "risk_factor": contract.risk_factor, "deductible": contract.deductible, \ - "excess": contract.excess, "insurancetype": contract.insurancetype, \ - "runtime": contract.runtime} for contract in self.underwritten_contracts if - contract.reinsurance_share != 1.0] + underwritten_risks = [ + { + "value": contract.value, + "category": contract.category, + "risk_factor": contract.risk_factor, + "deductible": contract.deductible, + "excess": contract.excess, + "insurancetype": contract.insurancetype, + "runtime": contract.runtime, + } + for contract in self.underwritten_contracts + if contract.reinsurance_share != 1.0 + ] """obtain risk model evaluation (VaR) for underwriting decisions and for capacity specific decisions""" # TODO: Enable reinsurance shares other tan 0.0 and 1.0 - expected_profit, acceptable_by_category, cash_left_by_categ, var_per_risk_per_categ, self.excess_capital = self.riskmodel.evaluate(underwritten_risks, self.cash) - # TODO: resolve insurance reinsurance inconsistency (insurer underwrite after capacity decisions, reinsurers before). + expected_profit, acceptable_by_category, cash_left_by_categ, var_per_risk_per_categ, self.excess_capital = self.riskmodel.evaluate( + underwritten_risks, self.cash + ) + # TODO: resolve insurance reinsurance inconsistency (insurer underwrite after capacity decisions, reinsurers before). # This is currently so because it minimizes the number of times we need to run self.riskmodel.evaluate(). # It would also be more consistent if excess capital would be updated at the end of the iteration. """handle adjusting capacity target and capacity""" @@ -172,7 +274,7 @@ def iterate(self, time): # TODO: split function so that only the sequence self.adjust_capacity_target(max_var_by_categ) actual_capacity = self.increase_capacity(time, max_var_by_categ) # seek reinsurance - #if self.is_insurer: + # if self.is_insurer: # # TODO: Why should only insurers be able to get reinsurance (not reinsurers)? (Technically, it should work) --> OBSOLETE # self.ask_reinsurance(time) # # TODO: make independent of insurer/reinsurer, but change this to different deductable values @@ -182,38 +284,56 @@ def iterate(self, time): # TODO: split function so that only the sequence self.pay_dividends(time) """make underwriting decisions, category-wise""" - #if expected_profit * 1./self.cash < self.profit_target: + # if expected_profit * 1./self.cash < self.profit_target: # self.acceptance_threshold = ((self.acceptance_threshold - .4) * 5. * self.acceptance_threshold_friction) / 5. + .4 - #else: + # else: # self.acceptance_threshold = (1 - self.acceptance_threshold_friction * (1 - (self.acceptance_threshold - .4) * 5.)) / 5. + .4 - growth_limit = max(50, 2 * len(self.underwritten_contracts) + contracts_dissolved) + growth_limit = max( + 50, 2 * len(self.underwritten_contracts) + contracts_dissolved + ) if sum(acceptable_by_category) > growth_limit: - acceptable_by_category = np.asarray(acceptable_by_category).astype(np.double) - acceptable_by_category = acceptable_by_category * growth_limit / sum(acceptable_by_category) + acceptable_by_category = np.asarray(acceptable_by_category).astype( + np.double + ) + acceptable_by_category = ( + acceptable_by_category * growth_limit / sum(acceptable_by_category) + ) acceptable_by_category = np.int64(np.round(acceptable_by_category)) - [risks_per_categ, number_risks_categ] = self.risks_reinrisks_organizer(new_risks) #Here the new risks are organized by category. + [risks_per_categ, number_risks_categ] = self.risks_reinrisks_organizer( + new_risks + ) # Here the new risks are organized by category. - for repetition in range(self.recursion_limit): # TODO: find an efficient way to stop the recursion if there are no more risks to accept or if it is not accepting any more over several iterations. + for repetition in range( + self.recursion_limit + ): # TODO: find an efficient way to stop the recursion if there are no more risks to accept or if it is not accepting any more over several iterations. former_risks_per_categ = copy.copy(risks_per_categ) - [risks_per_categ, not_accepted_risks] = self.process_newrisks_insurer(risks_per_categ, number_risks_categ, acceptable_by_category, - var_per_risk_per_categ, cash_left_by_categ, time) #Here we process all the new risks in order to keep the portfolio as balanced as possible. - if former_risks_per_categ == risks_per_categ: #Stop condition implemented. Might solve the previous TODO. + [risks_per_categ, not_accepted_risks] = self.process_newrisks_insurer( + risks_per_categ, + number_risks_categ, + acceptable_by_category, + var_per_risk_per_categ, + cash_left_by_categ, + time, + ) # Here we process all the new risks in order to keep the portfolio as balanced as possible. + if ( + former_risks_per_categ == risks_per_categ + ): # Stop condition implemented. Might solve the previous TODO. break # return unacceptables - #print(self.id, " now has ", len(self.underwritten_contracts), " & returns ", len(not_accepted_risks)) + # print(self.id, " now has ", len(self.underwritten_contracts), " & returns ", len(not_accepted_risks)) self.simulation.return_risks(not_accepted_risks) - #not implemented - #"""adjust liquidity, borrow or invest""" - #pass + # not implemented + # """adjust liquidity, borrow or invest""" + # pass self.market_permanency(time) self.roll_over(time) - + self.estimated_var() def enter_illiquidity(self, time): @@ -234,7 +354,7 @@ def enter_bankruptcy(self, time): This method is used when a firm does not have enough cash to pay all its obligations. It is only called from the method self.enter_illiquidity() which is only called from the method self.effect_payments(). This method dissolves the firm through the method self.dissolve().""" - self.dissolve(time, 'record_bankruptcy') + self.dissolve(time, "record_bankruptcy") def market_exit(self, time): """Market_exit Method. @@ -250,7 +370,7 @@ def market_exit(self, time): for obligation in due: self.pay(obligation) self.obligations = [] - self.dissolve(time, 'record_market_exit') + self.dissolve(time, "record_market_exit") def dissolve(self, time, record): """Dissolve Method. @@ -265,14 +385,27 @@ def dissolve(self, time, record): next iteration. Finally the type of dissolution is recorded and the operational state is set to false. Different class variables are reset during the process: self.risks_kept, self.reinrisks_kept, self.excess_capital and self.profits_losses.""" - [contract.dissolve(time) for contract in self.underwritten_contracts] # removing (dissolving) all risks immediately after bankruptcy (may not be realistic, they might instead be bought by another company) + [ + contract.dissolve(time) for contract in self.underwritten_contracts + ] # removing (dissolving) all risks immediately after bankruptcy (may not be realistic, they might instead be bought by another company) self.simulation.return_risks(self.risks_kept) self.risks_kept = [] self.reinrisks_kept = [] - obligation = {"amount": self.cash, "recipient": self.simulation, "due_time": time, "purpose": "Dissolution"} - self.pay(obligation) #This MUST be the last obligation before the dissolution of the firm. - self.excess_capital = 0 #Excess of capital is 0 after bankruptcy or market exit. - self.profits_losses = 0 #Profits and losses are 0 after bankruptcy or market exit. + obligation = { + "amount": self.cash, + "recipient": self.simulation, + "due_time": time, + "purpose": "Dissolution", + } + self.pay( + obligation + ) # This MUST be the last obligation before the dissolution of the firm. + self.excess_capital = ( + 0 + ) # Excess of capital is 0 after bankruptcy or market exit. + self.profits_losses = ( + 0 + ) # Profits and losses are 0 after bankruptcy or market exit. if self.operational: method_to_call = getattr(self.simulation, record) method_to_call() @@ -282,12 +415,19 @@ def dissolve(self, time, record): self.operational = False def receive_obligation(self, amount, recipient, due_time, purpose): - obligation = {"amount": amount, "recipient": recipient, "due_time": due_time, "purpose": purpose} + obligation = { + "amount": amount, + "recipient": recipient, + "due_time": due_time, + "purpose": purpose, + } self.obligations.append(obligation) def effect_payments(self, time): - due = [item for item in self.obligations if item["due_time"]<=time] - self.obligations = [item for item in self.obligations if item["due_time"]>time] + due = [item for item in self.obligations if item["due_time"] <= time] + self.obligations = [ + item for item in self.obligations if item["due_time"] > time + ] sum_due = sum([item["amount"] for item in due]) if sum_due > self.cash: self.obligations += due @@ -299,14 +439,13 @@ def effect_payments(self, time): for obligation in due: self.pay(obligation) - def pay(self, obligation): amount = obligation["amount"] recipient = obligation["recipient"] purpose = obligation["purpose"] if self.get_operational() and recipient.get_operational(): self.cash -= amount - if purpose is not 'dividend': + if purpose is not "dividend": self.profits_losses -= amount recipient.receive(amount) @@ -316,15 +455,19 @@ def receive(self, amount): self.profits_losses += amount def pay_dividends(self, time): - self.receive_obligation(self.per_period_dividend, self.owner, time, 'dividend') - + self.receive_obligation(self.per_period_dividend, self.owner, time, "dividend") + def obtain_yield(self, time): - amount = self.cash * self.interest_rate # TODO: agent should not award her own interest. This interest rate should be taken from self.simulation with a getter method - self.simulation.receive_obligation(amount, self, time, 'yields') - + amount = ( + self.cash * self.interest_rate + ) # TODO: agent should not award her own interest. This interest rate should be taken from self.simulation with a getter method + self.simulation.receive_obligation(amount, self, time, "yields") + def increase_capacity(self): - raise AttributeError( "Method is not implemented in MetaInsuranceOrg, just in inheriting InsuranceFirm instances" ) - + raise AttributeError( + "Method is not implemented in MetaInsuranceOrg, just in inheriting InsuranceFirm instances" + ) + def get_cash(self): return self.cash @@ -332,11 +475,11 @@ def get_excess_capital(self): return self.excess_capital def logme(self): - self.log('cash', self.cash) - self.log('underwritten_contracts', self.underwritten_contracts) - self.log('operational', self.operational) + self.log("cash", self.cash) + self.log("underwritten_contracts", self.underwritten_contracts) + self.log("operational", self.operational) - #def zeros(self): + # def zeros(self): # return 0 def len_underwritten_contracts(self): @@ -350,7 +493,7 @@ def get_profitslosses(self): def get_underwritten_contracts(self): return self.underwritten_contracts - + def get_pointer(self): return self @@ -362,69 +505,119 @@ def estimated_var(self): self.var_counter = 0 self.var_counter_per_risk = 0 self.var_sum = 0 - + if self.operational: for contract in self.underwritten_contracts: - self.counter_category[contract.category] = self.counter_category[contract.category] + 1 - self.var_category[contract.category] = self.var_category[contract.category] + contract.initial_VaR + self.counter_category[contract.category] = ( + self.counter_category[contract.category] + 1 + ) + self.var_category[contract.category] = ( + self.var_category[contract.category] + contract.initial_VaR + ) for category in range(len(self.counter_category)): - self.var_counter = self.var_counter + self.counter_category[category] * self.riskmodel.inaccuracy[category] + self.var_counter = ( + self.var_counter + + self.counter_category[category] + * self.riskmodel.inaccuracy[category] + ) self.var_sum = self.var_sum + self.var_category[category] if not sum(self.counter_category) == 0: - self.var_counter_per_risk = self.var_counter / sum(self.counter_category) + self.var_counter_per_risk = self.var_counter / sum( + self.counter_category + ) else: self.var_counter_per_risk = 0 def increase_capacity(self, time): - assert False, "Method not implemented. increase_capacity method should be implemented in inheriting classes" + assert ( + False + ), "Method not implemented. increase_capacity method should be implemented in inheriting classes" def adjust_dividend(self, time): - assert False, "Method not implemented. adjust_dividend method should be implemented in inheriting classes" - + assert ( + False + ), "Method not implemented. adjust_dividend method should be implemented in inheriting classes" + def adjust_capacity_target(self, time): - assert False, "Method not implemented. adjust_dividend method should be implemented in inheriting classes" + assert ( + False + ), "Method not implemented. adjust_dividend method should be implemented in inheriting classes" - def risks_reinrisks_organizer(self, new_risks): #This method organizes the new risks received by the insurer (or reinsurer) + def risks_reinrisks_organizer( + self, new_risks + ): # This method organizes the new risks received by the insurer (or reinsurer) - risks_per_categ = [[] for x in range(self.simulation_parameters["no_categories"])] #This method organizes the new risks received by the insurer (or reinsurer) by category in the nested list "risks_per_categ". - number_risks_categ = [[] for x in range(self.simulation_parameters["no_categories"])] #This method also counts the new risks received by the insurer (or reinsurer) by category in the list "number_risks_categ". + risks_per_categ = [ + [] for x in range(self.simulation_parameters["no_categories"]) + ] # This method organizes the new risks received by the insurer (or reinsurer) by category in the nested list "risks_per_categ". + number_risks_categ = [ + [] for x in range(self.simulation_parameters["no_categories"]) + ] # This method also counts the new risks received by the insurer (or reinsurer) by category in the list "number_risks_categ". for categ_id in range(self.simulation_parameters["no_categories"]): - risks_per_categ[categ_id] = [risk for risk in new_risks if risk["category"] == categ_id] + risks_per_categ[categ_id] = [ + risk for risk in new_risks if risk["category"] == categ_id + ] number_risks_categ[categ_id] = len(risks_per_categ[categ_id]) - return risks_per_categ, number_risks_categ #The method returns both risks_per_categ and risks_per_categ. + return ( + risks_per_categ, + number_risks_categ, + ) # The method returns both risks_per_categ and risks_per_categ. - def balanced_portfolio(self, risk, cash_left_by_categ, var_per_risk): #This method decides whether the portfolio is balanced enough to accept a new risk or not. If it is balanced enough return True otherwise False. - #This method also returns the cash available per category independently the risk is accepted or not. - cash_reserved_by_categ = self.cash - cash_left_by_categ #Here it is computed the cash already reserved by category + def balanced_portfolio( + self, risk, cash_left_by_categ, var_per_risk + ): # This method decides whether the portfolio is balanced enough to accept a new risk or not. If it is balanced enough return True otherwise False. + # This method also returns the cash available per category independently the risk is accepted or not. + cash_reserved_by_categ = ( + self.cash - cash_left_by_categ + ) # Here it is computed the cash already reserved by category _, std_pre = get_mean_std(cash_reserved_by_categ) cash_reserved_by_categ_store = np.copy(cash_reserved_by_categ) - if risk.get("insurancetype")=='excess-of-loss': - percentage_value_at_risk = self.riskmodel.getPPF(categ_id=risk["category"], tailSize=self.riskmodel.var_tail_prob) - expected_damage = percentage_value_at_risk * risk["value"] * risk["risk_factor"] \ - * self.riskmodel.inaccuracy[risk["category"]] - expected_claim = min(expected_damage, risk["value"] * risk["excess_fraction"]) - risk["value"] * risk["deductible_fraction"] + if risk.get("insurancetype") == "excess-of-loss": + percentage_value_at_risk = self.riskmodel.getPPF( + categ_id=risk["category"], tailSize=self.riskmodel.var_tail_prob + ) + expected_damage = ( + percentage_value_at_risk + * risk["value"] + * risk["risk_factor"] + * self.riskmodel.inaccuracy[risk["category"]] + ) + expected_claim = ( + min(expected_damage, risk["value"] * risk["excess_fraction"]) + - risk["value"] * risk["deductible_fraction"] + ) # record liquidity requirement and apply margin of safety for liquidity requirement - cash_reserved_by_categ_store[risk["category"]] += expected_claim * self.riskmodel.margin_of_safety #Here it is computed how the cash reserved by category would change if the new reinsurance risk was accepted + cash_reserved_by_categ_store[risk["category"]] += ( + expected_claim * self.riskmodel.margin_of_safety + ) # Here it is computed how the cash reserved by category would change if the new reinsurance risk was accepted else: - cash_reserved_by_categ_store[risk["category"]] += var_per_risk[risk["category"]] #Here it is computed how the cash reserved by category would change if the new insurance risk was accepted + cash_reserved_by_categ_store[risk["category"]] += var_per_risk[ + risk["category"] + ] # Here it is computed how the cash reserved by category would change if the new insurance risk was accepted - mean, std_post = get_mean_std(cash_reserved_by_categ_store) #Here it is computed the mean, std of the cash reserved by category after the new risk of reinrisk is accepted + mean, std_post = get_mean_std( + cash_reserved_by_categ_store + ) # Here it is computed the mean, std of the cash reserved by category after the new risk of reinrisk is accepted total_cash_reserved_by_categ_post = sum(cash_reserved_by_categ_store) - if (std_post * total_cash_reserved_by_categ_post/self.cash) <= (self.balance_ratio * mean) or std_post < std_pre: #The new risk is accepted is the standard deviation is reduced or the cash reserved by category is very well balanced. (std_post) <= (self.balance_ratio * mean) - for i in range(len(cash_left_by_categ)): #The balance condition is not taken into account if the cash reserve is far away from the limit. (total_cash_employed_by_categ_post/self.cash <<< 1) + if (std_post * total_cash_reserved_by_categ_post / self.cash) <= ( + self.balance_ratio * mean + ) or std_post < std_pre: # The new risk is accepted is the standard deviation is reduced or the cash reserved by category is very well balanced. (std_post) <= (self.balance_ratio * mean) + for i in range( + len(cash_left_by_categ) + ): # The balance condition is not taken into account if the cash reserve is far away from the limit. (total_cash_employed_by_categ_post/self.cash <<< 1) cash_left_by_categ[i] = self.cash - cash_reserved_by_categ_store[i] return True, cash_left_by_categ @@ -434,35 +627,63 @@ def balanced_portfolio(self, risk, cash_left_by_categ, var_per_risk): #This meth return False, cash_left_by_categ - def process_newrisks_reinsurer(self, reinrisks_per_categ, number_reinrisks_categ, time): #This method processes one by one the reinrisks contained in reinrisks_per_categ in order to decide whether they should be underwritten or not. - #It is done in this way to maintain the portfolio as balanced as possible. For that reason we process risk[C1], risk[C2], risk[C3], risk[C4], risk[C1], risk[C2], ... and so forth. + def process_newrisks_reinsurer( + self, reinrisks_per_categ, number_reinrisks_categ, time + ): # This method processes one by one the reinrisks contained in reinrisks_per_categ in order to decide whether they should be underwritten or not. + # It is done in this way to maintain the portfolio as balanced as possible. For that reason we process risk[C1], risk[C2], risk[C3], risk[C4], risk[C1], risk[C2], ... and so forth. for iterion in range(max(number_reinrisks_categ)): - for categ_id in range(self.simulation_parameters["no_categories"]): #Here we take only one risk per category at a time to achieve risk[C1], risk[C2], risk[C3], risk[C4], risk[C1], risk[C2], ... if possible. - if iterion < number_reinrisks_categ[categ_id] and reinrisks_per_categ[categ_id][iterion] is not None: + for categ_id in range( + self.simulation_parameters["no_categories"] + ): # Here we take only one risk per category at a time to achieve risk[C1], risk[C2], risk[C3], risk[C4], risk[C1], risk[C2], ... if possible. + if ( + iterion < number_reinrisks_categ[categ_id] + and reinrisks_per_categ[categ_id][iterion] is not None + ): risk_to_insure = reinrisks_per_categ[categ_id][iterion] - underwritten_risks = [{"value": contract.value, "category": contract.category, \ - "risk_factor": contract.risk_factor, - "deductible": contract.deductible, \ - "excess": contract.excess, "insurancetype": contract.insurancetype, \ - "runtime_left": (contract.expiration - time)} for contract in - self.underwritten_contracts if contract.insurancetype == "excess-of-loss"] + underwritten_risks = [ + { + "value": contract.value, + "category": contract.category, + "risk_factor": contract.risk_factor, + "deductible": contract.deductible, + "excess": contract.excess, + "insurancetype": contract.insurancetype, + "runtime_left": (contract.expiration - time), + } + for contract in self.underwritten_contracts + if contract.insurancetype == "excess-of-loss" + ] accept, cash_left_by_categ, var_this_risk, self.excess_capital = self.riskmodel.evaluate( - underwritten_risks, self.cash, - risk_to_insure) # TODO: change riskmodel.evaluate() to accept new risk to be evaluated and to account for existing non-proportional risks correctly -> DONE. + underwritten_risks, self.cash, risk_to_insure + ) # TODO: change riskmodel.evaluate() to accept new risk to be evaluated and to account for existing non-proportional risks correctly -> DONE. if accept: - per_value_reinsurance_premium = self.np_reinsurance_premium_share * risk_to_insure[ - "periodized_total_premium"] * risk_to_insure["runtime"] * (self.simulation.get_market_reinpremium()/self.simulation.get_market_premium()) / risk_to_insure[ - "value"] # TODO: rename this to per_value_premium in insurancecontract.py to avoid confusion - [condition, cash_left_by_categ] = self.balanced_portfolio(risk_to_insure, cash_left_by_categ, None) #Here it is check whether the portfolio is balanced or not if the reinrisk (risk_to_insure) is underwritten. Return True if it is balanced. False otherwise. + per_value_reinsurance_premium = ( + self.np_reinsurance_premium_share + * risk_to_insure["periodized_total_premium"] + * risk_to_insure["runtime"] + * ( + self.simulation.get_market_reinpremium() + / self.simulation.get_market_premium() + ) + / risk_to_insure["value"] + ) # TODO: rename this to per_value_premium in insurancecontract.py to avoid confusion + [condition, cash_left_by_categ] = self.balanced_portfolio( + risk_to_insure, cash_left_by_categ, None + ) # Here it is check whether the portfolio is balanced or not if the reinrisk (risk_to_insure) is underwritten. Return True if it is balanced. False otherwise. if condition: - contract = ReinsuranceContract(self, risk_to_insure, time, per_value_reinsurance_premium, - risk_to_insure["runtime"], \ - self.default_contract_payment_period, \ - expire_immediately=self.simulation_parameters[ - "expire_immediately"], \ - initial_VaR=var_this_risk, \ - insurancetype=risk_to_insure[ - "insurancetype"]) # TODO: implement excess of loss for reinsurance contracts + contract = ReinsuranceContract( + self, + risk_to_insure, + time, + per_value_reinsurance_premium, + risk_to_insure["runtime"], + self.default_contract_payment_period, + expire_immediately=self.simulation_parameters[ + "expire_immediately" + ], + initial_VaR=var_this_risk, + insurancetype=risk_to_insure["insurancetype"], + ) # TODO: implement excess of loss for reinsurance contracts self.underwritten_contracts.append(contract) self.cash_left_by_categ = cash_left_by_categ reinrisks_per_categ[categ_id][iterion] = None @@ -473,47 +694,78 @@ def process_newrisks_reinsurer(self, reinrisks_per_categ, number_reinrisks_categ if reinrisk is not None: not_accepted_reinrisks.append(reinrisk) - - return reinrisks_per_categ, not_accepted_reinrisks - def process_newrisks_insurer(self, risks_per_categ, number_risks_categ, acceptable_by_category, var_per_risk_per_categ, cash_left_by_categ, time): #This method processes one by one the risks contained in risks_per_categ in order to decide whether they should be underwritten or not. - #It is done in this way to maintain the portfolio as balanced as possible. For that reason we process risk[C1], risk[C2], risk[C3], risk[C4], risk[C1], risk[C2], ... and so forth. + def process_newrisks_insurer( + self, + risks_per_categ, + number_risks_categ, + acceptable_by_category, + var_per_risk_per_categ, + cash_left_by_categ, + time, + ): # This method processes one by one the risks contained in risks_per_categ in order to decide whether they should be underwritten or not. + # It is done in this way to maintain the portfolio as balanced as possible. For that reason we process risk[C1], risk[C2], risk[C3], risk[C4], risk[C1], risk[C2], ... and so forth. _cached_rvs = self.contract_runtime_dist.rvs() for iter in range(max(number_risks_categ)): - for categ_id in range(len(acceptable_by_category)): #Here we take only one risk per category at a time to achieve risk[C1], risk[C2], risk[C3], risk[C4], risk[C1], risk[C2], ... if possible. - if iter < number_risks_categ[categ_id] and acceptable_by_category[categ_id] > 0 and \ - risks_per_categ[categ_id][iter] is not None: + for categ_id in range( + len(acceptable_by_category) + ): # Here we take only one risk per category at a time to achieve risk[C1], risk[C2], risk[C3], risk[C4], risk[C1], risk[C2], ... if possible. + if ( + iter < number_risks_categ[categ_id] + and acceptable_by_category[categ_id] > 0 + and risks_per_categ[categ_id][iter] is not None + ): risk_to_insure = risks_per_categ[categ_id][iter] - if risk_to_insure.get("contract") is not None and risk_to_insure[ - "contract"].expiration > time: # risk_to_insure["contract"]: # required to rule out contracts that have exploded in the meantime - [condition, cash_left_by_categ] = self.balanced_portfolio(risk_to_insure, cash_left_by_categ, None) #Here it is check whether the portfolio is balanced or not if the reinrisk (risk_to_insure) is underwritten. Return True if it is balanced. False otherwise. + if ( + risk_to_insure.get("contract") is not None + and risk_to_insure["contract"].expiration > time + ): # risk_to_insure["contract"]: # required to rule out contracts that have exploded in the meantime + [condition, cash_left_by_categ] = self.balanced_portfolio( + risk_to_insure, cash_left_by_categ, None + ) # Here it is check whether the portfolio is balanced or not if the reinrisk (risk_to_insure) is underwritten. Return True if it is balanced. False otherwise. if condition: - contract = ReinsuranceContract(self, risk_to_insure, time, \ - self.simulation.get_reinsurance_market_premium(), - risk_to_insure["expiration"] - time, \ - self.default_contract_payment_period, \ - expire_immediately=self.simulation_parameters[ - "expire_immediately"], ) + contract = ReinsuranceContract( + self, + risk_to_insure, + time, + self.simulation.get_reinsurance_market_premium(), + risk_to_insure["expiration"] - time, + self.default_contract_payment_period, + expire_immediately=self.simulation_parameters[ + "expire_immediately" + ], + ) self.underwritten_contracts.append(contract) self.cash_left_by_categ = cash_left_by_categ risks_per_categ[categ_id][iter] = None # TODO: move this to insurancecontract (ca. line 14) -> DONE # TODO: do not write into other object's properties, use setter -> DONE else: - [condition, cash_left_by_categ] = self.balanced_portfolio(risk_to_insure, cash_left_by_categ, - var_per_risk_per_categ) #Here it is check whether the portfolio is balanced or not if the risk (risk_to_insure) is underwritten. Return True if it is balanced. False otherwise. + [condition, cash_left_by_categ] = self.balanced_portfolio( + risk_to_insure, cash_left_by_categ, var_per_risk_per_categ + ) # Here it is check whether the portfolio is balanced or not if the risk (risk_to_insure) is underwritten. Return True if it is balanced. False otherwise. if condition: - contract = InsuranceContract(self, risk_to_insure, time, self.simulation.get_market_premium(), \ - _cached_rvs, \ - self.default_contract_payment_period, \ - expire_immediately=self.simulation_parameters[ - "expire_immediately"], \ - initial_VaR=var_per_risk_per_categ[categ_id]) + contract = InsuranceContract( + self, + risk_to_insure, + time, + self.simulation.get_market_premium(), + _cached_rvs, + self.default_contract_payment_period, + expire_immediately=self.simulation_parameters[ + "expire_immediately" + ], + initial_VaR=var_per_risk_per_categ[categ_id], + ) self.underwritten_contracts.append(contract) self.cash_left_by_categ = cash_left_by_categ risks_per_categ[categ_id][iter] = None - acceptable_by_category[categ_id] -= 1 # TODO: allow different values per risk (i.e. sum over value (and reinsurance_share) or exposure instead of counting) + acceptable_by_category[ + categ_id + ] -= ( + 1 + ) # TODO: allow different values per risk (i.e. sum over value (and reinsurance_share) or exposure instead of counting) not_accepted_risks = [] for categ_id in range(len(acceptable_by_category)): @@ -523,42 +775,77 @@ def process_newrisks_insurer(self, risks_per_categ, number_risks_categ, acceptab return risks_per_categ, not_accepted_risks - - def market_permanency(self, time): #This method determines whether an insurer or reinsurer stays in the market. If it has very few risks underwritten or too much cash left for TOO LONG it eventually leaves the market. - # If it has very few risks underwritten it cannot balance the portfolio so it makes sense to leave the market. + def market_permanency( + self, time + ): # This method determines whether an insurer or reinsurer stays in the market. If it has very few risks underwritten or too much cash left for TOO LONG it eventually leaves the market. + # If it has very few risks underwritten it cannot balance the portfolio so it makes sense to leave the market. if not self.simulation_parameters["market_permanency_off"]: cash_left_by_categ = np.asarray(self.cash_left_by_categ) avg_cash_left = get_mean(cash_left_by_categ) - if self.cash < self.simulation_parameters["cash_permanency_limit"]: #If their level of cash is so low that they cannot underwrite anything they also leave the market. + if ( + self.cash < self.simulation_parameters["cash_permanency_limit"] + ): # If their level of cash is so low that they cannot underwrite anything they also leave the market. self.market_exit(time) else: if self.is_insurer: - if len(self.underwritten_contracts) < self.simulation_parameters["insurance_permanency_contracts_limit"] or avg_cash_left / self.cash > self.simulation_parameters["insurance_permanency_ratio_limit"]: - #Insurers leave the market if they have contracts under the limit or an excess capital over the limit for too long. + if ( + len(self.underwritten_contracts) + < self.simulation_parameters[ + "insurance_permanency_contracts_limit" + ] + or avg_cash_left / self.cash + > self.simulation_parameters["insurance_permanency_ratio_limit"] + ): + # Insurers leave the market if they have contracts under the limit or an excess capital over the limit for too long. self.market_permanency_counter += 1 else: - self.market_permanency_counter = 0 #All these limits maybe should be parameters in isleconfig.py - - if self.market_permanency_counter >= self.simulation_parameters["insurance_permanency_time_constraint"]: # Here we determine how much is too long. + self.market_permanency_counter = ( + 0 + ) # All these limits maybe should be parameters in isleconfig.py + + if ( + self.market_permanency_counter + >= self.simulation_parameters[ + "insurance_permanency_time_constraint" + ] + ): # Here we determine how much is too long. self.market_exit(time) if self.is_reinsurer: - if len(self.underwritten_contracts) < self.simulation_parameters["reinsurance_permanency_contracts_limit"] or avg_cash_left / self.cash > self.simulation_parameters["reinsurance_permanency_ratio_limit"]: - #Reinsurers leave the market if they have contracts under the limit or an excess capital over the limit for too long. - - self.market_permanency_counter += 1 #Insurers and reinsurers potentially have different reasons to leave the market. That's why the code is duplicated here. + if ( + len(self.underwritten_contracts) + < self.simulation_parameters[ + "reinsurance_permanency_contracts_limit" + ] + or avg_cash_left / self.cash + > self.simulation_parameters[ + "reinsurance_permanency_ratio_limit" + ] + ): + # Reinsurers leave the market if they have contracts under the limit or an excess capital over the limit for too long. + + self.market_permanency_counter += ( + 1 + ) # Insurers and reinsurers potentially have different reasons to leave the market. That's why the code is duplicated here. else: self.market_permanency_counter = 0 - if self.market_permanency_counter >= self.simulation_parameters["reinsurance_permanency_time_constraint"]: # Here we determine how much is too long. + if ( + self.market_permanency_counter + >= self.simulation_parameters[ + "reinsurance_permanency_time_constraint" + ] + ): # Here we determine how much is too long. self.market_exit(time) - def register_claim(self, claim): #This method records in insurancesimulation.py every claim made. It is called either from insurancecontract.py or reinsurancecontract.py respectively. + def register_claim( + self, claim + ): # This method records in insurancesimulation.py every claim made. It is called either from insurancecontract.py or reinsurancecontract.py respectively. self.simulation.record_claims(claim) def reset_pl(self): @@ -568,7 +855,7 @@ def reset_pl(self): Reset the profits and losses variable of each firm at the beginning of every iteration. It has to be run in insurancesimulation.py at the beginning of the iterate method""" self.profits_losses = 0 - def roll_over(self,time): + def roll_over(self, time): """Roll_over Method. Accepts arguments time: Type integer. The current time. No return value. @@ -581,13 +868,22 @@ def roll_over(self,time): are created and destroyed every iteration. The main reason to implemented this method is to avoid a lack of coverage that appears, if contracts are allowed to mature and are evaluated again the next iteration.""" - maturing_next = [contract for contract in self.underwritten_contracts if contract.expiration == time + 1] + maturing_next = [ + contract + for contract in self.underwritten_contracts + if contract.expiration == time + 1 + ] if self.is_insurer is True: for contract in maturing_next: contract.roll_over_flag = 1 - if np.random.uniform(0,1,1) > self.simulation_parameters["insurance_retention"]: - self.simulation.return_risks([contract.risk_data]) # TODO: This is not a retention, so the roll_over_flag might be confusing in this case + if ( + np.random.uniform(0, 1, 1) + > self.simulation_parameters["insurance_retention"] + ): + self.simulation.return_risks( + [contract.risk_data] + ) # TODO: This is not a retention, so the roll_over_flag might be confusing in this case else: self.risks_kept.append(contract.risk_data) @@ -595,14 +891,12 @@ def roll_over(self,time): for reincontract in maturing_next: if reincontract.property_holder.operational: reincontract.roll_over_flag = 1 - reinrisk = reincontract.property_holder.create_reinrisk(time, reincontract.category) - if np.random.uniform(0,1,1) < self.simulation_parameters["reinsurance_retention"]: + reinrisk = reincontract.property_holder.create_reinrisk( + time, reincontract.category + ) + if ( + np.random.uniform(0, 1, 1) + < self.simulation_parameters["reinsurance_retention"] + ): if reinrisk is not None: self.reinrisks_kept.append(reinrisk) - - - - - - - diff --git a/metaplotter.py b/metaplotter.py index 775b55e..858987e 100644 --- a/metaplotter.py +++ b/metaplotter.py @@ -5,11 +5,12 @@ import time import glob + def read_data(): # do not overwrite old pdfs - #if os.path.exists("data/fig_one_and_two_rm_comp.pdf"): + # if os.path.exists("data/fig_one_and_two_rm_comp.pdf"): # os.rename("data/fig_one_and_two_rm_comp.pdf", "data/fig_one_and_two_rm_comp_old_" + time.strftime('%Y_%b_%d_%H_%M') + ".pdf") - #if os.path.exists("data/fig_three_and_four_rm_comp.pdf"): + # if os.path.exists("data/fig_three_and_four_rm_comp.pdf"): # os.rename("data/fig_three_and_four_rm_comp.pdf", "data/fig_three_and_four_rm_comp_old_" + time.strftime('%Y_%b_%d_%H_%M') + ".pdf") upper_bound = 75 @@ -30,7 +31,12 @@ def read_data(): filenames_threes.sort() filenames_fours.sort() - assert len(filenames_ones) == len(filenames_twos) == len(filenames_threes) == len(filenames_fours) + assert ( + len(filenames_ones) + == len(filenames_twos) + == len(filenames_threes) + == len(filenames_fours) + ) all_filenames = filenames_ones + filenames_twos + filenames_threes + filenames_fours for filename in all_filenames: @@ -38,7 +44,7 @@ def read_data(): rfile = open(filename, "r") data = [eval(k) for k in rfile] rfile.close() - + # compute data series data_means = [] data_medians = [] @@ -53,21 +59,39 @@ def read_data(): data_medians = np.array(data_medians) data_q25 = np.array(data_q25) data_q75 = np.array(data_q75) - + # record data series timeseries_dict["mean"][filename] = data_means timeseries_dict["median"][filename] = data_medians timeseries_dict["quantile25"][filename] = data_q25 timeseries_dict["quantile75"][filename] = data_q75 return timeseries_dict - - -def plotting(output_label, timeseries_dict, riskmodelsetting1, riskmodelsetting2, series1, series2=None, additionalriskmodelsetting3=None, additionalriskmodelsetting4=None, plottype1="mean", plottype2="mean"): + +def plotting( + output_label, + timeseries_dict, + riskmodelsetting1, + riskmodelsetting2, + series1, + series2=None, + additionalriskmodelsetting3=None, + additionalriskmodelsetting4=None, + plottype1="mean", + plottype2="mean", +): # dictionaries colors = {"one": "red", "two": "blue", "three": "green", "four": "yellow"} - labels = {"contracts": "Contracts (Insurers)", "cash": "Liquidity (Insurers)", "operational": "Active Insurers", "premium": "Premium", "reincash": "Liquidity (Reinsurers)", "reincontracts": "Contracts (Reinsurers)", "reinoperational": "Active Reinsurers"} - + labels = { + "contracts": "Contracts (Insurers)", + "cash": "Liquidity (Insurers)", + "operational": "Active Insurers", + "premium": "Premium", + "reincash": "Liquidity (Reinsurers)", + "reincontracts": "Contracts (Reinsurers)", + "reinoperational": "Active Reinsurers", + } + # prepare labels, timeseries, etc. color1 = colors[riskmodelsetting1] color2 = colors[riskmodelsetting2] @@ -80,23 +104,33 @@ def plotting(output_label, timeseries_dict, riskmodelsetting1, riskmodelsetting2 plot_2_2 = "data/" + riskmodelsetting2 + "_" + series2 + ".dat" if additionalriskmodelsetting3 is not None: color3 = colors[additionalriskmodelsetting3] - label3 = str.upper(additionalriskmodelsetting3[0]) + additionalriskmodelsetting3[1:] + " riskmodels" + label3 = ( + str.upper(additionalriskmodelsetting3[0]) + + additionalriskmodelsetting3[1:] + + " riskmodels" + ) plot_1_3 = "data/" + additionalriskmodelsetting3 + "_" + series1 + ".dat" if series2 is not None: plot_2_3 = "data/" + additionalriskmodelsetting3 + "_" + series2 + ".dat" if additionalriskmodelsetting4 is not None: color4 = colors[additionalriskmodelsetting4] - label4 = str.upper(additionalriskmodelsetting4[0]) + additionalriskmodelsetting4[1:] + " riskmodels" + label4 = ( + str.upper(additionalriskmodelsetting4[0]) + + additionalriskmodelsetting4[1:] + + " riskmodels" + ) plot_1_4 = "data/" + additionalriskmodelsetting4 + "_" + series1 + ".dat" if series2 is not None: plot_2_4 = "data/" + additionalriskmodelsetting4 + "_" + series2 + ".dat" - + # Backup existing figures (so as not to overwrite them) outputfilename = "data/" + output_label + ".pdf" - backupfilename = "data/" + output_label + "_old_" + time.strftime('%Y_%b_%d_%H_%M') + ".pdf" + backupfilename = ( + "data/" + output_label + "_old_" + time.strftime("%Y_%b_%d_%H_%M") + ".pdf" + ) if os.path.exists(outputfilename): os.rename(outputfilename, backupfilename) - + # Plot and save fig = plt.figure() if series2 is not None: @@ -104,56 +138,190 @@ def plotting(output_label, timeseries_dict, riskmodelsetting1, riskmodelsetting2 else: ax0 = fig.add_subplot(111) if additionalriskmodelsetting3 is not None: - ax0.plot(range(len(timeseries_dict[plottype1][plot_1_3])), timeseries_dict[plottype1][plot_1_3], color=color3, label=label3) + ax0.plot( + range(len(timeseries_dict[plottype1][plot_1_3])), + timeseries_dict[plottype1][plot_1_3], + color=color3, + label=label3, + ) if additionalriskmodelsetting4 is not None: - ax0.plot(range(len(timeseries_dict[plottype1][plot_1_4])), timeseries_dict[plottype1][plot_1_4], color=color4, label=label4) - ax0.plot(range(len(timeseries_dict[plottype1][plot_1_1])), timeseries_dict[plottype1][plot_1_1], color=color1, label=label1) - ax0.plot(range(len(timeseries_dict[plottype1][plot_1_2])), timeseries_dict[plottype1][plot_1_2], color=color2, label=label2) - ax0.fill_between(range(len(timeseries_dict["quantile25"][plot_1_1])), timeseries_dict["quantile25"][plot_1_1], timeseries_dict["quantile75"][plot_1_1], facecolor=color1, alpha=0.25) - ax0.fill_between(range(len(timeseries_dict["quantile25"][plot_1_1])), timeseries_dict["quantile25"][plot_1_2], timeseries_dict["quantile75"][plot_1_2], facecolor=color2, alpha=0.25) - ax0.set_ylabel(labels[series1])#"Contracts") - ax0.legend(loc='best') + ax0.plot( + range(len(timeseries_dict[plottype1][plot_1_4])), + timeseries_dict[plottype1][plot_1_4], + color=color4, + label=label4, + ) + ax0.plot( + range(len(timeseries_dict[plottype1][plot_1_1])), + timeseries_dict[plottype1][plot_1_1], + color=color1, + label=label1, + ) + ax0.plot( + range(len(timeseries_dict[plottype1][plot_1_2])), + timeseries_dict[plottype1][plot_1_2], + color=color2, + label=label2, + ) + ax0.fill_between( + range(len(timeseries_dict["quantile25"][plot_1_1])), + timeseries_dict["quantile25"][plot_1_1], + timeseries_dict["quantile75"][plot_1_1], + facecolor=color1, + alpha=0.25, + ) + ax0.fill_between( + range(len(timeseries_dict["quantile25"][plot_1_1])), + timeseries_dict["quantile25"][plot_1_2], + timeseries_dict["quantile75"][plot_1_2], + facecolor=color2, + alpha=0.25, + ) + ax0.set_ylabel(labels[series1]) # "Contracts") + ax0.legend(loc="best") if series2 is not None: ax1 = fig.add_subplot(212) if additionalriskmodelsetting3 is not None: - ax1.plot(range(len(timeseries_dict[plottype2][plot_2_3])), timeseries_dict[plottype2][plot_2_3], color=color3, label=label3) + ax1.plot( + range(len(timeseries_dict[plottype2][plot_2_3])), + timeseries_dict[plottype2][plot_2_3], + color=color3, + label=label3, + ) if additionalriskmodelsetting4 is not None: - ax1.plot(range(len(timeseries_dict[plottype2][plot_2_4])), timeseries_dict[plottype2][plot_2_4], color=color4, label=label4) - ax1.plot(range(len(timeseries_dict[plottype2][plot_2_1])), timeseries_dict[plottype2][plot_2_1], color=color1, label=label1) - ax1.plot(range(len(timeseries_dict[plottype2][plot_2_2])), timeseries_dict[plottype2][plot_2_2], color=color2, label=label2) - ax1.fill_between(range(len(timeseries_dict["quantile25"][plot_2_1])), timeseries_dict["quantile25"][plot_2_1], timeseries_dict["quantile75"][plot_2_1], facecolor=color1, alpha=0.25) - ax1.fill_between(range(len(timeseries_dict["quantile25"][plot_2_1])), timeseries_dict["quantile25"][plot_2_2], timeseries_dict["quantile75"][plot_2_2], facecolor=color2, alpha=0.25) + ax1.plot( + range(len(timeseries_dict[plottype2][plot_2_4])), + timeseries_dict[plottype2][plot_2_4], + color=color4, + label=label4, + ) + ax1.plot( + range(len(timeseries_dict[plottype2][plot_2_1])), + timeseries_dict[plottype2][plot_2_1], + color=color1, + label=label1, + ) + ax1.plot( + range(len(timeseries_dict[plottype2][plot_2_2])), + timeseries_dict[plottype2][plot_2_2], + color=color2, + label=label2, + ) + ax1.fill_between( + range(len(timeseries_dict["quantile25"][plot_2_1])), + timeseries_dict["quantile25"][plot_2_1], + timeseries_dict["quantile75"][plot_2_1], + facecolor=color1, + alpha=0.25, + ) + ax1.fill_between( + range(len(timeseries_dict["quantile25"][plot_2_1])), + timeseries_dict["quantile25"][plot_2_2], + timeseries_dict["quantile75"][plot_2_2], + facecolor=color2, + alpha=0.25, + ) ax1.set_ylabel(labels[series2]) ax1.set_xlabel("Time") plt.savefig(outputfilename) plt.show() + timeseries = read_data() # for just two different riskmodel settings -plotting(output_label="fig_contracts_survival_1_2", timeseries_dict=timeseries, riskmodelsetting1="one", \ - riskmodelsetting2="two", series1="contracts", series2="operational", plottype1="mean", plottype2="median") -plotting(output_label="fig_reinsurers_contracts_survival_1_2", timeseries_dict=timeseries, riskmodelsetting1="one", \ - riskmodelsetting2="two", series1="reincontracts", series2="reinoperational", plottype1="mean", plottype2="median") -plotting(output_label="fig_premium_1_2", timeseries_dict=timeseries, riskmodelsetting1="one", riskmodelsetting2="two", \ - series1="premium", series2=None, plottype1="mean", plottype2=None) +plotting( + output_label="fig_contracts_survival_1_2", + timeseries_dict=timeseries, + riskmodelsetting1="one", + riskmodelsetting2="two", + series1="contracts", + series2="operational", + plottype1="mean", + plottype2="median", +) +plotting( + output_label="fig_reinsurers_contracts_survival_1_2", + timeseries_dict=timeseries, + riskmodelsetting1="one", + riskmodelsetting2="two", + series1="reincontracts", + series2="reinoperational", + plottype1="mean", + plottype2="median", +) +plotting( + output_label="fig_premium_1_2", + timeseries_dict=timeseries, + riskmodelsetting1="one", + riskmodelsetting2="two", + series1="premium", + series2=None, + plottype1="mean", + plottype2=None, +) raise SystemExit # for four different riskmodel settings -plotting(output_label="fig_contracts_survival_1_2", timeseries_dict=timeseries, riskmodelsetting1="one", \ - riskmodelsetting2="two", series1="contracts", series2="operational", additionalriskmodelsetting3="three", \ - additionalriskmodelsetting4="four", plottype1="mean", plottype2="median") -plotting(output_label="fig_contracts_survival_3_4", timeseries_dict=timeseries, riskmodelsetting1="three", \ - riskmodelsetting2="four", series1="contracts", series2="operational", additionalriskmodelsetting3="one", \ - additionalriskmodelsetting4="two", plottype1="mean", plottype2="median") -plotting(output_label="fig_reinsurers_contracts_survival_1_2", timeseries_dict=timeseries, riskmodelsetting1="one", \ - riskmodelsetting2="two", series1="reincontracts", series2="reinoperational", \ - additionalriskmodelsetting3="three", additionalriskmodelsetting4="four", plottype1="mean", plottype2="median") -plotting(output_label="fig_reinsurers_contracts_survival_3_4", timeseries_dict=timeseries, riskmodelsetting1="three", \ - riskmodelsetting2="four", series1="reincontracts", series2="reinoperational", \ - additionalriskmodelsetting3="one", additionalriskmodelsetting4="two", plottype1="mean", plottype2="median") -plotting(output_label="fig_premium_1_2", timeseries_dict=timeseries, riskmodelsetting1="one", riskmodelsetting2="two", \ - series1="premium", series2=None, additionalriskmodelsetting3="three", additionalriskmodelsetting4="four", \ - plottype1="mean", plottype2=None) - -#pdb.set_trace() +plotting( + output_label="fig_contracts_survival_1_2", + timeseries_dict=timeseries, + riskmodelsetting1="one", + riskmodelsetting2="two", + series1="contracts", + series2="operational", + additionalriskmodelsetting3="three", + additionalriskmodelsetting4="four", + plottype1="mean", + plottype2="median", +) +plotting( + output_label="fig_contracts_survival_3_4", + timeseries_dict=timeseries, + riskmodelsetting1="three", + riskmodelsetting2="four", + series1="contracts", + series2="operational", + additionalriskmodelsetting3="one", + additionalriskmodelsetting4="two", + plottype1="mean", + plottype2="median", +) +plotting( + output_label="fig_reinsurers_contracts_survival_1_2", + timeseries_dict=timeseries, + riskmodelsetting1="one", + riskmodelsetting2="two", + series1="reincontracts", + series2="reinoperational", + additionalriskmodelsetting3="three", + additionalriskmodelsetting4="four", + plottype1="mean", + plottype2="median", +) +plotting( + output_label="fig_reinsurers_contracts_survival_3_4", + timeseries_dict=timeseries, + riskmodelsetting1="three", + riskmodelsetting2="four", + series1="reincontracts", + series2="reinoperational", + additionalriskmodelsetting3="one", + additionalriskmodelsetting4="two", + plottype1="mean", + plottype2="median", +) +plotting( + output_label="fig_premium_1_2", + timeseries_dict=timeseries, + riskmodelsetting1="one", + riskmodelsetting2="two", + series1="premium", + series2=None, + additionalriskmodelsetting3="three", + additionalriskmodelsetting4="four", + plottype1="mean", + plottype2=None, +) + +# pdb.set_trace() diff --git a/metaplotter_pl_timescale.py b/metaplotter_pl_timescale.py index d261d11..f172949 100644 --- a/metaplotter_pl_timescale.py +++ b/metaplotter_pl_timescale.py @@ -5,11 +5,12 @@ import time import glob + def read_data(): # do not overwrite old pdfs - #if os.path.exists("data/fig_one_and_two_rm_comp.pdf"): + # if os.path.exists("data/fig_one_and_two_rm_comp.pdf"): # os.rename("data/fig_one_and_two_rm_comp.pdf", "data/fig_one_and_two_rm_comp_old_" + time.strftime('%Y_%b_%d_%H_%M') + ".pdf") - #if os.path.exists("data/fig_three_and_four_rm_comp.pdf"): + # if os.path.exists("data/fig_three_and_four_rm_comp.pdf"): # os.rename("data/fig_three_and_four_rm_comp.pdf", "data/fig_three_and_four_rm_comp_old_" + time.strftime('%Y_%b_%d_%H_%M') + ".pdf") upper_bound = 75 @@ -30,7 +31,7 @@ def read_data(): filenames_threes.sort() filenames_fours.sort() - #assert len(filenames_ones) == len(filenames_twos) == len(filenames_threes) == len(filenames_fours) + # assert len(filenames_ones) == len(filenames_twos) == len(filenames_threes) == len(filenames_fours) all_filenames = filenames_ones + filenames_twos + filenames_threes + filenames_fours for filename in all_filenames: @@ -38,7 +39,7 @@ def read_data(): rfile = open(filename, "r") data = [eval(k) for k in rfile] rfile.close() - + # compute data series data_means = [] data_medians = [] @@ -53,21 +54,41 @@ def read_data(): data_medians = np.array(data_medians) data_q25 = np.array(data_q25) data_q75 = np.array(data_q75) - + # record data series timeseries_dict["mean"][filename] = data_means timeseries_dict["median"][filename] = data_medians timeseries_dict["quantile25"][filename] = data_q25 timeseries_dict["quantile75"][filename] = data_q75 return timeseries_dict - - -def plotting(output_label, timeseries_dict, riskmodelsetting1, riskmodelsetting2, series1, series2=None, additionalriskmodelsetting3=None, additionalriskmodelsetting4=None, plottype1="mean", plottype2="mean"): + +def plotting( + output_label, + timeseries_dict, + riskmodelsetting1, + riskmodelsetting2, + series1, + series2=None, + additionalriskmodelsetting3=None, + additionalriskmodelsetting4=None, + plottype1="mean", + plottype2="mean", +): # dictionaries colors = {"one": "red", "two": "blue", "three": "green", "four": "yellow"} - labels = {"profitslosses": "Profits and Losses (Insurer)", "contracts": "Contracts (Insurers)", "cash": "Liquidity (Insurers)", "operational": "Active Insurers", "premium": "Premium", "reinprofitslosses": "Profits and Losses (Reinsurer)", "reincash": "Liquidity (Reinsurers)", "reincontracts": "Contracts (Reinsurers)", "reinoperational": "Active Reinsurers"} - + labels = { + "profitslosses": "Profits and Losses (Insurer)", + "contracts": "Contracts (Insurers)", + "cash": "Liquidity (Insurers)", + "operational": "Active Insurers", + "premium": "Premium", + "reinprofitslosses": "Profits and Losses (Reinsurer)", + "reincash": "Liquidity (Reinsurers)", + "reincontracts": "Contracts (Reinsurers)", + "reinoperational": "Active Reinsurers", + } + # prepare labels, timeseries, etc. color1 = colors[riskmodelsetting1] color2 = colors[riskmodelsetting2] @@ -80,23 +101,33 @@ def plotting(output_label, timeseries_dict, riskmodelsetting1, riskmodelsetting2 plot_2_2 = "data/" + riskmodelsetting2 + "_" + series2 + ".dat" if additionalriskmodelsetting3 is not None: color3 = colors[additionalriskmodelsetting3] - label3 = str.upper(additionalriskmodelsetting3[0]) + additionalriskmodelsetting3[1:] + " riskmodels" + label3 = ( + str.upper(additionalriskmodelsetting3[0]) + + additionalriskmodelsetting3[1:] + + " riskmodels" + ) plot_1_3 = "data/" + additionalriskmodelsetting3 + "_" + series1 + ".dat" if series2 is not None: plot_2_3 = "data/" + additionalriskmodelsetting3 + "_" + series2 + ".dat" if additionalriskmodelsetting4 is not None: color4 = colors[additionalriskmodelsetting4] - label4 = str.upper(additionalriskmodelsetting4[0]) + additionalriskmodelsetting4[1:] + " riskmodels" + label4 = ( + str.upper(additionalriskmodelsetting4[0]) + + additionalriskmodelsetting4[1:] + + " riskmodels" + ) plot_1_4 = "data/" + additionalriskmodelsetting4 + "_" + series1 + ".dat" if series2 is not None: plot_2_4 = "data/" + additionalriskmodelsetting4 + "_" + series2 + ".dat" - + # Backup existing figures (so as not to overwrite them) outputfilename = "data/" + output_label + ".pdf" - backupfilename = "data/" + output_label + "_old_" + time.strftime('%Y_%b_%d_%H_%M') + ".pdf" + backupfilename = ( + "data/" + output_label + "_old_" + time.strftime("%Y_%b_%d_%H_%M") + ".pdf" + ) if os.path.exists(outputfilename): os.rename(outputfilename, backupfilename) - + # Plot and save fig = plt.figure() if series2 is not None: @@ -105,39 +136,111 @@ def plotting(output_label, timeseries_dict, riskmodelsetting1, riskmodelsetting2 ax0 = fig.add_subplot(111) maxlen_plots = 0 if additionalriskmodelsetting3 is not None: - ax0.plot(range(len(timeseries_dict[plottype1][plot_1_3]))[200:], timeseries_dict[plottype1][plot_1_3][200:], color=color3, label=label3) + ax0.plot( + range(len(timeseries_dict[plottype1][plot_1_3]))[200:], + timeseries_dict[plottype1][plot_1_3][200:], + color=color3, + label=label3, + ) maxlen_plots = max(maxlen_plots, len(timeseries_dict[plottype1][plot_1_3])) if additionalriskmodelsetting4 is not None: - ax0.plot(range(len(timeseries_dict[plottype1][plot_1_4]))[200:], timeseries_dict[plottype1][plot_1_4][200:], color=color4, label=label4) + ax0.plot( + range(len(timeseries_dict[plottype1][plot_1_4]))[200:], + timeseries_dict[plottype1][plot_1_4][200:], + color=color4, + label=label4, + ) maxlen_plots = max(maxlen_plots, len(timeseries_dict[plottype1][plot_1_4])) - ax0.plot(range(len(timeseries_dict[plottype1][plot_1_1]))[200:], timeseries_dict[plottype1][plot_1_1][200:], color=color1, label=label1) - ax0.plot(range(len(timeseries_dict[plottype1][plot_1_2]))[200:], timeseries_dict[plottype1][plot_1_2][200:], color=color2, label=label2) - ax0.fill_between(range(len(timeseries_dict["quantile25"][plot_1_1]))[200:], timeseries_dict["quantile25"][plot_1_1][200:], timeseries_dict["quantile75"][plot_1_1][200:], facecolor=color1, alpha=0.25) - ax0.fill_between(range(len(timeseries_dict["quantile25"][plot_1_1]))[200:], timeseries_dict["quantile25"][plot_1_2][200:], timeseries_dict["quantile75"][plot_1_2][200:], facecolor=color2, alpha=0.25) - ax0.set_ylabel(labels[series1])#"Contracts") - maxlen_plots = max(maxlen_plots, len(timeseries_dict[plottype1][plot_1_1]), len(timeseries_dict[plottype1][plot_1_2])) - xticks = np.arange(200, maxlen_plots, step=120) + ax0.plot( + range(len(timeseries_dict[plottype1][plot_1_1]))[200:], + timeseries_dict[plottype1][plot_1_1][200:], + color=color1, + label=label1, + ) + ax0.plot( + range(len(timeseries_dict[plottype1][plot_1_2]))[200:], + timeseries_dict[plottype1][plot_1_2][200:], + color=color2, + label=label2, + ) + ax0.fill_between( + range(len(timeseries_dict["quantile25"][plot_1_1]))[200:], + timeseries_dict["quantile25"][plot_1_1][200:], + timeseries_dict["quantile75"][plot_1_1][200:], + facecolor=color1, + alpha=0.25, + ) + ax0.fill_between( + range(len(timeseries_dict["quantile25"][plot_1_1]))[200:], + timeseries_dict["quantile25"][plot_1_2][200:], + timeseries_dict["quantile75"][plot_1_2][200:], + facecolor=color2, + alpha=0.25, + ) + ax0.set_ylabel(labels[series1]) # "Contracts") + maxlen_plots = max( + maxlen_plots, + len(timeseries_dict[plottype1][plot_1_1]), + len(timeseries_dict[plottype1][plot_1_2]), + ) + xticks = np.arange(200, maxlen_plots, step=120) ax0.set_xticks(xticks) - ax0.set_xticklabels(["${0:d}$".format(int((xtc-200)/12)) for xtc in xticks]); + ax0.set_xticklabels(["${0:d}$".format(int((xtc - 200) / 12)) for xtc in xticks]) - ax0.legend(loc='best') + ax0.legend(loc="best") if series2 is not None: ax1 = fig.add_subplot(212) maxlen_plots = 0 if additionalriskmodelsetting3 is not None: - ax1.plot(range(len(timeseries_dict[plottype2][plot_2_3]))[200:], timeseries_dict[plottype2][plot_2_3][200:], color=color3, label=label3) + ax1.plot( + range(len(timeseries_dict[plottype2][plot_2_3]))[200:], + timeseries_dict[plottype2][plot_2_3][200:], + color=color3, + label=label3, + ) maxlen_plots = max(maxlen_plots, len(timeseries_dict[plottype1][plot_2_3])) if additionalriskmodelsetting4 is not None: - ax1.plot(range(len(timeseries_dict[plottype2][plot_2_4]))[200:], timeseries_dict[plottype2][plot_2_4][200:], color=color4, label=label4) + ax1.plot( + range(len(timeseries_dict[plottype2][plot_2_4]))[200:], + timeseries_dict[plottype2][plot_2_4][200:], + color=color4, + label=label4, + ) maxlen_plots = max(maxlen_plots, len(timeseries_dict[plottype1][plot_2_4])) - ax1.plot(range(len(timeseries_dict[plottype2][plot_2_1]))[200:], timeseries_dict[plottype2][plot_2_1][200:], color=color1, label=label1) - ax1.plot(range(len(timeseries_dict[plottype2][plot_2_2]))[200:], timeseries_dict[plottype2][plot_2_2][200:], color=color2, label=label2) - ax1.fill_between(range(len(timeseries_dict["quantile25"][plot_2_1]))[200:], timeseries_dict["quantile25"][plot_2_1][200:], timeseries_dict["quantile75"][plot_2_1][200:], facecolor=color1, alpha=0.25) - ax1.fill_between(range(len(timeseries_dict["quantile25"][plot_2_1]))[200:], timeseries_dict["quantile25"][plot_2_2][200:], timeseries_dict["quantile75"][plot_2_2][200:], facecolor=color2, alpha=0.25) - maxlen_plots = max(maxlen_plots, len(timeseries_dict[plottype1][plot_2_1]), len(timeseries_dict[plottype1][plot_2_2])) - xticks = np.arange(200, maxlen_plots, step=120) + ax1.plot( + range(len(timeseries_dict[plottype2][plot_2_1]))[200:], + timeseries_dict[plottype2][plot_2_1][200:], + color=color1, + label=label1, + ) + ax1.plot( + range(len(timeseries_dict[plottype2][plot_2_2]))[200:], + timeseries_dict[plottype2][plot_2_2][200:], + color=color2, + label=label2, + ) + ax1.fill_between( + range(len(timeseries_dict["quantile25"][plot_2_1]))[200:], + timeseries_dict["quantile25"][plot_2_1][200:], + timeseries_dict["quantile75"][plot_2_1][200:], + facecolor=color1, + alpha=0.25, + ) + ax1.fill_between( + range(len(timeseries_dict["quantile25"][plot_2_1]))[200:], + timeseries_dict["quantile25"][plot_2_2][200:], + timeseries_dict["quantile75"][plot_2_2][200:], + facecolor=color2, + alpha=0.25, + ) + maxlen_plots = max( + maxlen_plots, + len(timeseries_dict[plottype1][plot_2_1]), + len(timeseries_dict[plottype1][plot_2_2]), + ) + xticks = np.arange(200, maxlen_plots, step=120) ax1.set_xticks(xticks) - ax1.set_xticklabels(["${0:d}$".format(int((xtc-200)/12)) for xtc in xticks]); + ax1.set_xticklabels(["${0:d}$".format(int((xtc - 200) / 12)) for xtc in xticks]) ax1.set_ylabel(labels[series2]) ax1.set_xlabel("Years") else: @@ -145,32 +248,78 @@ def plotting(output_label, timeseries_dict, riskmodelsetting1, riskmodelsetting2 plt.savefig(outputfilename) plt.show() + timeseries = read_data() ## for just two different riskmodel settings -#plotting(output_label="fig_pl_survival_1_2", timeseries_dict=timeseries, riskmodelsetting1="one", \ +# plotting(output_label="fig_pl_survival_1_2", timeseries_dict=timeseries, riskmodelsetting1="one", \ # riskmodelsetting2="two", series1="profitslosses", series2="operational", plottype1="mean", plottype2="median") -#plotting(output_label="fig_reinsurers_pl_survival_1_2", timeseries_dict=timeseries, riskmodelsetting1="one", \ +# plotting(output_label="fig_reinsurers_pl_survival_1_2", timeseries_dict=timeseries, riskmodelsetting1="one", \ # riskmodelsetting2="two", series1="reinprofitslosses", series2="reinoperational", plottype1="mean", plottype2="median") -#plotting(output_label="fig_premium_1_2", timeseries_dict=timeseries, riskmodelsetting1="one", riskmodelsetting2="two", \ +# plotting(output_label="fig_premium_1_2", timeseries_dict=timeseries, riskmodelsetting1="one", riskmodelsetting2="two", \ # series1="premium", series2=None, plottype1="mean", plottype2=None) # -#raise SystemExit +# raise SystemExit # for four different riskmodel settings -plotting(output_label="fig_pl_survival_1_2", timeseries_dict=timeseries, riskmodelsetting1="one", \ - riskmodelsetting2="two", series1="profitslosses", series2="operational", additionalriskmodelsetting3="three", \ - additionalriskmodelsetting4="four", plottype1="mean", plottype2="median") -plotting(output_label="fig_pl_survival_3_4", timeseries_dict=timeseries, riskmodelsetting1="three", \ - riskmodelsetting2="four", series1="profitslosses", series2="operational", additionalriskmodelsetting3="one", \ - additionalriskmodelsetting4="two", plottype1="mean", plottype2="median") -plotting(output_label="fig_reinsurers_pl_survival_1_2", timeseries_dict=timeseries, riskmodelsetting1="one", \ - riskmodelsetting2="two", series1="reinprofitslosses", series2="reinoperational", \ - additionalriskmodelsetting3="three", additionalriskmodelsetting4="four", plottype1="mean", plottype2="median") -plotting(output_label="fig_reinsurers_pl_survival_3_4", timeseries_dict=timeseries, riskmodelsetting1="three", \ - riskmodelsetting2="four", series1="reinprofitslosses", series2="reinoperational", \ - additionalriskmodelsetting3="one", additionalriskmodelsetting4="two", plottype1="mean", plottype2="median") -plotting(output_label="fig_premium_1_2", timeseries_dict=timeseries, riskmodelsetting1="one", riskmodelsetting2="two", \ - series1="premium", series2=None, additionalriskmodelsetting3="three", additionalriskmodelsetting4="four", \ - plottype1="mean", plottype2=None) - -#pdb.set_trace() +plotting( + output_label="fig_pl_survival_1_2", + timeseries_dict=timeseries, + riskmodelsetting1="one", + riskmodelsetting2="two", + series1="profitslosses", + series2="operational", + additionalriskmodelsetting3="three", + additionalriskmodelsetting4="four", + plottype1="mean", + plottype2="median", +) +plotting( + output_label="fig_pl_survival_3_4", + timeseries_dict=timeseries, + riskmodelsetting1="three", + riskmodelsetting2="four", + series1="profitslosses", + series2="operational", + additionalriskmodelsetting3="one", + additionalriskmodelsetting4="two", + plottype1="mean", + plottype2="median", +) +plotting( + output_label="fig_reinsurers_pl_survival_1_2", + timeseries_dict=timeseries, + riskmodelsetting1="one", + riskmodelsetting2="two", + series1="reinprofitslosses", + series2="reinoperational", + additionalriskmodelsetting3="three", + additionalriskmodelsetting4="four", + plottype1="mean", + plottype2="median", +) +plotting( + output_label="fig_reinsurers_pl_survival_3_4", + timeseries_dict=timeseries, + riskmodelsetting1="three", + riskmodelsetting2="four", + series1="reinprofitslosses", + series2="reinoperational", + additionalriskmodelsetting3="one", + additionalriskmodelsetting4="two", + plottype1="mean", + plottype2="median", +) +plotting( + output_label="fig_premium_1_2", + timeseries_dict=timeseries, + riskmodelsetting1="one", + riskmodelsetting2="two", + series1="premium", + series2=None, + additionalriskmodelsetting3="three", + additionalriskmodelsetting4="four", + plottype1="mean", + plottype2=None, +) + +# pdb.set_trace() diff --git a/metaplotter_pl_timescale_additional_measures.py b/metaplotter_pl_timescale_additional_measures.py index 5b0c449..5b35274 100644 --- a/metaplotter_pl_timescale_additional_measures.py +++ b/metaplotter_pl_timescale_additional_measures.py @@ -5,11 +5,12 @@ import time import glob + def read_data(): # do not overwrite old pdfs - #if os.path.exists("data/fig_one_and_two_rm_comp.pdf"): + # if os.path.exists("data/fig_one_and_two_rm_comp.pdf"): # os.rename("data/fig_one_and_two_rm_comp.pdf", "data/fig_one_and_two_rm_comp_old_" + time.strftime('%Y_%b_%d_%H_%M') + ".pdf") - #if os.path.exists("data/fig_three_and_four_rm_comp.pdf"): + # if os.path.exists("data/fig_three_and_four_rm_comp.pdf"): # os.rename("data/fig_three_and_four_rm_comp.pdf", "data/fig_three_and_four_rm_comp_old_" + time.strftime('%Y_%b_%d_%H_%M') + ".pdf") upper_bound = 75 @@ -30,7 +31,7 @@ def read_data(): filenames_threes.sort() filenames_fours.sort() - #assert len(filenames_ones) == len(filenames_twos) == len(filenames_threes) == len(filenames_fours) + # assert len(filenames_ones) == len(filenames_twos) == len(filenames_threes) == len(filenames_fours) all_filenames = filenames_ones + filenames_twos + filenames_threes + filenames_fours for filename in all_filenames: @@ -38,7 +39,7 @@ def read_data(): rfile = open(filename, "r") data = [eval(k) for k in rfile] rfile.close() - + # compute data series data_means = [] data_medians = [] @@ -53,21 +54,45 @@ def read_data(): data_medians = np.array(data_medians) data_q25 = np.array(data_q25) data_q75 = np.array(data_q75) - + # record data series timeseries_dict["mean"][filename] = data_means timeseries_dict["median"][filename] = data_medians timeseries_dict["quantile25"][filename] = data_q25 timeseries_dict["quantile75"][filename] = data_q75 return timeseries_dict - - -def plotting(output_label, timeseries_dict, riskmodelsetting1, riskmodelsetting2, series1, series2=None, additionalriskmodelsetting3=None, additionalriskmodelsetting4=None, plottype1="mean", plottype2="mean"): + +def plotting( + output_label, + timeseries_dict, + riskmodelsetting1, + riskmodelsetting2, + series1, + series2=None, + additionalriskmodelsetting3=None, + additionalriskmodelsetting4=None, + plottype1="mean", + plottype2="mean", +): # dictionaries colors = {"one": "red", "two": "blue", "three": "green", "four": "yellow"} - labels = {"reinexcess_capital": "Excess Capital (Reinsurers)", "excess_capital": "Excess Capital (Insurers)", "cumulative_unrecovered_claims": "Uncovered Claims (cumulative)", "cumulative_bankruptcies": "Bankruptcies (cumulative)", "profitslosses": "Profits and Losses (Insurer)", "contracts": "Contracts (Insurers)", "cash": "Liquidity (Insurers)", "operational": "Active Insurers", "premium": "Premium", "reinprofitslosses": "Profits and Losses (Reinsurer)", "reincash": "Liquidity (Reinsurers)", "reincontracts": "Contracts (Reinsurers)", "reinoperational": "Active Reinsurers"} - + labels = { + "reinexcess_capital": "Excess Capital (Reinsurers)", + "excess_capital": "Excess Capital (Insurers)", + "cumulative_unrecovered_claims": "Uncovered Claims (cumulative)", + "cumulative_bankruptcies": "Bankruptcies (cumulative)", + "profitslosses": "Profits and Losses (Insurer)", + "contracts": "Contracts (Insurers)", + "cash": "Liquidity (Insurers)", + "operational": "Active Insurers", + "premium": "Premium", + "reinprofitslosses": "Profits and Losses (Reinsurer)", + "reincash": "Liquidity (Reinsurers)", + "reincontracts": "Contracts (Reinsurers)", + "reinoperational": "Active Reinsurers", + } + # prepare labels, timeseries, etc. color1 = colors[riskmodelsetting1] color2 = colors[riskmodelsetting2] @@ -80,23 +105,33 @@ def plotting(output_label, timeseries_dict, riskmodelsetting1, riskmodelsetting2 plot_2_2 = "data/" + riskmodelsetting2 + "_" + series2 + ".dat" if additionalriskmodelsetting3 is not None: color3 = colors[additionalriskmodelsetting3] - label3 = str.upper(additionalriskmodelsetting3[0]) + additionalriskmodelsetting3[1:] + " riskmodels" + label3 = ( + str.upper(additionalriskmodelsetting3[0]) + + additionalriskmodelsetting3[1:] + + " riskmodels" + ) plot_1_3 = "data/" + additionalriskmodelsetting3 + "_" + series1 + ".dat" if series2 is not None: plot_2_3 = "data/" + additionalriskmodelsetting3 + "_" + series2 + ".dat" if additionalriskmodelsetting4 is not None: color4 = colors[additionalriskmodelsetting4] - label4 = str.upper(additionalriskmodelsetting4[0]) + additionalriskmodelsetting4[1:] + " riskmodels" + label4 = ( + str.upper(additionalriskmodelsetting4[0]) + + additionalriskmodelsetting4[1:] + + " riskmodels" + ) plot_1_4 = "data/" + additionalriskmodelsetting4 + "_" + series1 + ".dat" if series2 is not None: plot_2_4 = "data/" + additionalriskmodelsetting4 + "_" + series2 + ".dat" - + # Backup existing figures (so as not to overwrite them) outputfilename = "data/" + output_label + ".pdf" - backupfilename = "data/" + output_label + "_old_" + time.strftime('%Y_%b_%d_%H_%M') + ".pdf" + backupfilename = ( + "data/" + output_label + "_old_" + time.strftime("%Y_%b_%d_%H_%M") + ".pdf" + ) if os.path.exists(outputfilename): os.rename(outputfilename, backupfilename) - + # Plot and save fig = plt.figure() if series2 is not None: @@ -105,39 +140,111 @@ def plotting(output_label, timeseries_dict, riskmodelsetting1, riskmodelsetting2 ax0 = fig.add_subplot(111) maxlen_plots = 0 if additionalriskmodelsetting3 is not None: - ax0.plot(range(len(timeseries_dict[plottype1][plot_1_3]))[200:], timeseries_dict[plottype1][plot_1_3][200:], color=color3, label=label3) + ax0.plot( + range(len(timeseries_dict[plottype1][plot_1_3]))[200:], + timeseries_dict[plottype1][plot_1_3][200:], + color=color3, + label=label3, + ) maxlen_plots = max(maxlen_plots, len(timeseries_dict[plottype1][plot_1_3])) if additionalriskmodelsetting4 is not None: - ax0.plot(range(len(timeseries_dict[plottype1][plot_1_4]))[200:], timeseries_dict[plottype1][plot_1_4][200:], color=color4, label=label4) + ax0.plot( + range(len(timeseries_dict[plottype1][plot_1_4]))[200:], + timeseries_dict[plottype1][plot_1_4][200:], + color=color4, + label=label4, + ) maxlen_plots = max(maxlen_plots, len(timeseries_dict[plottype1][plot_1_4])) - ax0.plot(range(len(timeseries_dict[plottype1][plot_1_1]))[200:], timeseries_dict[plottype1][plot_1_1][200:], color=color1, label=label1) - ax0.plot(range(len(timeseries_dict[plottype1][plot_1_2]))[200:], timeseries_dict[plottype1][plot_1_2][200:], color=color2, label=label2) - ax0.fill_between(range(len(timeseries_dict["quantile25"][plot_1_1]))[200:], timeseries_dict["quantile25"][plot_1_1][200:], timeseries_dict["quantile75"][plot_1_1][200:], facecolor=color1, alpha=0.25) - ax0.fill_between(range(len(timeseries_dict["quantile25"][plot_1_1]))[200:], timeseries_dict["quantile25"][plot_1_2][200:], timeseries_dict["quantile75"][plot_1_2][200:], facecolor=color2, alpha=0.25) - ax0.set_ylabel(labels[series1])#"Contracts") - maxlen_plots = max(maxlen_plots, len(timeseries_dict[plottype1][plot_1_1]), len(timeseries_dict[plottype1][plot_1_2])) - xticks = np.arange(200, maxlen_plots, step=120) + ax0.plot( + range(len(timeseries_dict[plottype1][plot_1_1]))[200:], + timeseries_dict[plottype1][plot_1_1][200:], + color=color1, + label=label1, + ) + ax0.plot( + range(len(timeseries_dict[plottype1][plot_1_2]))[200:], + timeseries_dict[plottype1][plot_1_2][200:], + color=color2, + label=label2, + ) + ax0.fill_between( + range(len(timeseries_dict["quantile25"][plot_1_1]))[200:], + timeseries_dict["quantile25"][plot_1_1][200:], + timeseries_dict["quantile75"][plot_1_1][200:], + facecolor=color1, + alpha=0.25, + ) + ax0.fill_between( + range(len(timeseries_dict["quantile25"][plot_1_1]))[200:], + timeseries_dict["quantile25"][plot_1_2][200:], + timeseries_dict["quantile75"][plot_1_2][200:], + facecolor=color2, + alpha=0.25, + ) + ax0.set_ylabel(labels[series1]) # "Contracts") + maxlen_plots = max( + maxlen_plots, + len(timeseries_dict[plottype1][plot_1_1]), + len(timeseries_dict[plottype1][plot_1_2]), + ) + xticks = np.arange(200, maxlen_plots, step=120) ax0.set_xticks(xticks) - ax0.set_xticklabels(["${0:d}$".format(int((xtc-200)/12)) for xtc in xticks]); + ax0.set_xticklabels(["${0:d}$".format(int((xtc - 200) / 12)) for xtc in xticks]) - ax0.legend(loc='best') + ax0.legend(loc="best") if series2 is not None: ax1 = fig.add_subplot(212) maxlen_plots = 0 if additionalriskmodelsetting3 is not None: - ax1.plot(range(len(timeseries_dict[plottype2][plot_2_3]))[200:], timeseries_dict[plottype2][plot_2_3][200:], color=color3, label=label3) + ax1.plot( + range(len(timeseries_dict[plottype2][plot_2_3]))[200:], + timeseries_dict[plottype2][plot_2_3][200:], + color=color3, + label=label3, + ) maxlen_plots = max(maxlen_plots, len(timeseries_dict[plottype1][plot_2_3])) if additionalriskmodelsetting4 is not None: - ax1.plot(range(len(timeseries_dict[plottype2][plot_2_4]))[200:], timeseries_dict[plottype2][plot_2_4][200:], color=color4, label=label4) + ax1.plot( + range(len(timeseries_dict[plottype2][plot_2_4]))[200:], + timeseries_dict[plottype2][plot_2_4][200:], + color=color4, + label=label4, + ) maxlen_plots = max(maxlen_plots, len(timeseries_dict[plottype1][plot_2_4])) - ax1.plot(range(len(timeseries_dict[plottype2][plot_2_1]))[200:], timeseries_dict[plottype2][plot_2_1][200:], color=color1, label=label1) - ax1.plot(range(len(timeseries_dict[plottype2][plot_2_2]))[200:], timeseries_dict[plottype2][plot_2_2][200:], color=color2, label=label2) - ax1.fill_between(range(len(timeseries_dict["quantile25"][plot_2_1]))[200:], timeseries_dict["quantile25"][plot_2_1][200:], timeseries_dict["quantile75"][plot_2_1][200:], facecolor=color1, alpha=0.25) - ax1.fill_between(range(len(timeseries_dict["quantile25"][plot_2_1]))[200:], timeseries_dict["quantile25"][plot_2_2][200:], timeseries_dict["quantile75"][plot_2_2][200:], facecolor=color2, alpha=0.25) - maxlen_plots = max(maxlen_plots, len(timeseries_dict[plottype1][plot_2_1]), len(timeseries_dict[plottype1][plot_2_2])) - xticks = np.arange(200, maxlen_plots, step=120) + ax1.plot( + range(len(timeseries_dict[plottype2][plot_2_1]))[200:], + timeseries_dict[plottype2][plot_2_1][200:], + color=color1, + label=label1, + ) + ax1.plot( + range(len(timeseries_dict[plottype2][plot_2_2]))[200:], + timeseries_dict[plottype2][plot_2_2][200:], + color=color2, + label=label2, + ) + ax1.fill_between( + range(len(timeseries_dict["quantile25"][plot_2_1]))[200:], + timeseries_dict["quantile25"][plot_2_1][200:], + timeseries_dict["quantile75"][plot_2_1][200:], + facecolor=color1, + alpha=0.25, + ) + ax1.fill_between( + range(len(timeseries_dict["quantile25"][plot_2_1]))[200:], + timeseries_dict["quantile25"][plot_2_2][200:], + timeseries_dict["quantile75"][plot_2_2][200:], + facecolor=color2, + alpha=0.25, + ) + maxlen_plots = max( + maxlen_plots, + len(timeseries_dict[plottype1][plot_2_1]), + len(timeseries_dict[plottype1][plot_2_2]), + ) + xticks = np.arange(200, maxlen_plots, step=120) ax1.set_xticks(xticks) - ax1.set_xticklabels(["${0:d}$".format(int((xtc-200)/12)) for xtc in xticks]); + ax1.set_xticklabels(["${0:d}$".format(int((xtc - 200) / 12)) for xtc in xticks]) ax1.set_ylabel(labels[series2]) ax1.set_xlabel("Years") else: @@ -145,43 +252,117 @@ def plotting(output_label, timeseries_dict, riskmodelsetting1, riskmodelsetting2 plt.savefig(outputfilename) plt.show() + timeseries = read_data() # for just two different riskmodel settings -#plotting(output_label="fig_pl_excap_1_2", timeseries_dict=timeseries, riskmodelsetting1="one", \ +# plotting(output_label="fig_pl_excap_1_2", timeseries_dict=timeseries, riskmodelsetting1="one", \ # riskmodelsetting2="two", series1="profitslosses", series2="excess_capital", plottype1="mean", plottype2="mean") -#plotting(output_label="fig_reinsurers_pl_excap_1_2", timeseries_dict=timeseries, riskmodelsetting1="one", \ +# plotting(output_label="fig_reinsurers_pl_excap_1_2", timeseries_dict=timeseries, riskmodelsetting1="one", \ # riskmodelsetting2="two", series1="reinprofitslosses", series2="reinexcess_capital", plottype1="mean", plottype2="mean") -#plotting(output_label="fig_bankruptcies_unrecovered_claims_1_2", timeseries_dict=timeseries, riskmodelsetting1="one", \ +# plotting(output_label="fig_bankruptcies_unrecovered_claims_1_2", timeseries_dict=timeseries, riskmodelsetting1="one", \ # riskmodelsetting2="two", series1="cumulative_bankruptcies", series2="cumulative_unrecovered_claims", plottype1="mean", plottype2="median") -#plotting(output_label="fig_premium_1_2", timeseries_dict=timeseries, riskmodelsetting1="one", riskmodelsetting2="two", \ +# plotting(output_label="fig_premium_1_2", timeseries_dict=timeseries, riskmodelsetting1="one", riskmodelsetting2="two", \ # series1="premium", series2=None, plottype1="mean", plottype2=None) # -#raise SystemExit +# raise SystemExit # for four different riskmodel settings -plotting(output_label="fig_pl_excap_1_2", timeseries_dict=timeseries, riskmodelsetting1="one", \ - riskmodelsetting2="two", series1="profitslosses", series2="excess_capital", additionalriskmodelsetting3="three", \ - additionalriskmodelsetting4="four", plottype1="mean", plottype2="mean") -plotting(output_label="fig_reinsurers_pl_excap_1_2", timeseries_dict=timeseries, riskmodelsetting1="one", \ - riskmodelsetting2="two", series1="reinprofitslosses", series2="reinexcess_capital", additionalriskmodelsetting3="three", \ - additionalriskmodelsetting4="four", plottype1="mean", plottype2="mean") -plotting(output_label="fig_bankruptcies_unrecovered_claims_1_2", timeseries_dict=timeseries, riskmodelsetting1="one", \ - riskmodelsetting2="two", series1="cumulative_bankruptcies", series2="cumulative_unrecovered_claims", additionalriskmodelsetting3="three", \ - additionalriskmodelsetting4="four", plottype1="mean", plottype2="median") -plotting(output_label="fig_premium_1_2", timeseries_dict=timeseries, riskmodelsetting1="one", riskmodelsetting2="two", \ - series1="premium", series2=None, additionalriskmodelsetting3="three", additionalriskmodelsetting4="four", plottype1="mean", plottype2=None) - -plotting(output_label="fig_pl_excap_3_4", timeseries_dict=timeseries, riskmodelsetting1="three", \ - riskmodelsetting2="four", series1="profitslosses", series2="excess_capital", additionalriskmodelsetting3="one", \ - additionalriskmodelsetting4="two", plottype1="mean", plottype2="mean") -plotting(output_label="fig_reinsurers_pl_excap_3_4", timeseries_dict=timeseries, riskmodelsetting1="three", \ - riskmodelsetting2="four", series1="reinprofitslosses", series2="reinexcess_capital", additionalriskmodelsetting3="one", \ - additionalriskmodelsetting4="two", plottype1="mean", plottype2="mean") -plotting(output_label="fig_bankruptcies_unrecovered_claims_3_4", timeseries_dict=timeseries, riskmodelsetting1="three", \ - riskmodelsetting2="four", series1="cumulative_bankruptcies", series2="cumulative_unrecovered_claims", additionalriskmodelsetting3="one", \ - additionalriskmodelsetting4="two", plottype1="mean", plottype2="median") -plotting(output_label="fig_premium_3_4", timeseries_dict=timeseries, riskmodelsetting1="three", riskmodelsetting2="four", \ - series1="premium", series2=None, additionalriskmodelsetting3="one", additionalriskmodelsetting4="two", plottype1="mean", plottype2=None) - - -#pdb.set_trace() +plotting( + output_label="fig_pl_excap_1_2", + timeseries_dict=timeseries, + riskmodelsetting1="one", + riskmodelsetting2="two", + series1="profitslosses", + series2="excess_capital", + additionalriskmodelsetting3="three", + additionalriskmodelsetting4="four", + plottype1="mean", + plottype2="mean", +) +plotting( + output_label="fig_reinsurers_pl_excap_1_2", + timeseries_dict=timeseries, + riskmodelsetting1="one", + riskmodelsetting2="two", + series1="reinprofitslosses", + series2="reinexcess_capital", + additionalriskmodelsetting3="three", + additionalriskmodelsetting4="four", + plottype1="mean", + plottype2="mean", +) +plotting( + output_label="fig_bankruptcies_unrecovered_claims_1_2", + timeseries_dict=timeseries, + riskmodelsetting1="one", + riskmodelsetting2="two", + series1="cumulative_bankruptcies", + series2="cumulative_unrecovered_claims", + additionalriskmodelsetting3="three", + additionalriskmodelsetting4="four", + plottype1="mean", + plottype2="median", +) +plotting( + output_label="fig_premium_1_2", + timeseries_dict=timeseries, + riskmodelsetting1="one", + riskmodelsetting2="two", + series1="premium", + series2=None, + additionalriskmodelsetting3="three", + additionalriskmodelsetting4="four", + plottype1="mean", + plottype2=None, +) + +plotting( + output_label="fig_pl_excap_3_4", + timeseries_dict=timeseries, + riskmodelsetting1="three", + riskmodelsetting2="four", + series1="profitslosses", + series2="excess_capital", + additionalriskmodelsetting3="one", + additionalriskmodelsetting4="two", + plottype1="mean", + plottype2="mean", +) +plotting( + output_label="fig_reinsurers_pl_excap_3_4", + timeseries_dict=timeseries, + riskmodelsetting1="three", + riskmodelsetting2="four", + series1="reinprofitslosses", + series2="reinexcess_capital", + additionalriskmodelsetting3="one", + additionalriskmodelsetting4="two", + plottype1="mean", + plottype2="mean", +) +plotting( + output_label="fig_bankruptcies_unrecovered_claims_3_4", + timeseries_dict=timeseries, + riskmodelsetting1="three", + riskmodelsetting2="four", + series1="cumulative_bankruptcies", + series2="cumulative_unrecovered_claims", + additionalriskmodelsetting3="one", + additionalriskmodelsetting4="two", + plottype1="mean", + plottype2="median", +) +plotting( + output_label="fig_premium_3_4", + timeseries_dict=timeseries, + riskmodelsetting1="three", + riskmodelsetting2="four", + series1="premium", + series2=None, + additionalriskmodelsetting3="one", + additionalriskmodelsetting4="two", + plottype1="mean", + plottype2=None, +) + +# pdb.set_trace() diff --git a/plotter.py b/plotter.py index 593b95f..fd348d2 100755 --- a/plotter.py +++ b/plotter.py @@ -1,20 +1,20 @@ import matplotlib.pyplot as plt import numpy as np -rfile = open("data/history_logs.dat","r") +rfile = open("data/history_logs.dat", "r") data = [eval(k) for k in rfile] -contracts = data[0]['total_contracts'] -op = data[0]['total_operational'] -cash = data[0]['total_cash'] -pl = data[0]['total_profitslosses'] -reincontracts = data[0]['total_reincontracts'] -reinop = data[0]['total_reinoperational'] -reincash = data[0]['total_reincash'] -reinpl = data[0]['total_reinprofitslosses'] -premium = data[0]['market_premium'] -catbop = data[0]['total_catbondsoperational'] +contracts = data[0]["total_contracts"] +op = data[0]["total_operational"] +cash = data[0]["total_cash"] +pl = data[0]["total_profitslosses"] +reincontracts = data[0]["total_reincontracts"] +reinop = data[0]["total_reinoperational"] +reincash = data[0]["total_reincash"] +reinpl = data[0]["total_reinprofitslosses"] +premium = data[0]["market_premium"] +catbop = data[0]["total_catbondsoperational"] rfile.close() @@ -34,22 +34,22 @@ fig1 = plt.figure() ax0 = fig1.add_subplot(511) ax0.get_xaxis().set_visible(False) -ax0.plot(range(len(cs)), cs,"b") +ax0.plot(range(len(cs)), cs, "b") ax0.set_ylabel("Contracts") ax1 = fig1.add_subplot(512) ax1.get_xaxis().set_visible(False) -ax1.plot(range(len(os)), os,"b") +ax1.plot(range(len(os)), os, "b") ax1.set_ylabel("Active firms") ax2 = fig1.add_subplot(513) ax2.get_xaxis().set_visible(False) -ax2.plot(range(len(hs)), hs,"b") +ax2.plot(range(len(hs)), hs, "b") ax2.set_ylabel("Cash") ax3 = fig1.add_subplot(514) ax3.get_xaxis().set_visible(False) -ax3.plot(range(len(pls)), pls,"b") +ax3.plot(range(len(pls)), pls, "b") ax3.set_ylabel("Profits, Losses") ax9 = fig1.add_subplot(515) -ax9.plot(range(len(ps)), ps,"k") +ax9.plot(range(len(ps)), ps, "k") ax9.set_ylabel("Premium") ax9.set_xlabel("Time") plt.savefig("data/single_replication_pt1.pdf") @@ -57,22 +57,22 @@ fig2 = plt.figure() ax4 = fig2.add_subplot(511) ax4.get_xaxis().set_visible(False) -ax4.plot(range(len(cre)), cre,"r") +ax4.plot(range(len(cre)), cre, "r") ax4.set_ylabel("Contracts") ax5 = fig2.add_subplot(512) ax5.get_xaxis().set_visible(False) -ax5.plot(range(len(ore)), ore,"r") +ax5.plot(range(len(ore)), ore, "r") ax5.set_ylabel("Active reinfirms") ax6 = fig2.add_subplot(513) ax6.get_xaxis().set_visible(False) -ax6.plot(range(len(hre)), hre,"r") +ax6.plot(range(len(hre)), hre, "r") ax6.set_ylabel("Cash") ax7 = fig2.add_subplot(514) ax7.get_xaxis().set_visible(False) -ax7.plot(range(len(plre)), plre,"r") +ax7.plot(range(len(plre)), plre, "r") ax7.set_ylabel("Profits, Losses") ax8 = fig2.add_subplot(515) -ax8.plot(range(len(ocb)), ocb,"m") +ax8.plot(range(len(ocb)), ocb, "m") ax8.set_ylabel("Active cat bonds") ax8.set_xlabel("Time") diff --git a/plotter_pl_timescale.py b/plotter_pl_timescale.py index 4f517ff..7d56744 100644 --- a/plotter_pl_timescale.py +++ b/plotter_pl_timescale.py @@ -1,12 +1,14 @@ import matplotlib.pyplot as plt import numpy as np + def get_data(name): rfile = open(name, "r") out = [eval(k) for k in rfile] rfile.close() return out + contracts = get_data("data/contracts.dat") op = get_data("data/operational.dat") cash = get_data("data/cash.dat") @@ -30,7 +32,7 @@ def get_data(name): c_re = [] -o_re= [] +o_re = [] h_re = [] @@ -40,18 +42,18 @@ def get_data(name): p_e = [] -for i in range(len(contracts[0])): #for every time period i +for i in range(len(contracts[0])): # for every time period i cs = np.mean([item[i] for item in contracts]) - #pls = np.mean([item[i] for item in pl]) + # pls = np.mean([item[i] for item in pl]) os = np.median([item[i] for item in op]) hs = np.median([item[i] for item in cash]) c_s.append(cs) o_s.append(os) h_s.append(hs) - if i>0: - pls = np.mean([item[i]-item[i-1] for item in cash]) - plre = np.mean([item[i]-item[i-1] for item in reincash]) + if i > 0: + pls = np.mean([item[i] - item[i - 1] for item in cash]) + plre = np.mean([item[i] - item[i - 1] for item in reincash]) pl_s.append(pls) pl_re.append(plre) @@ -61,47 +63,44 @@ def get_data(name): c_re.append(cre) o_re.append(ore) h_re.append(hre) - + ocb = np.median([item[i] for item in catbop]) o_cb.append(ocb) - + p_s = np.median([item[i] for item in premium]) p_e.append(p_s) - maxlen_plots = max(len(pl_s), len(pl_re), len(o_s), len(o_re), len(p_e)) -xticks = np.arange(200, maxlen_plots, step=120) +xticks = np.arange(200, maxlen_plots, step=120) fig0 = plt.figure() ax3 = fig0.add_subplot(511) -ax3.plot(range(len(pl_s))[200:], pl_s[200:],"b") +ax3.plot(range(len(pl_s))[200:], pl_s[200:], "b") ax3.set_ylabel("Profits, Losses") ax3.set_xticks(xticks) -ax3.set_xticklabels(["${0:d}$".format(int((xtc-200)/12)) for xtc in xticks]) +ax3.set_xticklabels(["${0:d}$".format(int((xtc - 200) / 12)) for xtc in xticks]) ax7 = fig0.add_subplot(512) -ax7.plot(range(len(pl_re))[200:], pl_re[200:],"r") +ax7.plot(range(len(pl_re))[200:], pl_re[200:], "r") ax7.set_ylabel("Profits, Losses (Reins.)") ax7.set_xticks(xticks) -ax7.set_xticklabels(["${0:d}$".format(int((xtc-200)/12)) for xtc in xticks]) +ax7.set_xticklabels(["${0:d}$".format(int((xtc - 200) / 12)) for xtc in xticks]) ax1 = fig0.add_subplot(513) -ax1.plot(range(len(o_s))[200:], o_s[200:],"b") +ax1.plot(range(len(o_s))[200:], o_s[200:], "b") ax1.set_ylabel("Active firms") ax1.set_xticks(xticks) -ax1.set_xticklabels(["${0:d}$".format(int((xtc-200)/12)) for xtc in xticks]) +ax1.set_xticklabels(["${0:d}$".format(int((xtc - 200) / 12)) for xtc in xticks]) ax5 = fig0.add_subplot(514) -ax5.plot(range(len(o_re))[200:], o_re[200:],"r") +ax5.plot(range(len(o_re))[200:], o_re[200:], "r") ax5.set_ylabel("Active reins. firms") ax5.set_xticks(xticks) -ax5.set_xticklabels(["${0:d}$".format(int((xtc-200)/12)) for xtc in xticks]) +ax5.set_xticklabels(["${0:d}$".format(int((xtc - 200) / 12)) for xtc in xticks]) ax9 = fig0.add_subplot(515) -ax9.plot(range(len(p_e))[200:], p_e[200:],"k") +ax9.plot(range(len(p_e))[200:], p_e[200:], "k") ax9.set_ylabel("Premium") ax9.set_xlabel("Years") ax9.set_xticks(xticks) -ax9.set_xticklabels(["${0:d}$".format(int((xtc-200)/12)) for xtc in xticks]) - +ax9.set_xticklabels(["${0:d}$".format(int((xtc - 200) / 12)) for xtc in xticks]) plt.savefig("data/single_replication_new.pdf") plt.show() - raise SystemExit diff --git a/reinsurancecontract.py b/reinsurancecontract.py index c9ced5a..86d011d 100644 --- a/reinsurancecontract.py +++ b/reinsurancecontract.py @@ -1,6 +1,7 @@ import numpy as np -from metainsurancecontract import MetaInsuranceContract +from metainsurancecontract import MetaInsuranceContract + class ReinsuranceContract(MetaInsuranceContract): """ReinsuranceContract class. @@ -9,18 +10,48 @@ class ReinsuranceContract(MetaInsuranceContract): and ReinsuranceContract objects. The signature of this class' constructor is the same as that of the InsuranceContract constructor. The class has two methods (explode, mature) that overwrite methods in InsuranceContract.""" - def __init__(self, insurer, properties, time, premium, runtime, payment_period, expire_immediately, initial_VaR=0.,\ - insurancetype="proportional", deductible_fraction=None, excess_fraction=None, reinsurance=0): - super(ReinsuranceContract, self).__init__(insurer, properties, time, premium, runtime, payment_period, \ - expire_immediately, initial_VaR, insurancetype, deductible_fraction, excess_fraction, reinsurance) - #self.is_reinsurancecontract = True - + + def __init__( + self, + insurer, + properties, + time, + premium, + runtime, + payment_period, + expire_immediately, + initial_VaR=0.0, + insurancetype="proportional", + deductible_fraction=None, + excess_fraction=None, + reinsurance=0, + ): + super(ReinsuranceContract, self).__init__( + insurer, + properties, + time, + premium, + runtime, + payment_period, + expire_immediately, + initial_VaR, + insurancetype, + deductible_fraction, + excess_fraction, + reinsurance, + ) + # self.is_reinsurancecontract = True + if self.insurancetype == "excess-of-loss": - self.property_holder.add_reinsurance(category=self.category, excess_fraction=self.excess_fraction, \ - deductible_fraction=self.deductible_fraction, contract=self) + self.property_holder.add_reinsurance( + category=self.category, + excess_fraction=self.excess_fraction, + deductible_fraction=self.deductible_fraction, + contract=self, + ) else: assert self.contract is not None - + def explode(self, time, damage_extent=None): """Explode method. Accepts agruments @@ -34,19 +65,25 @@ def explode(self, time, damage_extent=None): if self.insurancetype == "excess-of-loss" and damage_extent > self.deductible: claim = min(self.excess, damage_extent) - self.deductible - self.insurer.receive_obligation(claim, self.property_holder, time, 'claim') + self.insurer.receive_obligation(claim, self.property_holder, time, "claim") else: claim = min(self.excess, damage_extent) - self.deductible - self.insurer.receive_obligation(claim, self.property_holder, time + 1, 'claim') + self.insurer.receive_obligation( + claim, self.property_holder, time + 1, "claim" + ) # Reinsurer pays as soon as possible. - self.insurer.register_claim(claim) #Every reinsurance claim made is immediately registered. + self.insurer.register_claim( + claim + ) # Every reinsurance claim made is immediately registered. if self.expire_immediately: - self.current_claim += self.contract.claim # TODO: should proportional reinsurance claims be subject to excess_of_loss retrocession? If so, reorganize more straightforwardly - + self.current_claim += ( + self.contract.claim + ) # TODO: should proportional reinsurance claims be subject to excess_of_loss retrocession? If so, reorganize more straightforwardly + self.expiration = time - #self.terminating = True - + # self.terminating = True + def mature(self, time): """Mature method. Accepts arguments @@ -54,12 +91,15 @@ def mature(self, time): No return value. Removes any reinsurance functions this contract has and terminates any reinsurance contracts for this contract.""" - #self.terminating = True + # self.terminating = True self.terminate_reinsurance(time) - + if self.insurancetype == "excess-of-loss": - self.property_holder.delete_reinsurance(category=self.category, excess_fraction=self.excess_fraction, \ - deductible_fraction=self.deductible_fraction, contract=self) - else: #TODO: ? Instead: if self.insurancetype == "proportional": + self.property_holder.delete_reinsurance( + category=self.category, + excess_fraction=self.excess_fraction, + deductible_fraction=self.deductible_fraction, + contract=self, + ) + else: # TODO: ? Instead: if self.insurancetype == "proportional": self.contract.unreinsure() - diff --git a/reinsurancefirm.py b/reinsurancefirm.py index 1c1558b..ac2564f 100644 --- a/reinsurancefirm.py +++ b/reinsurancefirm.py @@ -1,9 +1,11 @@ -#from metainsuranceorg import MetaInsuranceOrg +# from metainsuranceorg import MetaInsuranceOrg from insurancefirm import InsuranceFirm + class ReinsuranceFirm(InsuranceFirm): """ReinsuranceFirm class. Inherits from InsuranceFirm.""" + def init(self, simulation_parameters, agent_parameters): """Constructor method. Accepts arguments diff --git a/resume.py b/resume.py index 7c60700..e122c94 100644 --- a/resume.py +++ b/resume.py @@ -6,7 +6,7 @@ import argparse import pickle import hashlib -import random +import random # import config file and apply configuration import isleconfig @@ -16,14 +16,37 @@ override_no_riskmodels = False # use argparse to handle command line arguments -parser = argparse.ArgumentParser(description='Model the Insurance sector') +parser = argparse.ArgumentParser(description="Model the Insurance sector") parser.add_argument("--abce", action="store_true", help="use abce") -parser.add_argument("--oneriskmodel", action="store_true", help="allow overriding the number of riskmodels from the standard config (with 1)") -parser.add_argument("--riskmodels", type=int, choices=[1,2,3,4], help="allow overriding the number of riskmodels from standard config (with 1 or other numbers)") -parser.add_argument("--replicid", type=int, help="if replication ID is given, pass this to the simulation so that the risk profile can be restored") -parser.add_argument("--replicating", action="store_true", help="if this is a simulation run designed to replicate another, override the config file parameter") -parser.add_argument("--randomseed", type=float, help="allow setting of numpy random seed") -parser.add_argument("--foreground", action="store_true", help="force foreground runs even if replication ID is given (which defaults to background runs)") +parser.add_argument( + "--oneriskmodel", + action="store_true", + help="allow overriding the number of riskmodels from the standard config (with 1)", +) +parser.add_argument( + "--riskmodels", + type=int, + choices=[1, 2, 3, 4], + help="allow overriding the number of riskmodels from standard config (with 1 or other numbers)", +) +parser.add_argument( + "--replicid", + type=int, + help="if replication ID is given, pass this to the simulation so that the risk profile can be restored", +) +parser.add_argument( + "--replicating", + action="store_true", + help="if this is a simulation run designed to replicate another, override the config file parameter", +) +parser.add_argument( + "--randomseed", type=float, help="allow setting of numpy random seed" +) +parser.add_argument( + "--foreground", + action="store_true", + help="force foreground runs even if replication ID is given (which defaults to background runs)", +) parser.add_argument("-p", "--showprogress", action="store_true", help="show timesteps") parser.add_argument("-v", "--verbose", action="store_true", help="more detailed output") args = parser.parse_args() @@ -39,7 +62,9 @@ replic_ID = args.replicid if args.replicating: isleconfig.replicating = True - assert replic_ID is not None, "Error: Replication requires a replication ID to identify run to be replicated" + assert ( + replic_ID is not None + ), "Error: Replication requires a replication ID to identify run to be replicated" if args.randomseed: randomseed = args.randomseed seed = int(randomseed) @@ -55,7 +80,7 @@ # import isle and abce modules if isleconfig.use_abce: - #print("Importing abce") + # print("Importing abce") import abce from abce import gui @@ -64,27 +89,30 @@ from riskmodel import RiskModel from reinsurancefirm import ReinsuranceFirm + # create conditional decorator def conditionally(decorator_function, condition): def wrapper(target_function): if not condition: return target_function return decorator_function(target_function) + return wrapper + # create non-abce placeholder gui decorator # TODO: replace this with more elegant solution if possible. Currently required since script will otherwise crash at the conditional decorator below since gui is then undefined if not isleconfig.use_abce: + def gui(*args, **kwargs): pass # main function -#@gui(simulation_parameters, serve=True) +# @gui(simulation_parameters, serve=True) @conditionally(gui(simulation_parameters, serve=False), isleconfig.use_abce) def main(): - with open("data/simulation_save.pkl", "br") as rfile: d = pickle.load(rfile) simulation = d["simulation"] @@ -93,106 +121,120 @@ def main(): random_seed = d["random_seed"] time = d["time"] simulation_parameters = d["simulation_parameters"] - + insurancefirms_group = list(simulation.insurancefirms) reinsurancefirms_group = list(simulation.reinsurancefirms) - - #np.random.seed(seed) + + # np.random.seed(seed) np.random.set_state(np_seed) random.setstate(random_seed) - + assert not isleconfig.use_abce, "Resuming will not work with abce" ## create simulation and world objects (identical in non-abce mode) - #if isleconfig.use_abce: + # if isleconfig.use_abce: # simulation = abce.Simulation(processes=1,random_seed = seed) # - - #simulation_parameters['simulation'] = world = InsuranceSimulation(override_no_riskmodels, replic_ID, simulation_parameters) + + # simulation_parameters['simulation'] = world = InsuranceSimulation(override_no_riskmodels, replic_ID, simulation_parameters) # - #if not isleconfig.use_abce: + # if not isleconfig.use_abce: # simulation = world # - # create agents: insurance firms - #insurancefirms_group = simulation.build_agents(InsuranceFirm, + # create agents: insurance firms + # insurancefirms_group = simulation.build_agents(InsuranceFirm, # 'insurancefirm', # parameters=simulation_parameters, # agent_parameters=world.agent_parameters["insurancefirm"]) # - #if isleconfig.use_abce: + # if isleconfig.use_abce: # insurancefirm_pointers = insurancefirms_group.get_pointer() - #else: + # else: # insurancefirm_pointers = insurancefirms_group - #world.accept_agents("insurancefirm", insurancefirm_pointers, insurancefirms_group) + # world.accept_agents("insurancefirm", insurancefirm_pointers, insurancefirms_group) # - # create agents: reinsurance firms - #reinsurancefirms_group = simulation.build_agents(ReinsuranceFirm, + # create agents: reinsurance firms + # reinsurancefirms_group = simulation.build_agents(ReinsuranceFirm, # 'reinsurance', # parameters=simulation_parameters, # agent_parameters=world.agent_parameters["reinsurance"]) - #if isleconfig.use_abce: + # if isleconfig.use_abce: # reinsurancefirm_pointers = reinsurancefirms_group.get_pointer() - #else: + # else: # reinsurancefirm_pointers = reinsurancefirms_group - #world.accept_agents("reinsurance", reinsurancefirm_pointers, reinsurancefirms_group) + # world.accept_agents("reinsurance", reinsurancefirm_pointers, reinsurancefirms_group) # - + # time iteration for t in range(time, simulation_parameters["max_time"]): - + # abce time step simulation.advance_round(t) - + # create new agents # TODO: write method for this; this code block is executed almost identically 4 times if world.insurance_firm_market_entry(agent_type="InsuranceFirm"): parameters = [np.random.choice(world.agent_parameters["insurancefirm"])] parameters[0]["id"] = world.get_unique_insurer_id() - new_insurance_firm = simulation.build_agents(InsuranceFirm, - 'insurancefirm', - parameters=simulation_parameters, - agent_parameters=parameters) + new_insurance_firm = simulation.build_agents( + InsuranceFirm, + "insurancefirm", + parameters=simulation_parameters, + agent_parameters=parameters, + ) insurancefirms_group += new_insurance_firm if isleconfig.use_abce: # TODO: fix abce # may fail in abce because addressing individual agents may not be allowed # may also fail because agent methods may not be callable directly - new_insurancefirm_pointer = [new_insurance_firm[0].get_pointer()] # index 0 because this is a list with just 1 object + new_insurancefirm_pointer = [ + new_insurance_firm[0].get_pointer() + ] # index 0 because this is a list with just 1 object else: new_insurancefirm_pointer = new_insurance_firm - world.accept_agents("insurancefirm", new_insurancefirm_pointer, new_insurance_firm, time=t) - + world.accept_agents( + "insurancefirm", new_insurancefirm_pointer, new_insurance_firm, time=t + ) + if world.insurance_firm_market_entry(agent_type="ReinsuranceFirm"): parameters = [np.random.choice(world.agent_parameters["reinsurance"])] parameters[0]["id"] = world.get_unique_reinsurer_id() - new_reinsurance_firm = simulation.build_agents(ReinsuranceFirm, - 'reinsurance', - parameters=simulation_parameters, - agent_parameters=parameters) + new_reinsurance_firm = simulation.build_agents( + ReinsuranceFirm, + "reinsurance", + parameters=simulation_parameters, + agent_parameters=parameters, + ) reinsurancefirms_group += new_reinsurance_firm if isleconfig.use_abce: # TODO: fix abce # may fail in abce because addressing individual agents may not be allowed # may also fail because agent methods may not be callable directly - new_reinsurancefirm_pointer = [new_reinsurance_firm[0].get_pointer()] # index 0 because this is a list with just 1 object + new_reinsurancefirm_pointer = [ + new_reinsurance_firm[0].get_pointer() + ] # index 0 because this is a list with just 1 object else: new_reinsurancefirm_pointer = new_reinsurance_firm - world.accept_agents("reinsurance", new_reinsurancefirm_pointer, new_reinsurance_firm, time=t) - + world.accept_agents( + "reinsurance", new_reinsurancefirm_pointer, new_reinsurance_firm, time=t + ) + # iterate simulation world.iterate(t) - + # log data if isleconfig.use_abce: - #insurancefirms.logme() - #reinsurancefirms.logme() - insurancefirms_group.agg_log(variables=['cash', 'operational'], len=['underwritten_contracts']) - #reinsurancefirms_group.agg_log(variables=['cash']) + # insurancefirms.logme() + # reinsurancefirms.logme() + insurancefirms_group.agg_log( + variables=["cash", "operational"], len=["underwritten_contracts"] + ) + # reinsurancefirms_group.agg_log(variables=['cash']) else: world.save_data() - - if t > 0 and t//50 == t/50: + + if t > 0 and t // 50 == t / 50: save_simulation(t, simulation, simulation_parameters, exit_now=False) - #print("here") - + # print("here") + # finish simulation, write logs simulation.finalize() @@ -209,12 +251,15 @@ def save_simulation(t, sim, sim_param, exit_now=False): pickle.dump(d, wfile, protocol=pickle.HIGHEST_PROTOCOL) with open("data/simulation_resave.pkl", "br") as rfile: file_contents = rfile.read() - #print("\nSimulation hashes: ", hashlib.sha512(str(d).encode()).hexdigest(), "; ", hashlib.sha512(str(file_contents).encode()).hexdigest()) - # note that the hash over the dict is for some reason not identical between runs. The hash over the state saved to the file is. - print("\nSimulation hash: ", hashlib.sha512(str(file_contents).encode()).hexdigest()) + # print("\nSimulation hashes: ", hashlib.sha512(str(d).encode()).hexdigest(), "; ", hashlib.sha512(str(file_contents).encode()).hexdigest()) + # note that the hash over the dict is for some reason not identical between runs. The hash over the state saved to the file is. + print( + "\nSimulation hash: ", hashlib.sha512(str(file_contents).encode()).hexdigest() + ) if exit_now: exit(0) - + + # main entry point if __name__ == "__main__": main() diff --git a/riskmodel.py b/riskmodel.py index 4769032..22d183a 100644 --- a/riskmodel.py +++ b/riskmodel.py @@ -1,4 +1,3 @@ - import math import numpy as np import sys, pdb @@ -7,10 +6,21 @@ from distributionreinsurance import ReinsuranceDistWrapper -class RiskModel(): - def __init__(self, damage_distribution, expire_immediately, cat_separation_distribution, norm_premium, \ - category_number, init_average_exposure, init_average_risk_factor, init_profit_estimate, \ - margin_of_safety, var_tail_prob, inaccuracy): +class RiskModel: + def __init__( + self, + damage_distribution, + expire_immediately, + cat_separation_distribution, + norm_premium, + category_number, + init_average_exposure, + init_average_risk_factor, + init_profit_estimate, + margin_of_safety, + var_tail_prob, + inaccuracy, + ): self.cat_separation_distribution = cat_separation_distribution self.norm_premium = norm_premium self.var_tail_prob = 0.02 @@ -22,67 +32,69 @@ def __init__(self, damage_distribution, expire_immediately, cat_separation_distr self.margin_of_safety = margin_of_safety """damage_distribution is some scipy frozen rv distribution wich is bound between 0 and 1 and indicates the share of risks suffering damage as part of any single catastrophic peril""" - self.damage_distribution = [damage_distribution for _ in range(self.category_number)] # TODO: separate that category wise? -> DONE. - self.damage_distribution_stack = [[] for _ in range(self.category_number)] - self.reinsurance_contract_stack = [[] for _ in range(self.category_number)] - #self.inaccuracy = np.random.uniform(9/10., 10/9., size=self.category_number) + self.damage_distribution = [ + damage_distribution for _ in range(self.category_number) + ] # TODO: separate that category wise? -> DONE. + self.damage_distribution_stack = [[] for _ in range(self.category_number)] + self.reinsurance_contract_stack = [[] for _ in range(self.category_number)] + # self.inaccuracy = np.random.uniform(9/10., 10/9., size=self.category_number) self.inaccuracy = inaccuracy - + def getPPF(self, categ_id, tailSize): """Method for getting quantile function of the damage distribution (value at risk) by category. Positional arguments: categ_id integer: category tailSize (float >=0, <=1): quantile Returns value-at-risk.""" - return self.damage_distribution[categ_id].ppf(1-tailSize) + return self.damage_distribution[categ_id].ppf(1 - tailSize) def get_categ_risks(self, risks, categ_id): - #categ_risks2 = [risk for risk in risks if risk["category"]==categ_id] + # categ_risks2 = [risk for risk in risks if risk["category"]==categ_id] categ_risks = [] for risk in risks: - if risk["category"]==categ_id: + if risk["category"] == categ_id: categ_risks.append(risk) - #assert categ_risks == categ_risks2 + # assert categ_risks == categ_risks2 return categ_risks - def compute_expectation(self, categ_risks, categ_id): #TODO: more intuitive name? - #average_exposure2 = np.mean([risk["excess"]-risk["deductible"] for risk in categ_risks]) + def compute_expectation(self, categ_risks, categ_id): # TODO: more intuitive name? + # average_exposure2 = np.mean([risk["excess"]-risk["deductible"] for risk in categ_risks]) # ##average_risk_factor = np.mean([risk["risk_factor"] for risk in categ_risks]) - #average_risk_factor2 = self.inaccuracy[categ_id] * np.mean([risk["risk_factor"] for risk in categ_risks]) + # average_risk_factor2 = self.inaccuracy[categ_id] * np.mean([risk["risk_factor"] for risk in categ_risks]) # ## compute expected profits from category - #mean_runtime2 = np.mean([risk["runtime"] for risk in categ_risks]) - + # mean_runtime2 = np.mean([risk["runtime"] for risk in categ_risks]) + exposures = [] risk_factors = [] runtimes = [] for risk in categ_risks: # TODO: factor in excess instead of value? - exposures.append(risk["value"]-risk["deductible"]) + exposures.append(risk["value"] - risk["deductible"]) risk_factors.append(risk["risk_factor"]) runtimes.append(risk["runtime"]) average_exposure = np.mean(exposures) average_risk_factor = self.inaccuracy[categ_id] * np.mean(risk_factors) mean_runtime = np.mean(runtimes) - #assert average_exposure == average_exposure2 - #assert average_risk_factor == average_risk_factor2 - #assert mean_runtime == mean_runtime2 - + # assert average_exposure == average_exposure2 + # assert average_risk_factor == average_risk_factor2 + # assert mean_runtime == mean_runtime2 + if self.expire_immediately: incr_expected_profits = -1 # TODO: fix the norm_premium estimation - #incr_expected_profits = (self.norm_premium - (1 - scipy.stats.poisson(1 / self.cat_separation_distribution.mean() * \ + # incr_expected_profits = (self.norm_premium - (1 - scipy.stats.poisson(1 / self.cat_separation_distribution.mean() * \ # mean_runtime).pmf(0)) * self.damage_distribution[categ_id].mean() * average_risk_factor) * average_exposure * len(categ_risks) else: incr_expected_profits = -1 # TODO: expected profits should only be returned once the expire_immediately == False case is fixed - #incr_expected_profits = (self.norm_premium - mean_runtime / self.cat_separation_distribution[categ_id].mean() * self.damage_distribution.mean() * average_risk_factor) * average_exposure * len(categ_risks) - + # incr_expected_profits = (self.norm_premium - mean_runtime / self.cat_separation_distribution[categ_id].mean() * self.damage_distribution.mean() * average_risk_factor) * average_exposure * len(categ_risks) + return average_risk_factor, average_exposure, incr_expected_profits - + def evaluate_proportional(self, risks, cash): - + assert len(cash) == self.category_number # prepare variables @@ -91,42 +103,62 @@ def evaluate_proportional(self, risks, cash): cash_left_by_category = np.copy(cash) expected_profits = 0 necessary_liquidity = 0 - + var_per_risk_per_categ = np.zeros(self.category_number) - + # compute acceptable risks by category for categ_id in range(self.category_number): - # compute number of acceptable risks of this category - + # compute number of acceptable risks of this category + categ_risks = self.get_categ_risks(risks=risks, categ_id=categ_id) - #categ_risks = [risk for risk in risks if risk["category"]==categ_id] - + # categ_risks = [risk for risk in risks if risk["category"]==categ_id] + if len(categ_risks) > 0: - average_risk_factor, average_exposure, incr_expected_profits = self.compute_expectation(categ_risks=categ_risks, categ_id=categ_id) + average_risk_factor, average_exposure, incr_expected_profits = self.compute_expectation( + categ_risks=categ_risks, categ_id=categ_id + ) else: average_risk_factor = self.init_average_risk_factor average_exposure = self.init_average_exposure incr_expected_profits = -1 # TODO: expected profits should only be returned once the expire_immediately == False case is fixed - #incr_expected_profits = 0 + # incr_expected_profits = 0 expected_profits += incr_expected_profits - + # compute value at risk - var_per_risk = self.getPPF(categ_id=categ_id, tailSize=self.var_tail_prob) * average_risk_factor * average_exposure * self.margin_of_safety - + var_per_risk = ( + self.getPPF(categ_id=categ_id, tailSize=self.var_tail_prob) + * average_risk_factor + * average_exposure + * self.margin_of_safety + ) + # record liquidity requirement and apply margin of safety for liquidity requirement - necessary_liquidity += var_per_risk * self.margin_of_safety * len(categ_risks) - #print("RISKMODEL: ", self.getPPF(categ_id=categ_id, tailSize=0.01) * average_risk_factor * average_exposure, " = PPF(0.01) * ", average_risk_factor, " * ", average_exposure, " vs. cash: ", cash[categ_id], "TOTAL_RISK_IN_CATEG: ", self.getPPF(categ_id=categ_id, tailSize=0.01) * average_risk_factor * average_exposure * len(categ_risks)) + necessary_liquidity += ( + var_per_risk * self.margin_of_safety * len(categ_risks) + ) + # print("RISKMODEL: ", self.getPPF(categ_id=categ_id, tailSize=0.01) * average_risk_factor * average_exposure, " = PPF(0.01) * ", average_risk_factor, " * ", average_exposure, " vs. cash: ", cash[categ_id], "TOTAL_RISK_IN_CATEG: ", self.getPPF(categ_id=categ_id, tailSize=0.01) * average_risk_factor * average_exposure * len(categ_risks)) if isleconfig.verbose: print(self.inaccuracy) - print("RISKMODEL: ", var_per_risk, " = PPF(0.02) * ", average_risk_factor, " * ", average_exposure, " vs. cash: ", cash[categ_id], "TOTAL_RISK_IN_CATEG: ", var_per_risk * len(categ_risks)) - #print("RISKMODEL: ", self.getPPF(categ_id=categ_id, tailSize=0.05) * average_risk_factor * average_exposure, " = PPF(0.05) * ", average_risk_factor, " * ", average_exposure, " vs. cash: ", cash[categ_id], "TOTAL_RISK_IN_CATEG: ", self.getPPF(categ_id=categ_id, tailSize=0.05) * average_risk_factor * average_exposure * len(categ_risks)) - #print("RISKMODEL: ", self.getPPF(categ_id=categ_id, tailSize=0.1) * average_risk_factor * average_exposure, " = PPF(0.1) * ", average_risk_factor, " * ", average_exposure, " vs. cash: ", cash[categ_id], "TOTAL_RISK_IN_CATEG: ", self.getPPF(categ_id=categ_id, tailSize=0.1) * average_risk_factor * average_exposure * len(categ_risks)) - #print("RISKMODEL: ", self.getPPF(categ_id=categ_id, tailSize=0.25) * average_risk_factor * average_exposure, " = PPF(0.25) * ", average_risk_factor, " * ", average_exposure, " vs. cash: ", cash[categ_id], "TOTAL_RISK_IN_CATEG: ", self.getPPF(categ_id=categ_id, tailSize=0.25) * average_risk_factor * average_exposure * len(categ_risks)) - #print("RISKMODEL: ", self.getPPF(categ_id=categ_id, tailSize=0.5) * average_risk_factor * average_exposure, " = PPF(0.5) * ", average_risk_factor, " * ", average_exposure, " vs. cash: ", cash[categ_id], "TOTAL_RISK_IN_CATEG: ", self.getPPF(categ_id=categ_id, tailSize=0.5) * average_risk_factor * average_exposure * len(categ_risks)) - #if cash[categ_id] < 0: + print( + "RISKMODEL: ", + var_per_risk, + " = PPF(0.02) * ", + average_risk_factor, + " * ", + average_exposure, + " vs. cash: ", + cash[categ_id], + "TOTAL_RISK_IN_CATEG: ", + var_per_risk * len(categ_risks), + ) + # print("RISKMODEL: ", self.getPPF(categ_id=categ_id, tailSize=0.05) * average_risk_factor * average_exposure, " = PPF(0.05) * ", average_risk_factor, " * ", average_exposure, " vs. cash: ", cash[categ_id], "TOTAL_RISK_IN_CATEG: ", self.getPPF(categ_id=categ_id, tailSize=0.05) * average_risk_factor * average_exposure * len(categ_risks)) + # print("RISKMODEL: ", self.getPPF(categ_id=categ_id, tailSize=0.1) * average_risk_factor * average_exposure, " = PPF(0.1) * ", average_risk_factor, " * ", average_exposure, " vs. cash: ", cash[categ_id], "TOTAL_RISK_IN_CATEG: ", self.getPPF(categ_id=categ_id, tailSize=0.1) * average_risk_factor * average_exposure * len(categ_risks)) + # print("RISKMODEL: ", self.getPPF(categ_id=categ_id, tailSize=0.25) * average_risk_factor * average_exposure, " = PPF(0.25) * ", average_risk_factor, " * ", average_exposure, " vs. cash: ", cash[categ_id], "TOTAL_RISK_IN_CATEG: ", self.getPPF(categ_id=categ_id, tailSize=0.25) * average_risk_factor * average_exposure * len(categ_risks)) + # print("RISKMODEL: ", self.getPPF(categ_id=categ_id, tailSize=0.5) * average_risk_factor * average_exposure, " = PPF(0.5) * ", average_risk_factor, " * ", average_exposure, " vs. cash: ", cash[categ_id], "TOTAL_RISK_IN_CATEG: ", self.getPPF(categ_id=categ_id, tailSize=0.5) * average_risk_factor * average_exposure * len(categ_risks)) + # if cash[categ_id] < 0: # pdb.set_trace() try: acceptable = int(math.floor(cash[categ_id] / var_per_risk)) @@ -149,24 +181,34 @@ def evaluate_proportional(self, risks, cash): expected_profits = self.init_profit_estimate * cash[0] else: expected_profits /= necessary_liquidity - + max_cash_by_categ = max(cash_left_by_category) floored_cash_by_categ = cash_left_by_category.copy() floored_cash_by_categ[floored_cash_by_categ < 0] = 0 remaining_acceptable_by_category_old = remaining_acceptable_by_category.copy() for categ_id in range(self.category_number): remaining_acceptable_by_category[categ_id] = math.floor( - remaining_acceptable_by_category[categ_id] * pow( - floored_cash_by_categ[categ_id] / max_cash_by_categ, 5)) + remaining_acceptable_by_category[categ_id] + * pow(floored_cash_by_categ[categ_id] / max_cash_by_categ, 5) + ) if isleconfig.verbose: - print("RISKMODEL returns: ", expected_profits, remaining_acceptable_by_category) - return expected_profits, remaining_acceptable_by_category, cash_left_by_category, var_per_risk_per_categ + print( + "RISKMODEL returns: ", + expected_profits, + remaining_acceptable_by_category, + ) + return ( + expected_profits, + remaining_acceptable_by_category, + cash_left_by_category, + var_per_risk_per_categ, + ) + + def evaluate_excess_of_loss(self, risks, cash, offered_risk=None): - def evaluate_excess_of_loss(self, risks, cash, offered_risk = None): - cash_left_by_categ = np.copy(cash) assert len(cash_left_by_categ) == self.category_number - + # prepare variables additional_required = np.zeros(self.category_number) additional_var_per_categ = np.zeros(self.category_number) @@ -174,40 +216,60 @@ def evaluate_excess_of_loss(self, risks, cash, offered_risk = None): # values at risk and liquidity requirements by category for categ_id in range(self.category_number): categ_risks = self.get_categ_risks(risks=risks, categ_id=categ_id) - + # TODO: allow for different risk distributions for different categories # TODO: factor in risk_factors - percentage_value_at_risk = self.getPPF(categ_id=categ_id, tailSize=self.var_tail_prob) - + percentage_value_at_risk = self.getPPF( + categ_id=categ_id, tailSize=self.var_tail_prob + ) + # compute liquidity requirements from existing contracts for risk in categ_risks: - expected_damage = percentage_value_at_risk * risk["value"] * risk["risk_factor"] \ - * self.inaccuracy[categ_id] - expected_claim = min(expected_damage, risk["excess"]) - risk["deductible"] - + expected_damage = ( + percentage_value_at_risk + * risk["value"] + * risk["risk_factor"] + * self.inaccuracy[categ_id] + ) + expected_claim = ( + min(expected_damage, risk["excess"]) - risk["deductible"] + ) + # record liquidity requirement and apply margin of safety for liquidity requirement cash_left_by_categ[categ_id] -= expected_claim * self.margin_of_safety - + # compute additional liquidity requirements from newly offered contract - if (offered_risk is not None) and (offered_risk.get("category") == categ_id): - expected_damage_fraction = percentage_value_at_risk * offered_risk["risk_factor"] \ - * self.inaccuracy[categ_id] - expected_claim_fraction = min(expected_damage_fraction, offered_risk["excess_fraction"]) - offered_risk["deductible_fraction"] + if (offered_risk is not None) and ( + offered_risk.get("category") == categ_id + ): + expected_damage_fraction = ( + percentage_value_at_risk + * offered_risk["risk_factor"] + * self.inaccuracy[categ_id] + ) + expected_claim_fraction = ( + min(expected_damage_fraction, offered_risk["excess_fraction"]) + - offered_risk["deductible_fraction"] + ) expected_claim_total = expected_claim_fraction * offered_risk["value"] - + # record liquidity requirement and apply margin of safety for liquidity requirement - additional_required[categ_id] += expected_claim_total * self.margin_of_safety + additional_required[categ_id] += ( + expected_claim_total * self.margin_of_safety + ) additional_var_per_categ[categ_id] += expected_claim_total - + # Additional value at risk should only occur in one category. Assert that this is the case. - assert sum(additional_var_per_categ > 0) <= 1 + assert sum(additional_var_per_categ > 0) <= 1 var_this_risk = max(additional_var_per_categ) - + return cash_left_by_categ, additional_required, var_this_risk def evaluate(self, risks, cash, offered_risk=None): # ensure that any risk to be considered supplied directly as argument is non-proportional/excess-of-loss - assert (offered_risk is None) or offered_risk.get("insurancetype") == "excess-of-loss" + assert (offered_risk is None) or offered_risk.get( + "insurancetype" + ) == "excess-of-loss" # construct cash_left_by_categ as a sequence, defining remaining liquidity by category if not isinstance(cash, (np.ndarray, list)): @@ -217,33 +279,60 @@ def evaluate(self, risks, cash, offered_risk=None): assert len(cash_left_by_categ) == self.category_number # sort current contracts - el_risks = [risk for risk in risks if risk["insurancetype"] == 'excess-of-loss'] - risks = [risk for risk in risks if risk["insurancetype"] == 'proportional'] + el_risks = [risk for risk in risks if risk["insurancetype"] == "excess-of-loss"] + risks = [risk for risk in risks if risk["insurancetype"] == "proportional"] # compute liquidity requirements and acceptable risks from existing contract if (offered_risk is not None) or (len(el_risks) > 0): - cash_left_by_categ, additional_required, var_this_risk = self.evaluate_excess_of_loss(el_risks, cash_left_by_categ, offered_risk) + cash_left_by_categ, additional_required, var_this_risk = self.evaluate_excess_of_loss( + el_risks, cash_left_by_categ, offered_risk + ) if (offered_risk is None) or (len(risks) > 0): - expected_profits_proportional, remaining_acceptable_by_categ, cash_left_by_categ, var_per_risk_per_categ = self.evaluate_proportional(risks, cash_left_by_categ) + expected_profits_proportional, remaining_acceptable_by_categ, cash_left_by_categ, var_per_risk_per_categ = self.evaluate_proportional( + risks, cash_left_by_categ + ) if offered_risk is None: # return numbers of remaining acceptable risks by category - return expected_profits_proportional, remaining_acceptable_by_categ, cash_left_by_categ, var_per_risk_per_categ, min(cash_left_by_categ) + return ( + expected_profits_proportional, + remaining_acceptable_by_categ, + cash_left_by_categ, + var_per_risk_per_categ, + min(cash_left_by_categ), + ) else: # return boolean value whether the offered excess_of_loss risk can be accepted if isleconfig.verbose: - print("REINSURANCE RISKMODEL", cash, cash_left_by_categ,(cash_left_by_categ - additional_required > 0).all()) + print( + "REINSURANCE RISKMODEL", + cash, + cash_left_by_categ, + (cash_left_by_categ - additional_required > 0).all(), + ) # if not (cash_left_by_categ - additional_required > 0).all(): # pdb.set_trace() - return (cash_left_by_categ - additional_required > 0).all(), cash_left_by_categ, var_this_risk, min(cash_left_by_categ) + return ( + (cash_left_by_categ - additional_required > 0).all(), + cash_left_by_categ, + var_this_risk, + min(cash_left_by_categ), + ) def add_reinsurance(self, categ_id, excess_fraction, deductible_fraction, contract): - self.damage_distribution_stack[categ_id].append(self.damage_distribution[categ_id]) + self.damage_distribution_stack[categ_id].append( + self.damage_distribution[categ_id] + ) self.reinsurance_contract_stack[categ_id].append(contract) - self.damage_distribution[categ_id] = ReinsuranceDistWrapper(lower_bound=deductible_fraction, \ - upper_bound=excess_fraction, \ - dist=self.damage_distribution[categ_id]) + self.damage_distribution[categ_id] = ReinsuranceDistWrapper( + lower_bound=deductible_fraction, + upper_bound=excess_fraction, + dist=self.damage_distribution[categ_id], + ) - def delete_reinsurance(self, categ_id, excess_fraction, deductible_fraction, contract): + def delete_reinsurance( + self, categ_id, excess_fraction, deductible_fraction, contract + ): assert self.reinsurance_contract_stack[categ_id][-1] == contract self.reinsurance_contract_stack[categ_id].pop() - self.damage_distribution[categ_id] = self.damage_distribution_stack[categ_id].pop() - + self.damage_distribution[categ_id] = self.damage_distribution_stack[ + categ_id + ].pop() diff --git a/setup.py b/setup.py index 65a9fdf..a7739ff 100644 --- a/setup.py +++ b/setup.py @@ -22,8 +22,8 @@ import isleconfig from distributiontruncated import TruncatedDistWrapper -class SetupSim(): +class SetupSim: def __init__(self): self.simulation_parameters = isleconfig.simulation_parameters @@ -32,11 +32,16 @@ def __init__(self): self.max_time = self.simulation_parameters["max_time"] self.no_categories = self.simulation_parameters["no_categories"] - """set distribution""" - self.non_truncated = scipy.stats.pareto(b=2, loc=0, scale=0.25) #It is assumed that the damages of the catastrophes are drawn from a truncated Pareto distribution. - self.damage_distribution = TruncatedDistWrapper(lower_bound=0.25, upper_bound=1., dist=self.non_truncated) - self.cat_separation_distribution = scipy.stats.expon(0, self.simulation_parameters["event_time_mean_separation"]) #It is assumed that the time between catastrophes is exponentially distributed. + self.non_truncated = scipy.stats.pareto( + b=2, loc=0, scale=0.25 + ) # It is assumed that the damages of the catastrophes are drawn from a truncated Pareto distribution. + self.damage_distribution = TruncatedDistWrapper( + lower_bound=0.25, upper_bound=1.0, dist=self.non_truncated + ) + self.cat_separation_distribution = scipy.stats.expon( + 0, self.simulation_parameters["event_time_mean_separation"] + ) # It is assumed that the time between catastrophes is exponentially distributed. """"random seeds""" self.np_seed = [] @@ -44,19 +49,33 @@ def __init__(self): self.general_rc_event_schedule = [] self.general_rc_event_damage = [] - def schedule(self, replications): #This method returns the lists of schedule times and damages for an ensemble of replications of the model. The argument (replications) is the number of replications. + def schedule( + self, replications + ): # This method returns the lists of schedule times and damages for an ensemble of replications of the model. The argument (replications) is the number of replications. - general_rc_event_schedule = [] #In this list will be stored the lists of schedule times of catastrophes for an ensemble of simulations of the model. ([[Schedule 1], [Schedule 2], [Schedule 3],...,[Schedule N]]) - general_rc_event_damage = [] #In this list will be stored the lists of schedule damages of catastrophes for an ensemble of simulations of the model. ([[Schedule 1], [Schedule 2], [Schedule 3],...,[Schedule N]]) + general_rc_event_schedule = ( + [] + ) # In this list will be stored the lists of schedule times of catastrophes for an ensemble of simulations of the model. ([[Schedule 1], [Schedule 2], [Schedule 3],...,[Schedule N]]) + general_rc_event_damage = ( + [] + ) # In this list will be stored the lists of schedule damages of catastrophes for an ensemble of simulations of the model. ([[Schedule 1], [Schedule 2], [Schedule 3],...,[Schedule N]]) for i in range(replications): - rc_event_schedule = [] #In this list will be stored the lists of times when there will be catastrophes for every category of the model during a single run. ([[times for C1],[times for C2],[times for C3],[times for C4]]) - rc_event_damage = [] #In this list will be stored the lists of catastrophe damages for every category of the model during a single run. ([[damages for C1],[damages for C2],[damages for C3],[damages for C4]]) + rc_event_schedule = ( + [] + ) # In this list will be stored the lists of times when there will be catastrophes for every category of the model during a single run. ([[times for C1],[times for C2],[times for C3],[times for C4]]) + rc_event_damage = ( + [] + ) # In this list will be stored the lists of catastrophe damages for every category of the model during a single run. ([[damages for C1],[damages for C2],[damages for C3],[damages for C4]]) for j in range(self.no_categories): - event_schedule = [] #In this list will be stored the times when there will be a catastrophe related to a particular category. - event_damage = [] #In this list will be stored the damages of a catastrophe related to a particular category. + event_schedule = ( + [] + ) # In this list will be stored the times when there will be a catastrophe related to a particular category. + event_damage = ( + [] + ) # In this list will be stored the damages of a catastrophe related to a particular category. total = 0 - while (total < self.max_time): + while total < self.max_time: separation_time = self.cat_separation_distribution.rvs() total += int(math.ceil(separation_time)) if total < self.max_time: @@ -70,22 +89,24 @@ def schedule(self, replications): #This method returns the lists of schedule ti return self.general_rc_event_schedule, self.general_rc_event_damage - def seeds(self, replications): #This method returns the seeds required for an ensemble of replications of the model. The argument (replications) is the number of replications. + def seeds( + self, replications + ): # This method returns the seeds required for an ensemble of replications of the model. The argument (replications) is the number of replications. """draw random variates for random seeds""" for i in range(replications): - np_seed, random_seed = scipy.stats.randint.rvs(0, 2**32 - 1, size=2) + np_seed, random_seed = scipy.stats.randint.rvs(0, 2 ** 32 - 1, size=2) self.np_seed.append(np_seed) self.random_seed.append(random_seed) return self.np_seed, self.random_seed - - def store(self, replications): #This method stores in a file the the schedules and random seeds required for an ensemble of replications of the model. The argument (replications) is the number of replications. - #With the information stored it is possible to replicate the entire behavior of the ensemble at a later time. + def store( + self, replications + ): # This method stores in a file the the schedules and random seeds required for an ensemble of replications of the model. The argument (replications) is the number of replications. + # With the information stored it is possible to replicate the entire behavior of the ensemble at a later time. event_schedules = [] for i in range(replications): - """pack to dict""" d = {} d["np_seed"] = self.np_seed[i] @@ -97,7 +118,9 @@ def store(self, replications): #This method stores in a file the the schedules """ ensure that logging directory exists""" if not os.path.isdir("data"): - assert not os.path.exists("data"), "./data exists as regular file. This filename is required for the logging and event schedule directory" + assert not os.path.exists( + "data" + ), "./data exists as regular file. This filename is required for the logging and event schedule directory" os.makedirs("data") """Save as both pickle and txt""" @@ -106,21 +129,29 @@ def store(self, replications): #This method stores in a file the the schedules with open("./data/risk_event_schedules.txt", "w") as wfile: for rep_schedule in event_schedules: - wfile.write(str(rep_schedule).replace("\n","").replace("array", "np.array").replace("uint32", "np.uint32") + "\n") - - - def obtain_ensemble(self, replications): #This method returns all the information (schedules and seeds) required to run an ensemble of simulations of the model. Since it also stores the information in a file it will be possible to replicate the ensemble at a later time. The argument (replications) is the number of replications. - #This method will be called either form ensemble.py or start.py - [general_rc_event_schedule, general_rc_event_damage] = self.schedule(replications) + wfile.write( + str(rep_schedule) + .replace("\n", "") + .replace("array", "np.array") + .replace("uint32", "np.uint32") + + "\n" + ) + + def obtain_ensemble( + self, replications + ): # This method returns all the information (schedules and seeds) required to run an ensemble of simulations of the model. Since it also stores the information in a file it will be possible to replicate the ensemble at a later time. The argument (replications) is the number of replications. + # This method will be called either form ensemble.py or start.py + [general_rc_event_schedule, general_rc_event_damage] = self.schedule( + replications + ) [np_seeds, random_seeds] = self.seeds(replications) self.store(replications) - return general_rc_event_schedule, general_rc_event_damage, np_seeds, random_seeds - - - - - - + return ( + general_rc_event_schedule, + general_rc_event_damage, + np_seeds, + random_seeds, + ) diff --git a/start.py b/start.py index b75e0a9..1841f18 100644 --- a/start.py +++ b/start.py @@ -8,147 +8,200 @@ import pickle import hashlib import random -import copy -import importlib # import config file and apply configuration import isleconfig +import insurancesimulation +import insurancefirm +import reinsurancefirm +import logger +import calibrationscore + simulation_parameters = isleconfig.simulation_parameters replic_ID = None override_no_riskmodels = False -from insurancesimulation import InsuranceSimulation -from insurancefirm import InsuranceFirm -from riskmodel import RiskModel -from reinsurancefirm import ReinsuranceFirm -import logger -import calibrationscore - + # ensure that logging directory exists if not os.path.isdir("data"): - assert not os.path.exists("data"), "./data exists as regular file. This filename is required for the logging directory" + if os.path.exists("data"): + raise Exception( + "./data exists as regular file. This filename is required for the logging directory" + ) os.makedirs("data") + # create conditional decorator def conditionally(decorator_function, condition): def wrapper(target_function): if not condition: return target_function return decorator_function(target_function) + return wrapper + # create non-abce placeholder gui decorator # TODO: replace this with more elegant solution if possible. Currently required since script will otherwise crash at the conditional decorator below since gui is then undefined if not isleconfig.use_abce: + def gui(*args, **kwargs): pass # main function -#@gui(simulation_parameters, serve=True) +# @gui(simulation_parameters, serve=True) @conditionally(gui(simulation_parameters, serve=False), isleconfig.use_abce) -def main(simulation_parameters, rc_event_schedule, rc_event_damage, np_seed, random_seed, save_iter, requested_logs=None): - +def main( + simulation_parameters, + rc_event_schedule, + rc_event_damage, + np_seed, + random_seed, + save_iter, + requested_logs=None, +): np.random.seed(np_seed) random.seed(random_seed) # create simulation and world objects (identical in non-abce mode) if isleconfig.use_abce: - simulation = abce.Simulation(processes=1,random_seed = random_seed) + simulation = abce.Simulation(processes=1, random_seed=random_seed) - simulation_parameters['simulation'] = world = InsuranceSimulation(override_no_riskmodels, replic_ID, simulation_parameters, rc_event_schedule, rc_event_damage) + simulation_parameters[ + "simulation" + ] = world = insurancesimulation.InsuranceSimulation( + override_no_riskmodels, + replic_ID, + simulation_parameters, + rc_event_schedule, + rc_event_damage, + ) if not isleconfig.use_abce: simulation = world - - # create agents: insurance firms - insurancefirms_group = simulation.build_agents(InsuranceFirm, - 'insurancefirm', - parameters=simulation_parameters, - agent_parameters=world.agent_parameters["insurancefirm"]) - + + # create agents: insurance firms + insurancefirms_group = simulation.build_agents( + insurancefirm.InsuranceFirm, + "insurancefirm", + parameters=simulation_parameters, + agent_parameters=world.agent_parameters["insurancefirm"], + ) + if isleconfig.use_abce: insurancefirm_pointers = insurancefirms_group.get_pointer() else: insurancefirm_pointers = insurancefirms_group world.accept_agents("insurancefirm", insurancefirm_pointers, insurancefirms_group) - # create agents: reinsurance firms - reinsurancefirms_group = simulation.build_agents(ReinsuranceFirm, - 'reinsurance', - parameters=simulation_parameters, - agent_parameters=world.agent_parameters["reinsurance"]) + # create agents: reinsurance firms + reinsurancefirms_group = simulation.build_agents( + reinsurancefirm.ReinsuranceFirm, + "reinsurance", + parameters=simulation_parameters, + agent_parameters=world.agent_parameters["reinsurance"], + ) if isleconfig.use_abce: reinsurancefirm_pointers = reinsurancefirms_group.get_pointer() else: reinsurancefirm_pointers = reinsurancefirms_group world.accept_agents("reinsurance", reinsurancefirm_pointers, reinsurancefirms_group) - + # time iteration for t in range(simulation_parameters["max_time"]): - + # abce time step simulation.advance_round(t) - + # create new agents # TODO: write method for this; this code block is executed almost identically 4 times if world.insurance_firm_market_entry(agent_type="InsuranceFirm"): parameters = [np.random.choice(world.agent_parameters["insurancefirm"])] - parameters = [world.agent_parameters["insurancefirm"][simulation.insurance_entry_index()]] + parameters = [ + world.agent_parameters["insurancefirm"][ + simulation.insurance_entry_index() + ] + ] parameters[0]["id"] = world.get_unique_insurer_id() - new_insurance_firm = simulation.build_agents(InsuranceFirm, - 'insurancefirm', - parameters=simulation_parameters, - agent_parameters=parameters) + new_insurance_firm = simulation.build_agents( + insurancefirm.InsuranceFirm, + "insurancefirm", + parameters=simulation_parameters, + agent_parameters=parameters, + ) insurancefirms_group += new_insurance_firm if isleconfig.use_abce: # TODO: fix abce # may fail in abce because addressing individual agents may not be allowed # may also fail because agent methods may not be callable directly - new_insurancefirm_pointer = [new_insurance_firm[0].get_pointer()] # index 0 because this is a list with just 1 object + new_insurancefirm_pointer = [ + new_insurance_firm[0].get_pointer() + ] # index 0 because this is a list with just 1 object else: new_insurancefirm_pointer = new_insurance_firm - world.accept_agents("insurancefirm", new_insurancefirm_pointer, new_insurance_firm, time=t) - + world.accept_agents( + "insurancefirm", new_insurancefirm_pointer, new_insurance_firm, time=t + ) + if world.insurance_firm_market_entry(agent_type="ReinsuranceFirm"): parameters = [np.random.choice(world.agent_parameters["reinsurance"])] - parameters[0]["initial_cash"] = world.reinsurance_capital_entry() #Since the value of the reinrisks varies overtime it makes sense that the market entry of reinsures depends on those values. The method world.reinsurance_capital_entry() determines the capital market entry of reinsurers. - parameters = [world.agent_parameters["reinsurance"][simulation.reinsurance_entry_index()]] + parameters[0][ + "initial_cash" + ] = ( + world.reinsurance_capital_entry() + ) # Since the value of the reinrisks varies overtime it makes sense that the market entry of reinsures depends on those values. The method world.reinsurance_capital_entry() determines the capital market entry of reinsurers. + parameters = [ + world.agent_parameters["reinsurance"][ + simulation.reinsurance_entry_index() + ] + ] parameters[0]["id"] = world.get_unique_reinsurer_id() - new_reinsurance_firm = simulation.build_agents(ReinsuranceFirm, - 'reinsurance', - parameters=simulation_parameters, - agent_parameters=parameters) + new_reinsurance_firm = simulation.build_agents( + reinsurancefirm.ReinsuranceFirm, + "reinsurance", + parameters=simulation_parameters, + agent_parameters=parameters, + ) reinsurancefirms_group += new_reinsurance_firm if isleconfig.use_abce: # TODO: fix abce # may fail in abce because addressing individual agents may not be allowed # may also fail because agent methods may not be callable directly - new_reinsurancefirm_pointer = [new_reinsurance_firm[0].get_pointer()] # index 0 because this is a list with just 1 object + new_reinsurancefirm_pointer = [ + new_reinsurance_firm[0].get_pointer() + ] # index 0 because this is a list with just 1 object else: new_reinsurancefirm_pointer = new_reinsurance_firm - world.accept_agents("reinsurance", new_reinsurancefirm_pointer, new_reinsurance_firm, time=t) - + world.accept_agents( + "reinsurance", new_reinsurancefirm_pointer, new_reinsurance_firm, time=t + ) + # iterate simulation world.iterate(t) - + # log data if isleconfig.use_abce: - #insurancefirms.logme() - #reinsurancefirms.logme() - insurancefirms_group.agg_log(variables=['cash', 'operational'], len=['underwritten_contracts']) - #reinsurancefirms_group.agg_log(variables=['cash']) + # insurancefirms.logme() + # reinsurancefirms.logme() + insurancefirms_group.agg_log( + variables=["cash", "operational"], len=["underwritten_contracts"] + ) + # reinsurancefirms_group.agg_log(variables=['cash']) else: world.save_data() - - if t%50 == save_iter: + + if t % 50 == save_iter: save_simulation(t, simulation, simulation_parameters, exit_now=False) - + # finish simulation, write logs simulation.finalize() - return simulation.obtain_log(requested_logs) #It is required to return this list to download all the data generated by a single run of the model from the cloud. + return simulation.obtain_log( + requested_logs + ) # It is required to return this list to download all the data generated by a single run of the model from the cloud. + # save function def save_simulation(t, sim, sim_param, exit_now=False): @@ -162,47 +215,83 @@ def save_simulation(t, sim, sim_param, exit_now=False): pickle.dump(d, wfile, protocol=pickle.HIGHEST_PROTOCOL) with open("data/simulation_save.pkl", "br") as rfile: file_contents = rfile.read() - #print("\nSimulation hashes: ", hashlib.sha512(str(d).encode()).hexdigest(), "; ", hashlib.sha512(str(file_contents).encode()).hexdigest()) + # print("\nSimulation hashes: ", hashlib.sha512(str(d).encode()).hexdigest(), "; ", hashlib.sha512(str(file_contents).encode()).hexdigest()) # note that the hash over the dict is for some reason not identical between runs. The hash over the state saved to the file is. - print("\nSimulation hash: ", hashlib.sha512(str(file_contents).encode()).hexdigest()) + print( + "\nSimulation hash: ", hashlib.sha512(str(file_contents).encode()).hexdigest() + ) if exit_now: exit(0) + # main entry point if __name__ == "__main__": """ use argparse to handle command line arguments""" - parser = argparse.ArgumentParser(description='Model the Insurance sector') + parser = argparse.ArgumentParser(description="Model the Insurance sector") parser.add_argument("--abce", action="store_true", help="use abce") - parser.add_argument("--oneriskmodel", action="store_true", - help="allow overriding the number of riskmodels from the standard config (with 1)") - parser.add_argument("--riskmodels", type=int, choices=[1, 2, 3, 4], - help="allow overriding the number of riskmodels from standard config (with 1 or other numbers)") - parser.add_argument("--replicid", type=int, - help="if replication ID is given, pass this to the simulation so that the risk profile can be restored") - parser.add_argument("--replicating", action="store_true", - help="if this is a simulation run designed to replicate another, override the config file parameter") - parser.add_argument("--randomseed", type=float, help="allow setting of numpy random seed") - parser.add_argument("--foreground", action="store_true", - help="force foreground runs even if replication ID is given (which defaults to background runs)") - parser.add_argument("--shownetwork", action="store_true", help="show reinsurance relations as network") - parser.add_argument("-p", "--showprogress", action="store_true", help="show timesteps") - parser.add_argument("-v", "--verbose", action="store_true", help="more detailed output") - parser.add_argument("--save_iterations", type=int, help="number of iterations to iterate before saving world state") + parser.add_argument( + "--oneriskmodel", + action="store_true", + help="allow overriding the number of riskmodels from the standard config (with 1)", + ) + parser.add_argument( + "--riskmodels", + type=int, + choices=[1, 2, 3, 4], + help="allow overriding the number of riskmodels from standard config (with 1 or other numbers)", + ) + parser.add_argument( + "--replicid", + type=int, + help="if replication ID is given, pass this to the simulation so that the risk profile can be restored", + ) + parser.add_argument( + "--replicating", + action="store_true", + help="if this is a simulation run designed to replicate another, override the config file parameter", + ) + parser.add_argument( + "--randomseed", type=float, help="allow setting of numpy random seed" + ) + parser.add_argument( + "--foreground", + action="store_true", + help="force foreground runs even if replication ID is given (which defaults to background runs)", + ) + parser.add_argument( + "--shownetwork", + action="store_true", + help="show reinsurance relations as network", + ) + parser.add_argument( + "-p", "--showprogress", action="store_true", help="show timesteps" + ) + parser.add_argument( + "-v", "--verbose", action="store_true", help="more detailed output" + ) + parser.add_argument( + "--save_iterations", + type=int, + help="number of iterations to iterate before saving world state", + ) args = parser.parse_args() if args.abce: isleconfig.use_abce = True + raise Exception("ABCE not supported, probably won't become so") if args.oneriskmodel: isleconfig.oneriskmodel = True override_no_riskmodels = 1 if args.riskmodels: override_no_riskmodels = args.riskmodels - if args.replicid is not None: # TODO: this is broken, must be fixed or removed + if args.replicid is not None: # TODO: this is broken, must be fixed or removed replic_ID = args.replicid if args.replicating: isleconfig.replicating = True - assert replic_ID is not None, "Error: Replication requires a replication ID to identify run to be replicated" + assert ( + replic_ID is not None + ), "Error: Replication requires a replication ID to identify run to be replicated" if args.randomseed: randomseed = args.randomseed seed = int(randomseed) @@ -213,11 +302,6 @@ def save_simulation(t, sim, sim_param, exit_now=False): isleconfig.force_foreground = True if args.shownetwork: isleconfig.show_network = True - """Option requires reloading of InsuranceSimulation so that modules to show network can be loaded. - # TODO: change all module imports of the form "from module import class" to "import module". """ - import insurancesimulation - importlib.reload(insurancesimulation) - from insurancesimulation import InsuranceSimulation if args.showprogress: isleconfig.showprogress = True if args.verbose: @@ -232,20 +316,36 @@ def save_simulation(t, sim, sim_param, exit_now=False): # print("Importing abce") import abce from abce import gui - + from setup import SetupSim - setup = SetupSim() #Here the setup for the simulation is done. - [general_rc_event_schedule, general_rc_event_damage, np_seeds, random_seeds] = setup.obtain_ensemble(1) #Only one ensemble. This part will only be run locally (laptop). - log = main(simulation_parameters, general_rc_event_schedule[0], general_rc_event_damage[0], np_seeds[0], random_seeds[0], save_iter) - + setup = SetupSim() # Here the setup for the simulation is done. + [ + general_rc_event_schedule, + general_rc_event_damage, + np_seeds, + random_seeds, + ] = setup.obtain_ensemble( + 1 + ) # Only one ensemble. This part will only be run locally (laptop). + + log = main( + simulation_parameters, + general_rc_event_schedule[0], + general_rc_event_damage[0], + np_seeds[0], + random_seeds[0], + save_iter, + ) + """ Restore the log at the end of the single simulation run for saving and for potential further study """ - is_background = (not isleconfig.force_foreground) and (isleconfig.replicating or (replic_ID in locals())) + is_background = (not isleconfig.force_foreground) and ( + isleconfig.replicating or (replic_ID in locals()) + ) L = logger.Logger() L.restore_logger_object(list(log)) L.save_log(is_background) - + """ Obtain calibration score """ CS = calibrationscore.CalibrationScore(L) score = CS.test_all() - diff --git a/visualisation.py b/visualisation.py index 2ce6814..ec949d5 100644 --- a/visualisation.py +++ b/visualisation.py @@ -5,9 +5,18 @@ import argparse - class TimeSeries(object): - def __init__(self, series_list, title="",xlabel="Time", colour='k', axlst=None, fig=None, percentiles=None, alpha=0.7): + def __init__( + self, + series_list, + title="", + xlabel="Time", + colour="k", + axlst=None, + fig=None, + percentiles=None, + alpha=0.7, + ): self.series_list = series_list self.size = len(series_list) self.xlabel = xlabel @@ -15,22 +24,32 @@ def __init__(self, series_list, title="",xlabel="Time", colour='k', axlst=None, self.alpha = alpha self.percentiles = percentiles self.title = title - self.timesteps = [t for t in range(len(series_list[0][0]))] # assume all data series are the same size + self.timesteps = [ + t for t in range(len(series_list[0][0])) + ] # assume all data series are the same size if axlst is not None and fig is not None: self.axlst = axlst self.fig = fig else: - self.fig, self.axlst = plt.subplots(self.size,sharex=True) + self.fig, self.axlst = plt.subplots(self.size, sharex=True) - #self.plot() # we create the object when we want the plot so call plot() in the constructor + # self.plot() # we create the object when we want the plot so call plot() in the constructor def plot(self): - for i, (series, series_label, fill_lower, fill_upper) in enumerate(self.series_list): - self.axlst[i].plot(self.timesteps, series,color=self.colour) + for i, (series, series_label, fill_lower, fill_upper) in enumerate( + self.series_list + ): + self.axlst[i].plot(self.timesteps, series, color=self.colour) self.axlst[i].set_ylabel(series_label) if fill_lower is not None and fill_upper is not None: - self.axlst[i].fill_between(self.timesteps, fill_lower, fill_upper, color=self.colour, alpha=self.alpha) - self.axlst[self.size-1].set_xlabel(self.xlabel) + self.axlst[i].fill_between( + self.timesteps, + fill_lower, + fill_upper, + color=self.colour, + alpha=self.alpha, + ) + self.axlst[self.size - 1].set_xlabel(self.xlabel) self.fig.suptitle(self.title) return self.fig, self.axlst @@ -38,20 +57,24 @@ def save(self, filename): self.fig.savefig("{filename}".format(filename=filename)) return + class InsuranceFirmAnimation(object): - '''class takes in a run of insurance data and produces animations ''' + """class takes in a run of insurance data and produces animations """ + def __init__(self, data): self.data = data self.fig, self.ax = plt.subplots() self.stream = self.data_stream() - self.ani = animation.FuncAnimation(self.fig, self.update, repeat=False, interval=100,) - #init_func=self.setup_plot) + self.ani = animation.FuncAnimation( + self.fig, self.update, repeat=False, interval=100 + ) + # init_func=self.setup_plot) def setup_plot(self): # initial drawing of the plot - casharr,idarr = next(self.stream) - self.pie = self.ax.pie(casharr, labels=idarr,autopct='%1.0f%%') - return self.pie, + casharr, idarr = next(self.stream) + self.pie = self.ax.pie(casharr, labels=idarr, autopct="%1.0f%%") + return (self.pie,) def data_stream(self): # unpack data in a format ready for update() @@ -62,59 +85,81 @@ def data_stream(self): if operational: casharr.append(cash) idarr.append(id) - yield casharr,idarr + yield casharr, idarr def update(self, i): # clear plot and redraw self.ax.clear() - self.ax.axis('equal') - casharr,idarr = next(self.stream) - self.pie = self.ax.pie(casharr, labels=idarr,autopct='%1.0f%%') - self.ax.set_title("Timestep : {:,.0f} | Total cash : {:,.0f}".format(i,sum(casharr))) - return self.pie, + self.ax.axis("equal") + casharr, idarr = next(self.stream) + self.pie = self.ax.pie(casharr, labels=idarr, autopct="%1.0f%%") + self.ax.set_title( + "Timestep : {:,.0f} | Total cash : {:,.0f}".format(i, sum(casharr)) + ) + return (self.pie,) - def save(self,filename): - self.ani.save(filename, writer='ffmpeg', dpi=80) + def save(self, filename): + self.ani.save(filename, writer="ffmpeg", dpi=80) def show(self): plt.show() + class visualisation(object): def __init__(self, history_logs_list): self.history_logs_list = history_logs_list # unused data in history_logs - #self.excess_capital = history_logs['total_excess_capital'] - #self.reinexcess_capital = history_logs['total_reinexcess_capital'] - #self.diffvar = history_logs['market_diffvar'] - #self.cumulative_bankruptcies = history_logs['cumulative_bankruptcies'] - #self.cumulative_unrecovered_claims = history_logs['cumulative_unrecovered_claims'] + # self.excess_capital = history_logs['total_excess_capital'] + # self.reinexcess_capital = history_logs['total_reinexcess_capital'] + # self.diffvar = history_logs['market_diffvar'] + # self.cumulative_bankruptcies = history_logs['cumulative_bankruptcies'] + # self.cumulative_unrecovered_claims = history_logs['cumulative_unrecovered_claims'] return def insurer_pie_animation(self, run=0): data = self.history_logs_list[run] - insurance_cash = np.array(data['insurance_firms_cash']) + insurance_cash = np.array(data["insurance_firms_cash"]) self.ins_pie_anim = InsuranceFirmAnimation(insurance_cash) return self.ins_pie_anim def reinsurer_pie_animation(self, run=0): data = self.history_logs_list[run] - reinsurance_cash = np.array(data['reinsurance_firms_cash']) + reinsurance_cash = np.array(data["reinsurance_firms_cash"]) self.reins_pie_anim = InsuranceFirmAnimation(reinsurance_cash) return self.reins_pie_anim - def insurer_time_series(self, runs=None, axlst=None, fig=None, title="Insurer", colour='black', percentiles=[25,75]): + def insurer_time_series( + self, + runs=None, + axlst=None, + fig=None, + title="Insurer", + colour="black", + percentiles=[25, 75], + ): # runs should be a list of the indexes you want included in the ensemble for consideration if runs: data = [self.history_logs_list[x] for x in runs] else: data = self.history_logs_list - + # Take the element-wise means/medians of the ensemble set (axis=0) - contracts_agg = [history_logs['total_contracts'] for history_logs in self.history_logs_list] - profitslosses_agg = [history_logs['total_profitslosses'] for history_logs in self.history_logs_list] - operational_agg = [history_logs['total_operational'] for history_logs in self.history_logs_list] - cash_agg = [history_logs['total_cash'] for history_logs in self.history_logs_list] - premium_agg = [history_logs['market_premium'] for history_logs in self.history_logs_list] + contracts_agg = [ + history_logs["total_contracts"] for history_logs in self.history_logs_list + ] + profitslosses_agg = [ + history_logs["total_profitslosses"] + for history_logs in self.history_logs_list + ] + operational_agg = [ + history_logs["total_operational"] for history_logs in self.history_logs_list + ] + cash_agg = [ + history_logs["total_cash"] for history_logs in self.history_logs_list + ] + premium_agg = [ + history_logs["market_premium"] for history_logs in self.history_logs_list + ] contracts = np.mean(contracts_agg, axis=0) profitslosses = np.mean(profitslosses_agg, axis=0) @@ -122,16 +167,56 @@ def insurer_time_series(self, runs=None, axlst=None, fig=None, title="Insurer", cash = np.median(cash_agg, axis=0) premium = np.median(premium_agg, axis=0) - self.ins_time_series = TimeSeries([ - (contracts, 'Contracts', np.percentile(contracts_agg,percentiles[0], axis=0), np.percentile(contracts_agg, percentiles[1], axis=0)), - (profitslosses, 'Profitslosses', np.percentile(profitslosses_agg,percentiles[0], axis=0), np.percentile(profitslosses_agg, percentiles[1], axis=0)), - (operational, 'Operational', np.percentile(operational_agg,percentiles[0], axis=0), np.percentile(operational_agg, percentiles[1], axis=0)), - (cash, 'Cash', np.percentile(cash_agg,percentiles[0], axis=0), np.percentile(cash_agg, percentiles[1], axis=0)), - (premium, "Premium", np.percentile(premium_agg,percentiles[0], axis=0), np.percentile(premium_agg, percentiles[1], axis=0)), - ],title=title, xlabel = "Time", axlst=axlst, fig=fig, colour=colour).plot() + self.ins_time_series = TimeSeries( + [ + ( + contracts, + "Contracts", + np.percentile(contracts_agg, percentiles[0], axis=0), + np.percentile(contracts_agg, percentiles[1], axis=0), + ), + ( + profitslosses, + "Profitslosses", + np.percentile(profitslosses_agg, percentiles[0], axis=0), + np.percentile(profitslosses_agg, percentiles[1], axis=0), + ), + ( + operational, + "Operational", + np.percentile(operational_agg, percentiles[0], axis=0), + np.percentile(operational_agg, percentiles[1], axis=0), + ), + ( + cash, + "Cash", + np.percentile(cash_agg, percentiles[0], axis=0), + np.percentile(cash_agg, percentiles[1], axis=0), + ), + ( + premium, + "Premium", + np.percentile(premium_agg, percentiles[0], axis=0), + np.percentile(premium_agg, percentiles[1], axis=0), + ), + ], + title=title, + xlabel="Time", + axlst=axlst, + fig=fig, + colour=colour, + ).plot() return self.ins_time_series - def reinsurer_time_series(self, runs=None, axlst=None, fig=None, title="Reinsurer", colour='black', percentiles=[25,75]): + def reinsurer_time_series( + self, + runs=None, + axlst=None, + fig=None, + title="Reinsurer", + colour="black", + percentiles=[25, 75], + ): # runs should be a list of the indexes you want included in the ensemble for consideration if runs: data = [self.history_logs_list[x] for x in runs] @@ -139,11 +224,25 @@ def reinsurer_time_series(self, runs=None, axlst=None, fig=None, title="Reinsure data = self.history_logs_list # Take the element-wise means/medians of the ensemble set (axis=0) - reincontracts_agg = [history_logs['total_reincontracts'] for history_logs in self.history_logs_list] - reinprofitslosses_agg = [history_logs['total_reinprofitslosses'] for history_logs in self.history_logs_list] - reinoperational_agg = [history_logs['total_reinoperational'] for history_logs in self.history_logs_list] - reincash_agg = [history_logs['total_reincash'] for history_logs in self.history_logs_list] - catbonds_number_agg = [history_logs['total_catbondsoperational'] for history_logs in self.history_logs_list] + reincontracts_agg = [ + history_logs["total_reincontracts"] + for history_logs in self.history_logs_list + ] + reinprofitslosses_agg = [ + history_logs["total_reinprofitslosses"] + for history_logs in self.history_logs_list + ] + reinoperational_agg = [ + history_logs["total_reinoperational"] + for history_logs in self.history_logs_list + ] + reincash_agg = [ + history_logs["total_reincash"] for history_logs in self.history_logs_list + ] + catbonds_number_agg = [ + history_logs["total_catbondsoperational"] + for history_logs in self.history_logs_list + ] reincontracts = np.mean(reincontracts_agg, axis=0) reinprofitslosses = np.mean(reinprofitslosses_agg, axis=0) @@ -151,73 +250,168 @@ def reinsurer_time_series(self, runs=None, axlst=None, fig=None, title="Reinsure reincash = np.median(reincash_agg, axis=0) catbonds_number = np.median(catbonds_number_agg, axis=0) - self.reins_time_series = TimeSeries([ - (reincontracts, 'Contracts', np.percentile(reincontracts_agg,percentiles[0], axis=0), np.percentile(reincontracts_agg, percentiles[1], axis=0)), - (reinprofitslosses, 'Profitslosses', np.percentile(reinprofitslosses_agg,percentiles[0], axis=0), np.percentile(reinprofitslosses_agg, percentiles[1], axis=0)), - (reinoperational, 'Operational', np.percentile(reinoperational_agg,percentiles[0], axis=0), np.percentile(reinoperational_agg, percentiles[1], axis=0)), - (reincash, 'Cash', np.percentile(reincash_agg,percentiles[0], axis=0), np.percentile(reincash_agg, percentiles[1], axis=0)), - (catbonds_number, "Activate Cat Bonds", np.percentile(catbonds_number_agg,percentiles[0], axis=0), np.percentile(catbonds_number_agg, percentiles[1], axis=0)), - ],title= title, xlabel = "Time", axlst=axlst, fig=fig, colour=colour).plot() + self.reins_time_series = TimeSeries( + [ + ( + reincontracts, + "Contracts", + np.percentile(reincontracts_agg, percentiles[0], axis=0), + np.percentile(reincontracts_agg, percentiles[1], axis=0), + ), + ( + reinprofitslosses, + "Profitslosses", + np.percentile(reinprofitslosses_agg, percentiles[0], axis=0), + np.percentile(reinprofitslosses_agg, percentiles[1], axis=0), + ), + ( + reinoperational, + "Operational", + np.percentile(reinoperational_agg, percentiles[0], axis=0), + np.percentile(reinoperational_agg, percentiles[1], axis=0), + ), + ( + reincash, + "Cash", + np.percentile(reincash_agg, percentiles[0], axis=0), + np.percentile(reincash_agg, percentiles[1], axis=0), + ), + ( + catbonds_number, + "Activate Cat Bonds", + np.percentile(catbonds_number_agg, percentiles[0], axis=0), + np.percentile(catbonds_number_agg, percentiles[1], axis=0), + ), + ], + title=title, + xlabel="Time", + axlst=axlst, + fig=fig, + colour=colour, + ).plot() return self.reins_time_series def metaplotter_timescale(self): # Take the element-wise means/medians of the ensemble set (axis=0) - contracts = np.mean([history_logs['total_contracts'] for history_logs in self.history_logs_list],axis=0) - profitslosses = np.mean([history_logs['total_profitslosses'] for history_logs in self.history_logs_list],axis=0) - operational = np.median([history_logs['total_operational'] for history_logs in self.history_logs_list],axis=0) - cash = np.median([history_logs['total_cash'] for history_logs in self.history_logs_list],axis=0) - premium = np.median([history_logs['market_premium'] for history_logs in self.history_logs_list],axis=0) - reincontracts = np.mean([history_logs['total_reincontracts'] for history_logs in self.history_logs_list],axis=0) - reinprofitslosses = np.mean([history_logs['total_reinprofitslosses'] for history_logs in self.history_logs_list],axis=0) - reinoperational = np.median([history_logs['total_reinoperational'] for history_logs in self.history_logs_list],axis=0) - reincash = np.median([history_logs['total_reincash'] for history_logs in self.history_logs_list],axis=0) - catbonds_number = np.median([history_logs['total_catbondsoperational'] for history_logs in self.history_logs_list],axis=0) + contracts = np.mean( + [ + history_logs["total_contracts"] + for history_logs in self.history_logs_list + ], + axis=0, + ) + profitslosses = np.mean( + [ + history_logs["total_profitslosses"] + for history_logs in self.history_logs_list + ], + axis=0, + ) + operational = np.median( + [ + history_logs["total_operational"] + for history_logs in self.history_logs_list + ], + axis=0, + ) + cash = np.median( + [history_logs["total_cash"] for history_logs in self.history_logs_list], + axis=0, + ) + premium = np.median( + [history_logs["market_premium"] for history_logs in self.history_logs_list], + axis=0, + ) + reincontracts = np.mean( + [ + history_logs["total_reincontracts"] + for history_logs in self.history_logs_list + ], + axis=0, + ) + reinprofitslosses = np.mean( + [ + history_logs["total_reinprofitslosses"] + for history_logs in self.history_logs_list + ], + axis=0, + ) + reinoperational = np.median( + [ + history_logs["total_reinoperational"] + for history_logs in self.history_logs_list + ], + axis=0, + ) + reincash = np.median( + [history_logs["total_reincash"] for history_logs in self.history_logs_list], + axis=0, + ) + catbonds_number = np.median( + [ + history_logs["total_catbondsoperational"] + for history_logs in self.history_logs_list + ], + axis=0, + ) return def show(self): plt.show() return + class compare_riskmodels(object): - def __init__(self,vis_list, colour_list): + def __init__(self, vis_list, colour_list): # take in list of visualisation objects and call their plot methods self.vis_list = vis_list self.colour_list = colour_list - - def create_insurer_timeseries(self, fig=None, axlst=None, percentiles=[25,75]): + + def create_insurer_timeseries(self, fig=None, axlst=None, percentiles=[25, 75]): # create the time series for each object in turn and superpose them? fig = axlst = None - for vis,colour in zip(self.vis_list, self.colour_list): - (fig, axlst) = vis.insurer_time_series(fig=fig, axlst=axlst, colour=colour, percentiles=percentiles) + for vis, colour in zip(self.vis_list, self.colour_list): + (fig, axlst) = vis.insurer_time_series( + fig=fig, axlst=axlst, colour=colour, percentiles=percentiles + ) - def create_reinsurer_timeseries(self, fig=None, axlst=None, percentiles=[25,75]): + def create_reinsurer_timeseries(self, fig=None, axlst=None, percentiles=[25, 75]): # create the time series for each object in turn and superpose them? fig = axlst = None - for vis,colour in zip(self.vis_list, self.colour_list): - (fig, axlst) = vis.reinsurer_time_series(fig=fig, axlst=axlst, colour=colour, percentiles=percentiles) + for vis, colour in zip(self.vis_list, self.colour_list): + (fig, axlst) = vis.reinsurer_time_series( + fig=fig, axlst=axlst, colour=colour, percentiles=percentiles + ) def show(self): plt.show() + def save(self): # logic to save plots pass - -if __name__ == "__main__": +if __name__ == "__main__": + # use argparse to handle command line arguments - parser = argparse.ArgumentParser(description='Model the Insurance sector') - parser.add_argument("--single", action="store_true", help="plot time series of a single run of the insurance model") - parser.add_argument("--comparison", action="store_true", help="plot the result of an ensemble of replicatons of the insurance model") + parser = argparse.ArgumentParser(description="Model the Insurance sector") + parser.add_argument( + "--single", + action="store_true", + help="plot time series of a single run of the insurance model", + ) + parser.add_argument( + "--comparison", + action="store_true", + help="plot the result of an ensemble of replicatons of the insurance model", + ) args = parser.parse_args() - if args.single: - # load in data from the history_logs dictionarywith open("data/history_logs.dat","r") as rfile: - with open("data/history_logs.dat","r") as rfile: - history_logs_list = [eval(k) for k in rfile] # one dict on each line + with open("data/history_logs.dat", "r") as rfile: + history_logs_list = [eval(k) for k in rfile] # one dict on each line # first create visualisation object, then create graph/animation objects as necessary vis = visualisation(history_logs_list) vis.insurer_pie_animation() @@ -227,26 +421,27 @@ def save(self): vis.show() N = len(history_logs_list) - if args.comparison: # for each run, generate an animation and time series for insurer and reinsurer # TODO: provide some way for these to be lined up nicely rather than having to manually arrange screen - #for i in range(N): + # for i in range(N): # vis.insurer_pie_animation(run=i) # vis.insurer_time_series(runs=[i]) # vis.reinsurer_pie_animation(run=i) # vis.reinsurer_time_series(runs=[i]) # vis.show() vis_list = [] - filenames = ["./data/"+x+"_history_logs.dat" for x in ["one","two","three","four"]] + filenames = [ + "./data/" + x + "_history_logs.dat" for x in ["one", "two", "three", "four"] + ] for filename in filenames: - with open(filename,'r') as rfile: - history_logs_list = [eval(k) for k in rfile] # one dict on each line + with open(filename, "r") as rfile: + history_logs_list = [eval(k) for k in rfile] # one dict on each line vis_list.append(visualisation(history_logs_list)) - colour_list = ['blue', 'yellow', 'red', 'green'] + colour_list = ["blue", "yellow", "red", "green"] cmp_rsk = compare_riskmodels(vis_list, colour_list) - cmp_rsk.create_insurer_timeseries(percentiles=[10,90]) - cmp_rsk.create_reinsurer_timeseries(percentiles=[10,90]) + cmp_rsk.create_insurer_timeseries(percentiles=[10, 90]) + cmp_rsk.create_reinsurer_timeseries(percentiles=[10, 90]) cmp_rsk.show() diff --git a/visualization_network.py b/visualization_network.py index 82d9129..3e76ee9 100644 --- a/visualization_network.py +++ b/visualization_network.py @@ -2,64 +2,102 @@ import matplotlib.pyplot as plt import numpy as np -class ReinsuranceNetwork(): + +class ReinsuranceNetwork: def __init__(self, insurancefirms, reinsurancefirms, catbonds): """save entities""" self.insurancefirms = insurancefirms self.reinsurancefirms = reinsurancefirms self.catbonds = catbonds - + """obtain lists of operational entities""" op_entities = {} self.num_entities = {} - for firmtype, firmlist in [("insurers", self.insurancefirms), ("reinsurers", self.reinsurancefirms), ("catbonds", self.catbonds)]: + for firmtype, firmlist in [ + ("insurers", self.insurancefirms), + ("reinsurers", self.reinsurancefirms), + ("catbonds", self.catbonds), + ]: op_firmtype = [firm for firm in firmlist if firm.operational] op_entities[firmtype] = op_firmtype self.num_entities[firmtype] = len(op_firmtype) - - #op_entities_flat = [firm for firm in entities_list for entities_list in op_entities] + + # op_entities_flat = [firm for firm in entities_list for entities_list in op_entities] self.network_size = sum(self.num_entities.values()) - + """create weigthed adjacency matrix""" - weights_matrix = np.zeros(self.network_size**2).reshape(self.network_size, self.network_size) - for idx_to, firm in enumerate(op_entities["insurers"] + op_entities["reinsurers"]): + weights_matrix = np.zeros(self.network_size ** 2).reshape( + self.network_size, self.network_size + ) + for idx_to, firm in enumerate( + op_entities["insurers"] + op_entities["reinsurers"] + ): eolrs = firm.get_excess_of_loss_reinsurance() for eolr in eolrs: - #pdb.set_trace() - idx_from = self.num_entities["insurers"] + (op_entities["reinsurers"] + op_entities["catbonds"]).index(eolr["reinsurer"]) + # pdb.set_trace() + idx_from = self.num_entities["insurers"] + ( + op_entities["reinsurers"] + op_entities["catbonds"] + ).index(eolr["reinsurer"]) weights_matrix[idx_from][idx_to] = eolr["value"] - + """unweighted adjacency matrix""" adj_matrix = np.sign(weights_matrix) - + """define network""" - self.network = nx.from_numpy_array(weights_matrix, create_using=nx.DiGraph()) # weighted - self.network_unweighted = nx.from_numpy_array(adj_matrix, create_using=nx.DiGraph()) # unweighted - + self.network = nx.from_numpy_array( + weights_matrix, create_using=nx.DiGraph() + ) # weighted + self.network_unweighted = nx.from_numpy_array( + adj_matrix, create_using=nx.DiGraph() + ) # unweighted + def compute_measures(self): """obtain measures""" - #degrees = self.network.degree() + # degrees = self.network.degree() degree_distr = dict(self.network.degree()).values() in_degree_distr = dict(self.network.in_degree()).values() out_degree_distr = dict(self.network.out_degree()).values() is_connected = nx.is_weakly_connected(self.network) - #is_connected = nx.is_strongly_connected(self.network) # must always be False + # is_connected = nx.is_strongly_connected(self.network) # must always be False try: node_centralities = nx.eigenvector_centrality(self.network) except: node_centralities = nx.betweenness_centrality(self.network) # TODO: and more, choose more meaningful ones... - - print("Graph is connected: ", is_connected, "\nIn degrees ", in_degree_distr, "\nOut degrees", out_degree_distr, \ - "\nCentralities", node_centralities) - def visualize(self): + print( + "Graph is connected: ", + is_connected, + "\nIn degrees ", + in_degree_distr, + "\nOut degrees", + out_degree_distr, + "\nCentralities", + node_centralities, + ) + + def visualize(self): """visualize""" plt.figure() firmtypes = np.ones(self.network_size) - firmtypes[self.num_entities["insurers"]:self.num_entities["insurers"]+self.num_entities["reinsurers"]] = 0.5 - firmtypes[self.num_entities["insurers"]+self.num_entities["reinsurers"]:] = 1.3 - print(firmtypes, self.num_entities["insurers"], self.num_entities["insurers"] + self.num_entities["reinsurers"]) + firmtypes[ + self.num_entities["insurers"] : self.num_entities["insurers"] + + self.num_entities["reinsurers"] + ] = 0.5 + firmtypes[ + self.num_entities["insurers"] + self.num_entities["reinsurers"] : + ] = 1.3 + print( + firmtypes, + self.num_entities["insurers"], + self.num_entities["insurers"] + self.num_entities["reinsurers"], + ) pos = nx.spring_layout(self.network_unweighted) - nx.draw(self.network_unweighted, pos, node_color=firmtypes, with_labels=True, cmap=plt.cm.winter) + nx.draw( + self.network_unweighted, + pos, + node_color=firmtypes, + with_labels=True, + cmap=plt.cm.winter, + ) plt.show() From d86bf04fc7e1a513554c47884c0850d8b8423501 Mon Sep 17 00:00:00 2001 From: KloskaT Date: Wed, 3 Jul 2019 14:54:20 +0100 Subject: [PATCH 002/125] Removed most abce dependance, except for gui function, and some unnecessary parameter assignments (they were immediately overwritten) --- insurancesimulation.py | 54 ++++-------------- start.py | 125 ++++++++++++----------------------------- 2 files changed, 49 insertions(+), 130 deletions(-) diff --git a/insurancesimulation.py b/insurancesimulation.py index f7383d8..d1d200d 100644 --- a/insurancesimulation.py +++ b/insurancesimulation.py @@ -1,5 +1,5 @@ from insurancefirm import InsuranceFirm -#from riskmodel import RiskModel +# from riskmodel import RiskModel from reinsurancefirm import ReinsuranceFirm from distributiontruncated import TruncatedDistWrapper import numpy as np @@ -14,13 +14,6 @@ if isleconfig.show_network: import visualization_network -if isleconfig.use_abce: - import abce - #print("abce imported") -#else: -# print("abce not imported") - - class InsuranceSimulation(): def __init__(self, override_no_riskmodels, replic_ID, simulation_parameters, rc_event_schedule, rc_event_damage): @@ -212,10 +205,8 @@ def __init__(self, override_no_riskmodels, replic_ID, simulation_parameters, rc_ self.insurance_models_counter = np.zeros(self.simulation_parameters["no_categories"]) self.reinsurance_models_counter = np.zeros(self.simulation_parameters["no_categories"]) - - def build_agents(self, agent_class, agent_class_string, parameters, agent_parameters): - #assert agent_parameters == self.agent_parameters[agent_class_string] #assert fits only the initial creation of agents, not later additions # TODO: fix + # assert agent_parameters == self.agent_parameters[agent_class_string] #assert fits only the initial creation of agents, not later additions # TODO: fix agents = [] for ap in agent_parameters: agents.append(agent_class(parameters, ap)) @@ -272,9 +263,8 @@ def iterate(self, t): self.reset_pls() - # adjust market premiums - sum_capital = sum([agent.get_cash() for agent in self.insurancefirms]) #TODO: include reinsurancefirms + sum_capital = sum([agent.get_cash() for agent in self.insurancefirms]) # TODO: include reinsurancefirms self.adjust_market_premium(capital=sum_capital) sum_capital = sum([agent.get_cash() for agent in self.reinsurancefirms]) # TODO: include reinsurancefirms self.adjust_reinsurance_market_premium(capital=sum_capital) @@ -291,8 +281,8 @@ def iterate(self, t): print("Something wrong; past events not deleted", file=sys.stderr) if len(self.rc_event_schedule[categ_id]) > 0 and self.rc_event_schedule[categ_id][0] == t: self.rc_event_schedule[categ_id] = self.rc_event_schedule[categ_id][1:] - damage_extent = copy.copy(self.rc_event_damage[categ_id][0]) #Schedules of catastrophes and damages must me generated at the same time. - self.inflict_peril(categ_id=categ_id, damage=damage_extent, t=t)# TODO: consider splitting the following lines from this method and running it with nb.jit + damage_extent = copy.copy(self.rc_event_damage[categ_id][0]) # Schedules of catastrophes and damages must be generated at the same time. + self.inflict_peril(categ_id=categ_id, damage=damage_extent, t=t) # TODO: consider splitting the following lines from this method and running it with nb.jit self.rc_event_damage[categ_id] = self.rc_event_damage[categ_id][1:] else: if isleconfig.verbose: @@ -307,14 +297,6 @@ def iterate(self, t): # iterate reinsurnace firm agents for reinagent in self.reinsurancefirms: reinagent.iterate(t) - # TODO: is the following necessary for abce to work (log) properly? - #if isleconfig.use_abce: - # self.reinsurancefirms_group.iterate(time=t) - #else: - # for reinagent in self.reinsurancefirms: - # reinagent.iterate(t) - - # remove all non-accepted reinsurance risks self.reinrisks = [] @@ -324,12 +306,6 @@ def iterate(self, t): # iterate insurance firm agents for agent in self.insurancefirms: agent.iterate(t) - # TODO: is the following necessary for abce to work (log) properly? - #if isleconfig.use_abce: - # self.insurancefirms_group.iterate(time=t) - #else: - # for agent in self.insurancefirms: - # agent.iterate(t) # iterate catbonds for agent in self.catbonds: @@ -350,15 +326,12 @@ def iterate(self, t): if reinsurer.operational: if reinsurer.riskmodel.inaccuracy == self.inaccuracy[i]: self.reinsurance_models_counter[i] += 1 - - #print(isleconfig.show_network) - # TODO: use network representation in a more generic way, perhaps only once at the end to characterize the network and use for calibration(?) + if isleconfig.show_network and t % 40 == 0 and t > 0: RN = visualization_network.ReinsuranceNetwork(self.insurancefirms, self.reinsurancefirms, self.catbonds) RN.compute_measures() RN.visualize() - - + def save_data(self): """Method to collect statistics about the current state of the simulation. Will pass these to the Logger object (self.logger) to be recorded. @@ -418,13 +391,10 @@ def save_data(self): def obtain_log(self, requested_logs=None): #This function allows to return in a list all the data generated by the model. There is no other way to transfer it back from the cloud. return self.logger.obtain_log(requested_logs) - def advance_round(self, *args): - pass - def finalize(self, *args): - """Function to handle oberations after the end of the simulation run. + """Function to handle operations after the end of the simulation run. Currently empty. - It may be used to handle e.g. loging by including: + It may be used to handle e.g. logging by including: self.log() but logging has been moved to start.py and ensemble.py """ @@ -803,7 +773,7 @@ def reinsurance_entry_index(self): def get_operational(self): return True - def reinsurance_capital_entry(self): #This method determines the capital market entry of reinsurers. It is only run in start.py. + def reinsurance_capital_entry(self): # This method determines the capital market entry of reinsurers. It is only run in start.py. capital_per_non_re_cat = [] for reinrisk in self.not_accepted_reinrisks: @@ -813,10 +783,10 @@ def reinsurance_capital_entry(self): #This method determines the capital mar capital_per_non_re_cat = np.random.choice(capital_per_non_re_cat, 10) #Only 10 values sampled randomly are considered. (Too low?) entry = max(capital_per_non_re_cat) #For market entry the maximum of the sample is considered. entry = 2 * entry #The capital market entry of those values will be the double of the maximum. - else: #Otherwise the default reinsurance cash market entry is considered. + else: # Otherwise the default reinsurance cash market entry is considered. entry = self.simulation_parameters["initial_reinagent_cash"] - return entry #The capital market entry is returned. + return entry # The capital market entry is returned. def reset_pls(self): """Reset_pls Method. diff --git a/start.py b/start.py index b75e0a9..5e52335 100644 --- a/start.py +++ b/start.py @@ -1,4 +1,4 @@ -# import common packages +"""import common packages and necessary classes""" import numpy as np import scipy.stats import math @@ -10,27 +10,24 @@ import random import copy import importlib - -# import config file and apply configuration import isleconfig - -simulation_parameters = isleconfig.simulation_parameters -replic_ID = None -override_no_riskmodels = False - from insurancesimulation import InsuranceSimulation from insurancefirm import InsuranceFirm -from riskmodel import RiskModel from reinsurancefirm import ReinsuranceFirm import logger import calibrationscore - -# ensure that logging directory exists + +simulation_parameters = isleconfig.simulation_parameters +replic_ID = None +override_no_riskmodels = False + +"""Creates data file for logs if does not exist""" if not os.path.isdir("data"): assert not os.path.exists("data"), "./data exists as regular file. This filename is required for the logging directory" os.makedirs("data") -# create conditional decorator + +"""create conditional decorator""" def conditionally(decorator_function, condition): def wrapper(target_function): if not condition: @@ -38,63 +35,42 @@ def wrapper(target_function): return decorator_function(target_function) return wrapper -# create non-abce placeholder gui decorator -# TODO: replace this with more elegant solution if possible. Currently required since script will otherwise crash at the conditional decorator below since gui is then undefined -if not isleconfig.use_abce: - def gui(*args, **kwargs): - pass +"""Create placeholder gui decorator""" +# TODO: replace this with more elegant solution if possible. Currently required since script will otherwise crash\ +# at the conditional decorator below since gui is then undefined +def gui(*args, **kwargs): + pass -# main function -#@gui(simulation_parameters, serve=True) @conditionally(gui(simulation_parameters, serve=False), isleconfig.use_abce) +# TODO: Fix/remove gui and remove last of abce def main(simulation_parameters, rc_event_schedule, rc_event_damage, np_seed, random_seed, save_iter, requested_logs=None): np.random.seed(np_seed) random.seed(random_seed) - # create simulation and world objects (identical in non-abce mode) - if isleconfig.use_abce: - simulation = abce.Simulation(processes=1,random_seed = random_seed) - + """create simulation and world objects""" simulation_parameters['simulation'] = world = InsuranceSimulation(override_no_riskmodels, replic_ID, simulation_parameters, rc_event_schedule, rc_event_damage) - - if not isleconfig.use_abce: - simulation = world + simulation = world - # create agents: insurance firms - insurancefirms_group = simulation.build_agents(InsuranceFirm, - 'insurancefirm', - parameters=simulation_parameters, + """create agents: insurance firms according to number in isleconfig.py, adds them all to the simulation instance""" + insurancefirms_group = simulation.build_agents(InsuranceFirm, 'insurancefirm', parameters=simulation_parameters, agent_parameters=world.agent_parameters["insurancefirm"]) - - if isleconfig.use_abce: - insurancefirm_pointers = insurancefirms_group.get_pointer() - else: - insurancefirm_pointers = insurancefirms_group + insurancefirm_pointers = insurancefirms_group world.accept_agents("insurancefirm", insurancefirm_pointers, insurancefirms_group) - # create agents: reinsurance firms - reinsurancefirms_group = simulation.build_agents(ReinsuranceFirm, - 'reinsurance', - parameters=simulation_parameters, + """create agents: reinsurance firms according to number in isleconfig.py, adds them all to simulation instance""" + reinsurancefirms_group = simulation.build_agents(ReinsuranceFirm, 'reinsurance', parameters=simulation_parameters, agent_parameters=world.agent_parameters["reinsurance"]) - if isleconfig.use_abce: - reinsurancefirm_pointers = reinsurancefirms_group.get_pointer() - else: - reinsurancefirm_pointers = reinsurancefirms_group + reinsurancefirm_pointers = reinsurancefirms_group world.accept_agents("reinsurance", reinsurancefirm_pointers, reinsurancefirms_group) - # time iteration + """Time iteration""" for t in range(simulation_parameters["max_time"]): - # abce time step - simulation.advance_round(t) - - # create new agents # TODO: write method for this; this code block is executed almost identically 4 times + "create new agents and accepts them based on probability" # TODO: write method for this; this code block is executed almost identically 4 times if world.insurance_firm_market_entry(agent_type="InsuranceFirm"): - parameters = [np.random.choice(world.agent_parameters["insurancefirm"])] parameters = [world.agent_parameters["insurancefirm"][simulation.insurance_entry_index()]] parameters[0]["id"] = world.get_unique_insurer_id() new_insurance_firm = simulation.build_agents(InsuranceFirm, @@ -102,18 +78,10 @@ def main(simulation_parameters, rc_event_schedule, rc_event_damage, np_seed, ran parameters=simulation_parameters, agent_parameters=parameters) insurancefirms_group += new_insurance_firm - if isleconfig.use_abce: - # TODO: fix abce - # may fail in abce because addressing individual agents may not be allowed - # may also fail because agent methods may not be callable directly - new_insurancefirm_pointer = [new_insurance_firm[0].get_pointer()] # index 0 because this is a list with just 1 object - else: - new_insurancefirm_pointer = new_insurance_firm + new_insurancefirm_pointer = new_insurance_firm world.accept_agents("insurancefirm", new_insurancefirm_pointer, new_insurance_firm, time=t) if world.insurance_firm_market_entry(agent_type="ReinsuranceFirm"): - parameters = [np.random.choice(world.agent_parameters["reinsurance"])] - parameters[0]["initial_cash"] = world.reinsurance_capital_entry() #Since the value of the reinrisks varies overtime it makes sense that the market entry of reinsures depends on those values. The method world.reinsurance_capital_entry() determines the capital market entry of reinsurers. parameters = [world.agent_parameters["reinsurance"][simulation.reinsurance_entry_index()]] parameters[0]["id"] = world.get_unique_reinsurer_id() new_reinsurance_firm = simulation.build_agents(ReinsuranceFirm, @@ -121,36 +89,26 @@ def main(simulation_parameters, rc_event_schedule, rc_event_damage, np_seed, ran parameters=simulation_parameters, agent_parameters=parameters) reinsurancefirms_group += new_reinsurance_firm - if isleconfig.use_abce: - # TODO: fix abce - # may fail in abce because addressing individual agents may not be allowed - # may also fail because agent methods may not be callable directly - new_reinsurancefirm_pointer = [new_reinsurance_firm[0].get_pointer()] # index 0 because this is a list with just 1 object - else: - new_reinsurancefirm_pointer = new_reinsurance_firm + new_reinsurancefirm_pointer = new_reinsurance_firm world.accept_agents("reinsurance", new_reinsurancefirm_pointer, new_reinsurance_firm, time=t) - # iterate simulation + "iterate simulation" world.iterate(t) - # log data - if isleconfig.use_abce: - #insurancefirms.logme() - #reinsurancefirms.logme() - insurancefirms_group.agg_log(variables=['cash', 'operational'], len=['underwritten_contracts']) - #reinsurancefirms_group.agg_log(variables=['cash']) - else: - world.save_data() + "log data" + world.save_data() if t%50 == save_iter: save_simulation(t, simulation, simulation_parameters, exit_now=False) - # finish simulation, write logs + """finish simulation, write logs""" simulation.finalize() - return simulation.obtain_log(requested_logs) #It is required to return this list to download all the data generated by a single run of the model from the cloud. + return simulation.obtain_log(requested_logs) + # It is required to return this list to download all the data generated by a single run of the model from the cloud. + -# save function +"""save function""" def save_simulation(t, sim, sim_param, exit_now=False): d = {} d["np_seed"] = np.random.get_state() @@ -168,12 +126,12 @@ def save_simulation(t, sim, sim_param, exit_now=False): if exit_now: exit(0) -# main entry point + +"""main entry point""" if __name__ == "__main__": """ use argparse to handle command line arguments""" parser = argparse.ArgumentParser(description='Model the Insurance sector') - parser.add_argument("--abce", action="store_true", help="use abce") parser.add_argument("--oneriskmodel", action="store_true", help="allow overriding the number of riskmodels from the standard config (with 1)") parser.add_argument("--riskmodels", type=int, choices=[1, 2, 3, 4], @@ -191,8 +149,6 @@ def save_simulation(t, sim, sim_param, exit_now=False): parser.add_argument("--save_iterations", type=int, help="number of iterations to iterate before saving world state") args = parser.parse_args() - if args.abce: - isleconfig.use_abce = True if args.oneriskmodel: isleconfig.oneriskmodel = True override_no_riskmodels = 1 @@ -226,12 +182,6 @@ def save_simulation(t, sim, sim_param, exit_now=False): save_iter = args.save_iterations else: save_iter = 200 - - """ import abce module if required """ - if isleconfig.use_abce: - # print("Importing abce") - import abce - from abce import gui from setup import SetupSim setup = SetupSim() #Here the setup for the simulation is done. @@ -248,4 +198,3 @@ def save_simulation(t, sim, sim_param, exit_now=False): """ Obtain calibration score """ CS = calibrationscore.CalibrationScore(L) score = CS.test_all() - From bbe554fe90e603295e3130a79254aaabbabd9f09 Mon Sep 17 00:00:00 2001 From: Chris Hughes Date: Wed, 3 Jul 2019 15:58:24 +0100 Subject: [PATCH 003/125] A bit more aesthetic cleanup --- catbond.py | 2 +- insurancecontract.py | 7 ----- insurancesimulation.py | 62 +++++++++++++++++++----------------------- reinsurancecontract.py | 7 +++-- resume.py | 16 +++++------ start.py | 20 +++++++------- 6 files changed, 52 insertions(+), 62 deletions(-) diff --git a/catbond.py b/catbond.py index 9f7ee68..e6603a2 100644 --- a/catbond.py +++ b/catbond.py @@ -308,7 +308,7 @@ def parent_iterate( # and categ_risks[i]["risk_factor"] < self.acceptance_threshold): if ( categ_risks[i].get("contract") is not None - ): # categ_risks[i]["reinsurance"]: + ): # categ_risks[i]["reinsurancefirm"]: if ( categ_risks[i]["contract"].expiration > time ): # required to rule out contracts that have exploded in the meantime diff --git a/insurancecontract.py b/insurancecontract.py index 55a8fc3..c54645f 100644 --- a/insurancecontract.py +++ b/insurancecontract.py @@ -4,12 +4,6 @@ class InsuranceContract(MetaInsuranceContract): - """ReinsuranceContract class. - Inherits from InsuranceContract. - Constructor is not currently required but may be used in the future to distinguish InsuranceContract - and ReinsuranceContract objects. - The signature of this class' constructor is the same as that of the InsuranceContract constructor. - The class has two methods (explode, mature) that overwrite methods in InsuranceContract.""" def __init__( self, @@ -56,7 +50,6 @@ def explode(self, time, uniform_value, damage_extent): # np.mean(np.random.beta(1, 1./mu -1, size=90000)) # if np.random.uniform(0, 1) < self.risk_factor: if uniform_value < self.risk_factor: - # if True: claim = min(self.excess, damage_extent * self.value) - self.deductible self.insurer.register_claim( claim diff --git a/insurancesimulation.py b/insurancesimulation.py index 8389b0f..d486513 100644 --- a/insurancesimulation.py +++ b/insurancesimulation.py @@ -205,8 +205,8 @@ def __init__( # prepare setting up agents (to be done from start.py) self.agent_parameters = { "insurancefirm": [], - "reinsurance": [], - } # TODO: rename reinsurance -> reinsurancefirm (also in start.py and below in method accept_agents + "reinsurancefirm": [], + } self.insurer_id_counter = 0 # TODO: collapse the following two loops into one generic one? @@ -270,7 +270,7 @@ def __init__( riskmodel_config = risk_model_configurations[ i % len(risk_model_configurations) ] - self.agent_parameters["reinsurance"].append( + self.agent_parameters["reinsurancefirm"].append( { "id": self.get_unique_reinsurer_id(), "initial_cash": simulation_parameters["initial_reinagent_cash"], @@ -360,7 +360,7 @@ def accept_agents(self, agent_class_string, agents, agent_group=None, time=0): # remove new agent cash from simulation cash to ensure stock flow consistency new_agent_cash = sum([agent.cash for agent in agents]) self.reduce_money_supply(new_agent_cash) - elif agent_class_string == "reinsurance": + elif agent_class_string == "reinsurancefirm": try: self.reinsurancefirms += agents self.reinsurancefirms_group = agent_group @@ -403,11 +403,11 @@ def iterate(self, t): # adjust market premiums sum_capital = sum( [agent.get_cash() for agent in self.insurancefirms] - ) # TODO: include reinsurancefirms + ) self.adjust_market_premium(capital=sum_capital) sum_capital = sum( [agent.get_cash() for agent in self.reinsurancefirms] - ) # TODO: include reinsurancefirms + ) self.adjust_reinsurance_market_premium(capital=sum_capital) # pay obligations @@ -415,11 +415,8 @@ def iterate(self, t): # identify perils and effect claims for categ_id in range(len(self.rc_event_schedule)): - try: - if len(self.rc_event_schedule[categ_id]) > 0: - assert self.rc_event_schedule[categ_id][0] >= t - except: - print("Something wrong; past events not deleted", file=sys.stderr) + if self.rc_event_schedule[categ_id] and self.rc_event_schedule[categ_id][0] < t: + raise RuntimeWarning("Something wrong; past events not deleted") if ( len(self.rc_event_schedule[categ_id]) > 0 and self.rc_event_schedule[categ_id][0] == t @@ -430,7 +427,7 @@ def iterate(self, t): ) # Schedules of catastrophes and damages must me generated at the same time. self.inflict_peril( categ_id=categ_id, damage=damage_extent, t=t - ) # TODO: consider splitting the following lines from this method and running it with nb.jit + ) self.rc_event_damage[categ_id] = self.rc_event_damage[categ_id][1:] else: if isleconfig.verbose: @@ -679,7 +676,8 @@ def receive(self, amount): self.money_supply += amount def reduce_money_supply(self, amount): - """Method to reduce money supply immediately and without payment recipient (used to adjust money supply to compensate for agent endowment).""" + """Method to reduce money supply immediately and without payment recipient (used to adjust money supply + to compensate for agent endowment).""" self.money_supply -= amount assert self.money_supply >= 0 @@ -969,16 +967,16 @@ def restore_state_and_risk_categories(self): mersennetwister_randomseed = eval(line) found = True rfile.close() + if not found: + raise Exception( + "mersennetwister randomseed for current replication ID number {0:d} not found in data file. Exiting.".format( + self.replic_ID + )) np.random.set_state(mersennetwister_randomseed) - assert ( - found - ), "mersennetwister randomseed for current replication ID number {0:d} not found in data file. Exiting.".format( - self.replic_ID - ) - def insurance_firm_market_entry( - self, prob=-1, agent_type="InsuranceFirm" - ): # TODO: replace method name with a more descriptive one + def insurance_firm_enters_market( + self, prob=-1, agent_type="InsuranceFirm" + ): if prob == -1: if agent_type == "InsuranceFirm": prob = self.simulation_parameters[ @@ -989,15 +987,10 @@ def insurance_firm_market_entry( "reinsurance_firm_market_entry_probability" ] else: - assert ( - False - ), "Unknown agent type. Simulation requested to create agent of type {0:s}".format( + raise ValueError("Unknown agent type. Simulation requested to create agent of type {0:s}".format( agent_type - ) - if np.random.random() < prob: - return True - else: - return False + )) + return np.random.random() < prob def record_bankruptcy(self): """Record_bankruptcy Method. @@ -1020,7 +1013,8 @@ def record_unrecovered_claims(self, loss): def record_claims( self, claims - ): # This method records every claim made to insurers and reinsurers. It is called from both insurers and reinsurers (metainsuranceorg.py). + ): # This method records every claim made to insurers and reinsurers. + # It is called from both insurers and reinsurers (metainsuranceorg.py). self.cumulative_claims += claims def log(self): @@ -1057,7 +1051,7 @@ def compute_market_diffvar(self): return totaldiff # self.history_logs['market_diffvar'].append(totaldiff) - def count_underwritten_and_reinsured_risks_by_category(self): + def count_underwritten_and_reinsured_risks_by_category(self): # QUERY does this do anything? underwritten_risks = 0 reinsured_risks = 0 underwritten_per_category = np.zeros( @@ -1066,7 +1060,7 @@ def count_underwritten_and_reinsured_risks_by_category(self): reinsured_per_category = np.zeros(self.simulation_parameters["no_categories"]) for firm in self.insurancefirms: if firm.operational: - underwritten_by_category += firm.counter_category + underwritten_per_category += firm.counter_category if ( self.simulation_parameters["simulation_reinsurance_type"] == "non-proportional" @@ -1091,12 +1085,12 @@ def get_unique_reinsurer_id(self): def insurance_entry_index(self): return self.insurance_models_counter[ - 0 : self.simulation_parameters["no_riskmodels"] + 0:self.simulation_parameters["no_riskmodels"] ].argmin() def reinsurance_entry_index(self): return self.reinsurance_models_counter[ - 0 : self.simulation_parameters["no_riskmodels"] + 0:self.simulation_parameters["no_riskmodels"] ].argmin() def get_operational(self): diff --git a/reinsurancecontract.py b/reinsurancecontract.py index 86d011d..3f5e680 100644 --- a/reinsurancecontract.py +++ b/reinsurancecontract.py @@ -42,6 +42,8 @@ def __init__( ) # self.is_reinsurancecontract = True + if self.insurancetype not in ["excess-of-loss", "proportional"]: + raise ValueError(f'Unrecognised insurance type "{self.insurancetype}"') if self.insurancetype == "excess-of-loss": self.property_holder.add_reinsurance( category=self.category, @@ -66,7 +68,7 @@ def explode(self, time, damage_extent=None): if self.insurancetype == "excess-of-loss" and damage_extent > self.deductible: claim = min(self.excess, damage_extent) - self.deductible self.insurer.receive_obligation(claim, self.property_holder, time, "claim") - else: + else: # TODO: should this be elif == "proportional"? claim = min(self.excess, damage_extent) - self.deductible self.insurer.receive_obligation( claim, self.property_holder, time + 1, "claim" @@ -79,7 +81,8 @@ def explode(self, time, damage_extent=None): if self.expire_immediately: self.current_claim += ( self.contract.claim - ) # TODO: should proportional reinsurance claims be subject to excess_of_loss retrocession? If so, reorganize more straightforwardly + ) # TODO: should proportional reinsurance claims be subject to excess_of_loss retrocession? + # If so, reorganize more straightforwardly self.expiration = time # self.terminating = True diff --git a/resume.py b/resume.py index e122c94..f08b6c6 100644 --- a/resume.py +++ b/resume.py @@ -154,14 +154,14 @@ def main(): # # create agents: reinsurance firms # reinsurancefirms_group = simulation.build_agents(ReinsuranceFirm, - # 'reinsurance', + # "reinsurancefirm", # parameters=simulation_parameters, - # agent_parameters=world.agent_parameters["reinsurance"]) + # agent_parameters=world.agent_parameters["reinsurancefirm"]) # if isleconfig.use_abce: # reinsurancefirm_pointers = reinsurancefirms_group.get_pointer() # else: # reinsurancefirm_pointers = reinsurancefirms_group - # world.accept_agents("reinsurance", reinsurancefirm_pointers, reinsurancefirms_group) + # world.accept_agents("reinsurancefirm", reinsurancefirm_pointers, reinsurancefirms_group) # # time iteration @@ -171,7 +171,7 @@ def main(): simulation.advance_round(t) # create new agents # TODO: write method for this; this code block is executed almost identically 4 times - if world.insurance_firm_market_entry(agent_type="InsuranceFirm"): + if world.insurance_firm_enters_market(agent_type="InsuranceFirm"): parameters = [np.random.choice(world.agent_parameters["insurancefirm"])] parameters[0]["id"] = world.get_unique_insurer_id() new_insurance_firm = simulation.build_agents( @@ -194,12 +194,12 @@ def main(): "insurancefirm", new_insurancefirm_pointer, new_insurance_firm, time=t ) - if world.insurance_firm_market_entry(agent_type="ReinsuranceFirm"): - parameters = [np.random.choice(world.agent_parameters["reinsurance"])] + if world.insurance_firm_enters_market(agent_type="ReinsuranceFirm"): + parameters = [np.random.choice(world.agent_parameters["reinsurancefirm"])] parameters[0]["id"] = world.get_unique_reinsurer_id() new_reinsurance_firm = simulation.build_agents( ReinsuranceFirm, - "reinsurance", + "reinsurancefirm", parameters=simulation_parameters, agent_parameters=parameters, ) @@ -214,7 +214,7 @@ def main(): else: new_reinsurancefirm_pointer = new_reinsurance_firm world.accept_agents( - "reinsurance", new_reinsurancefirm_pointer, new_reinsurance_firm, time=t + "reinsurancefirm", new_reinsurancefirm_pointer, new_reinsurance_firm, time=t ) # iterate simulation diff --git a/start.py b/start.py index 1841f18..3da7eb1 100644 --- a/start.py +++ b/start.py @@ -26,7 +26,7 @@ # ensure that logging directory exists if not os.path.isdir("data"): if os.path.exists("data"): - raise Exception( + raise FileExistsError( "./data exists as regular file. This filename is required for the logging directory" ) os.makedirs("data") @@ -100,15 +100,15 @@ def main( # create agents: reinsurance firms reinsurancefirms_group = simulation.build_agents( reinsurancefirm.ReinsuranceFirm, - "reinsurance", + "reinsurancefirm", parameters=simulation_parameters, - agent_parameters=world.agent_parameters["reinsurance"], + agent_parameters=world.agent_parameters["reinsurancefirm"], ) if isleconfig.use_abce: reinsurancefirm_pointers = reinsurancefirms_group.get_pointer() else: reinsurancefirm_pointers = reinsurancefirms_group - world.accept_agents("reinsurance", reinsurancefirm_pointers, reinsurancefirms_group) + world.accept_agents("reinsurancefirm", reinsurancefirm_pointers, reinsurancefirms_group) # time iteration for t in range(simulation_parameters["max_time"]): @@ -117,7 +117,7 @@ def main( simulation.advance_round(t) # create new agents # TODO: write method for this; this code block is executed almost identically 4 times - if world.insurance_firm_market_entry(agent_type="InsuranceFirm"): + if world.insurance_firm_enters_market(agent_type="InsuranceFirm"): parameters = [np.random.choice(world.agent_parameters["insurancefirm"])] parameters = [ world.agent_parameters["insurancefirm"][ @@ -145,22 +145,22 @@ def main( "insurancefirm", new_insurancefirm_pointer, new_insurance_firm, time=t ) - if world.insurance_firm_market_entry(agent_type="ReinsuranceFirm"): - parameters = [np.random.choice(world.agent_parameters["reinsurance"])] + if world.insurance_firm_enters_market(agent_type="ReinsuranceFirm"): + parameters = [np.random.choice(world.agent_parameters["reinsurancefirm"])] parameters[0][ "initial_cash" ] = ( world.reinsurance_capital_entry() ) # Since the value of the reinrisks varies overtime it makes sense that the market entry of reinsures depends on those values. The method world.reinsurance_capital_entry() determines the capital market entry of reinsurers. parameters = [ - world.agent_parameters["reinsurance"][ + world.agent_parameters["reinsurancefirm"][ simulation.reinsurance_entry_index() ] ] parameters[0]["id"] = world.get_unique_reinsurer_id() new_reinsurance_firm = simulation.build_agents( reinsurancefirm.ReinsuranceFirm, - "reinsurance", + "reinsurancefirm", parameters=simulation_parameters, agent_parameters=parameters, ) @@ -175,7 +175,7 @@ def main( else: new_reinsurancefirm_pointer = new_reinsurance_firm world.accept_agents( - "reinsurance", new_reinsurancefirm_pointer, new_reinsurance_firm, time=t + "reinsurancefirm", new_reinsurancefirm_pointer, new_reinsurance_firm, time=t ) # iterate simulation From b4783d236e1a3fd59113fd17b7e9392f773e92b4 Mon Sep 17 00:00:00 2001 From: KloskaT Date: Wed, 3 Jul 2019 16:30:36 +0100 Subject: [PATCH 004/125] Cleaned up the init file for InsuranceSimulation and added a function for a general loop to initialise agent parameters. --- insurancesimulation.py | 159 +++++++++++++++++------------------------ 1 file changed, 66 insertions(+), 93 deletions(-) diff --git a/insurancesimulation.py b/insurancesimulation.py index d1d200d..52fea3a 100644 --- a/insurancesimulation.py +++ b/insurancesimulation.py @@ -17,44 +17,38 @@ class InsuranceSimulation(): def __init__(self, override_no_riskmodels, replic_ID, simulation_parameters, rc_event_schedule, rc_event_damage): - # override one-riskmodel case (this is to ensure all other parameters are truly identical for comparison runs) + + "Override one-riskmodel case (this is to ensure all other parameters are truly identical for comparison runs)" if override_no_riskmodels: simulation_parameters["no_riskmodels"] = override_no_riskmodels self.number_riskmodels = simulation_parameters["no_riskmodels"] - # save parameters - if (replic_ID is None) or (isleconfig.force_foreground): + "Save parameters, sets parameters of sim according to isleconfig.py" + if (replic_ID is None) or isleconfig.force_foreground: self.background_run = False else: self.background_run = True self.replic_ID = replic_ID self.simulation_parameters = simulation_parameters - - # unpack parameters, set up environment (distributions etc.) - - # damage distribution - # TODO: control damage distribution via parameters, not directly - #self.damage_distribution = scipy.stats.uniform(loc=0, scale=1) - non_truncated = scipy.stats.pareto(b=2, loc=0, scale=0.25) - self.damage_distribution = TruncatedDistWrapper(lower_bound=0.25, upper_bound=1., dist=non_truncated) - # remaining parameters + "Unpacks parameters and sets distributions" self.catbonds_off = simulation_parameters["catbonds_off"] self.reinsurance_off = simulation_parameters["reinsurance_off"] - self.cat_separation_distribution = scipy.stats.expon(0, simulation_parameters["event_time_mean_separation"]) + self.total_no_risks = simulation_parameters["no_risks"] self.risk_factor_lower_bound = simulation_parameters["risk_factor_lower_bound"] + self.cat_separation_distribution = scipy.stats.expon(0, simulation_parameters["event_time_mean_separation"]) self.risk_factor_spread = simulation_parameters["risk_factor_upper_bound"] - simulation_parameters["risk_factor_lower_bound"] self.risk_factor_distribution = scipy.stats.uniform(loc=self.risk_factor_lower_bound, scale=self.risk_factor_spread) if not simulation_parameters["risk_factors_present"]: self.risk_factor_distribution = scipy.stats.uniform(loc=1.0, scale=0) - #self.risk_value_distribution = scipy.stats.uniform(loc=100, scale=9900) self.risk_value_distribution = scipy.stats.uniform(loc=1000, scale=0) - risk_factor_mean = self.risk_factor_distribution.mean() if np.isnan(risk_factor_mean): # unfortunately scipy.stats.mean is not well-defined if scale = 0 risk_factor_mean = self.risk_factor_distribution.rvs() + non_truncated = scipy.stats.pareto(b=2, loc=0, scale=0.25) + self.damage_distribution = TruncatedDistWrapper(lower_bound=0.25, upper_bound=1., dist=non_truncated) - # set initial market price (normalized, i.e. must be multiplied by value or excess-deductible) + "set initial market price (normalized, i.e. must be multiplied by value or excess-deductible)" if self.simulation_parameters["expire_immediately"]: assert self.cat_separation_distribution.dist.name == "expon" expected_damage_frequency = 1 - scipy.stats.poisson(1 / self.simulation_parameters["event_time_mean_separation"] * \ @@ -63,55 +57,42 @@ def __init__(self, override_no_riskmodels, replic_ID, simulation_parameters, rc_ expected_damage_frequency = self.simulation_parameters["mean_contract_runtime"] / \ self.cat_separation_distribution.mean() self.norm_premium = expected_damage_frequency * self.damage_distribution.mean() * \ - risk_factor_mean * \ - (1 + self.simulation_parameters["norm_profit_markup"]) - - self.market_premium = self.norm_premium - self.reinsurance_market_premium = self.market_premium # TODO: is this problematic as initial value? (later it is recomputed in every iteration) - self.total_no_risks = simulation_parameters["no_risks"] + risk_factor_mean * (1 + self.simulation_parameters["norm_profit_markup"]) + self.reinsurance_market_premium = self.market_premium = self.norm_premium - # set up monetary system (should instead be with the customers, if customers are modeled explicitly) + "Set up monetary system (should instead be with the customers, if customers are modeled explicitly)" self.money_supply = self.simulation_parameters["money_supply"] self.obligations = [] - # set up risk categories + "Set up risk categories" self.riskcategories = list(range(self.simulation_parameters["no_categories"])) self.rc_event_schedule = [] self.rc_event_damage = [] - self.rc_event_schedule_initial = [] #For debugging (cloud debugging) purposes is good to store the initial schedule of catastrophes - self.rc_event_damage_initial = [] #and damages that will be use in a single run of the model. + self.rc_event_schedule_initial = [] # For debugging (cloud debugging) purposes is good to store the initial schedule of catastrophes + self.rc_event_damage_initial = [] # and damages that will be use in a single run of the model. - if rc_event_schedule is not None and rc_event_damage is not None: #If we have schedules pass as arguments we used them. + if rc_event_schedule is not None and rc_event_damage is not None: # If we have schedules pass as arguments we used them. self.rc_event_schedule = copy.copy(rc_event_schedule) self.rc_event_schedule_initial = copy.copy(rc_event_schedule) self.rc_event_damage = copy.copy(rc_event_damage) self.rc_event_damage_initial = copy.copy(rc_event_damage) - else: #Otherwise the schedules and damages are generated. + else: # Otherwise the schedules and damages are generated. self.setup_risk_categories_caller() - - # set up risks + "Set up risks" risk_value_mean = self.risk_value_distribution.mean() - if np.isnan(risk_value_mean): # unfortunately scipy.stats.mean is not well-defined if scale = 0 + if np.isnan(risk_value_mean): # unfortunately scipy.stats.mean is not well-defined if scale = 0 risk_value_mean = self.risk_value_distribution.rvs() rrisk_factors = self.risk_factor_distribution.rvs(size=self.simulation_parameters["no_risks"]) rvalues = self.risk_value_distribution.rvs(size=self.simulation_parameters["no_risks"]) rcategories = np.random.randint(0, self.simulation_parameters["no_categories"], size=self.simulation_parameters["no_risks"]) self.risks = [{"risk_factor": rrisk_factors[i], "value": rvalues[i], "category": rcategories[i], "owner": self} for i in range(self.simulation_parameters["no_risks"])] - self.risks_counter = [0,0,0,0] for item in self.risks: self.risks_counter[item["category"]] = self.risks_counter[item["category"]] + 1 - - # set up risk models - #inaccuracy = [[(1./self.simulation_parameters["riskmodel_inaccuracy_parameter"] if (i + j) % 2 == 0 \ - # else self.simulation_parameters["riskmodel_inaccuracy_parameter"]) \ - # for i in range(self.simulation_parameters["no_categories"])] \ - # for j in range(self.simulation_parameters["no_riskmodels"])] - self.inaccuracy = self.get_all_riskmodel_combinations(self.simulation_parameters["no_categories"], self.simulation_parameters["riskmodel_inaccuracy_parameter"]) self.inaccuracy = random.sample(self.inaccuracy, self.simulation_parameters["no_riskmodels"]) @@ -129,75 +110,31 @@ def __init__(self, override_no_riskmodels, replic_ID, simulation_parameters, rc_ "inaccuracy_by_categ": self.inaccuracy[i]} \ for i in range(self.simulation_parameters["no_riskmodels"])] - # prepare setting up agents (to be done from start.py) - self.agent_parameters = {"insurancefirm": [], "reinsurance": []} # TODO: rename reinsurance -> reinsurancefirm (also in start.py and below in method accept_agents - - self.insurer_id_counter = 0 - # TODO: collapse the following two loops into one generic one? - for i in range(simulation_parameters["no_insurancefirms"]): - if simulation_parameters['static_non-proportional_reinsurance_levels']: - insurance_reinsurance_level = simulation_parameters["default_non-proportional_reinsurance_deductible"] - else: - insurance_reinsurance_level = np.random.uniform(simulation_parameters["insurance_reinsurance_levels_lower_bound"], simulation_parameters["insurance_reinsurance_levels_upper_bound"]) - - riskmodel_config = risk_model_configurations[i % len(risk_model_configurations)] - self.agent_parameters["insurancefirm"].append({'id': self.get_unique_insurer_id(), 'initial_cash': simulation_parameters["initial_agent_cash"], - 'riskmodel_config': riskmodel_config, 'norm_premium': self.norm_premium, - 'profit_target': simulation_parameters["norm_profit_markup"], - 'initial_acceptance_threshold': simulation_parameters["initial_acceptance_threshold"], - 'acceptance_threshold_friction': simulation_parameters["acceptance_threshold_friction"], - 'reinsurance_limit': simulation_parameters["reinsurance_limit"], - 'non-proportional_reinsurance_level': insurance_reinsurance_level, - 'capacity_target_decrement_threshold': simulation_parameters['capacity_target_decrement_threshold'], - 'capacity_target_increment_threshold': simulation_parameters['capacity_target_increment_threshold'], - 'capacity_target_decrement_factor': simulation_parameters['capacity_target_decrement_factor'], - 'capacity_target_increment_factor': simulation_parameters['capacity_target_increment_factor'], - 'interest_rate': simulation_parameters["interest_rate"]}) - - self.reinsurer_id_counter = 0 - for i in range(simulation_parameters["no_reinsurancefirms"]): - if simulation_parameters['static_non-proportional_reinsurance_levels']: - reinsurance_reinsurance_level = simulation_parameters["default_non-proportional_reinsurance_deductible"] - else: - reinsurance_reinsurance_level = np.random.uniform(simulation_parameters["reinsurance_reinsurance_levels_lower_bound"], simulation_parameters["reinsurance_reinsurance_levels_upper_bound"]) + "Prepare setting up agents (to be done from start.py)" + self.agent_parameters = {"insurancefirm": [], "reinsurancefirm": []} + self.initialize_agent_parameters("insurancefirm", simulation_parameters, risk_model_configurations) + self.initialize_agent_parameters("reinsurancefirm", simulation_parameters, risk_model_configurations) - riskmodel_config = risk_model_configurations[i % len(risk_model_configurations)] - self.agent_parameters["reinsurance"].append({'id': self.get_unique_reinsurer_id(), 'initial_cash': simulation_parameters["initial_reinagent_cash"], - 'riskmodel_config': riskmodel_config, 'norm_premium': self.norm_premium, - 'profit_target': simulation_parameters["norm_profit_markup"], - 'initial_acceptance_threshold': simulation_parameters["initial_acceptance_threshold"], - 'acceptance_threshold_friction': simulation_parameters["acceptance_threshold_friction"], - 'reinsurance_limit': simulation_parameters["reinsurance_limit"], - 'non-proportional_reinsurance_level': reinsurance_reinsurance_level, - 'capacity_target_decrement_threshold': simulation_parameters['capacity_target_decrement_threshold'], - 'capacity_target_increment_threshold': simulation_parameters['capacity_target_increment_threshold'], - 'capacity_target_decrement_factor': simulation_parameters['capacity_target_decrement_factor'], - 'capacity_target_increment_factor': simulation_parameters['capacity_target_increment_factor'], - 'interest_rate': simulation_parameters["interest_rate"]}) - - # set up remaining list variables - - # agent lists + "Agent lists" self.reinsurancefirms = [] self.insurancefirms = [] self.catbonds = [] - # lists of agent weights + "Lists of agent weights" self.insurers_weights = {} self.reinsurers_weights = {} - - # list of reinsurance risks offered for underwriting + "List of reinsurance risks offered for underwriting" self.reinrisks = [] self.not_accepted_reinrisks = [] - # cumulative variables for history and logging + "Cumulative variables for history and logging" self.cumulative_bankruptcies = 0 self.cumulative_market_exits = 0 self.cumulative_unrecovered_claims = 0.0 self.cumulative_claims = 0.0 - # lists for logging history + "Lists for logging history" self.logger = logger.Logger(no_riskmodels=simulation_parameters["no_riskmodels"], rc_event_schedule_initial=self.rc_event_schedule_initial, rc_event_damage_initial=self.rc_event_damage_initial) @@ -205,6 +142,42 @@ def __init__(self, override_no_riskmodels, replic_ID, simulation_parameters, rc_ self.insurance_models_counter = np.zeros(self.simulation_parameters["no_categories"]) self.reinsurance_models_counter = np.zeros(self.simulation_parameters["no_categories"]) + def initialize_agent_parameters(self, firmtype, simulation_parameters, risk_model_configurations): + """General function for initialising the agent parameters""" + if firmtype == "insurancefirm": + self.insurer_id_counter = 0 + no_firms = simulation_parameters["no_insurancefirms"] + initial_cash = "initial_agent_cash" + reinsurance_level_lowerbound = simulation_parameters["insurance_reinsurance_levels_lower_bound"] + reinsurance_level_upperbound = simulation_parameters["insurance_reinsurance_levels_upper_bound"] + + elif firmtype == "reinsurancefirm": + self.reinsurer_id_counter = 0 + no_firms = simulation_parameters["no_reinsurancefirms"] + initial_cash = "initial_reinagent_cash" + reinsurance_level_lowerbound = simulation_parameters["reinsurance_reinsurance_levels_lower_bound"] + reinsurance_level_upperbound = simulation_parameters["reinsurance_reinsurance_levels_upper_bound"] + + for i in range(no_firms): + if simulation_parameters['static_non-proportional_reinsurance_levels']: + reinsurance_level = simulation_parameters["default_non-proportional_reinsurance_deductible"] + else: + reinsurance_level = np.random.uniform(reinsurance_level_lowerbound, reinsurance_level_upperbound) + + riskmodel_config = risk_model_configurations[i % len(risk_model_configurations)] + self.agent_parameters[firmtype].append({'id': self.get_unique_insurer_id(), 'initial_cash': simulation_parameters[initial_cash], + 'riskmodel_config': riskmodel_config, 'norm_premium': self.norm_premium, + 'profit_target': simulation_parameters["norm_profit_markup"], + 'initial_acceptance_threshold': simulation_parameters["initial_acceptance_threshold"], + 'acceptance_threshold_friction': simulation_parameters["acceptance_threshold_friction"], + 'reinsurance_limit': simulation_parameters["reinsurance_limit"], + 'non-proportional_reinsurance_level': reinsurance_level, + 'capacity_target_decrement_threshold': simulation_parameters['capacity_target_decrement_threshold'], + 'capacity_target_increment_threshold': simulation_parameters['capacity_target_increment_threshold'], + 'capacity_target_decrement_factor': simulation_parameters['capacity_target_decrement_factor'], + 'capacity_target_increment_factor': simulation_parameters['capacity_target_increment_factor'], + 'interest_rate': simulation_parameters["interest_rate"]}) + def build_agents(self, agent_class, agent_class_string, parameters, agent_parameters): # assert agent_parameters == self.agent_parameters[agent_class_string] #assert fits only the initial creation of agents, not later additions # TODO: fix agents = [] @@ -227,7 +200,7 @@ def accept_agents(self, agent_class_string, agents, agent_group=None, time=0): # remove new agent cash from simulation cash to ensure stock flow consistency new_agent_cash = sum([agent.cash for agent in agents]) self.reduce_money_supply(new_agent_cash) - elif agent_class_string == "reinsurance": + elif agent_class_string == "reinsurancefirm": try: self.reinsurancefirms += agents self.reinsurancefirms_group = agent_group From 77f7972e1932c0b37ded329a653fde6bb1448c8d Mon Sep 17 00:00:00 2001 From: Chris Hughes Date: Wed, 3 Jul 2019 17:13:43 +0100 Subject: [PATCH 005/125] Removed ABCE. There are probably some simplifications that can be made now as a result, especially in insurancesimulation.py --- genericagentabce.py | 5 - insurancecontract.py | 1 - insurancesimulation.py | 203 +++++++++++++++++------------------------ isleconfig.py | 3 +- metainsuranceorg.py | 9 +- reinsurancecontract.py | 2 +- requirements.txt | 1 - resume.py | 103 +++------------------ start.py | 111 ++++++---------------- 9 files changed, 127 insertions(+), 311 deletions(-) delete mode 100644 genericagentabce.py diff --git a/genericagentabce.py b/genericagentabce.py deleted file mode 100644 index 28d9531..0000000 --- a/genericagentabce.py +++ /dev/null @@ -1,5 +0,0 @@ -import abce - - -class GenericAgent(abce.Agent): - pass diff --git a/insurancecontract.py b/insurancecontract.py index c54645f..c7b3b5d 100644 --- a/insurancecontract.py +++ b/insurancecontract.py @@ -4,7 +4,6 @@ class InsuranceContract(MetaInsuranceContract): - def __init__( self, insurer, diff --git a/insurancesimulation.py b/insurancesimulation.py index d486513..9da3e9b 100644 --- a/insurancesimulation.py +++ b/insurancesimulation.py @@ -11,16 +11,7 @@ import random import copy import logger - - -if isleconfig.use_abce: - import abce - - # print("abce imported") - - -# else: -# print("abce not imported") +import warnings class InsuranceSimulation: @@ -60,7 +51,7 @@ def __init__( self.reinsurance_off = simulation_parameters["reinsurance_off"] self.cat_separation_distribution = scipy.stats.expon( 0, simulation_parameters["event_time_mean_separation"] - ) + ) # TODO: research whether this is accurate self.risk_factor_lower_bound = simulation_parameters["risk_factor_lower_bound"] self.risk_factor_spread = ( simulation_parameters["risk_factor_upper_bound"] @@ -71,6 +62,7 @@ def __init__( ) if not simulation_parameters["risk_factors_present"]: self.risk_factor_distribution = scipy.stats.uniform(loc=1.0, scale=0) + # TODO: see if there is a better way of implementing a constant rv # self.risk_value_distribution = scipy.stats.uniform(loc=100, scale=9900) self.risk_value_distribution = scipy.stats.uniform(loc=1000, scale=0) @@ -203,10 +195,7 @@ def __init__( ] # prepare setting up agents (to be done from start.py) - self.agent_parameters = { - "insurancefirm": [], - "reinsurancefirm": [], - } + self.agent_parameters = {"insurancefirm": [], "reinsurancefirm": []} self.insurer_id_counter = 0 # TODO: collapse the following two loops into one generic one? @@ -377,17 +366,15 @@ def accept_agents(self, agent_class_string, agents, agent_group=None, time=0): print(sys.exc_info()) pdb.set_trace() else: - assert False, "Error: Unexpected agent class used {0:s}".format( - agent_class_string - ) + raise ValueError(f"Error: Unexpected agent class used {agent_class_string}") def delete_agents(self, agent_class_string, agents): if agent_class_string == "catbond": for agent in agents: self.catbonds.remove(agent) else: - assert False, "Trying to remove unremovable agent, type: {0:s}".format( - agent_class_string + raise ValueError( + f"Trying to remove unremovable agent, type: {agent_class_string}" ) def iterate(self, t): @@ -396,18 +383,14 @@ def iterate(self, t): print() print(t, ": ", len(self.risks)) if isleconfig.showprogress: - print("\rTime: {0:4d}".format(t), end="") + print(f"\rTime: {t}", end="") self.reset_pls() # adjust market premiums - sum_capital = sum( - [agent.get_cash() for agent in self.insurancefirms] - ) + sum_capital = sum([agent.get_cash() for agent in self.insurancefirms]) self.adjust_market_premium(capital=sum_capital) - sum_capital = sum( - [agent.get_cash() for agent in self.reinsurancefirms] - ) + sum_capital = sum([agent.get_cash() for agent in self.reinsurancefirms]) self.adjust_reinsurance_market_premium(capital=sum_capital) # pay obligations @@ -415,8 +398,13 @@ def iterate(self, t): # identify perils and effect claims for categ_id in range(len(self.rc_event_schedule)): - if self.rc_event_schedule[categ_id] and self.rc_event_schedule[categ_id][0] < t: - raise RuntimeWarning("Something wrong; past events not deleted") + if ( + self.rc_event_schedule[categ_id] + and self.rc_event_schedule[categ_id][0] < t + ): + warnings.warn( + "Something wrong; past events not deleted", RuntimeWarning + ) if ( len(self.rc_event_schedule[categ_id]) > 0 and self.rc_event_schedule[categ_id][0] == t @@ -425,9 +413,7 @@ def iterate(self, t): damage_extent = copy.copy( self.rc_event_damage[categ_id][0] ) # Schedules of catastrophes and damages must me generated at the same time. - self.inflict_peril( - categ_id=categ_id, damage=damage_extent, t=t - ) + self.inflict_peril(categ_id=categ_id, damage=damage_extent, t=t) self.rc_event_damage[categ_id] = self.rc_event_damage[categ_id][1:] else: if isleconfig.verbose: @@ -442,15 +428,8 @@ def iterate(self, t): # iterate reinsurnace firm agents for reinagent in self.reinsurancefirms: reinagent.iterate(t) - # TODO: is the following necessary for abce to work (log) properly? - # if isleconfig.use_abce: - # self.reinsurancefirms_group.iterate(time=t) - # else: - # for reinagent in self.reinsurancefirms: - # reinagent.iterate(t) # remove all non-accepted reinsurance risks - self.reinrisks = [] # reset weights @@ -459,12 +438,6 @@ def iterate(self, t): # iterate insurance firm agents for agent in self.insurancefirms: agent.iterate(t) - # TODO: is the following necessary for abce to work (log) properly? - # if isleconfig.use_abce: - # self.insurancefirms_group.iterate(time=t) - # else: - # for agent in self.insurancefirms: - # agent.iterate(t) # iterate catbonds for agent in self.catbonds: @@ -474,7 +447,11 @@ def iterate(self, t): self.simulation_parameters["no_categories"] ) - for insurer in self.insurancefirms: + for ( + insurer + ) in ( + self.insurancefirms + ): # TODO: this and the next look like they could be cleaner for i in range(len(self.inaccuracy)): if insurer.operational: if insurer.riskmodel.inaccuracy == self.inaccuracy[i]: @@ -610,9 +587,6 @@ def obtain_log( ): # This function allows to return in a list all the data generated by the model. There is no other way to transfer it back from the cloud. return self.logger.obtain_log(requested_logs) - def advance_round(self, *args): - pass - def finalize(self, *args): """Function to handle oberations after the end of the simulation run. Currently empty. @@ -630,7 +604,7 @@ def inflict_peril(self, categ_id, damage, t): if contract.category == categ_id ] if isleconfig.verbose: - print("**** PERIL ", damage) + print("**** PERIL", damage) damagevalues = np.random.beta( 1, 1.0 / damage - 1, size=self.risks_counter[categ_id] ) @@ -663,10 +637,8 @@ def pay(self, obligation): amount = obligation["amount"] recipient = obligation["recipient"] purpose = obligation["purpose"] - try: - assert self.money_supply > amount - except: - print("Something wrong: economy out of money", file=sys.stderr) + if not self.money_supply > amount: + warnings.warn("Something wrong: economy out of money", RuntimeWarning) if self.get_operational() and recipient.get_operational(): self.money_supply -= amount recipient.receive(amount) @@ -702,7 +674,9 @@ def reset_reinsurance_weights(self): if operational_no > 0: - if reinrisks_no / operational_no > 1: + if ( + reinrisks_no > operational_no + ): # TODO: verify this - should all risk go to a reinsurer? weights = reinrisks_no / operational_no for reinsurer in self.reinsurancefirms: self.reinsurers_weights[reinsurer.id] = math.floor(weights) @@ -734,7 +708,7 @@ def reset_insurance_weights(self): if operational_no > 0: - if risks_no / operational_no > 1: + if risks_no > operational_no: # TODO: as above weights = risks_no / operational_no for insurer in self.insurancefirms: self.insurers_weights[insurer.id] = math.floor(weights) @@ -766,13 +740,10 @@ def adjust_market_premium(self, capital): * self.simulation_parameters["no_risks"] ) ) - if ( - self.market_premium - < self.norm_premium * self.simulation_parameters["lower_price_limit"] - ): - self.market_premium = ( - self.norm_premium * self.simulation_parameters["lower_price_limit"] - ) + self.market_premium = min( + self.market_premium, + self.norm_premium * self.simulation_parameters["lower_price_limit"], + ) def adjust_reinsurance_market_premium(self, capital): """Adjust_market_premium Method. @@ -793,13 +764,10 @@ def adjust_reinsurance_market_premium(self, capital): * self.simulation_parameters["no_risks"] ) ) - if ( - self.reinsurance_market_premium - < self.norm_premium * self.simulation_parameters["lower_price_limit"] - ): - self.reinsurance_market_premium = ( - self.norm_premium * self.simulation_parameters["lower_price_limit"] - ) + self.reinsurance_market_premium = min( + self.reinsurance_market_premium, + self.norm_premium * self.simulation_parameters["lower_price_limit"], + ) def get_market_premium(self): """Get_market_premium Method. @@ -821,7 +789,7 @@ def get_reinsurance_premium(self, np_reinsurance_deductible_fraction): # TODO: cut this out of the insurance market premium -> OBSOLETE?? # TODO: make premiums dependend on the deductible per value (np_reinsurance_deductible_fraction) -> DONE. # TODO: make max_reduction into simulation_parameter ? - if self.reinsurance_off: + if self.reinsurance_off: # TODO: I don't understand why this is this way return float("inf") max_reduction = 0.1 return self.reinsurance_market_premium * ( @@ -830,21 +798,25 @@ def get_reinsurance_premium(self, np_reinsurance_deductible_fraction): def get_cat_bond_price(self, np_reinsurance_deductible_fraction): # TODO: implement function dependent on total capital in cat bonds and on deductible () - # TODO: make max_reduction and max_CB_surcharge into simulation_parameters ? + # TODO: make max_reduction and max_cat_bond_surcharge into simulation_parameters ? if self.catbonds_off: return float("inf") max_reduction = 0.9 - max_CB_surcharge = 0.5 + max_cat_bond_surcharge = 0.5 return self.reinsurance_market_premium * ( - 1.0 + max_CB_surcharge - max_reduction * np_reinsurance_deductible_fraction + 1.0 + + max_cat_bond_surcharge + - max_reduction * np_reinsurance_deductible_fraction ) - def append_reinrisks(self, item): - if len(item) > 0: + def append_reinrisks( + self, item + ): # TODO: do we want some type/structure verification on these? + if item: self.reinrisks.append(item) def remove_reinrisks(self, risko): - if risko != None: + if risko is not None: self.reinrisks.remove(risko) def get_reinrisks(self): @@ -937,46 +909,39 @@ def save_state_and_risk_categories(self): .replace("array", "np.array") .replace("uint32", "np.uint32") ) - wfile = open("data/replication_randomseed.dat", "a") - wfile.write(mersennetwoster_randomseed + "\n") - wfile.close() + with open("data/replication_randomseed.dat", "a") as wfile: + wfile.write(mersennetwoster_randomseed + "\n") # save event schedule - wfile = open("data/replication_rc_event_schedule.dat", "a") - wfile.write(str(self.rc_event_schedule) + "\n") - wfile.close() + with open("data/replication_rc_event_schedule.dat", "a") as wfile: + wfile.write(str(self.rc_event_schedule) + "\n") def restore_state_and_risk_categories(self): - rfile = open("data/replication_rc_event_schedule.dat", "r") - found = False - for i, line in enumerate(rfile): - # print(i, self.replic_ID) - if i == self.replic_ID: - self.rc_event_schedule = eval(line) - found = True - rfile.close() - assert ( - found - ), "rc event schedule for current replication ID number {0:d} not found in data file. Exiting.".format( - self.replic_ID - ) - rfile = open("data/replication_randomseed.dat", "r") - found = False - for i, line in enumerate(rfile): - # print(i, self.replic_ID) - if i == self.replic_ID: - mersennetwister_randomseed = eval(line) - found = True - rfile.close() + with open("data/replication_rc_event_schedule.dat", "r") as rfile: + found = False + for i, line in enumerate(rfile): + # print(i, self.replic_ID) + if i == self.replic_ID: + self.rc_event_schedule = eval(line) + found = True if not found: raise Exception( - "mersennetwister randomseed for current replication ID number {0:d} not found in data file. Exiting.".format( - self.replic_ID - )) + f"rc event schedule for current replication ID number {self.replic_ID} not found in data file." + ) + + with open("data/replication_randomseed.dat", "r") as rfile: + found = False + for i, line in enumerate(rfile): + # print(i, self.replic_ID) + if i == self.replic_ID: + mersennetwister_randomseed = eval(line) + found = True + if not found: + raise Exception( + f"mersennetwister randomseed for current replication ID number {self.replic_ID} not found in data file. Exiting." + ) np.random.set_state(mersennetwister_randomseed) - def insurance_firm_enters_market( - self, prob=-1, agent_type="InsuranceFirm" - ): + def insurance_firm_enters_market(self, prob=-1, agent_type="InsuranceFirm"): if prob == -1: if agent_type == "InsuranceFirm": prob = self.simulation_parameters[ @@ -987,9 +952,9 @@ def insurance_firm_enters_market( "reinsurance_firm_market_entry_probability" ] else: - raise ValueError("Unknown agent type. Simulation requested to create agent of type {0:s}".format( - agent_type - )) + raise ValueError( + f"Unknown agent type. Simulation requested to create agent of type {agent_type}" + ) return np.random.random() < prob def record_bankruptcy(self): @@ -1020,7 +985,9 @@ def record_claims( def log(self): self.logger.save_log(self.background_run) - def compute_market_diffvar(self): + def compute_market_diffvar( + self + ): # TODO: could do with cleanup - list comprehension? varsfirms = [] for firm in self.insurancefirms: @@ -1051,7 +1018,9 @@ def compute_market_diffvar(self): return totaldiff # self.history_logs['market_diffvar'].append(totaldiff) - def count_underwritten_and_reinsured_risks_by_category(self): # QUERY does this do anything? + def count_underwritten_and_reinsured_risks_by_category( + self + ): # QUERY does this do anything? underwritten_risks = 0 reinsured_risks = 0 underwritten_per_category = np.zeros( @@ -1085,12 +1054,12 @@ def get_unique_reinsurer_id(self): def insurance_entry_index(self): return self.insurance_models_counter[ - 0:self.simulation_parameters["no_riskmodels"] + 0 : self.simulation_parameters["no_riskmodels"] ].argmin() def reinsurance_entry_index(self): return self.reinsurance_models_counter[ - 0:self.simulation_parameters["no_riskmodels"] + 0 : self.simulation_parameters["no_riskmodels"] ].argmin() def get_operational(self): diff --git a/isleconfig.py b/isleconfig.py index 6e5a1df..2db2694 100644 --- a/isleconfig.py +++ b/isleconfig.py @@ -1,4 +1,3 @@ -use_abce = False oneriskmodel = False replicating = False force_foreground = False @@ -27,7 +26,7 @@ "mean_contract_runtime": 12, "contract_runtime_halfspread": 2, "default_contract_payment_period": 3, - "max_time": 100, + "max_time": 1000, "money_supply": 2000000000, "event_time_mean_separation": 100 / 3.0, "expire_immediately": False, diff --git a/metainsuranceorg.py b/metainsuranceorg.py index f9ea1ec..3802bda 100644 --- a/metainsuranceorg.py +++ b/metainsuranceorg.py @@ -8,14 +8,7 @@ import sys, pdb import uuid -if isleconfig.use_abce: - from genericagentabce import GenericAgent - - # print("abce imported") -else: - from genericagent import GenericAgent - - # print("abce not imported") +from genericagent import GenericAgent def get_mean(x): diff --git a/reinsurancecontract.py b/reinsurancecontract.py index 3f5e680..cca88b9 100644 --- a/reinsurancecontract.py +++ b/reinsurancecontract.py @@ -82,7 +82,7 @@ def explode(self, time, damage_extent=None): self.current_claim += ( self.contract.claim ) # TODO: should proportional reinsurance claims be subject to excess_of_loss retrocession? - # If so, reorganize more straightforwardly + # If so, reorganize more straightforwardly self.expiration = time # self.terminating = True diff --git a/requirements.txt b/requirements.txt index 340f889..be2bd82 100644 --- a/requirements.txt +++ b/requirements.txt @@ -3,4 +3,3 @@ numpy>=1.13.3 matplotlib>=2.1.1 networkx>=2.0 argparse>=1.1 -git+https://github.com/ABC-E/abce diff --git a/resume.py b/resume.py index f08b6c6..2db63fb 100644 --- a/resume.py +++ b/resume.py @@ -17,7 +17,7 @@ # use argparse to handle command line arguments parser = argparse.ArgumentParser(description="Model the Insurance sector") -parser.add_argument("--abce", action="store_true", help="use abce") +parser.add_argument("--abce", action="store_true", help="[REMOVED] use abce") parser.add_argument( "--oneriskmodel", action="store_true", @@ -52,7 +52,7 @@ args = parser.parse_args() if args.abce: - isleconfig.use_abce = True + raise Exception("ABCE is not and will not be supported") if args.oneriskmodel: isleconfig.oneriskmodel = True override_no_riskmodels = 1 @@ -78,11 +78,7 @@ if args.verbose: isleconfig.verbose = True -# import isle and abce modules -if isleconfig.use_abce: - # print("Importing abce") - import abce - from abce import gui +# import isle modules from insurancesimulation import InsuranceSimulation from insurancefirm import InsuranceFirm @@ -90,29 +86,8 @@ from reinsurancefirm import ReinsuranceFirm -# create conditional decorator -def conditionally(decorator_function, condition): - def wrapper(target_function): - if not condition: - return target_function - return decorator_function(target_function) - - return wrapper - - -# create non-abce placeholder gui decorator -# TODO: replace this with more elegant solution if possible. Currently required since script will otherwise crash at the conditional decorator below since gui is then undefined -if not isleconfig.use_abce: - - def gui(*args, **kwargs): - pass - - # main function - -# @gui(simulation_parameters, serve=True) -@conditionally(gui(simulation_parameters, serve=False), isleconfig.use_abce) -def main(): +def main(): # TODO: this script should probably be an argument for start.py with open("data/simulation_save.pkl", "br") as rfile: d = pickle.load(rfile) simulation = d["simulation"] @@ -129,41 +104,6 @@ def main(): np.random.set_state(np_seed) random.setstate(random_seed) - assert not isleconfig.use_abce, "Resuming will not work with abce" - ## create simulation and world objects (identical in non-abce mode) - # if isleconfig.use_abce: - # simulation = abce.Simulation(processes=1,random_seed = seed) - # - - # simulation_parameters['simulation'] = world = InsuranceSimulation(override_no_riskmodels, replic_ID, simulation_parameters) - # - # if not isleconfig.use_abce: - # simulation = world - # - # create agents: insurance firms - # insurancefirms_group = simulation.build_agents(InsuranceFirm, - # 'insurancefirm', - # parameters=simulation_parameters, - # agent_parameters=world.agent_parameters["insurancefirm"]) - # - # if isleconfig.use_abce: - # insurancefirm_pointers = insurancefirms_group.get_pointer() - # else: - # insurancefirm_pointers = insurancefirms_group - # world.accept_agents("insurancefirm", insurancefirm_pointers, insurancefirms_group) - # - # create agents: reinsurance firms - # reinsurancefirms_group = simulation.build_agents(ReinsuranceFirm, - # "reinsurancefirm", - # parameters=simulation_parameters, - # agent_parameters=world.agent_parameters["reinsurancefirm"]) - # if isleconfig.use_abce: - # reinsurancefirm_pointers = reinsurancefirms_group.get_pointer() - # else: - # reinsurancefirm_pointers = reinsurancefirms_group - # world.accept_agents("reinsurancefirm", reinsurancefirm_pointers, reinsurancefirms_group) - # - # time iteration for t in range(time, simulation_parameters["max_time"]): @@ -181,15 +121,7 @@ def main(): agent_parameters=parameters, ) insurancefirms_group += new_insurance_firm - if isleconfig.use_abce: - # TODO: fix abce - # may fail in abce because addressing individual agents may not be allowed - # may also fail because agent methods may not be callable directly - new_insurancefirm_pointer = [ - new_insurance_firm[0].get_pointer() - ] # index 0 because this is a list with just 1 object - else: - new_insurancefirm_pointer = new_insurance_firm + new_insurancefirm_pointer = new_insurance_firm world.accept_agents( "insurancefirm", new_insurancefirm_pointer, new_insurance_firm, time=t ) @@ -204,32 +136,19 @@ def main(): agent_parameters=parameters, ) reinsurancefirms_group += new_reinsurance_firm - if isleconfig.use_abce: - # TODO: fix abce - # may fail in abce because addressing individual agents may not be allowed - # may also fail because agent methods may not be callable directly - new_reinsurancefirm_pointer = [ - new_reinsurance_firm[0].get_pointer() - ] # index 0 because this is a list with just 1 object - else: - new_reinsurancefirm_pointer = new_reinsurance_firm + new_reinsurancefirm_pointer = new_reinsurance_firm world.accept_agents( - "reinsurancefirm", new_reinsurancefirm_pointer, new_reinsurance_firm, time=t + "reinsurancefirm", + new_reinsurancefirm_pointer, + new_reinsurance_firm, + time=t, ) # iterate simulation world.iterate(t) # log data - if isleconfig.use_abce: - # insurancefirms.logme() - # reinsurancefirms.logme() - insurancefirms_group.agg_log( - variables=["cash", "operational"], len=["underwritten_contracts"] - ) - # reinsurancefirms_group.agg_log(variables=['cash']) - else: - world.save_data() + world.save_data() if t > 0 and t // 50 == t / 50: save_simulation(t, simulation, simulation_parameters, exit_now=False) diff --git a/start.py b/start.py index 3da7eb1..6ec0a7b 100644 --- a/start.py +++ b/start.py @@ -22,7 +22,6 @@ replic_ID = None override_no_riskmodels = False - # ensure that logging directory exists if not os.path.isdir("data"): if os.path.exists("data"): @@ -32,28 +31,7 @@ os.makedirs("data") -# create conditional decorator -def conditionally(decorator_function, condition): - def wrapper(target_function): - if not condition: - return target_function - return decorator_function(target_function) - - return wrapper - - -# create non-abce placeholder gui decorator -# TODO: replace this with more elegant solution if possible. Currently required since script will otherwise crash at the conditional decorator below since gui is then undefined -if not isleconfig.use_abce: - - def gui(*args, **kwargs): - pass - - # main function - -# @gui(simulation_parameters, serve=True) -@conditionally(gui(simulation_parameters, serve=False), isleconfig.use_abce) def main( simulation_parameters, rc_event_schedule, @@ -66,10 +44,6 @@ def main( np.random.seed(np_seed) random.seed(random_seed) - # create simulation and world objects (identical in non-abce mode) - if isleconfig.use_abce: - simulation = abce.Simulation(processes=1, random_seed=random_seed) - simulation_parameters[ "simulation" ] = world = insurancesimulation.InsuranceSimulation( @@ -80,8 +54,7 @@ def main( rc_event_damage, ) - if not isleconfig.use_abce: - simulation = world + simulation = world # create agents: insurance firms insurancefirms_group = simulation.build_agents( @@ -91,10 +64,7 @@ def main( agent_parameters=world.agent_parameters["insurancefirm"], ) - if isleconfig.use_abce: - insurancefirm_pointers = insurancefirms_group.get_pointer() - else: - insurancefirm_pointers = insurancefirms_group + insurancefirm_pointers = insurancefirms_group world.accept_agents("insurancefirm", insurancefirm_pointers, insurancefirms_group) # create agents: reinsurance firms @@ -104,21 +74,20 @@ def main( parameters=simulation_parameters, agent_parameters=world.agent_parameters["reinsurancefirm"], ) - if isleconfig.use_abce: - reinsurancefirm_pointers = reinsurancefirms_group.get_pointer() - else: - reinsurancefirm_pointers = reinsurancefirms_group - world.accept_agents("reinsurancefirm", reinsurancefirm_pointers, reinsurancefirms_group) + # TODO: I suspect that there is no distinction between pointers and group now abce is gone + reinsurancefirm_pointers = reinsurancefirms_group + world.accept_agents( + "reinsurancefirm", reinsurancefirm_pointers, reinsurancefirms_group + ) # time iteration for t in range(simulation_parameters["max_time"]): - # abce time step - simulation.advance_round(t) - # create new agents # TODO: write method for this; this code block is executed almost identically 4 times if world.insurance_firm_enters_market(agent_type="InsuranceFirm"): - parameters = [np.random.choice(world.agent_parameters["insurancefirm"])] + parameters = [ + np.random.choice(world.agent_parameters["insurancefirm"]) + ] # Which of these should be used? parameters = [ world.agent_parameters["insurancefirm"][ simulation.insurance_entry_index() @@ -132,15 +101,7 @@ def main( agent_parameters=parameters, ) insurancefirms_group += new_insurance_firm - if isleconfig.use_abce: - # TODO: fix abce - # may fail in abce because addressing individual agents may not be allowed - # may also fail because agent methods may not be callable directly - new_insurancefirm_pointer = [ - new_insurance_firm[0].get_pointer() - ] # index 0 because this is a list with just 1 object - else: - new_insurancefirm_pointer = new_insurance_firm + new_insurancefirm_pointer = new_insurance_firm world.accept_agents( "insurancefirm", new_insurancefirm_pointer, new_insurance_firm, time=t ) @@ -165,32 +126,19 @@ def main( agent_parameters=parameters, ) reinsurancefirms_group += new_reinsurance_firm - if isleconfig.use_abce: - # TODO: fix abce - # may fail in abce because addressing individual agents may not be allowed - # may also fail because agent methods may not be callable directly - new_reinsurancefirm_pointer = [ - new_reinsurance_firm[0].get_pointer() - ] # index 0 because this is a list with just 1 object - else: - new_reinsurancefirm_pointer = new_reinsurance_firm + new_reinsurancefirm_pointer = new_reinsurance_firm world.accept_agents( - "reinsurancefirm", new_reinsurancefirm_pointer, new_reinsurance_firm, time=t + "reinsurancefirm", + new_reinsurancefirm_pointer, + new_reinsurance_firm, + time=t, ) # iterate simulation world.iterate(t) # log data - if isleconfig.use_abce: - # insurancefirms.logme() - # reinsurancefirms.logme() - insurancefirms_group.agg_log( - variables=["cash", "operational"], len=["underwritten_contracts"] - ) - # reinsurancefirms_group.agg_log(variables=['cash']) - else: - world.save_data() + world.save_data() if t % 50 == save_iter: save_simulation(t, simulation, simulation_parameters, exit_now=False) @@ -205,12 +153,13 @@ def main( # save function def save_simulation(t, sim, sim_param, exit_now=False): - d = {} - d["np_seed"] = np.random.get_state() - d["random_seed"] = random.getstate() - d["time"] = t - d["simulation"] = sim - d["simulation_parameters"] = sim_param + d = { + "np_seed": np.random.get_state(), + "random_seed": random.getstate(), + "time": t, + "simulation": sim, + "simulation_parameters": sim_param, + } with open("data/simulation_save.pkl", "bw") as wfile: pickle.dump(d, wfile, protocol=pickle.HIGHEST_PROTOCOL) with open("data/simulation_save.pkl", "br") as rfile: @@ -229,7 +178,7 @@ def save_simulation(t, sim, sim_param, exit_now=False): """ use argparse to handle command line arguments""" parser = argparse.ArgumentParser(description="Model the Insurance sector") - parser.add_argument("--abce", action="store_true", help="use abce") + parser.add_argument("--abce", action="store_true", help="[REMOVED] use abce") parser.add_argument( "--oneriskmodel", action="store_true", @@ -278,8 +227,7 @@ def save_simulation(t, sim, sim_param, exit_now=False): args = parser.parse_args() if args.abce: - isleconfig.use_abce = True - raise Exception("ABCE not supported, probably won't become so") + raise Exception("ABCE is not and will not be supported") if args.oneriskmodel: isleconfig.oneriskmodel = True override_no_riskmodels = 1 @@ -311,12 +259,6 @@ def save_simulation(t, sim, sim_param, exit_now=False): else: save_iter = 200 - """ import abce module if required """ - if isleconfig.use_abce: - # print("Importing abce") - import abce - from abce import gui - from setup import SetupSim setup = SetupSim() # Here the setup for the simulation is done. @@ -329,6 +271,7 @@ def save_simulation(t, sim, sim_param, exit_now=False): 1 ) # Only one ensemble. This part will only be run locally (laptop). + # Run the main program log = main( simulation_parameters, general_rc_event_schedule[0], From bf53795f3e71d5260dfa8278a04abbb7ec94ac4a Mon Sep 17 00:00:00 2001 From: Chris Hughes Date: Thu, 4 Jul 2019 16:17:39 +0100 Subject: [PATCH 006/125] More cleanup and minor fixes. --- catbond.py | 316 +-------------------------------------- insurancecontract.py | 4 +- insurancefirm.py | 79 +++++----- insurancesimulation.py | 220 +++++++++++++-------------- isleconfig.py | 2 +- metainsurancecontract.py | 6 +- metainsuranceorg.py | 182 +++++++++++----------- reinsurancecontract.py | 4 +- resume.py | 11 +- start.py | 70 ++++----- 10 files changed, 286 insertions(+), 608 deletions(-) diff --git a/catbond.py b/catbond.py index e6603a2..ff72838 100644 --- a/catbond.py +++ b/catbond.py @@ -29,82 +29,6 @@ def init( # TODO: change start and InsuranceSimulation so that it iterates CatBonds # old parent class init, cat bond class should be much smaller - def parent_init(self, simulation_parameters, agent_parameters): - # def init(self, simulation_parameters, agent_parameters): - self.simulation = simulation_parameters["simulation"] - self.simulation_parameters = simulation_parameters - self.contract_runtime_dist = scipy.stats.randint( - simulation_parameters["mean_contract_runtime"] - - simulation_parameters["contract_runtime_halfspread"], - simulation_parameters["mean_contract_runtime"] - + simulation_parameters["contract_runtime_halfspread"] - + 1, - ) - self.default_contract_payment_period = simulation_parameters[ - "default_contract_payment_period" - ] - self.id = agent_parameters["id"] - self.cash = agent_parameters["initial_cash"] - self.premium = agent_parameters["norm_premium"] - self.profit_target = agent_parameters["profit_target"] - self.acceptance_threshold = agent_parameters[ - "initial_acceptance_threshold" - ] # 0.5 - self.acceptance_threshold_friction = agent_parameters[ - "acceptance_threshold_friction" - ] # 0.9 #1.0 to switch off - self.interest_rate = agent_parameters["interest_rate"] - self.reinsurance_limit = agent_parameters["reinsurance_limit"] - self.simulation_no_risk_categories = simulation_parameters["no_categories"] - self.simulation_reinsurance_type = simulation_parameters[ - "simulation_reinsurance_type" - ] - - rm_config = agent_parameters["riskmodel_config"] - self.riskmodel = RiskModel( - damage_distribution=rm_config["damage_distribution"], - expire_immediately=rm_config["expire_immediately"], - cat_separation_distribution=rm_config["cat_separation_distribution"], - norm_premium=rm_config["norm_premium"], - category_number=rm_config["no_categories"], - init_average_exposure=rm_config["risk_value_mean"], - init_average_risk_factor=rm_config["risk_factor_mean"], - init_profit_estimate=rm_config["norm_profit_markup"], - margin_of_safety=rm_config["margin_of_safety"], - var_tail_prob=rm_config["var_tail_prob"], - inaccuracy=rm_config["inaccuracy_by_categ"], - ) - - self.category_reinsurance = [ - None for i in range(self.simulation_no_risk_categories) - ] - if self.simulation_reinsurance_type == "non-proportional": - self.np_reinsurance_deductible_fraction = simulation_parameters[ - "default_non-proportional_reinsurance_deductible" - ] - self.np_reinsurance_excess_fraction = simulation_parameters[ - "default_non-proportional_reinsurance_excess" - ] - self.np_reinsurance_premium_share = simulation_parameters[ - "default_non-proportional_reinsurance_premium_share" - ] - self.obligations = [] - self.underwritten_contracts = [] - # self.reinsurance_contracts = [] - self.operational = True - self.is_insurer = True - self.is_reinsurer = False - - """set up risk value estimate variables""" - self.var_counter = 0 # sum over risk model inaccuracies for all contracts - self.var_counter_per_risk = 0 # average risk model inaccuracy across contracts - self.var_sum = 0 # sum over initial VaR for all contracts - self.counter_category = np.zeros( - self.simulation_no_risk_categories - ) # var_counter disaggregated by category - self.var_category = np.zeros( - self.simulation_no_risk_categories - ) # var_sum disaggregated by category def iterate(self, time): """obtain investments yield""" @@ -140,247 +64,14 @@ def iterate(self, time): if self.underwritten_contracts == []: self.mature_bond() # TODO: mature_bond method should check if operational - else: # TODO: dividend should only be payed according to pre-arranged schedule, and only if no risk events have materialized so far + # TODO: dividend should only be payed according to pre-arranged schedule, + # and only if no risk events have materialized so far + else: if self.operational: self.pay_dividends(time) # self.estimated_var() # cannot compute VaR for catbond as catbond does not have a riskmodel - # old parent class iterate, cat bond class should be much smaller - def parent_iterate( - self, time - ): # TODO: split function so that only the sequence of events remains here and everything else is in separate methods - """obtain investments yield""" - self.obtain_yield(time) - - """realize due payments""" - self.effect_payments(time) - if isleconfig.verbose: - print( - time, - ":", - self.id, - len(self.underwritten_contracts), - self.cash, - self.operational, - ) - - self.make_reinsurance_claims(time) - - """mature contracts""" - if isleconfig.verbose: - print("Number of underwritten contracts ", len(self.underwritten_contracts)) - maturing = [ - contract - for contract in self.underwritten_contracts - if contract.expiration <= time - ] - for contract in maturing: - self.underwritten_contracts.remove(contract) - contract.mature(time) - contracts_dissolved = len(maturing) - - """effect payments from contracts""" - [contract.check_payment_due(time) for contract in self.underwritten_contracts] - - if self.operational: - - """request risks to be considered for underwriting in the next period and collect those for this period""" - new_risks = [] - if self.is_insurer: - new_risks += self.simulation.solicit_insurance_requests( - self.id, self.cash - ) - if self.is_reinsurer: - new_risks += self.simulation.solicit_reinsurance_requests( - self.id, self.cash - ) - contracts_offered = len(new_risks) - try: - assert contracts_offered > 2 * contracts_dissolved - except: - print( - "Something wrong; agent {0:d} receives too few new contracts {1:d} <= {2:d}".format( - self.id, contracts_offered, 2 * contracts_dissolved - ), - file=sys.stderr, - ) - # print(self.id, " has ", len(self.underwritten_contracts), " & receives ", contracts_offered, " & lost ", contracts_dissolved) - - new_nonproportional_risks = [ - risk - for risk in new_risks - if risk.get("insurancetype") == "excess-of-loss" - and risk["owner"] is not self - ] - new_risks = [ - risk - for risk in new_risks - if risk.get("insurancetype") in ["proportional", None] - and risk["owner"] is not self - ] - - underwritten_risks = [ - { - "value": contract.value, - "category": contract.category, - "risk_factor": contract.risk_factor, - "deductible": contract.deductible, - "excess": contract.excess, - "insurancetype": contract.insurancetype, - "runtime": contract.runtime, - } - for contract in self.underwritten_contracts - if contract.reinsurance_share != 1.0 - ] - - """deal with non-proportional risks first as they must evaluate each request separatly, then with proportional ones""" - for risk in new_nonproportional_risks: - accept, var_this_risk = self.riskmodel.evaluate( - underwritten_risks, self.cash, risk - ) # TODO: change riskmodel.evaluate() to accept new risk to be evaluated and to account for existing non-proportional risks correctly -> DONE. - if accept: - per_value_reinsurance_premium = ( - self.np_reinsurance_premium_share - * risk["periodized_total_premium"] - * risk["runtime"] - / risk["value"] - ) # TODO: rename this to per_value_premium in insurancecontract.py to avoid confusion - contract = ReinsuranceContract( - self, - risk, - time, - per_value_reinsurance_premium, - risk["runtime"], - self.default_contract_payment_period, - expire_immediately=self.simulation_parameters[ - "expire_immediately" - ], - initial_VaR=var_this_risk, - insurancetype=risk["insurancetype"], - ) # TODO: implement excess of loss for reinsurance contracts - self.underwritten_contracts.append(contract) - # pass # TODO: write this nonproportional risk acceptance decision section based on commented code in the lines above this -> DONE. - - """make underwriting decisions, category-wise""" - # TODO: Enable reinsurance shares other tan 0.0 and 1.0 - expected_profit, acceptable_by_category, var_per_risk_per_categ = self.riskmodel.evaluate( - underwritten_risks, self.cash - ) - - # if expected_profit * 1./self.cash < self.profit_target: - # self.acceptance_threshold = ((self.acceptance_threshold - .4) * 5. * self.acceptance_threshold_friction) / 5. + .4 - # else: - # self.acceptance_threshold = (1 - self.acceptance_threshold_friction * (1 - (self.acceptance_threshold - .4) * 5.)) / 5. + .4 - - growth_limit = max( - 50, 2 * len(self.underwritten_contracts) + contracts_dissolved - ) - if sum(acceptable_by_category) > growth_limit: - acceptable_by_category = np.asarray(acceptable_by_category) - acceptable_by_category = ( - acceptable_by_category * growth_limit / sum(acceptable_by_category) - ) - acceptable_by_category = np.int64(np.round(acceptable_by_category)) - - not_accepted_risks = [] - for categ_id in range(len(acceptable_by_category)): - categ_risks = [ - risk for risk in new_risks if risk["category"] == categ_id - ] - new_risks = [risk for risk in new_risks if risk["category"] != categ_id] - categ_risks = sorted(categ_risks, key=lambda risk: risk["risk_factor"]) - i = 0 - if isleconfig.verbose: - print( - "InsuranceFirm underwrote: ", - len(self.underwritten_contracts), - " will accept: ", - acceptable_by_category[categ_id], - " out of ", - len(categ_risks), - "acceptance threshold: ", - self.acceptance_threshold, - ) - while ( - acceptable_by_category[categ_id] > 0 and len(categ_risks) > i - ): # \ - # and categ_risks[i]["risk_factor"] < self.acceptance_threshold): - if ( - categ_risks[i].get("contract") is not None - ): # categ_risks[i]["reinsurancefirm"]: - if ( - categ_risks[i]["contract"].expiration > time - ): # required to rule out contracts that have exploded in the meantime - # print("ACCEPTING", categ_risks[i]["contract"].expiration, categ_risks[i]["expiration"], categ_risks[i]["identifier"], categ_risks[i].get("contract").terminating) - contract = ReinsuranceContract( - self, - categ_risks[i], - time, - self.simulation.get_market_premium(), - categ_risks[i]["expiration"] - time, - self.default_contract_payment_period, - expire_immediately=self.simulation_parameters[ - "expire_immediately" - ], - ) - self.underwritten_contracts.append(contract) - # categ_risks[i]["contract"].reincontract = contract - # TODO: move this to insurancecontract (ca. line 14) -> DONE - # TODO: do not write into other object's properties, use setter -> DONE - - assert ( - categ_risks[i]["contract"].expiration - >= contract.expiration - ), "Reinsurancecontract lasts longer than insurancecontract: {0:d}>{1:d} (EXPIRATION2: {2:d} Time: {3:d})".format( - contract.expiration, - categ_risks[i]["contract"].expiration, - categ_risks[i]["expiration"], - time, - ) - # else: - # pass - else: - contract = InsuranceContract( - self, - categ_risks[i], - time, - self.simulation.get_market_premium(), - self.contract_runtime_dist.rvs(), - self.default_contract_payment_period, - expire_immediately=self.simulation_parameters[ - "expire_immediately" - ], - initial_VaR=var_per_risk_per_categ[categ_id], - ) - self.underwritten_contracts.append(contract) - acceptable_by_category[ - categ_id - ] -= ( - 1 - ) # TODO: allow different values per risk (i.e. sum over value (and reinsurance_share) or exposure instead of counting) - i += 1 - - not_accepted_risks += categ_risks[i:] - not_accepted_risks = [ - risk for risk in not_accepted_risks if risk.get("contract") is None - ] - - # seek reinsurance - if self.is_insurer: - # TODO: Why should only insurers be able to get reinsurance (not reinsurers)? (Technically, it should work) - self.ask_reinsurance(time) - - # return unacceptables - # print(self.id, " now has ", len(self.underwritten_contracts), " & returns ", len(not_accepted_risks)) - self.simulation.return_risks(not_accepted_risks) - - # not implemented - # """adjust liquidity, borrow or invest""" - # pass - - self.estimated_var() - def set_owner(self, owner): self.owner = owner if isleconfig.verbose: @@ -397,6 +88,7 @@ def mature_bond(self): "due_time": 1, "purpose": "mature", } + self.pay(obligation) self.simulation.delete_agents("catbond", [self]) self.operational = False diff --git a/insurancecontract.py b/insurancecontract.py index c7b3b5d..cbb6b75 100644 --- a/insurancecontract.py +++ b/insurancecontract.py @@ -13,7 +13,7 @@ def __init__( runtime, payment_period, expire_immediately, - initial_VaR=0.0, + initial_var=0.0, insurancetype="proportional", deductible_fraction=None, excess_fraction=None, @@ -27,7 +27,7 @@ def __init__( runtime, payment_period, expire_immediately, - initial_VaR, + initial_var, insurancetype, deductible_fraction, excess_fraction, diff --git a/insurancefirm.py b/insurancefirm.py index 554b0d0..1d0ff82 100644 --- a/insurancefirm.py +++ b/insurancefirm.py @@ -7,8 +7,10 @@ class InsuranceFirm(MetaInsuranceOrg): """ReinsuranceFirm class. - Inherits from InsuranceFirm.""" + Inherits from MetaInsuranceFirm.""" + # QUERY: now abce is gone can all of these inits become proper __init__s? + # In fact, can we do away with genericagent.py entirely? def init(self, simulation_parameters, agent_parameters): """Constructor method. Accepts arguments @@ -22,19 +24,17 @@ def init(self, simulation_parameters, agent_parameters): def adjust_dividends(self, time, actual_capacity): # TODO: Implement algorithm from flowchart profits = self.get_profitslosses() - self.per_period_dividend = max( - 0, self.dividend_share_of_profits * profits - ) # max function ensures that no negative dividends are paid - # if profits < 0: # no dividends when losses are written + self.per_period_dividend = max(0, self.dividend_share_of_profits * profits) + # max function ensures that no negative dividends are paid + # if profits < 0: # no dividends when losses are written # self.per_period_dividend = 0 - if ( - actual_capacity < self.capacity_target - ): # no dividends if firm misses capital target + if actual_capacity < self.capacity_target: + # no dividends if firm misses capital target self.per_period_dividend = 0 - def get_reinsurance_VaR_estimate(self, max_var): + def get_reinsurance_var_estimate(self, max_var): reinsurance_factor_estimate = ( - sum( + len( [ 1 for categ_id in range(self.simulation_no_risk_categories) @@ -44,19 +44,20 @@ def get_reinsurance_VaR_estimate(self, max_var): * 1.0 / self.simulation_no_risk_categories ) * (1.0 - self.np_reinsurance_deductible_fraction) - reinsurance_VaR_estimate = max_var * (1.0 + reinsurance_factor_estimate) - return reinsurance_VaR_estimate + reinsurance_var_estimate = max_var * (1.0 + reinsurance_factor_estimate) + return reinsurance_var_estimate def adjust_capacity_target(self, max_var): - reinsurance_VaR_estimate = self.get_reinsurance_VaR_estimate(max_var) - try: + reinsurance_var_estimate = self.get_reinsurance_var_estimate(max_var) + if max_var + reinsurance_var_estimate == 0: + # TODO: why is this being called with max_var = 0 anyway? + capacity_target_var_ratio_estimate = float("inf") + else: capacity_target_var_ratio_estimate = ( - (self.capacity_target + reinsurance_VaR_estimate) + (self.capacity_target + reinsurance_var_estimate) * 1.0 - / (max_var + reinsurance_VaR_estimate) + / (max_var + reinsurance_var_estimate) ) - except RuntimeError: - pass if ( capacity_target_var_ratio_estimate > self.capacity_target_increment_threshold @@ -70,16 +71,16 @@ def adjust_capacity_target(self, max_var): return def get_capacity(self, max_var): - if ( - max_var < self.cash - ): # ensure presence of sufficiently much cash to cover VaR - reinsurance_VaR_estimate = self.get_reinsurance_VaR_estimate(max_var) - return self.cash + reinsurance_VaR_estimate + # ensure presence of sufficiently much cash to cover VaR + if max_var < self.cash: + reinsurance_var_estimate = self.get_reinsurance_var_estimate(max_var) + return self.cash + reinsurance_var_estimate # else: # (This point is only reached when insurer is in severe financial difficulty. Ensure insurer recovers complete coverage.) return self.cash def increase_capacity(self, time, max_var): - """This is implemented for non-proportional reinsurance only. Otherwise the price comparison is not meaningful. Assert non-proportional mode.""" + """This is implemented for non-proportional reinsurance only. + Otherwise the price comparison is not meaningful. Assert non-proportional mode.""" assert self.simulation_reinsurance_type == "non-proportional" """get prices""" reinsurance_price = self.simulation.get_reinsurance_premium( @@ -129,7 +130,7 @@ def increase_capacity_by_category( ): if isleconfig.verbose: print( - "IF {0:d} increasing capacity in period {1:d}, cat bond price: {2:f}, reinsurance premium {3:f}".format( + f"IF {0:d} increasing capacity in period {1:d}, cat bond price: {2:f}, reinsurance premium {3:f}".format( self.id, time, cat_bond_price, reinsurance_price ) ) @@ -173,7 +174,7 @@ def ask_reinsurance(self, time): def ask_reinsurance_non_proportional(self, time): """ Method for requesting excess of loss reinsurance for all underwritten contracts by category. - The method calculates the combined valur at risk. With a probability it then creates a combined + The method calculates the combined value at risk. With a probability it then creates a combined reinsurance risk that may then be underwritten by a reinsurance firm. Arguments: time: integer @@ -182,7 +183,9 @@ def ask_reinsurance_non_proportional(self, time): """ """Evaluate by risk category""" for categ_id in range(self.simulation_no_risk_categories): - """Seek reinsurance only with probability 10% if not already reinsured""" # TODO: find a more generic way to decide whether to request reinsurance for category in this period + """Seek reinsurance only with probability 10% if not already reinsured""" + # QUERY It doesn't actually have the 10% chance? + # TODO: find a more generic way to decide whether to request reinsurance for category in this period if self.category_reinsurance[categ_id] is None: self.ask_reinsurance_non_proportional_by_category(time, categ_id) @@ -225,19 +228,11 @@ def ask_reinsurance_non_proportional_by_category(self, time, categ_id): self.simulation.append_reinrisks(risk) def ask_reinsurance_proportional(self): - nonreinsured = [] - for contract in self.underwritten_contracts: - if contract.reincontract == None: - nonreinsured.append(contract) - - # nonreinsured_b = [contract - # for contract in self.underwritten_contracts - # if contract.reincontract == None] - # - # try: - # assert nonreinsured == nonreinsured_b - # except: - # pdb.set_trace() + nonreinsured = [ + contract + for contract in self.underwritten_contracts + if contract.reincontract is None + ] nonreinsured.reverse() @@ -328,7 +323,7 @@ def issue_cat_bond(self, time, categ_id, per_value_per_period_premium=0): risk["runtime"], self.default_contract_payment_period, expire_immediately=self.simulation_parameters["expire_immediately"], - initial_VaR=var_this_risk, + initial_var=var_this_risk, insurancetype=risk["insurancetype"], ) # per_value_reinsurance_premium = 0 because the insurance firm does not continue to make payments to the cat bond. Only once. @@ -357,7 +352,7 @@ def make_reinsurance_claims(self, time): categ_id, claims, is_proportional = contract.get_and_reset_current_claim() if is_proportional: claims_this_turn[categ_id] += claims - if contract.reincontract != None: + if contract.reincontract is not None: contract.reincontract.explode(time, claims) for categ_id in range(self.simulation_no_risk_categories): diff --git a/insurancesimulation.py b/insurancesimulation.py index 9da3e9b..b3c4506 100644 --- a/insurancesimulation.py +++ b/insurancesimulation.py @@ -22,6 +22,11 @@ def __init__( simulation_parameters, rc_event_schedule, rc_event_damage, + damage_distribution=TruncatedDistWrapper( + lower_bound=0.25, + upper_bound=1.0, + dist=scipy.stats.pareto(b=2, loc=0, scale=0.25), + ), ): # override one-riskmodel case (this is to ensure all other parameters are truly identical for comparison runs) if override_no_riskmodels: @@ -29,7 +34,7 @@ def __init__( self.number_riskmodels = simulation_parameters["no_riskmodels"] # save parameters - if (replic_ID is None) or (isleconfig.force_foreground): + if (replic_ID is None) or isleconfig.force_foreground: self.background_run = False else: self.background_run = True @@ -37,48 +42,41 @@ def __init__( self.simulation_parameters = simulation_parameters # unpack parameters, set up environment (distributions etc.) - - # damage distribution - # TODO: control damage distribution via parameters, not directly - # self.damage_distribution = scipy.stats.uniform(loc=0, scale=1) - non_truncated = scipy.stats.pareto(b=2, loc=0, scale=0.25) - self.damage_distribution = TruncatedDistWrapper( - lower_bound=0.25, upper_bound=1.0, dist=non_truncated - ) + self.damage_distribution = damage_distribution # remaining parameters self.catbonds_off = simulation_parameters["catbonds_off"] self.reinsurance_off = simulation_parameters["reinsurance_off"] self.cat_separation_distribution = scipy.stats.expon( 0, simulation_parameters["event_time_mean_separation"] - ) # TODO: research whether this is accurate + ) + # TODO: research whether this is accurate, is it different for different types of catastrophy? self.risk_factor_lower_bound = simulation_parameters["risk_factor_lower_bound"] self.risk_factor_spread = ( simulation_parameters["risk_factor_upper_bound"] - - simulation_parameters["risk_factor_lower_bound"] - ) - self.risk_factor_distribution = scipy.stats.uniform( - loc=self.risk_factor_lower_bound, scale=self.risk_factor_spread + - self.risk_factor_lower_bound ) - if not simulation_parameters["risk_factors_present"]: + if simulation_parameters["risk_factors_present"]: + self.risk_factor_distribution = scipy.stats.uniform( + loc=self.risk_factor_lower_bound, scale=self.risk_factor_spread + ) + else: self.risk_factor_distribution = scipy.stats.uniform(loc=1.0, scale=0) - # TODO: see if there is a better way of implementing a constant rv + # TODO: figure out a better way of implementing a constant rv # self.risk_value_distribution = scipy.stats.uniform(loc=100, scale=9900) self.risk_value_distribution = scipy.stats.uniform(loc=1000, scale=0) risk_factor_mean = self.risk_factor_distribution.mean() - if np.isnan( - risk_factor_mean - ): # unfortunately scipy.stats.mean is not well-defined if scale = 0 + # unfortunately scipy.stats.mean is not well-defined if scale = 0 + if np.isnan(risk_factor_mean): risk_factor_mean = self.risk_factor_distribution.rvs() # set initial market price (normalized, i.e. must be multiplied by value or excess-deductible) if self.simulation_parameters["expire_immediately"]: assert self.cat_separation_distribution.dist.name == "expon" expected_damage_frequency = 1 - scipy.stats.poisson( - 1 + self.simulation_parameters["mean_contract_runtime"] / self.simulation_parameters["event_time_mean_separation"] - * self.simulation_parameters["mean_contract_runtime"] ).pmf(0) else: expected_damage_frequency = ( @@ -93,26 +91,25 @@ def __init__( ) self.market_premium = self.norm_premium - self.reinsurance_market_premium = ( - self.market_premium - ) # TODO: is this problematic as initial value? (later it is recomputed in every iteration) + self.reinsurance_market_premium = self.market_premium + # TODO: is this problematic as initial value? (later it is recomputed in every iteration) + self.total_no_risks = simulation_parameters["no_risks"] # set up monetary system (should instead be with the customers, if customers are modeled explicitly) self.money_supply = self.simulation_parameters["money_supply"] self.obligations = [] + # QUERY Why is this a property of the simulation rather than of the obligated parties? # set up risk categories + # QUERY What do risk categories represent? Different types of catastrophes? self.riskcategories = list(range(self.simulation_parameters["no_categories"])) self.rc_event_schedule = [] self.rc_event_damage = [] - self.rc_event_schedule_initial = ( - [] - ) # For debugging (cloud debugging) purposes is good to store the initial schedule of catastrophes - self.rc_event_damage_initial = ( - [] - ) # and damages that will be use in a single run of the model. - + self.rc_event_schedule_initial = [] + # For debugging (cloud debugging) purposes is good to store the initial schedule of catastrophes + # and damages that will be use in a single run of the model. + self.rc_event_damage_initial = [] if ( rc_event_schedule is not None and rc_event_damage is not None ): # If we have schedules pass as arguments we used them. @@ -126,10 +123,11 @@ def __init__( # set up risks risk_value_mean = self.risk_value_distribution.mean() - if np.isnan( - risk_value_mean - ): # unfortunately scipy.stats.mean is not well-defined if scale = 0 + if np.isnan(risk_value_mean): + # unfortunately scipy.stats.mean is not well-defined if scale = 0 risk_value_mean = self.risk_value_distribution.rvs() + + # QUERY: I'm stumped, what is the risk factor distribution? rrisk_factors = self.risk_factor_distribution.rvs( size=self.simulation_parameters["no_risks"] ) @@ -209,7 +207,7 @@ def __init__( simulation_parameters["insurance_reinsurance_levels_lower_bound"], simulation_parameters["insurance_reinsurance_levels_upper_bound"], ) - + # The initial set of insurers are approximately uniformly distributed over the possible risk models riskmodel_config = risk_model_configurations[ i % len(risk_model_configurations) ] @@ -291,7 +289,6 @@ def __init__( ) # set up remaining list variables - # agent lists self.reinsurancefirms = [] self.insurancefirms = [] @@ -325,40 +322,42 @@ def __init__( self.simulation_parameters["no_categories"] ) + # QUERY: Now abce is gone can we merge all of the agent creation into here out of start.py? def build_agents( self, agent_class, agent_class_string, parameters, agent_parameters ): - # assert agent_parameters == self.agent_parameters[agent_class_string] #assert fits only the initial creation of agents, not later additions # TODO: fix + # assert agent_parameters == self.agent_parameters[agent_class_string] + # #assert fits only the initial creation of agents, not later additions # TODO: fix agents = [] for ap in agent_parameters: agents.append(agent_class(parameters, ap)) return agents - def accept_agents(self, agent_class_string, agents, agent_group=None, time=0): + def accept_agents(self, agent_class_string, agents, time=0): # TODO: fix agent id's for late entrants (both firms and catbonds) if agent_class_string == "insurancefirm": try: self.insurancefirms += agents - self.insurancefirms_group = agent_group - except: + self.insurancefirms_group = agents + except: # QUERY: Why? print(sys.exc_info()) pdb.set_trace() # fix self.history_logs['individual_contracts'] list for agent in agents: self.logger.add_insurance_agent() # remove new agent cash from simulation cash to ensure stock flow consistency - new_agent_cash = sum([agent.cash for agent in agents]) - self.reduce_money_supply(new_agent_cash) + total_new_agent_cash = sum([agent.cash for agent in agents]) + self.reduce_money_supply(total_new_agent_cash) elif agent_class_string == "reinsurancefirm": try: self.reinsurancefirms += agents - self.reinsurancefirms_group = agent_group + self.reinsurancefirms_group = agents except: print(sys.exc_info()) pdb.set_trace() # remove new agent cash from simulation cash to ensure stock flow consistency - new_agent_cash = sum([agent.cash for agent in agents]) - self.reduce_money_supply(new_agent_cash) + total_new_agent_cash = sum([agent.cash for agent in agents]) + self.reduce_money_supply(total_new_agent_cash) elif agent_class_string == "catbond": try: self.catbonds += agents @@ -415,6 +414,8 @@ def iterate(self, t): ) # Schedules of catastrophes and damages must me generated at the same time. self.inflict_peril(categ_id=categ_id, damage=damage_extent, t=t) self.rc_event_damage[categ_id] = self.rc_event_damage[categ_id][1:] + # TODO: Ideally don't want to be taking from the beginning of lists, + # consider having soonest events at the end of the list. else: if isleconfig.verbose: print("Next peril ", self.rc_event_schedule[categ_id]) @@ -452,8 +453,8 @@ def iterate(self, t): ) in ( self.insurancefirms ): # TODO: this and the next look like they could be cleaner - for i in range(len(self.inaccuracy)): - if insurer.operational: + if insurer.operational: + for i in range(len(self.inaccuracy)): if insurer.riskmodel.inaccuracy == self.inaccuracy[i]: self.insurance_models_counter[i] += 1 @@ -468,15 +469,16 @@ def iterate(self, t): self.reinsurance_models_counter[i] += 1 # print(isleconfig.show_network) - # TODO: use network representation in a more generic way, perhaps only once at the end to characterize the network and use for calibration(?) + # TODO: use network representation in a more generic way, perhaps only once at the end to characterize + # the network and use for calibration(?) if isleconfig.show_network and t % 40 == 0 and t > 0: import visualization_network - RN = visualization_network.ReinsuranceNetwork( + rn = visualization_network.ReinsuranceNetwork( self.insurancefirms, self.reinsurancefirms, self.catbonds ) - RN.compute_measures() - RN.visualize() + rn.compute_measures() + rn.visualize() def save_data(self): """Method to collect statistics about the current state of the simulation. Will pass these to the @@ -543,7 +545,7 @@ def save_data(self): ] """ prepare dict """ - current_log = {} + current_log = {} # TODO: rewrite this as a single dictionary literal? current_log["total_cash"] = total_cash_no current_log["total_excess_capital"] = total_excess_capital current_log["total_profitslosses"] = total_profitslosses @@ -582,9 +584,9 @@ def save_data(self): """ call to Logger object """ self.logger.record_data(current_log) - def obtain_log( - self, requested_logs=None - ): # This function allows to return in a list all the data generated by the model. There is no other way to transfer it back from the cloud. + # This function allows to return in a list all the data generated by the model. There is no other way to transfer + # it back from the cloud. + def obtain_log(self, requested_logs=None): return self.logger.obtain_log(requested_logs) def finalize(self, *args): @@ -740,7 +742,7 @@ def adjust_market_premium(self, capital): * self.simulation_parameters["no_risks"] ) ) - self.market_premium = min( + self.market_premium = max( self.market_premium, self.norm_premium * self.simulation_parameters["lower_price_limit"], ) @@ -764,7 +766,7 @@ def adjust_reinsurance_market_premium(self, capital): * self.simulation_parameters["no_risks"] ) ) - self.reinsurance_market_premium = min( + self.reinsurance_market_premium = max( self.reinsurance_market_premium, self.norm_premium * self.simulation_parameters["lower_price_limit"], ) @@ -789,9 +791,10 @@ def get_reinsurance_premium(self, np_reinsurance_deductible_fraction): # TODO: cut this out of the insurance market premium -> OBSOLETE?? # TODO: make premiums dependend on the deductible per value (np_reinsurance_deductible_fraction) -> DONE. # TODO: make max_reduction into simulation_parameter ? - if self.reinsurance_off: # TODO: I don't understand why this is this way + if self.reinsurance_off: return float("inf") max_reduction = 0.1 + # QUERY: why is this this way? return self.reinsurance_market_premium * ( 1.0 - max_reduction * np_reinsurance_deductible_fraction ) @@ -809,9 +812,7 @@ def get_cat_bond_price(self, np_reinsurance_deductible_fraction): - max_reduction * np_reinsurance_deductible_fraction ) - def append_reinrisks( - self, item - ): # TODO: do we want some type/structure verification on these? + def append_reinrisks(self, item): if item: self.reinrisks.append(item) @@ -857,6 +858,7 @@ def return_risks(self, not_accepted_risks): def return_reinrisks(self, not_accepted_risks): self.not_accepted_reinrisks += not_accepted_risks + # QUERY: What does this represent? def get_all_riskmodel_combinations(self, n, rm_factor): riskmodels = [] for i in range(self.simulation_parameters["no_categories"]): @@ -867,6 +869,7 @@ def get_all_riskmodel_combinations(self, n, rm_factor): riskmodels.append(riskmodel_combination.tolist()) return riskmodels + # TODO: could make an Event() class with time, damage, etc. def setup_risk_categories(self): for i in self.riskcategories: event_schedule = [] @@ -877,21 +880,18 @@ def setup_risk_categories(self): total += int(math.ceil(separation_time)) if total < self.simulation_parameters["max_time"]: event_schedule.append(total) - event_damage.append( - self.damage_distribution.rvs() - ) # Schedules of catastrophes and damages must me generated at the same time. Reason: replication across different risk models. + event_damage.append(self.damage_distribution.rvs()) + # Schedules of catastrophes and damages must me generated at the same time. Reason: replication + # across different risk models. self.rc_event_schedule.append(event_schedule) self.rc_event_damage.append(event_damage) - self.rc_event_schedule_initial = copy.copy( - self.rc_event_damage - ) # For debugging (cloud debugging) purposes is good to store the initial schedule of catastrophes - self.rc_event_damage_initial = copy.copy( - self.rc_event_damage - ) # and damages that will be use in a single run of the model. + # For debugging (cloud debugging) purposes is good to store the initial schedule of catastrophes + # and damages that will be use in a single run of the model. + self.rc_event_schedule_initial = copy.copy(self.rc_event_damage) + self.rc_event_damage_initial = copy.copy(self.rc_event_damage) def setup_risk_categories_caller(self): - # if self.background_run: if self.replic_ID is not None: if isleconfig.replicating: self.restore_state_and_risk_categories() @@ -921,7 +921,9 @@ def restore_state_and_risk_categories(self): for i, line in enumerate(rfile): # print(i, self.replic_ID) if i == self.replic_ID: - self.rc_event_schedule = eval(line) + self.rc_event_schedule = eval( + line + ) # TODO: eval could be considered dangerous found = True if not found: raise Exception( @@ -976,51 +978,44 @@ def record_market_exit(self): def record_unrecovered_claims(self, loss): self.cumulative_unrecovered_claims += loss - def record_claims( - self, claims - ): # This method records every claim made to insurers and reinsurers. + def record_claims(self, claims): + # This method records every claim made to insurers and reinsurers. # It is called from both insurers and reinsurers (metainsuranceorg.py). self.cumulative_claims += claims def log(self): self.logger.save_log(self.background_run) - def compute_market_diffvar( - self - ): # TODO: could do with cleanup - list comprehension? - - varsfirms = [] - for firm in self.insurancefirms: - if firm.operational: - varsfirms.append(firm.var_counter_per_risk) - totalina = sum(varsfirms) + def compute_market_diffvar(self): + totalina = sum( + [ + firm.var_counter_per_risk + for firm in self.insurancefirms + if firm.operational + ] + ) - varsfirms = [] - for firm in self.insurancefirms: - if firm.operational: - varsfirms.append(1) - totalreal = sum(varsfirms) + totalreal = len([firm for firm in self.insurancefirms if firm.operational]) - varsreinfirms = [] - for reinfirm in self.reinsurancefirms: - if reinfirm.operational: - varsreinfirms.append(reinfirm.var_counter_per_risk) - totalina = totalina + sum(varsreinfirms) + totalina += sum( + [ + reinfirm.var_counter_per_risk + for reinfirm in self.reinsurancefirms + if reinfirm.operational + ] + ) - varsreinfirms = [] - for reinfirm in self.reinsurancefirms: - if reinfirm.operational: - varsreinfirms.append(1) - totalreal = totalreal + sum(varsreinfirms) + totalreal += len( + [reinfirm for reinfirm in self.reinsurancefirms if reinfirm.operational] + ) totaldiff = totalina - totalreal return totaldiff # self.history_logs['market_diffvar'].append(totaldiff) - def count_underwritten_and_reinsured_risks_by_category( - self - ): # QUERY does this do anything? + def count_underwritten_and_reinsured_risks_by_category(self): + # QUERY does this do anything? It doesn't return anything and doesn't look like it changes any variables underwritten_risks = 0 reinsured_risks = 0 underwritten_per_category = np.zeros( @@ -1063,21 +1058,22 @@ def reinsurance_entry_index(self): ].argmin() def get_operational(self): + # QUERY: because the simulation can recieve money so is always operational? return True - def reinsurance_capital_entry( - self - ): # This method determines the capital market entry of reinsurers. It is only run in start.py. + def reinsurance_capital_entry(self): + # This method determines the capital market entry (initial cash) of reinsurers. It is only run in start.py. capital_per_non_re_cat = [] for reinrisk in self.not_accepted_reinrisks: - capital_per_non_re_cat.append( - reinrisk["value"] - ) # It takes all the values of the reinsurance risks NOT REINSURED. - - if ( - len(capital_per_non_re_cat) > 0 - ): # We only perform this action if there are reinsurance contracts that has not been reinsured in the last period of time. + capital_per_non_re_cat.append(reinrisk["value"]) + # It takes all the values of the reinsurance risks NOT REINSURED. + + # If there are any non-reinsured risks going, take a sample of them and have starting capital equal to twice + # the maximum value among that sample. # QUERY: why this particular value? + if len(capital_per_non_re_cat) > 0: + # We only perform this action if there are reinsurance contracts that have + # not been reinsured in the last time period. capital_per_non_re_cat = np.random.choice( capital_per_non_re_cat, 10 ) # Only 10 values sampled randomly are considered. (Too low?) diff --git a/isleconfig.py b/isleconfig.py index 2db2694..4ca35c1 100644 --- a/isleconfig.py +++ b/isleconfig.py @@ -29,7 +29,7 @@ "max_time": 1000, "money_supply": 2000000000, "event_time_mean_separation": 100 / 3.0, - "expire_immediately": False, + "expire_immediately": False, # QUERY: What does this mean? "risk_factors_present": False, "risk_factor_lower_bound": 0.4, "risk_factor_upper_bound": 0.6, diff --git a/metainsurancecontract.py b/metainsurancecontract.py index 7df9cb7..0954f07 100644 --- a/metainsurancecontract.py +++ b/metainsurancecontract.py @@ -12,7 +12,7 @@ def __init__( runtime, payment_period, expire_immediately, - initial_VaR=0.0, + initial_var=0.0, insurancetype="proportional", deductible_fraction=None, excess_fraction=None, @@ -28,7 +28,7 @@ def __init__( payment_period: Type integer. expire_immediately: Type boolean. True if the contract expires with the first risk event. False if multiple risk events are covered. - initial_VaR: Type float. Initial value at risk. Used only to compute true and estimated value at risk. + initial_var: Type float. Initial value at risk. Used only to compute true and estimated value at risk. optional: insurancetype: Type string. The type of this contract, especially "proportional" vs "excess_of_loss" deductible: Type float (or int) @@ -57,7 +57,7 @@ def __init__( self.expire_immediately = expire_immediately self.terminating = False self.current_claim = 0 - self.initial_VaR = initial_VaR + self.initial_VaR = initial_var # set deductible from argument, risk property or default value, whichever first is not None default_deductible_fraction = 0.0 diff --git a/metainsuranceorg.py b/metainsuranceorg.py index 3802bda..93dca55 100644 --- a/metainsuranceorg.py +++ b/metainsuranceorg.py @@ -139,6 +139,7 @@ def init(self, simulation_parameters, agent_parameters): self.reinrisks_kept = [] self.balance_ratio = simulation_parameters["insurers_balance_ratio"] self.recursion_limit = simulation_parameters["insurers_recursion_limit"] + # TODO: Check that this shouldn't have to sum to self.cash self.cash_left_by_categ = [ self.cash for i in range(self.simulation_parameters["no_categories"]) ] @@ -179,7 +180,8 @@ def iterate( contracts_dissolved = len(maturing) """effect payments from contracts""" - [contract.check_payment_due(time) for contract in self.underwritten_contracts] + for contract in self.underwritten_contracts: + contract.check_payment_due(time) if self.operational: @@ -214,7 +216,8 @@ def iterate( and risk["owner"] is not self ] - """deal with non-proportional risks first as they must evaluate each request separatly, then with proportional ones""" + """deal with non-proportional risks first as they must evaluate each request separatly, + then with proportional ones""" [ reinrisks_per_categ, @@ -223,16 +226,17 @@ def iterate( new_nonproportional_risks ) # Here the new reinrisks are organized by category. - for repetition in range( - self.recursion_limit - ): # TODO: find an efficient way to stop the recursion if there are no more risks to accept or if it is not accepting any more over several iterations. + for repetition in range(self.recursion_limit): + # TODO: find an efficient way to stop the recursion if there are no more risks to accept or if it is + # not accepting any more over several iterations. former_reinrisks_per_categ = copy.copy(reinrisks_per_categ) [ reinrisks_per_categ, not_accepted_reinrisks, ] = self.process_newrisks_reinsurer( reinrisks_per_categ, number_reinrisks_categ, time - ) # Here we process all the new reinrisks in order to keep the portfolio as balanced as possible. + ) + # Here we process all the new reinrisks in order to keep the portfolio as balanced as possible. if ( former_reinrisks_per_categ == reinrisks_per_categ ): # Stop condition implemented. Might solve the previous TODO. @@ -240,6 +244,7 @@ def iterate( self.simulation.return_reinrisks(not_accepted_reinrisks) + # QUERY: it's typically dangerous to compare floats with !=, is it okay in this case? underwritten_risks = [ { "value": contract.value, @@ -260,8 +265,8 @@ def iterate( underwritten_risks, self.cash ) # TODO: resolve insurance reinsurance inconsistency (insurer underwrite after capacity decisions, reinsurers before). - # This is currently so because it minimizes the number of times we need to run self.riskmodel.evaluate(). - # It would also be more consistent if excess capital would be updated at the end of the iteration. + # This is currently so because it minimizes the number of times we need to run self.riskmodel.evaluate(). + # It would also be more consistent if excess capital would be updated at the end of the iteration. """handle adjusting capacity target and capacity""" max_var_by_categ = self.cash - self.excess_capital self.adjust_capacity_target(max_var_by_categ) @@ -378,9 +383,11 @@ def dissolve(self, time, record): next iteration. Finally the type of dissolution is recorded and the operational state is set to false. Different class variables are reset during the process: self.risks_kept, self.reinrisks_kept, self.excess_capital and self.profits_losses.""" - [ - contract.dissolve(time) for contract in self.underwritten_contracts - ] # removing (dissolving) all risks immediately after bankruptcy (may not be realistic, they might instead be bought by another company) + for contract in self.underwritten_contracts: + contract.dissolve(time) + # removing (dissolving) all risks immediately after bankruptcy (may not be realistic, + # they might instead be bought by another company) + # TODO: implement buyouts self.simulation.return_risks(self.risks_kept) self.risks_kept = [] self.reinrisks_kept = [] @@ -417,16 +424,20 @@ def receive_obligation(self, amount, recipient, due_time, purpose): self.obligations.append(obligation) def effect_payments(self, time): + # TODO: don't really want to be reconstructing lists every time (unless the oblications are naturally sorted by + # time, in which case this could be done slightly better). Low priority, but something to consider due = [item for item in self.obligations if item["due_time"] <= time] self.obligations = [ item for item in self.obligations if item["due_time"] > time ] + # TODO: could this cause a firm to enter illiquidity if it has obligations to non-operational firms? sum_due = sum([item["amount"] for item in due]) if sum_due > self.cash: self.obligations += due self.enter_illiquidity(time) self.simulation.record_unrecovered_claims(sum_due - self.cash) - # TODO: is this record of uncovered claims correct or should it be sum_due (since the company is impounded and self.cash will also not be paid out for quite some time)? + # TODO: is this record of uncovered claims correct or should it be sum_due (since the company is + # impounded and self.cash will also not be paid out for quite some time)? # TODO: effect partial payment else: for obligation in due: @@ -451,16 +462,11 @@ def pay_dividends(self, time): self.receive_obligation(self.per_period_dividend, self.owner, time, "dividend") def obtain_yield(self, time): - amount = ( - self.cash * self.interest_rate - ) # TODO: agent should not award her own interest. This interest rate should be taken from self.simulation with a getter method + amount = self.cash * self.interest_rate + # TODO: agent should not award her own interest. + # This interest rate should be taken from self.simulation with a getter method self.simulation.receive_obligation(amount, self, time, "yields") - def increase_capacity(self): - raise AttributeError( - "Method is not implemented in MetaInsuranceOrg, just in inheriting InsuranceFirm instances" - ) - def get_cash(self): return self.cash @@ -472,10 +478,7 @@ def logme(self): self.log("underwritten_contracts", self.underwritten_contracts) self.log("operational", self.operational) - # def zeros(self): - # return 0 - - def len_underwritten_contracts(self): + def number_underwritten_contracts(self): return len(self.underwritten_contracts) def get_operational(self): @@ -487,11 +490,7 @@ def get_profitslosses(self): def get_underwritten_contracts(self): return self.underwritten_contracts - def get_pointer(self): - return self - def estimated_var(self): - self.counter_category = np.zeros(self.simulation_no_risk_categories) self.var_category = np.zeros(self.simulation_no_risk_categories) @@ -524,24 +523,23 @@ def estimated_var(self): else: self.var_counter_per_risk = 0 - def increase_capacity(self, time): - assert ( - False - ), "Method not implemented. increase_capacity method should be implemented in inheriting classes" + def increase_capacity(self): + raise NotImplementedError( + "Method is not implemented in MetaInsuranceOrg, just in inheriting InsuranceFirm instances" + ) def adjust_dividend(self, time): - assert ( - False - ), "Method not implemented. adjust_dividend method should be implemented in inheriting classes" + raise NotImplementedError( + "Method not implemented. adjust_dividend method should be implemented in inheriting classes" + ) def adjust_capacity_target(self, time): - assert ( - False - ), "Method not implemented. adjust_dividend method should be implemented in inheriting classes" + raise NotImplementedError( + "Method not implemented. adjust_dividend method should be implemented in inheriting classes" + ) - def risks_reinrisks_organizer( - self, new_risks - ): # This method organizes the new risks received by the insurer (or reinsurer) + def risks_reinrisks_organizer(self, new_risks): + # This method organizes the new risks received by the insurer (or reinsurer) risks_per_categ = [ [] for x in range(self.simulation_parameters["no_categories"]) @@ -559,11 +557,11 @@ def risks_reinrisks_organizer( return ( risks_per_categ, number_risks_categ, - ) # The method returns both risks_per_categ and risks_per_categ. + ) # The method returns both risks_per_categ and number_risks_categ. - def balanced_portfolio( - self, risk, cash_left_by_categ, var_per_risk - ): # This method decides whether the portfolio is balanced enough to accept a new risk or not. If it is balanced enough return True otherwise False. + def balanced_portfolio(self, risk, cash_left_by_categ, var_per_risk): + # This method decides whether the portfolio is balanced enough to accept a new risk or not. + # If it is balanced enough return True, otherwise False. # This method also returns the cash available per category independently the risk is accepted or not. cash_reserved_by_categ = ( self.cash - cash_left_by_categ @@ -622,12 +620,15 @@ def balanced_portfolio( def process_newrisks_reinsurer( self, reinrisks_per_categ, number_reinrisks_categ, time - ): # This method processes one by one the reinrisks contained in reinrisks_per_categ in order to decide whether they should be underwritten or not. - # It is done in this way to maintain the portfolio as balanced as possible. For that reason we process risk[C1], risk[C2], risk[C3], risk[C4], risk[C1], risk[C2], ... and so forth. + ): + # This method processes one by one the reinrisks contained in reinrisks_per_categ in + # order to decide whether they should be underwritten or not. + # It is done in this way to maintain the portfolio as balanced as possible. + # For that reason we process risk[C1], risk[C2], risk[C3], risk[C4], risk[C1], risk[C2], ... and so forth. for iterion in range(max(number_reinrisks_categ)): - for categ_id in range( - self.simulation_parameters["no_categories"] - ): # Here we take only one risk per category at a time to achieve risk[C1], risk[C2], risk[C3], risk[C4], risk[C1], risk[C2], ... if possible. + for categ_id in range(self.simulation_parameters["no_categories"]): + # Here we take only one risk per category at a time to achieve risk[C1], risk[C2], risk[C3], + # risk[C4], risk[C1], risk[C2], ... if possible. if ( iterion < number_reinrisks_categ[categ_id] and reinrisks_per_categ[categ_id][iterion] is not None @@ -648,7 +649,9 @@ def process_newrisks_reinsurer( ] accept, cash_left_by_categ, var_this_risk, self.excess_capital = self.riskmodel.evaluate( underwritten_risks, self.cash, risk_to_insure - ) # TODO: change riskmodel.evaluate() to accept new risk to be evaluated and to account for existing non-proportional risks correctly -> DONE. + ) + # TODO: change riskmodel.evaluate() to accept new risk to be evaluated and + # to account for existing non-proportional risks correctly -> DONE. if accept: per_value_reinsurance_premium = ( self.np_reinsurance_premium_share @@ -662,7 +665,9 @@ def process_newrisks_reinsurer( ) # TODO: rename this to per_value_premium in insurancecontract.py to avoid confusion [condition, cash_left_by_categ] = self.balanced_portfolio( risk_to_insure, cash_left_by_categ, None - ) # Here it is check whether the portfolio is balanced or not if the reinrisk (risk_to_insure) is underwritten. Return True if it is balanced. False otherwise. + ) + # Here it is check whether the portfolio is balanced or not if the reinrisk + # (risk_to_insure) is underwritten. Return True if it is balanced. False otherwise. if condition: contract = ReinsuranceContract( self, @@ -674,7 +679,7 @@ def process_newrisks_reinsurer( expire_immediately=self.simulation_parameters[ "expire_immediately" ], - initial_VaR=var_this_risk, + initial_var=var_this_risk, insurancetype=risk_to_insure["insurancetype"], ) # TODO: implement excess of loss for reinsurance contracts self.underwritten_contracts.append(contract) @@ -697,13 +702,15 @@ def process_newrisks_insurer( var_per_risk_per_categ, cash_left_by_categ, time, - ): # This method processes one by one the risks contained in risks_per_categ in order to decide whether they should be underwritten or not. - # It is done in this way to maintain the portfolio as balanced as possible. For that reason we process risk[C1], risk[C2], risk[C3], risk[C4], risk[C1], risk[C2], ... and so forth. + ): + # This method processes one by one the risks contained in risks_per_categ in order to decide whether they should + # be underwritten or not. It is done in this way to maintain the portfolio as balanced as possible. For that + # reason we process risk[C1], risk[C2], risk[C3], risk[C4], risk[C1], risk[C2], ... and so forth. _cached_rvs = self.contract_runtime_dist.rvs() for iter in range(max(number_risks_categ)): - for categ_id in range( - len(acceptable_by_category) - ): # Here we take only one risk per category at a time to achieve risk[C1], risk[C2], risk[C3], risk[C4], risk[C1], risk[C2], ... if possible. + for categ_id in range(len(acceptable_by_category)): + # Here we take only one risk per category at a time to achieve risk[C1], risk[C2], risk[C3], + # risk[C4], risk[C1], risk[C2], ... if possible. if ( iter < number_risks_categ[categ_id] and acceptable_by_category[categ_id] > 0 @@ -713,10 +720,13 @@ def process_newrisks_insurer( if ( risk_to_insure.get("contract") is not None and risk_to_insure["contract"].expiration > time - ): # risk_to_insure["contract"]: # required to rule out contracts that have exploded in the meantime + ): + # risk_to_insure["contract"]: # required to rule out contracts that have exploded in the meantime [condition, cash_left_by_categ] = self.balanced_portfolio( risk_to_insure, cash_left_by_categ, None - ) # Here it is check whether the portfolio is balanced or not if the reinrisk (risk_to_insure) is underwritten. Return True if it is balanced. False otherwise. + ) + # Here it is check whether the portfolio is balanced or not if the reinrisk (risk_to_insure) is + # underwritten. Return True if it is balanced. False otherwise. if condition: contract = ReinsuranceContract( self, @@ -737,7 +747,9 @@ def process_newrisks_insurer( else: [condition, cash_left_by_categ] = self.balanced_portfolio( risk_to_insure, cash_left_by_categ, var_per_risk_per_categ - ) # Here it is check whether the portfolio is balanced or not if the risk (risk_to_insure) is underwritten. Return True if it is balanced. False otherwise. + ) + # Here it is check whether the portfolio is balanced or not if the risk (risk_to_insure) is + # underwritten. Return True if it is balanced. False otherwise. if condition: contract = InsuranceContract( self, @@ -749,16 +761,14 @@ def process_newrisks_insurer( expire_immediately=self.simulation_parameters[ "expire_immediately" ], - initial_VaR=var_per_risk_per_categ[categ_id], + initial_var=var_per_risk_per_categ[categ_id], ) self.underwritten_contracts.append(contract) self.cash_left_by_categ = cash_left_by_categ risks_per_categ[categ_id][iter] = None - acceptable_by_category[ - categ_id - ] -= ( - 1 - ) # TODO: allow different values per risk (i.e. sum over value (and reinsurance_share) or exposure instead of counting) + acceptable_by_category[categ_id] -= 1 + # TODO: allow different values per risk (i.e. sum over value (and reinsurance_share) or + # exposure instead of counting) not_accepted_risks = [] for categ_id in range(len(acceptable_by_category)): @@ -768,9 +778,9 @@ def process_newrisks_insurer( return risks_per_categ, not_accepted_risks - def market_permanency( - self, time - ): # This method determines whether an insurer or reinsurer stays in the market. If it has very few risks underwritten or too much cash left for TOO LONG it eventually leaves the market. + def market_permanency(self, time): + # This method determines whether an insurer or reinsurer stays in the market. + # If it has very few risks underwritten or too much cash left for TOO LONG it eventually leaves the market. # If it has very few risks underwritten it cannot balance the portfolio so it makes sense to leave the market. if not self.simulation_parameters["market_permanency_off"]: @@ -793,19 +803,18 @@ def market_permanency( or avg_cash_left / self.cash > self.simulation_parameters["insurance_permanency_ratio_limit"] ): - # Insurers leave the market if they have contracts under the limit or an excess capital over the limit for too long. + # Insurers leave the market if they have contracts under the limit or an excess capital + # over the limit for too long. self.market_permanency_counter += 1 else: - self.market_permanency_counter = ( - 0 - ) # All these limits maybe should be parameters in isleconfig.py - + self.market_permanency_counter = 0 if ( self.market_permanency_counter >= self.simulation_parameters[ "insurance_permanency_time_constraint" ] - ): # Here we determine how much is too long. + ): + # Here we determine how much is too long. self.market_exit(time) if self.is_reinsurer: @@ -820,11 +829,12 @@ def market_permanency( "reinsurance_permanency_ratio_limit" ] ): - # Reinsurers leave the market if they have contracts under the limit or an excess capital over the limit for too long. + # Reinsurers leave the market if they have contracts under the limit or an excess capital + # over the limit for too long. - self.market_permanency_counter += ( - 1 - ) # Insurers and reinsurers potentially have different reasons to leave the market. That's why the code is duplicated here. + self.market_permanency_counter += 1 + # Insurers and reinsurers potentially have different reasons to leave the market. + # That's why the code is duplicated here. else: self.market_permanency_counter = 0 @@ -833,19 +843,21 @@ def market_permanency( >= self.simulation_parameters[ "reinsurance_permanency_time_constraint" ] - ): # Here we determine how much is too long. + ): + # Here we determine how much is too long. self.market_exit(time) - def register_claim( - self, claim - ): # This method records in insurancesimulation.py every claim made. It is called either from insurancecontract.py or reinsurancecontract.py respectively. + def register_claim(self, claim): + # This method records in insurancesimulation.py every claim made. It is called either from insurancecontract.py + # or reinsurancecontract.py respectively. self.simulation.record_claims(claim) def reset_pl(self): """Reset_pl Method. Accepts no arguments: No return value. - Reset the profits and losses variable of each firm at the beginning of every iteration. It has to be run in insurancesimulation.py at the beginning of the iterate method""" + Reset the profits and losses variable of each firm at the beginning of every iteration. + It has to be run in insurancesimulation.py at the beginning of the iterate method""" self.profits_losses = 0 def roll_over(self, time): diff --git a/reinsurancecontract.py b/reinsurancecontract.py index cca88b9..8bb90c1 100644 --- a/reinsurancecontract.py +++ b/reinsurancecontract.py @@ -20,7 +20,7 @@ def __init__( runtime, payment_period, expire_immediately, - initial_VaR=0.0, + initial_var=0.0, insurancetype="proportional", deductible_fraction=None, excess_fraction=None, @@ -34,7 +34,7 @@ def __init__( runtime, payment_period, expire_immediately, - initial_VaR, + initial_var, insurancetype, deductible_fraction, excess_fraction, diff --git a/resume.py b/resume.py index 2db63fb..2d066e3 100644 --- a/resume.py +++ b/resume.py @@ -122,9 +122,7 @@ def main(): # TODO: this script should probably be an argument for start.py ) insurancefirms_group += new_insurance_firm new_insurancefirm_pointer = new_insurance_firm - world.accept_agents( - "insurancefirm", new_insurancefirm_pointer, new_insurance_firm, time=t - ) + world.accept_agents("insurancefirm", new_insurancefirm_pointer, time=t) if world.insurance_firm_enters_market(agent_type="ReinsuranceFirm"): parameters = [np.random.choice(world.agent_parameters["reinsurancefirm"])] @@ -137,12 +135,7 @@ def main(): # TODO: this script should probably be an argument for start.py ) reinsurancefirms_group += new_reinsurance_firm new_reinsurancefirm_pointer = new_reinsurance_firm - world.accept_agents( - "reinsurancefirm", - new_reinsurancefirm_pointer, - new_reinsurance_firm, - time=t, - ) + world.accept_agents("reinsurancefirm", new_reinsurancefirm_pointer, time=t) # iterate simulation world.iterate(t) diff --git a/start.py b/start.py index 6ec0a7b..c4c208a 100644 --- a/start.py +++ b/start.py @@ -46,7 +46,7 @@ def main( simulation_parameters[ "simulation" - ] = world = insurancesimulation.InsuranceSimulation( + ] = simulation = insurancesimulation.InsuranceSimulation( override_no_riskmodels, replic_ID, simulation_parameters, @@ -54,46 +54,42 @@ def main( rc_event_damage, ) - simulation = world - # create agents: insurance firms insurancefirms_group = simulation.build_agents( insurancefirm.InsuranceFirm, "insurancefirm", parameters=simulation_parameters, - agent_parameters=world.agent_parameters["insurancefirm"], + agent_parameters=simulation.agent_parameters["insurancefirm"], ) - insurancefirm_pointers = insurancefirms_group - world.accept_agents("insurancefirm", insurancefirm_pointers, insurancefirms_group) + simulation.accept_agents("insurancefirm", insurancefirms_group) # create agents: reinsurance firms reinsurancefirms_group = simulation.build_agents( reinsurancefirm.ReinsuranceFirm, "reinsurancefirm", parameters=simulation_parameters, - agent_parameters=world.agent_parameters["reinsurancefirm"], - ) - # TODO: I suspect that there is no distinction between pointers and group now abce is gone - reinsurancefirm_pointers = reinsurancefirms_group - world.accept_agents( - "reinsurancefirm", reinsurancefirm_pointers, reinsurancefirms_group + agent_parameters=simulation.agent_parameters["reinsurancefirm"], ) + simulation.accept_agents("reinsurancefirm", reinsurancefirms_group) # time iteration for t in range(simulation_parameters["max_time"]): - - # create new agents # TODO: write method for this; this code block is executed almost identically 4 times - if world.insurance_firm_enters_market(agent_type="InsuranceFirm"): + # create new agents # TODO: write method for this; this code block is executed almost identically 4 times + # In fact this should probably all go in insurancesimulation.py, as part of simulation.iterate(t) + if simulation.insurance_firm_enters_market(agent_type="InsuranceFirm"): parameters = [ - np.random.choice(world.agent_parameters["insurancefirm"]) + np.random.choice(simulation.agent_parameters["insurancefirm"]) ] # Which of these should be used? parameters = [ - world.agent_parameters["insurancefirm"][ + simulation.agent_parameters["insurancefirm"][ simulation.insurance_entry_index() ] ] - parameters[0]["id"] = world.get_unique_insurer_id() + # As far as I can tell, there are only {no_riskmodels} distinct values for parameters, why does + # simulation.agent_parameters["insurancefirm"] need to have length {no_insurancefirms}? + # Also why do the new insurers always use the least popular risk model? + parameters[0]["id"] = simulation.get_unique_insurer_id() new_insurance_firm = simulation.build_agents( insurancefirm.InsuranceFirm, "insurancefirm", @@ -101,24 +97,24 @@ def main( agent_parameters=parameters, ) insurancefirms_group += new_insurance_firm - new_insurancefirm_pointer = new_insurance_firm - world.accept_agents( - "insurancefirm", new_insurancefirm_pointer, new_insurance_firm, time=t - ) + simulation.accept_agents("insurancefirm", new_insurance_firm, time=t) - if world.insurance_firm_enters_market(agent_type="ReinsuranceFirm"): - parameters = [np.random.choice(world.agent_parameters["reinsurancefirm"])] - parameters[0][ - "initial_cash" - ] = ( - world.reinsurance_capital_entry() - ) # Since the value of the reinrisks varies overtime it makes sense that the market entry of reinsures depends on those values. The method world.reinsurance_capital_entry() determines the capital market entry of reinsurers. + if simulation.insurance_firm_enters_market(agent_type="ReinsuranceFirm"): parameters = [ - world.agent_parameters["reinsurancefirm"][ + np.random.choice(simulation.agent_parameters["reinsurancefirm"]) + ] + # The reinsurance firms do just pick a random riskmodel when they are created. It is weighted by the initial + # distribution, I think # TODO: is this right? + parameters[0]["initial_cash"] = simulation.reinsurance_capital_entry() + # Since the value of the reinrisks varies overtime it makes sense that the market entry of reinsures + # depends on those values. The method world.reinsurance_capital_entry() determines the capital + # market entry of reinsurers. + parameters = [ + simulation.agent_parameters["reinsurancefirm"][ simulation.reinsurance_entry_index() ] ] - parameters[0]["id"] = world.get_unique_reinsurer_id() + parameters[0]["id"] = simulation.get_unique_reinsurer_id() new_reinsurance_firm = simulation.build_agents( reinsurancefirm.ReinsuranceFirm, "reinsurancefirm", @@ -126,19 +122,13 @@ def main( agent_parameters=parameters, ) reinsurancefirms_group += new_reinsurance_firm - new_reinsurancefirm_pointer = new_reinsurance_firm - world.accept_agents( - "reinsurancefirm", - new_reinsurancefirm_pointer, - new_reinsurance_firm, - time=t, - ) + simulation.accept_agents("reinsurancefirm", new_reinsurance_firm, time=t) # iterate simulation - world.iterate(t) + simulation.iterate(t) # log data - world.save_data() + simulation.save_data() if t % 50 == save_iter: save_simulation(t, simulation, simulation_parameters, exit_now=False) From 8c9b86791817765efb5ecc3fe34b8ed83cdea306 Mon Sep 17 00:00:00 2001 From: KloskaT Date: Thu, 4 Jul 2019 17:31:21 +0100 Subject: [PATCH 007/125] Added docstring to every function in the simulation class. Small typo corrections/structure changes. --- catbond.py | 1 + distributiontruncated.py | 1 + ensemble.py | 15 +-- insurancefirm.py | 19 +--- insurancesimulation.py | 213 ++++++++++++++++++++++++++++++++------- isleconfig.py | 142 +++++++++++++------------- metainsurancecontract.py | 3 - metainsuranceorg.py | 83 ++++++--------- reinsurancecontract.py | 4 +- riskmodel.py | 10 +- setup.py | 10 +- start.py | 18 ++-- 12 files changed, 307 insertions(+), 212 deletions(-) diff --git a/catbond.py b/catbond.py index 1445011..4740497 100644 --- a/catbond.py +++ b/catbond.py @@ -9,6 +9,7 @@ import sys, pdb import uuid + class CatBond(MetaInsuranceOrg): def init(self, simulation, per_period_premium, owner, interest_rate = 0): # do we need simulation parameters self.simulation = simulation diff --git a/distributiontruncated.py b/distributiontruncated.py index d862d8d..f0c36f3 100644 --- a/distributiontruncated.py +++ b/distributiontruncated.py @@ -3,6 +3,7 @@ from math import ceil import scipy.integrate + class TruncatedDistWrapper(): def __init__(self, dist, lower_bound=0, upper_bound=1): self.dist = dist diff --git a/ensemble.py b/ensemble.py index 92c3d49..442d340 100644 --- a/ensemble.py +++ b/ensemble.py @@ -29,13 +29,13 @@ def rake(hostname): """Configuration of the ensemble""" - replications = 70 #Number of replications to be carried out for each configuration. Usually one risk model, two risk models, three risk models, four risk models. + replications = 70 # Number of replications to be carried out for each configuration. Usually one risk model, two risk models, three risk models, four risk models. model = start.main m = operation(model, include_modules = True) - riskmodels = [1,2,3,4] #The number of risk models that will be used. + riskmodels = [1,2,3,4] # The number of risk models that will be used. parameters = isleconfig.simulation_parameters @@ -81,11 +81,11 @@ def rake(hostname): assert "number_riskmodels" in requested_logs - + """Configure log directory and ensure that the directory exists""" dir_prefix = "/data/" directory = os.getcwd() + dir_prefix - try: #Here it is checked whether the directory to collect the results exists or not. If not it is created. + try: # Here it is checked whether the directory to collect the results exists or not. If not it is created. os.stat(directory) except: os.mkdir(directory) @@ -95,11 +95,11 @@ def rake(hostname): filename = os.getcwd() + dir_prefix + nums[str(i)] + "_history_logs.dat" if os.path.exists(filename): os.remove(filename) - + """Setup of the simulations""" - setup = SetupSim() #Here the setup for the simulation is done. + setup = SetupSim() # Here the setup for the simulation is done. [general_rc_event_schedule, general_rc_event_damage, np_seeds, random_seeds] = setup.obtain_ensemble(replications) #Since this script is used to carry out simulations in the cloud will usually have more than 1 replication.. save_iter = isleconfig.simulation_parameters["max_time"] + 2 # never save simulation state in ensemble runs (resuming is impossible anyway) @@ -108,7 +108,8 @@ def rake(hostname): simulation_parameters = copy.copy(parameters) #Here the parameters used for the simulation are loaded. Clone is needed otherwise all the runs will be carried out with the last number of thee loop. simulation_parameters["no_riskmodels"] = i #Since we want to obtain ensembles for different number of risk models, we vary here the number of risks models. job = [m(simulation_parameters, general_rc_event_schedule[x], general_rc_event_damage[x], np_seeds[x], random_seeds[x], save_iter, list(requested_logs.keys())) for x in range(replications)] #Here is assembled each job with the corresponding: simulation parameters, time events, damage events, seeds, simulation state save interval (never, i.e. longer than max_time), and list of requested logs. - jobs.append(job) #All jobs are collected in the jobs list. + jobs.append(job) # All jobs are collected in the jobs list. + """Here the jobs are submitted""" diff --git a/insurancefirm.py b/insurancefirm.py index 61d5746..a20fa1c 100644 --- a/insurancefirm.py +++ b/insurancefirm.py @@ -4,6 +4,7 @@ from reinsurancecontract import ReinsuranceContract import isleconfig + class InsuranceFirm(MetaInsuranceOrg): """ReinsuranceFirm class. Inherits from InsuranceFirm.""" @@ -58,13 +59,13 @@ def increase_capacity(self, time, max_var): cat_bond_price = self.simulation.get_cat_bond_price(self.np_reinsurance_deductible_fraction) capacity = None if not reinsurance_price == cat_bond_price == float('inf'): - categ_ids = [ categ_id for categ_id in range(self.simulation_no_risk_categories) if (self.category_reinsurance[categ_id] is None)] + categ_ids = [categ_id for categ_id in range(self.simulation_no_risk_categories) if (self.category_reinsurance[categ_id] is None)] if len(categ_ids) > 1: np.random.shuffle(categ_ids) while len(categ_ids) >= 1: categ_id = categ_ids.pop() capacity = self.get_capacity(max_var) - if self.capacity_target < capacity: # just one per iteration, unless capital target is unmatched + if self.capacity_target < capacity: # just one per iteration, unless capital target is unmatched if self.increase_capacity_by_category(time, categ_id, reinsurance_price=reinsurance_price, cat_bond_price=cat_bond_price, force=False): categ_ids = [] else: @@ -143,7 +144,6 @@ def characterize_underwritten_risks_by_category(self, time, categ_id): avg_risk_factor /= number_risks return total_value, avg_risk_factor, number_risks, periodized_total_premium - def ask_reinsurance_non_proportional_by_category(self, time, categ_id): """Proceed with creation of reinsurance risk only if category is not empty.""" total_value, avg_risk_factor, number_risks, periodized_total_premium = self.characterize_underwritten_risks_by_category(time, categ_id) @@ -164,15 +164,6 @@ def ask_reinsurance_proportional(self): if contract.reincontract == None: nonreinsured.append(contract) - #nonreinsured_b = [contract - # for contract in self.underwritten_contracts - # if contract.reincontract == None] - # - #try: - # assert nonreinsured == nonreinsured_b - #except: - # pdb.set_trace() - nonreinsured.reverse() if len(nonreinsured) >= (1 - self.reinsurance_limit) * len(self.underwritten_contracts): @@ -186,7 +177,6 @@ def ask_reinsurance_proportional(self): "expiration": contract.expiration, "contract": contract, "risk_factor": contract.risk_factor} - #print("CREATING", risk["expiration"], contract.expiration, risk["contract"].expiration, risk["identifier"]) self.simulation.append_reinrisks(risk) counter += 1 else: @@ -195,12 +185,10 @@ def ask_reinsurance_proportional(self): def add_reinsurance(self, category, excess_fraction, deductible_fraction, contract): self.riskmodel.add_reinsurance(category, excess_fraction, deductible_fraction, contract) self.category_reinsurance[category] = contract - #pass def delete_reinsurance(self, category, excess_fraction, deductible_fraction, contract): self.riskmodel.delete_reinsurance(category, excess_fraction, deductible_fraction, contract) self.category_reinsurance[category] = None - #pass def issue_cat_bond(self, time, categ_id, per_value_per_period_premium = 0): # premium is for usual reinsurance contracts paid using per value market premium @@ -219,7 +207,6 @@ def issue_cat_bond(self, time, categ_id, per_value_per_period_premium = 0): _, _, var_this_risk, _ = self.riskmodel.evaluate([], self.cash, risk) per_period_premium = per_value_per_period_premium * risk["value"] total_premium = sum([per_period_premium * ((1/(1+self.interest_rate))**i) for i in range(risk["runtime"])]) # TODO: or is it range(1, risk["runtime"]+1)? - #catbond = CatBond(self.simulation, per_period_premium) catbond = CatBond(self.simulation, per_period_premium, self.interest_rate) # TODO: shift obtain_yield method to insurancesimulation, thereby making it unnecessary to drag parameters like self.interest_rate from instance to instance and from class to class """add contract; contract is a quasi-reinsurance contract""" diff --git a/insurancesimulation.py b/insurancesimulation.py index 52fea3a..6cbd4bf 100644 --- a/insurancesimulation.py +++ b/insurancesimulation.py @@ -17,6 +17,13 @@ class InsuranceSimulation(): def __init__(self, override_no_riskmodels, replic_ID, simulation_parameters, rc_event_schedule, rc_event_damage): + """Initialises the simulation (Called from start.py) + Accepts: + override_no_riskmodels: Boolean determining if number of risk models should be overwritten + replic_ID: Integer, used if want to replicate data over multiple runs + simulation parameters: DataDict from isleconfig + rc_event_schedule: List of when event will occur, allows for replication + re_event_damage: List of severity of each event, allows for replication""" "Override one-riskmodel case (this is to ensure all other parameters are truly identical for comparison runs)" if override_no_riskmodels: @@ -37,11 +44,12 @@ def __init__(self, override_no_riskmodels, replic_ID, simulation_parameters, rc_ self.total_no_risks = simulation_parameters["no_risks"] self.risk_factor_lower_bound = simulation_parameters["risk_factor_lower_bound"] self.cat_separation_distribution = scipy.stats.expon(0, simulation_parameters["event_time_mean_separation"]) + self.risk_factor_spread = simulation_parameters["risk_factor_upper_bound"] - simulation_parameters["risk_factor_lower_bound"] self.risk_factor_distribution = scipy.stats.uniform(loc=self.risk_factor_lower_bound, scale=self.risk_factor_spread) if not simulation_parameters["risk_factors_present"]: self.risk_factor_distribution = scipy.stats.uniform(loc=1.0, scale=0) - self.risk_value_distribution = scipy.stats.uniform(loc=1000, scale=0) + self.risk_value_distribution = scipy.stats.uniform(loc=1000, scale=0) #TODO is this correct? risk_factor_mean = self.risk_factor_distribution.mean() if np.isnan(risk_factor_mean): # unfortunately scipy.stats.mean is not well-defined if scale = 0 risk_factor_mean = self.risk_factor_distribution.rvs() @@ -93,7 +101,7 @@ def __init__(self, override_no_riskmodels, replic_ID, simulation_parameters, rc_ for item in self.risks: self.risks_counter[item["category"]] = self.risks_counter[item["category"]] + 1 - self.inaccuracy = self.get_all_riskmodel_combinations(self.simulation_parameters["no_categories"], self.simulation_parameters["riskmodel_inaccuracy_parameter"]) + self.inaccuracy = self.get_all_riskmodel_combinations(self.simulation_parameters["riskmodel_inaccuracy_parameter"]) self.inaccuracy = random.sample(self.inaccuracy, self.simulation_parameters["no_riskmodels"]) @@ -110,7 +118,7 @@ def __init__(self, override_no_riskmodels, replic_ID, simulation_parameters, rc_ "inaccuracy_by_categ": self.inaccuracy[i]} \ for i in range(self.simulation_parameters["no_riskmodels"])] - "Prepare setting up agents (to be done from start.py)" + "Setting up agents (to be done from start.py)" self.agent_parameters = {"insurancefirm": [], "reinsurancefirm": []} self.initialize_agent_parameters("insurancefirm", simulation_parameters, risk_model_configurations) self.initialize_agent_parameters("reinsurancefirm", simulation_parameters, risk_model_configurations) @@ -143,7 +151,10 @@ def __init__(self, override_no_riskmodels, replic_ID, simulation_parameters, rc_ self.reinsurance_models_counter = np.zeros(self.simulation_parameters["no_categories"]) def initialize_agent_parameters(self, firmtype, simulation_parameters, risk_model_configurations): - """General function for initialising the agent parameters""" + """General function for initialising the agent parameters + Takes the firm type as argument, also needing sim params and risk configs + Creates the agent parameters of both firm types for the initial number specified in isleconfig.py + Returns None""" if firmtype == "insurancefirm": self.insurer_id_counter = 0 no_firms = simulation_parameters["no_insurancefirms"] @@ -179,13 +190,30 @@ def initialize_agent_parameters(self, firmtype, simulation_parameters, risk_mode 'interest_rate': simulation_parameters["interest_rate"]}) def build_agents(self, agent_class, agent_class_string, parameters, agent_parameters): - # assert agent_parameters == self.agent_parameters[agent_class_string] #assert fits only the initial creation of agents, not later additions # TODO: fix + """Method for building new agents, only used for re/insurance firms. Loops through the agent parameters for each + initialised agent to create an instance of them using re/insurancefirm. + Accepts: + Agent_class: class of agent, either InsuranceFirm or ReinsuranceFirm. + agent_class_string: String Type containing string of agent class. Not used. + parameters: DataDict, contains config parameters. + agent_parameters: DataDict of agent parameters. + Returns: + agents: List Type, list of agent class instances created by loop""" agents = [] for ap in agent_parameters: agents.append(agent_class(parameters, ap)) return agents def accept_agents(self, agent_class_string, agents, agent_group=None, time=0): + """Method to 'accept' agents in that it adds agent to relevant list of agents kept by simulation + instance, also adds agent to logger. Also takes created agents initial cash out of economy. + Accepts: + agent_class_string: String Type. + agents: List type of agent class instances. + agent_group: List type of agent class instances. + time: Integer type, not used + Returns: + None""" # TODO: fix agent id's for late entrants (both firms and catbonds) if agent_class_string == "insurancefirm": try: @@ -220,6 +248,9 @@ def accept_agents(self, agent_class_string, agents, agent_group=None, time=0): assert False, "Error: Unexpected agent class used {0:s}".format(agent_class_string) def delete_agents(self, agent_class_string, agents): + """Method for deleting catbonds as it is only agent that is allowed to be removed + alters lists of catbonds + Returns none""" if agent_class_string == "catbond": for agent in agents: self.catbonds.remove(agent) @@ -227,7 +258,12 @@ def delete_agents(self, agent_class_string, agents): assert False, "Trying to remove unremovable agent, type: {0:s}".format(agent_class_string) def iterate(self, t): - + """Function that is called from start.py for each iteration that settles obligations, capital then reselects + risks for the insurance and reinsurance companies to evaluate. Firms are then iterated through to accept + new risks, pay obligations, increase capacity etc. + Accepts: + t: Integer, current time step + Returns None""" if isleconfig.verbose: print() print(t, ": ", len(self.risks)) @@ -236,16 +272,16 @@ def iterate(self, t): self.reset_pls() - # adjust market premiums - sum_capital = sum([agent.get_cash() for agent in self.insurancefirms]) # TODO: include reinsurancefirms + # Adjust market premiums + sum_capital = sum([agent.get_cash() for agent in self.insurancefirms]) self.adjust_market_premium(capital=sum_capital) - sum_capital = sum([agent.get_cash() for agent in self.reinsurancefirms]) # TODO: include reinsurancefirms + sum_capital = sum([agent.get_cash() for agent in self.reinsurancefirms]) self.adjust_reinsurance_market_premium(capital=sum_capital) - # pay obligations + # Pay obligations self.effect_payments(t) - # identify perils and effect claims + # Identify perils and effect claims for categ_id in range(len(self.rc_event_schedule)): try: if len(self.rc_event_schedule[categ_id]) > 0: @@ -261,26 +297,26 @@ def iterate(self, t): if isleconfig.verbose: print("Next peril ", self.rc_event_schedule[categ_id]) - # shuffle risks (insurance and reinsurance risks) + # Shuffle risks (insurance and reinsurance risks) self.shuffle_risks() - # reset reinweights + # Reset reinweights self.reset_reinsurance_weights() - # iterate reinsurnace firm agents + # Iterate reinsurnace firm agents for reinagent in self.reinsurancefirms: reinagent.iterate(t) self.reinrisks = [] - # reset weights + # Reset weights self.reset_insurance_weights() - # iterate insurance firm agents + # Iterate insurance firm agents for agent in self.insurancefirms: agent.iterate(t) - # iterate catbonds + # Iterate catbonds for agent in self.catbonds: agent.iterate(t) @@ -361,7 +397,9 @@ def save_data(self): """ call to Logger object """ self.logger.record_data(current_log) - def obtain_log(self, requested_logs=None): #This function allows to return in a list all the data generated by the model. There is no other way to transfer it back from the cloud. + def obtain_log(self, requested_logs=None): + """This function allows to return in a list all the data generated by the model. There is no other way to + transfer it back from the cloud.""" return self.logger.obtain_log(requested_logs) def finalize(self, *args): @@ -374,6 +412,13 @@ def finalize(self, *args): pass def inflict_peril(self, categ_id, damage, t): + """Method that calculates percentage damage done to each underwritten risk that is affected in the category + that event happened in. Passes values to allow calculation contracts to be resolved. + Arguments: + ID of category events took place + Given severity of damage from pareto distribution + Time iteration + No return value""" affected_contracts = [contract for insurer in self.insurancefirms for contract in insurer.underwritten_contracts if contract.category == categ_id] if isleconfig.verbose: print("**** PERIL ", damage) @@ -382,18 +427,34 @@ def inflict_peril(self, categ_id, damage, t): [contract.explode(t, uniformvalues[i], damagevalues[i]) for i, contract in enumerate(affected_contracts)] def receive_obligation(self, amount, recipient, due_time, purpose): + """Method for adding obligation to list that is resolved at the start if each iteration of simulation. Only + called by metainsuranceorg for adding interest to cash. + Arguments + Amount: obligation value + Recipient: Who obligation is owed to + Due Time + Purpose: Reason for obligation (Interest due) + Returns None""" + obligation = {"amount": amount, "recipient": recipient, "due_time": due_time, "purpose": purpose} self.obligations.append(obligation) def effect_payments(self, time): + """Method for checking and paying obligation if due. + Arguments + Current time to allow check if due + Returns None""" due = [item for item in self.obligations if item["due_time"]<=time] - #print("SIMULATION obligations: ", len(self.obligations), " of which are due: ", len(due)) self.obligations = [item for item in self.obligations if item["due_time"]>time] sum_due = sum([item["amount"] for item in due]) for obligation in due: self.pay(obligation) def pay(self, obligation): + """Method for paying obligations called from effect_payments + Accepts: + Obligation that must be payed + Returns None""" amount = obligation["amount"] recipient = obligation["recipient"] purpose = obligation["purpose"] @@ -406,16 +467,19 @@ def pay(self, obligation): recipient.receive(amount) def receive(self, amount): - """Method to accept cash payments.""" + """Method to accept cash payments. + Accepts: Amount due""" self.money_supply += amount def reduce_money_supply(self, amount): - """Method to reduce money supply immediately and without payment recipient (used to adjust money supply to compensate for agent endowment).""" + """Method to reduce money supply immediately and without payment recipient + (used to adjust money supply to compensate for agent endowment).""" self.money_supply -= amount assert self.money_supply >= 0 def reset_reinsurance_weights(self): - + """Method for clearing and setting reinsurance weights dependant on how many reinsurance companies exist and + how many offered reinsurance risks there are.""" self.not_accepted_reinrisks = [] operational_reinfirms = [reinsurancefirm for reinsurancefirm in self.reinsurancefirms if reinsurancefirm.operational] @@ -443,7 +507,9 @@ def reset_reinsurance_weights(self): self.not_accepted_reinrisks = self.reinrisks def reset_insurance_weights(self): - + """Method for clearing and setting insurance weights dependant on how many insurance companies exist and + how many insurance risks are offered. This determined which risks are sent to metainsuranceorg + iteration.""" operational_no = sum([insurancefirm.operational for insurancefirm in self.insurancefirms]) operational_firms = [insurancefirm for insurancefirm in self.insurancefirms if insurancefirm.operational] @@ -467,6 +533,7 @@ def reset_insurance_weights(self): self.insurers_weights[operational_firms[s].id] += 1 def shuffle_risks(self): + """Method for shuffling risks.""" np.random.shuffle(self.reinrisks) np.random.shuffle(self.risks) @@ -513,6 +580,10 @@ def get_market_reinpremium(self): return self.reinsurance_market_premium def get_reinsurance_premium(self, np_reinsurance_deductible_fraction): + """Method to determine reinsurance premium based on deductible fraction + Accepts: + np_reinsurance_deductible_fraction: Type Integer + Returns reinsurance premium (Type: Integer)""" # TODO: cut this out of the insurance market premium -> OBSOLETE?? # TODO: make premiums dependend on the deductible per value (np_reinsurance_deductible_fraction) -> DONE. # TODO: make max_reduction into simulation_parameter ? @@ -531,19 +602,30 @@ def get_cat_bond_price(self, np_reinsurance_deductible_fraction): return self.reinsurance_market_premium * (1. + max_CB_surcharge - max_reduction * np_reinsurance_deductible_fraction) def append_reinrisks(self, item): - if(len(item) > 0): + """Method for appending reinrisks to simulation instance. Called from insurancefirm + Accepts: item (Type: List)""" + if len(item) > 0: self.reinrisks.append(item) def remove_reinrisks(self,risko): - if(risko != None): + """Method for removing reinsurance risks from the simulation instance. Redundant?""" + if risko != None: self.reinrisks.remove(risko) def get_reinrisks(self): + """Method for shuffling reinsurance risks + Returns: reinsurance risks""" np.random.shuffle(self.reinrisks) return self.reinrisks def solicit_insurance_requests(self, id, cash, insurer): - + """Method for determining which risks are to be assessed by firms based on insurer weights + Accepts: + id: Type integer + cash: Type Integer + insurer: Type firm metainsuranceorg instance + Returns: + risks_to_be_sent: Type List""" risks_to_be_sent = self.risks[:int(self.insurers_weights[insurer.id])] self.risks = self.risks[int(self.insurers_weights[insurer.id]):] for risk in insurer.risks_kept: @@ -556,6 +638,13 @@ def solicit_insurance_requests(self, id, cash, insurer): return risks_to_be_sent def solicit_reinsurance_requests(self, id, cash, reinsurer): + """Method for determining which reinsurance risks are to be assessed by firms based on reinsurer weights + Accepts: + id: Type integer + cash: Type Integer + reinsurer: Type firm metainsuranceorg instance + Returns: + reinrisks_to_be_sent: Type List""" reinrisks_to_be_sent = self.reinrisks[:int(self.reinsurers_weights[reinsurer.id])] self.reinrisks = self.reinrisks[int(self.reinsurers_weights[reinsurer.id]):] @@ -569,12 +658,28 @@ def solicit_reinsurance_requests(self, id, cash, reinsurer): return reinrisks_to_be_sent def return_risks(self, not_accepted_risks): + """Method for adding risks that were not deemed acceptable to underwrite back to list of uninsured risks + Accepts: + not_accepted_risks: Type List + Returns None""" self.risks += not_accepted_risks def return_reinrisks(self, not_accepted_risks): + """Method for adding reinsuracne risks that were not deemed acceptable to list of unaccepted reinsurance risks + Cleared every round and is never called so redundant? + Accepts: + not_accepted_risks: Type List + Returns None""" + # TODO:Remove? self.not_accepted_reinrisks += not_accepted_risks - def get_all_riskmodel_combinations(self, n, rm_factor): + def get_all_riskmodel_combinations(self, rm_factor): + """Method for calculating riskmodels for each category based on the risk model inaccuracy parameter, and is + used purely to assign inaccuracy. Currently all equal and overwritten immediately. + Accepts: + rm_factor: Type Integer = risk model inaccuracy parameter + Returns: + riskmodels: Type list""" riskmodels = [] for i in range(self.simulation_parameters["no_categories"]): riskmodel_combination = rm_factor * np.ones(self.simulation_parameters["no_categories"]) @@ -583,11 +688,15 @@ def get_all_riskmodel_combinations(self, n, rm_factor): return riskmodels def setup_risk_categories(self): + """Method for generating the schedule of events and the percentage damage/severity caused by the event. + Only called if risk categories have not already been set as want to keep equal to allow for comparison. + Both must also be calculated at the same time to allow for replication. + Event schedule and damage based on distributions set in __init__.""" for i in self.riskcategories: event_schedule = [] event_damage = [] total = 0 - while (total < self.simulation_parameters["max_time"]): + while total < self.simulation_parameters["max_time"]: separation_time = self.cat_separation_distribution.rvs() total += int(math.ceil(separation_time)) if total < self.simulation_parameters["max_time"]: @@ -600,7 +709,8 @@ def setup_risk_categories(self): self.rc_event_damage_initial = copy.copy(self.rc_event_damage) #and damages that will be use in a single run of the model. def setup_risk_categories_caller(self): - #if self.background_run: + """Method for calling setup_risk_categories. If conditions are set such that the system is replicating it is + not called otherwise calls setup.""" if self.replic_ID is not None: if isleconfig.replicating: self.restore_state_and_risk_categories() @@ -611,22 +721,22 @@ def setup_risk_categories_caller(self): self.setup_risk_categories() def save_state_and_risk_categories(self): - # save numpy Mersenne Twister state + """Method to save numpy Mersenne Twister state and event schedule to allow for replication and continuation.""" mersennetwoster_randomseed = str(np.random.get_state()) mersennetwoster_randomseed = mersennetwoster_randomseed.replace("\n","").replace("array", "np.array").replace("uint32", "np.uint32") wfile = open("data/replication_randomseed.dat","a") wfile.write(mersennetwoster_randomseed+"\n") wfile.close() - # save event schedule + wfile = open("data/replication_rc_event_schedule.dat","a") wfile.write(str(self.rc_event_schedule)+"\n") wfile.close() def restore_state_and_risk_categories(self): + """Method to access saved event schedule, seed, and Mersenne twister state to allow for continuation.""" rfile = open("data/replication_rc_event_schedule.dat","r") found = False for i, line in enumerate(rfile): - #print(i, self.replic_ID) if i == self.replic_ID: self.rc_event_schedule = eval(line) found = True @@ -644,6 +754,8 @@ def restore_state_and_risk_categories(self): assert found, "mersennetwister randomseed for current replication ID number {0:d} not found in data file. Exiting.".format(self.replic_ID) def insurance_firm_market_entry(self, prob=-1, agent_type="InsuranceFirm"): # TODO: replace method name with a more descriptive one + """Method to determine if re/insurance firm enters the market based on set entry probabilities and a random + integer generated between 0, 1.""" if prob == -1: if agent_type == "InsuranceFirm": prob = self.simulation_parameters["insurance_firm_market_entry_probability"] @@ -673,16 +785,27 @@ def record_market_exit(self): self.cumulative_market_exits += 1 def record_unrecovered_claims(self, loss): + """Method for recording unrecovered claims. If firm runs out of money it cannot pay more claims and so that + money is lost and recorded using this method. + Accepts: + loss: Type integer, value of lost claim + Returns: + None""" self.cumulative_unrecovered_claims += loss - def record_claims(self, claims): #This method records every claim made to insurers and reinsurers. It is called from both insurers and reinsurers (metainsuranceorg.py). + def record_claims(self, claims): + """This method records every claim made to insurers and reinsurers. It is called from both insurers and + reinsurers (metainsuranceorg.py).""" self.cumulative_claims += claims def log(self): + """Method to log if a background run or not dependant on parameters force_foreground and if the run is + replicating or not.""" self.logger.save_log(self.background_run) def compute_market_diffvar(self): - + """Method for calculating difference between number of all firms and the total value at risk. Used only in save + data when adding to the logger data dict.""" varsfirms = [] for firm in self.insurancefirms: if firm.operational: @@ -713,6 +836,7 @@ def compute_market_diffvar(self): #self.history_logs['market_diffvar'].append(totaldiff) def count_underwritten_and_reinsured_risks_by_category(self): + """Not Used - Should be removed?""" underwritten_risks = 0 reinsured_risks = 0 underwritten_per_category = np.zeros(self.simulation_parameters["no_categories"]) @@ -728,25 +852,44 @@ def count_underwritten_and_reinsured_risks_by_category(self): reinsured_per_category += firm.counter_category def get_unique_insurer_id(self): + """Method for getting unique id for insurer. Used in initialising agents in start.py and insurancesimulation. + Iterates after each call so id is unique to each firm. + Returns: + current_id: Type integer""" current_id = self.insurer_id_counter self.insurer_id_counter += 1 return current_id def get_unique_reinsurer_id(self): + """Method for getting unique id for insurer. Used in initialising agents in start.py and insurancesimulation. + Iterates after each call so id is unique to each firm. + Returns: + current_id: Type integer""" current_id = self.reinsurer_id_counter self.reinsurer_id_counter += 1 return current_id def insurance_entry_index(self): + """Method that returns the entry index for insurance firms, i.e. the index for the initial agent parameters + that is taken from the list of already created parameters. + Returns: + Indices of the type of riskmodel that the least firms are using.""" return self.insurance_models_counter[0:self.simulation_parameters["no_riskmodels"]].argmin() def reinsurance_entry_index(self): + """Method that returns the entry index for reinsurance firms, i.e. the index for the initial agent parameters + that is taken from the list of already created parameters. + Returns: + Indices of the type of riskmodel that the least reinsurance firms are using.""" return self.reinsurance_models_counter[0:self.simulation_parameters["no_riskmodels"]].argmin() def get_operational(self): + """Method to return if simulation is operational. Always true. Used only in pay methods above and + metainsuranceorg.""" return True - def reinsurance_capital_entry(self): # This method determines the capital market entry of reinsurers. It is only run in start.py. + def reinsurance_capital_entry(self): + """This method determines the capital market entry of reinsurers. It is not run anywhere.""" capital_per_non_re_cat = [] for reinrisk in self.not_accepted_reinrisks: diff --git a/isleconfig.py b/isleconfig.py index 2885c21..c32cc40 100644 --- a/isleconfig.py +++ b/isleconfig.py @@ -3,77 +3,77 @@ replicating = False force_foreground = False verbose = False -showprogress = False -show_network = False # Should network be visualized? This should be False by default, to be overridden by commandline arguments -slim_log = True # Should logs be small in ensemble runs (only aggregated level data)? +showprogress = True +show_network = True # Should network be visualized? This should be False by default, to be overridden by commandline arguments +slim_log = False # Should logs be small in ensemble runs (only aggregated level data)? -simulation_parameters={"no_categories": 4, - "no_insurancefirms": 20, - "no_reinsurancefirms": 4, - "no_riskmodels": 2, - "riskmodel_inaccuracy_parameter": 2, # values >=1; inaccuracy higher with higher values - "riskmodel_margin_of_safety": 2, # values >=1; factor of additional liquidity beyond value at risk - "margin_increase": 0, # This parameter modifies the margin of safety depending on the number of risks models available in the market. When is 0 all risk models have the same margin of safety. - "value_at_risk_tail_probability": 0.005, # values <1, >0, usually close to 0; tail probability at which the value at risk is taken by the risk models - "norm_profit_markup": 0.15, - "rein_norm_profit_markup": 0.15, - "dividend_share_of_profits": 0.4, - "mean_contract_runtime": 12, - "contract_runtime_halfspread": 2, - "default_contract_payment_period": 3, - "max_time": 1000, - "money_supply": 2000000000, - "event_time_mean_separation": 100 / 3., - "expire_immediately": False, - "risk_factors_present": False, - "risk_factor_lower_bound": 0.4, - "risk_factor_upper_bound": 0.6, - "initial_acceptance_threshold": 0.5, - "acceptance_threshold_friction": 0.9, - "insurance_firm_market_entry_probability": 0.3, #0.02, - "reinsurance_firm_market_entry_probability": 0.05, #0.004, - "simulation_reinsurance_type": 'non-proportional', - "default_non-proportional_reinsurance_deductible": 0.3, - "default_non-proportional_reinsurance_excess": 1.0, - "default_non-proportional_reinsurance_premium_share": 0.3, - "static_non-proportional_reinsurance_levels": False, - "catbonds_off": True, - "reinsurance_off": False, - "capacity_target_decrement_threshold": 1.8, - "capacity_target_increment_threshold": 1.2, - "capacity_target_decrement_factor": 24/25., - "capacity_target_increment_factor": 25/24., - # Retention parameters - "insurance_retention": 0.85, # Ratio of insurance contracts retained every iteration. - "reinsurance_retention": 1, # Ratio of reinsurance contracts retained every iteration. - #Premium sensitivity parameters - "premium_sensitivity": 5, # This parameter represents how sensitive is the variation of the insurance premium with respect of the capital of the market. Higher means more sensitive. - "reinpremium_sensitivity": 6, # This parameter represents how sensitive is the variation of the reinsurance premium with respect of the capital of the market. Higher means more sensitive. - #Balanced portfolio parameters - "insurers_balance_ratio": 0.1, # This ratio represents how low we want to keep the standard deviation of the cash reserved below the mean for insurers. Lower means more balanced. - "reinsurers_balance_ratio": 20, # This ratio represents how low we want to keep the standard deviation of the cash reserved below the mean for reinsurers. Lower means more balanced. (Deactivated for the moment) - "insurers_recursion_limit": 50, # Intensity of the recursion algorithm to balance the portfolio of risks for insurers. - "reinsurers_recursion_limit": 10, # Intensity of the recursion algorithm to balance the portfolio of risks for reinsurers. - #Market permanency parameters - "market_permanency_off": False, # This parameter activates (deactivates) the following market permanency constraints. - "cash_permanency_limit": 100, # This parameter enforces the limit under which the firms leave the market because they cannot underwrite anything. - "insurance_permanency_contracts_limit": 4, # If insurers stay for too long under this limit of contracts they deccide to leave the market. - "insurance_permanency_ratio_limit": 0.6, # If insurers stay for too long under this limit they deccide to leave the market because they have too much capital. - "insurance_permanency_time_constraint": 24, # This parameter defines the period that the insurers wait if they have few capital or few contract before leaving the market. - "reinsurance_permanency_contracts_limit": 2, # If reinsurers stay for too long under this limit of contracts they deccide to leave the market. - "reinsurance_permanency_ratio_limit": 0.8, # If reinsurers stay for too long under this limit they deccide to leave the market because they have too much capital. - "reinsurance_permanency_time_constraint": 48, # This parameter defines the period that the reinsurers wait if they have few capital or few contract before leaving the market. - #Insurance and Reinsurance deductibles - "insurance_reinsurance_levels_lower_bound": 0.25, - "insurance_reinsurance_levels_upper_bound": 0.30, - "reinsurance_reinsurance_levels_lower_bound": 0.5, - "reinsurance_reinsurance_levels_upper_bound": 0.95, - "initial_agent_cash": 80000, - "initial_reinagent_cash": 2000000, - "interest_rate": 0.001, - "reinsurance_limit": 0.1, - "upper_price_limit": 1.2, - "lower_price_limit": 0.85, - "no_risks": 20000} +simulation_parameters = {"no_categories": 4, + "no_insurancefirms": 20, + "no_reinsurancefirms": 4, + "no_riskmodels": 2, + "riskmodel_inaccuracy_parameter": 2, # values >=1; inaccuracy higher with higher values + "riskmodel_margin_of_safety": 2, # values >=1; factor of additional liquidity beyond value at risk + "margin_increase": 0, # This parameter modifies the margin of safety depending on the number of risks models available in the market. When is 0 all risk models have the same margin of safety. + "value_at_risk_tail_probability": 0.005, # values <1, >0, usually close to 0; tail probability at which the value at risk is taken by the risk models + "norm_profit_markup": 0.15, + "rein_norm_profit_markup": 0.15, + "dividend_share_of_profits": 0.4, + "mean_contract_runtime": 12, + "contract_runtime_halfspread": 2, + "default_contract_payment_period": 3, + "max_time": 1000, + "money_supply": 2000000000, + "event_time_mean_separation": 100 / 3., + "expire_immediately": False, + "risk_factors_present": False, + "risk_factor_lower_bound": 0.4, + "risk_factor_upper_bound": 0.6, + "initial_acceptance_threshold": 0.5, + "acceptance_threshold_friction": 0.9, + "insurance_firm_market_entry_probability": 0.3, #0.02, + "reinsurance_firm_market_entry_probability": 0.05, #0.004, + "simulation_reinsurance_type": 'non-proportional', + "default_non-proportional_reinsurance_deductible": 0.3, + "default_non-proportional_reinsurance_excess": 1.0, + "default_non-proportional_reinsurance_premium_share": 0.3, + "static_non-proportional_reinsurance_levels": False, + "catbonds_off": False, + "reinsurance_off": False, + "capacity_target_decrement_threshold": 1.8, + "capacity_target_increment_threshold": 1.2, + "capacity_target_decrement_factor": 24/25., + "capacity_target_increment_factor": 25/24., + # Retention parameters + "insurance_retention": 0.85, # Ratio of insurance contracts retained every iteration. + "reinsurance_retention": 1, # Ratio of reinsurance contracts retained every iteration. + # Premium sensitivity parameters + "premium_sensitivity": 5, # This parameter represents how sensitive is the variation of the insurance premium with respect of the capital of the market. Higher means more sensitive. + "reinpremium_sensitivity": 6, # This parameter represents how sensitive is the variation of the reinsurance premium with respect of the capital of the market. Higher means more sensitive. + # Balanced portfolio parameters + "insurers_balance_ratio": 0.1, # This ratio represents how low we want to keep the standard deviation of the cash reserved below the mean for insurers. Lower means more balanced. + "reinsurers_balance_ratio": 20, # This ratio represents how low we want to keep the standard deviation of the cash reserved below the mean for reinsurers. Lower means more balanced. (Deactivated for the moment) + "insurers_recursion_limit": 50, # Intensity of the recursion algorithm to balance the portfolio of risks for insurers. + "reinsurers_recursion_limit": 10, # Intensity of the recursion algorithm to balance the portfolio of risks for reinsurers. + # Market permanency parameters + "market_permanency_off": False, # This parameter activates (deactivates) the following market permanency constraints. + "cash_permanency_limit": 100, # This parameter enforces the limit under which the firms leave the market because they cannot underwrite anything. + "insurance_permanency_contracts_limit": 4, # If insurers stay for too long under this limit of contracts they decide to leave the market. + "insurance_permanency_ratio_limit": 0.6, # If insurers stay for too long under this limit they decide to leave the market because they have too much capital. + "insurance_permanency_time_constraint": 24, # This parameter defines the period that the insurers wait if they have few capital or few contract before leaving the market. + "reinsurance_permanency_contracts_limit": 2, # If reinsurers stay for too long under this limit of contracts they decide to leave the market. + "reinsurance_permanency_ratio_limit": 0.8, # If reinsurers stay for too long under this limit they decide to leave the market because they have too much capital. + "reinsurance_permanency_time_constraint": 48, # This parameter defines the period that the reinsurers wait if they have few capital or few contract before leaving the market. + # Insurance and Reinsurance deductibles + "insurance_reinsurance_levels_lower_bound": 0.25, + "insurance_reinsurance_levels_upper_bound": 0.30, + "reinsurance_reinsurance_levels_lower_bound": 0.5, + "reinsurance_reinsurance_levels_upper_bound": 0.95, + "initial_agent_cash": 80000, + "initial_reinagent_cash": 2000000, + "interest_rate": 0.001, + "reinsurance_limit": 0.1, + "upper_price_limit": 1.2, + "lower_price_limit": 0.85, + "no_risks": 20000} diff --git a/metainsurancecontract.py b/metainsurancecontract.py index 20d17bb..139d2b7 100644 --- a/metainsurancecontract.py +++ b/metainsurancecontract.py @@ -79,8 +79,6 @@ def __init__(self, insurer, properties, time, premium, runtime, payment_period, # This flag is set to 1, when the contract is about to expire and there is an attempt to roll it over. self.roll_over_flag = 0 - - def check_payment_due(self, time): if len(self.payment_times) > 0 and time >= self.payment_times[0]: # Create obligation for premium payment @@ -96,7 +94,6 @@ def get_and_reset_current_claim(self): self.current_claim = 0 return self.category, current_claim, (self.insurancetype == "proportional") - def terminate_reinsurance(self, time): """Terminate reinsurance method. Accepts arguments diff --git a/metainsuranceorg.py b/metainsuranceorg.py index c0fa39e..6e876f7 100644 --- a/metainsuranceorg.py +++ b/metainsuranceorg.py @@ -6,15 +6,10 @@ from insurancecontract import InsuranceContract from reinsurancecontract import ReinsuranceContract from riskmodel import RiskModel +from genericagent import GenericAgent import sys, pdb import uuid -if isleconfig.use_abce: - from genericagentabce import GenericAgent - #print("abce imported") -else: - from genericagent import GenericAgent - #print("abce not imported") def get_mean(x): return sum(x) / len(x) @@ -24,6 +19,7 @@ def get_mean_std(x): variance = sum((val - m) ** 2 for val in x) return m, np.sqrt(variance / len(x)) + class MetaInsuranceOrg(GenericAgent): def init(self, simulation_parameters, agent_parameters): self.simulation = simulation_parameters['simulation'] @@ -50,7 +46,7 @@ def init(self, simulation_parameters, agent_parameters): self.simulation_reinsurance_type = simulation_parameters["simulation_reinsurance_type"] self.dividend_share_of_profits = simulation_parameters["dividend_share_of_profits"] - self.owner = self.simulation # TODO: Make this into agent_parameter value? + self.owner = self.simulation # TODO: Make this into agent_parameter value? self.per_period_dividend = 0 self.cash_last_periods = list(np.zeros(4, dtype=int)*self.cash) @@ -62,16 +58,16 @@ def init(self, simulation_parameters, agent_parameters): margin_of_safety_correction = (rm_config["margin_of_safety"] + (simulation_parameters["no_riskmodels"] - 1) * simulation_parameters["margin_increase"]) self.riskmodel = RiskModel(damage_distribution=rm_config["damage_distribution"], \ - expire_immediately=rm_config["expire_immediately"], \ - cat_separation_distribution=rm_config["cat_separation_distribution"], \ - norm_premium=rm_config["norm_premium"], \ - category_number=rm_config["no_categories"], \ - init_average_exposure=rm_config["risk_value_mean"], \ - init_average_risk_factor=rm_config["risk_factor_mean"], \ - init_profit_estimate=rm_config["norm_profit_markup"], \ - margin_of_safety=margin_of_safety_correction, \ - var_tail_prob=rm_config["var_tail_prob"], \ - inaccuracy=rm_config["inaccuracy_by_categ"]) + expire_immediately=rm_config["expire_immediately"], \ + cat_separation_distribution=rm_config["cat_separation_distribution"], \ + norm_premium=rm_config["norm_premium"], \ + category_number=rm_config["no_categories"], \ + init_average_exposure=rm_config["risk_value_mean"], \ + init_average_risk_factor=rm_config["risk_factor_mean"], \ + init_profit_estimate=rm_config["norm_profit_markup"], \ + margin_of_safety=margin_of_safety_correction, \ + var_tail_prob=rm_config["var_tail_prob"], \ + inaccuracy=rm_config["inaccuracy_by_categ"]) self.category_reinsurance = [None for i in range(self.simulation_no_risk_categories)] if self.simulation_reinsurance_type == 'non-proportional': @@ -142,9 +138,8 @@ def iterate(self, time): # TODO: split function so that only the sequence new_nonproportional_risks = [risk for risk in new_risks if risk.get("insurancetype")=='excess-of-loss' and risk["owner"] is not self] new_risks = [risk for risk in new_risks if risk.get("insurancetype") in ['proportional', None] and risk["owner"] is not self] - - """deal with non-proportional risks first as they must evaluate each request separatly, then with proportional ones""" + """deal with non-proportional risks first as they must evaluate each request separatly, then with proportional ones""" [reinrisks_per_categ, number_reinrisks_categ] = self.risks_reinrisks_organizer(new_nonproportional_risks) #Here the new reinrisks are organized by category. for repetition in range(self.recursion_limit): # TODO: find an efficient way to stop the recursion if there are no more risks to accept or if it is not accepting any more over several iterations. @@ -162,11 +157,12 @@ def iterate(self, time): # TODO: split function so that only the sequence contract.reinsurance_share != 1.0] """obtain risk model evaluation (VaR) for underwriting decisions and for capacity specific decisions""" - # TODO: Enable reinsurance shares other tan 0.0 and 1.0 + # TODO: Enable reinsurance shares other than 0.0 and 1.0 expected_profit, acceptable_by_category, cash_left_by_categ, var_per_risk_per_categ, self.excess_capital = self.riskmodel.evaluate(underwritten_risks, self.cash) # TODO: resolve insurance reinsurance inconsistency (insurer underwrite after capacity decisions, reinsurers before). # This is currently so because it minimizes the number of times we need to run self.riskmodel.evaluate(). # It would also be more consistent if excess capital would be updated at the end of the iteration. + """handle adjusting capacity target and capacity""" max_var_by_categ = self.cash - self.excess_capital self.adjust_capacity_target(max_var_by_categ) @@ -175,18 +171,14 @@ def iterate(self, time): # TODO: split function so that only the sequence #if self.is_insurer: # # TODO: Why should only insurers be able to get reinsurance (not reinsurers)? (Technically, it should work) --> OBSOLETE # self.ask_reinsurance(time) - # # TODO: make independent of insurer/reinsurer, but change this to different deductable values + # # TODO: make independent of insurer/reinsurer, but change this to different deductible values + """handle capital market interactions: capital history, dividends""" self.cash_last_periods = [self.cash] + self.cash_last_periods[:3] self.adjust_dividends(time, actual_capacity) self.pay_dividends(time) """make underwriting decisions, category-wise""" - #if expected_profit * 1./self.cash < self.profit_target: - # self.acceptance_threshold = ((self.acceptance_threshold - .4) * 5. * self.acceptance_threshold_friction) / 5. + .4 - #else: - # self.acceptance_threshold = (1 - self.acceptance_threshold_friction * (1 - (self.acceptance_threshold - .4) * 5.)) / 5. + .4 - growth_limit = max(50, 2 * len(self.underwritten_contracts) + contracts_dissolved) if sum(acceptable_by_category) > growth_limit: acceptable_by_category = np.asarray(acceptable_by_category).astype(np.double) @@ -202,13 +194,11 @@ def iterate(self, time): # TODO: split function so that only the sequence if former_risks_per_categ == risks_per_categ: #Stop condition implemented. Might solve the previous TODO. break - # return unacceptables - #print(self.id, " now has ", len(self.underwritten_contracts), " & returns ", len(not_accepted_risks)) self.simulation.return_risks(not_accepted_risks) - #not implemented - #"""adjust liquidity, borrow or invest""" - #pass + """not implemented + adjust liquidity, borrow or invest + pass""" self.market_permanency(time) @@ -270,9 +260,9 @@ def dissolve(self, time, record): self.risks_kept = [] self.reinrisks_kept = [] obligation = {"amount": self.cash, "recipient": self.simulation, "due_time": time, "purpose": "Dissolution"} - self.pay(obligation) #This MUST be the last obligation before the dissolution of the firm. - self.excess_capital = 0 #Excess of capital is 0 after bankruptcy or market exit. - self.profits_losses = 0 #Profits and losses are 0 after bankruptcy or market exit. + self.pay(obligation) # This MUST be the last obligation before the dissolution of the firm. + self.excess_capital = 0 # Excess of capital is 0 after bankruptcy or market exit. + self.profits_losses = 0 # Profits and losses are 0 after bankruptcy or market exit. if self.operational: method_to_call = getattr(self.simulation, record) method_to_call() @@ -299,7 +289,6 @@ def effect_payments(self, time): for obligation in due: self.pay(obligation) - def pay(self, obligation): amount = obligation["amount"] recipient = obligation["recipient"] @@ -321,9 +310,6 @@ def pay_dividends(self, time): def obtain_yield(self, time): amount = self.cash * self.interest_rate # TODO: agent should not award her own interest. This interest rate should be taken from self.simulation with a getter method self.simulation.receive_obligation(amount, self, time, 'yields') - - def increase_capacity(self): - raise AttributeError( "Method is not implemented in MetaInsuranceOrg, just in inheriting InsuranceFirm instances" ) def get_cash(self): return self.cash @@ -336,9 +322,6 @@ def logme(self): self.log('underwritten_contracts', self.underwritten_contracts) self.log('operational', self.operational) - #def zeros(self): - # return 0 - def len_underwritten_contracts(self): return len(self.underwritten_contracts) @@ -378,16 +361,14 @@ def estimated_var(self): else: self.var_counter_per_risk = 0 - def increase_capacity(self, time): - assert False, "Method not implemented. increase_capacity method should be implemented in inheriting classes" - def adjust_dividend(self, time): assert False, "Method not implemented. adjust_dividend method should be implemented in inheriting classes" def adjust_capacity_target(self, time): assert False, "Method not implemented. adjust_dividend method should be implemented in inheriting classes" - def risks_reinrisks_organizer(self, new_risks): #This method organizes the new risks received by the insurer (or reinsurer) + def risks_reinrisks_organizer(self, new_risks): # + """This method organizes the new risks received by the insurer (or reinsurer)""" risks_per_categ = [[] for x in range(self.simulation_parameters["no_categories"])] #This method organizes the new risks received by the insurer (or reinsurer) by category in the nested list "risks_per_categ". number_risks_categ = [[] for x in range(self.simulation_parameters["no_categories"])] #This method also counts the new risks received by the insurer (or reinsurer) by category in the list "number_risks_categ". @@ -434,8 +415,9 @@ def balanced_portfolio(self, risk, cash_left_by_categ, var_per_risk): #This meth return False, cash_left_by_categ - def process_newrisks_reinsurer(self, reinrisks_per_categ, number_reinrisks_categ, time): #This method processes one by one the reinrisks contained in reinrisks_per_categ in order to decide whether they should be underwritten or not. - #It is done in this way to maintain the portfolio as balanced as possible. For that reason we process risk[C1], risk[C2], risk[C3], risk[C4], risk[C1], risk[C2], ... and so forth. + def process_newrisks_reinsurer(self, reinrisks_per_categ, number_reinrisks_categ, time): + """This method processes one by one the reinrisks contained in reinrisks_per_categ in order to decide whether they should be underwritten or not. + It is done in this way to maintain the portfolio as balanced as possible. For that reason we process risk[C1], risk[C2], risk[C3], risk[C4], risk[C1], risk[C2], ... and so forth.""" for iterion in range(max(number_reinrisks_categ)): for categ_id in range(self.simulation_parameters["no_categories"]): #Here we take only one risk per category at a time to achieve risk[C1], risk[C2], risk[C3], risk[C4], risk[C1], risk[C2], ... if possible. if iterion < number_reinrisks_categ[categ_id] and reinrisks_per_categ[categ_id][iterion] is not None: @@ -523,7 +505,6 @@ def process_newrisks_insurer(self, risks_per_categ, number_risks_categ, acceptab return risks_per_categ, not_accepted_risks - def market_permanency(self, time): #This method determines whether an insurer or reinsurer stays in the market. If it has very few risks underwritten or too much cash left for TOO LONG it eventually leaves the market. # If it has very few risks underwritten it cannot balance the portfolio so it makes sense to leave the market. if not self.simulation_parameters["market_permanency_off"]: @@ -600,9 +581,3 @@ def roll_over(self,time): if reinrisk is not None: self.reinrisks_kept.append(reinrisk) - - - - - - diff --git a/reinsurancecontract.py b/reinsurancecontract.py index c9ced5a..22fbf10 100644 --- a/reinsurancecontract.py +++ b/reinsurancecontract.py @@ -23,7 +23,7 @@ def __init__(self, insurer, properties, time, premium, runtime, payment_period, def explode(self, time, damage_extent=None): """Explode method. - Accepts agruments + Accepts arguments time: Type integer. The current time. uniform_value: Not used damage_extent: Type float. The absolute damage in excess-of-loss reinsurance (not relative as in @@ -50,7 +50,7 @@ def explode(self, time, damage_extent=None): def mature(self, time): """Mature method. Accepts arguments - time: Tyoe integer. The current time. + time: Type integer. The current time. No return value. Removes any reinsurance functions this contract has and terminates any reinsurance contracts for this contract.""" diff --git a/riskmodel.py b/riskmodel.py index 4769032..8ad0c69 100644 --- a/riskmodel.py +++ b/riskmodel.py @@ -20,12 +20,11 @@ def __init__(self, damage_distribution, expire_immediately, cat_separation_distr self.init_average_risk_factor = init_average_risk_factor self.init_profit_estimate = init_profit_estimate self.margin_of_safety = margin_of_safety - """damage_distribution is some scipy frozen rv distribution wich is bound between 0 and 1 and indicates + """damage_distribution is some scipy frozen rv distribution which is bound between 0 and 1 and indicates the share of risks suffering damage as part of any single catastrophic peril""" self.damage_distribution = [damage_distribution for _ in range(self.category_number)] # TODO: separate that category wise? -> DONE. self.damage_distribution_stack = [[] for _ in range(self.category_number)] - self.reinsurance_contract_stack = [[] for _ in range(self.category_number)] - #self.inaccuracy = np.random.uniform(9/10., 10/9., size=self.category_number) + self.reinsurance_contract_stack = [[] for _ in range(self.category_number)] self.inaccuracy = inaccuracy def getPPF(self, categ_id, tailSize): @@ -102,7 +101,7 @@ def evaluate_proportional(self, risks, cash): #categ_risks = [risk for risk in risks if risk["category"]==categ_id] if len(categ_risks) > 0: - average_risk_factor, average_exposure, incr_expected_profits = self.compute_expectation(categ_risks=categ_risks, categ_id=categ_id) + average_risk_factor, average_exposure, incr_expected_profits = self.compute_expectation(categ_risks=categ_risks, categ_id=categ_id) else: average_risk_factor = self.init_average_risk_factor average_exposure = self.init_average_exposure @@ -219,6 +218,7 @@ def evaluate(self, risks, cash, offered_risk=None): # sort current contracts el_risks = [risk for risk in risks if risk["insurancetype"] == 'excess-of-loss'] risks = [risk for risk in risks if risk["insurancetype"] == 'proportional'] + # compute liquidity requirements and acceptable risks from existing contract if (offered_risk is not None) or (len(el_risks) > 0): cash_left_by_categ, additional_required, var_this_risk = self.evaluate_excess_of_loss(el_risks, cash_left_by_categ, offered_risk) @@ -246,4 +246,4 @@ def delete_reinsurance(self, categ_id, excess_fraction, deductible_fraction, con assert self.reinsurance_contract_stack[categ_id][-1] == contract self.reinsurance_contract_stack[categ_id].pop() self.damage_distribution[categ_id] = self.damage_distribution_stack[categ_id].pop() - + diff --git a/setup.py b/setup.py index 65a9fdf..f7579e1 100644 --- a/setup.py +++ b/setup.py @@ -22,6 +22,7 @@ import isleconfig from distributiontruncated import TruncatedDistWrapper + class SetupSim(): def __init__(self): @@ -73,13 +74,12 @@ def schedule(self, replications): #This method returns the lists of schedule ti def seeds(self, replications): #This method returns the seeds required for an ensemble of replications of the model. The argument (replications) is the number of replications. """draw random variates for random seeds""" for i in range(replications): - np_seed, random_seed = scipy.stats.randint.rvs(0, 2**32 - 1, size=2) + np_seed, random_seed = scipy.stats.randint.rvs(0, 2**16 - 1, size=2) self.np_seed.append(np_seed) self.random_seed.append(random_seed) return self.np_seed, self.random_seed - def store(self, replications): #This method stores in a file the the schedules and random seeds required for an ensemble of replications of the model. The argument (replications) is the number of replications. #With the information stored it is possible to replicate the entire behavior of the ensemble at a later time. event_schedules = [] @@ -108,7 +108,6 @@ def store(self, replications): #This method stores in a file the the schedules for rep_schedule in event_schedules: wfile.write(str(rep_schedule).replace("\n","").replace("array", "np.array").replace("uint32", "np.uint32") + "\n") - def obtain_ensemble(self, replications): #This method returns all the information (schedules and seeds) required to run an ensemble of simulations of the model. Since it also stores the information in a file it will be possible to replicate the ensemble at a later time. The argument (replications) is the number of replications. #This method will be called either form ensemble.py or start.py [general_rc_event_schedule, general_rc_event_damage] = self.schedule(replications) @@ -119,8 +118,3 @@ def obtain_ensemble(self, replications): #This method returns all the informati return general_rc_event_schedule, general_rc_event_damage, np_seeds, random_seeds - - - - - diff --git a/start.py b/start.py index 5e52335..5c82008 100644 --- a/start.py +++ b/start.py @@ -61,10 +61,10 @@ def main(simulation_parameters, rc_event_schedule, rc_event_damage, np_seed, ran world.accept_agents("insurancefirm", insurancefirm_pointers, insurancefirms_group) """create agents: reinsurance firms according to number in isleconfig.py, adds them all to simulation instance""" - reinsurancefirms_group = simulation.build_agents(ReinsuranceFirm, 'reinsurance', parameters=simulation_parameters, - agent_parameters=world.agent_parameters["reinsurance"]) + reinsurancefirms_group = simulation.build_agents(ReinsuranceFirm, 'reinsurancefirm', parameters=simulation_parameters, + agent_parameters=world.agent_parameters["reinsurancefirm"]) reinsurancefirm_pointers = reinsurancefirms_group - world.accept_agents("reinsurance", reinsurancefirm_pointers, reinsurancefirms_group) + world.accept_agents("reinsurancefirm", reinsurancefirm_pointers, reinsurancefirms_group) """Time iteration""" for t in range(simulation_parameters["max_time"]): @@ -73,24 +73,20 @@ def main(simulation_parameters, rc_event_schedule, rc_event_damage, np_seed, ran if world.insurance_firm_market_entry(agent_type="InsuranceFirm"): parameters = [world.agent_parameters["insurancefirm"][simulation.insurance_entry_index()]] parameters[0]["id"] = world.get_unique_insurer_id() - new_insurance_firm = simulation.build_agents(InsuranceFirm, - 'insurancefirm', - parameters=simulation_parameters, + new_insurance_firm = simulation.build_agents(InsuranceFirm, 'insurancefirm', parameters=simulation_parameters, agent_parameters=parameters) insurancefirms_group += new_insurance_firm new_insurancefirm_pointer = new_insurance_firm world.accept_agents("insurancefirm", new_insurancefirm_pointer, new_insurance_firm, time=t) if world.insurance_firm_market_entry(agent_type="ReinsuranceFirm"): - parameters = [world.agent_parameters["reinsurance"][simulation.reinsurance_entry_index()]] + parameters = [world.agent_parameters["reinsurancefirm"][simulation.reinsurance_entry_index()]] parameters[0]["id"] = world.get_unique_reinsurer_id() - new_reinsurance_firm = simulation.build_agents(ReinsuranceFirm, - 'reinsurance', - parameters=simulation_parameters, + new_reinsurance_firm = simulation.build_agents(ReinsuranceFirm, 'reinsurancefirm', parameters=simulation_parameters, agent_parameters=parameters) reinsurancefirms_group += new_reinsurance_firm new_reinsurancefirm_pointer = new_reinsurance_firm - world.accept_agents("reinsurance", new_reinsurancefirm_pointer, new_reinsurance_firm, time=t) + world.accept_agents("reinsurancefirm", new_reinsurancefirm_pointer, new_reinsurance_firm, time=t) "iterate simulation" world.iterate(t) From 2b72fb95f4ac3376a37ba8edfe31c34bbe55995a Mon Sep 17 00:00:00 2001 From: KloskaT Date: Fri, 5 Jul 2019 10:12:26 +0100 Subject: [PATCH 008/125] Removed redundant functions from insurancesimulation: reinsurance_capital_entry, count_underwritten_risks_by_category Removed redundant functions from metainsuranceorg: adjust_dividend, adjust_capacity_target. --- insurancesimulation.py | 81 ++++++++++++++---------------------------- metainsuranceorg.py | 8 +---- 2 files changed, 28 insertions(+), 61 deletions(-) diff --git a/insurancesimulation.py b/insurancesimulation.py index 6cbd4bf..fb95b94 100644 --- a/insurancesimulation.py +++ b/insurancesimulation.py @@ -453,7 +453,7 @@ def effect_payments(self, time): def pay(self, obligation): """Method for paying obligations called from effect_payments Accepts: - Obligation that must be payed + Obligation: Type DataDict with categories amount, recipient, due time, purpose. Returns None""" amount = obligation["amount"] recipient = obligation["recipient"] @@ -467,13 +467,17 @@ def pay(self, obligation): recipient.receive(amount) def receive(self, amount): - """Method to accept cash payments. - Accepts: Amount due""" + """Method to accept cash payments. As insurance simulation cash is economy, adds money to total economy. + Accepts: + Amount due: Type Integer + Returns None""" self.money_supply += amount def reduce_money_supply(self, amount): """Method to reduce money supply immediately and without payment recipient - (used to adjust money supply to compensate for agent endowment).""" + (used to adjust money supply to compensate for agent endowment). + Accepts: + amount: Type Integer""" self.money_supply -= amount assert self.money_supply >= 0 @@ -607,11 +611,6 @@ def append_reinrisks(self, item): if len(item) > 0: self.reinrisks.append(item) - def remove_reinrisks(self,risko): - """Method for removing reinsurance risks from the simulation instance. Redundant?""" - if risko != None: - self.reinrisks.remove(risko) - def get_reinrisks(self): """Method for shuffling reinsurance risks Returns: reinsurance risks""" @@ -661,7 +660,7 @@ def return_risks(self, not_accepted_risks): """Method for adding risks that were not deemed acceptable to underwrite back to list of uninsured risks Accepts: not_accepted_risks: Type List - Returns None""" + No return value""" self.risks += not_accepted_risks def return_reinrisks(self, not_accepted_risks): @@ -753,16 +752,21 @@ def restore_state_and_risk_categories(self): np.random.set_state(mersennetwister_randomseed) assert found, "mersennetwister randomseed for current replication ID number {0:d} not found in data file. Exiting.".format(self.replic_ID) - def insurance_firm_market_entry(self, prob=-1, agent_type="InsuranceFirm"): # TODO: replace method name with a more descriptive one + def insurance_firm_market_entry(self, agent_type="InsuranceFirm"): """Method to determine if re/insurance firm enters the market based on set entry probabilities and a random - integer generated between 0, 1.""" - if prob == -1: - if agent_type == "InsuranceFirm": - prob = self.simulation_parameters["insurance_firm_market_entry_probability"] - elif agent_type == "ReinsuranceFirm": - prob = self.simulation_parameters["reinsurance_firm_market_entry_probability"] - else: - assert False, "Unknown agent type. Simulation requested to create agent of type {0:s}".format(agent_type) + integer generated between 0, 1. + Accepts: + agent_type: Type String + Returns: + True if firm can enter market + False if firm cannot enter market""" + + if agent_type == "InsuranceFirm": + prob = self.simulation_parameters["insurance_firm_market_entry_probability"] + elif agent_type == "ReinsuranceFirm": + prob = self.simulation_parameters["reinsurance_firm_market_entry_probability"] + else: + assert False, "Unknown agent type. Simulation requested to create agent of type {0:s}".format(agent_type) if np.random.random() < prob: return True else: @@ -789,8 +793,7 @@ def record_unrecovered_claims(self, loss): money is lost and recorded using this method. Accepts: loss: Type integer, value of lost claim - Returns: - None""" + No return value""" self.cumulative_unrecovered_claims += loss def record_claims(self, claims): @@ -835,22 +838,6 @@ def compute_market_diffvar(self): return totaldiff #self.history_logs['market_diffvar'].append(totaldiff) - def count_underwritten_and_reinsured_risks_by_category(self): - """Not Used - Should be removed?""" - underwritten_risks = 0 - reinsured_risks = 0 - underwritten_per_category = np.zeros(self.simulation_parameters["no_categories"]) - reinsured_per_category = np.zeros(self.simulation_parameters["no_categories"]) - for firm in self.insurancefirms: - if firm.operational: - underwritten_by_category += firm.counter_category - if self.simulation_parameters["simulation_reinsurance_type"] == "non-proportional": - reinsured_per_category += firm.counter_category * firm.category_reinsurance - if self.simulation_parameters["simulation_reinsurance_type"] == "proportional": - for firm in self.insurancefirms: - if firm.operational: - reinsured_per_category += firm.counter_category - def get_unique_insurer_id(self): """Method for getting unique id for insurer. Used in initialising agents in start.py and insurancesimulation. Iterates after each call so id is unique to each firm. @@ -885,25 +872,11 @@ def reinsurance_entry_index(self): def get_operational(self): """Method to return if simulation is operational. Always true. Used only in pay methods above and - metainsuranceorg.""" + metainsuranceorg. + Accepts no arguments + Returns True""" return True - def reinsurance_capital_entry(self): - """This method determines the capital market entry of reinsurers. It is not run anywhere.""" - capital_per_non_re_cat = [] - - for reinrisk in self.not_accepted_reinrisks: - capital_per_non_re_cat.append(reinrisk["value"]) #It takes all the values of the reinsurance risks NOT REINSURED. - - if len(capital_per_non_re_cat) > 0: #We only perform this action if there are reinsurance contracts that has not been reinsured in the last period of time. - capital_per_non_re_cat = np.random.choice(capital_per_non_re_cat, 10) #Only 10 values sampled randomly are considered. (Too low?) - entry = max(capital_per_non_re_cat) #For market entry the maximum of the sample is considered. - entry = 2 * entry #The capital market entry of those values will be the double of the maximum. - else: # Otherwise the default reinsurance cash market entry is considered. - entry = self.simulation_parameters["initial_reinagent_cash"] - - return entry # The capital market entry is returned. - def reset_pls(self): """Reset_pls Method. Accepts no arguments. diff --git a/metainsuranceorg.py b/metainsuranceorg.py index 6e876f7..f44c594 100644 --- a/metainsuranceorg.py +++ b/metainsuranceorg.py @@ -361,12 +361,6 @@ def estimated_var(self): else: self.var_counter_per_risk = 0 - def adjust_dividend(self, time): - assert False, "Method not implemented. adjust_dividend method should be implemented in inheriting classes" - - def adjust_capacity_target(self, time): - assert False, "Method not implemented. adjust_dividend method should be implemented in inheriting classes" - def risks_reinrisks_organizer(self, new_risks): # """This method organizes the new risks received by the insurer (or reinsurer)""" @@ -549,7 +543,7 @@ def reset_pl(self): Reset the profits and losses variable of each firm at the beginning of every iteration. It has to be run in insurancesimulation.py at the beginning of the iterate method""" self.profits_losses = 0 - def roll_over(self,time): + def roll_over(self, time): """Roll_over Method. Accepts arguments time: Type integer. The current time. No return value. From 202b1ab3dea8b8b2e9d400679a9ea610b80ccd38 Mon Sep 17 00:00:00 2001 From: Chris Hughes Date: Fri, 5 Jul 2019 13:33:11 +0100 Subject: [PATCH 009/125] Reimplemented replication using file names instead of replic_id, more cleanup --- catbond.py | 8 +- distributiontruncated.py | 4 +- insurancefirm.py | 4 +- insurancesimulation.py | 1 + isleconfig.py | 2 +- metainsuranceorg.py | 131 ++++++++++++++++------------- reinsurancefirm.py | 4 +- riskmodel.py | 17 ++-- setup.py | 172 +++++++++++++++++++++++++-------------- start.py | 52 ++++++++---- 10 files changed, 244 insertions(+), 151 deletions(-) diff --git a/catbond.py b/catbond.py index ff72838..724b9ed 100644 --- a/catbond.py +++ b/catbond.py @@ -10,7 +10,7 @@ class CatBond(MetaInsuranceOrg): - def init( + def __init__( self, simulation, per_period_premium, owner, interest_rate=0 ): # do we need simulation parameters self.simulation = simulation @@ -22,9 +22,9 @@ def init( self.operational = True self.owner = owner self.per_period_dividend = per_period_premium - self.interest_rate = ( - interest_rate - ) # TODO: shift obtain_yield method to insurancesimulation, thereby making it unnecessary to drag parameters like self.interest_rate from instance to instance and from class to class + self.interest_rate = interest_rate + # TODO: shift obtain_yield method to insurancesimulation, thereby making it unnecessary to drag parameters like + # self.interest_rate from instance to instance and from class to class # self.simulation_no_risk_categories = self.simulation.simulation_parameters["no_categories"] # TODO: change start and InsuranceSimulation so that it iterates CatBonds diff --git a/distributiontruncated.py b/distributiontruncated.py index c50387b..7ab297a 100644 --- a/distributiontruncated.py +++ b/distributiontruncated.py @@ -13,6 +13,7 @@ def __init__(self, dist, lower_bound=0, upper_bound=1): self.upper_bound = upper_bound assert self.upper_bound > self.lower_bound + @functools.lru_cache(maxsize=1024) def pdf(self, x): x = np.array(x, ndmin=1) r = map( @@ -26,6 +27,7 @@ def pdf(self, x): r = float(r) return r + @functools.lru_cache(maxsize=1024) def cdf(self, x): x = np.array(x, ndmin=1) r = map( @@ -42,7 +44,7 @@ def cdf(self, x): r = float(r) return r - @functools.lru_cache(maxsize=512) + @functools.lru_cache(maxsize=1024) def ppf(self, x): x = np.array(x, ndmin=1) assert (x >= 0).all() and (x <= 1).all() diff --git a/insurancefirm.py b/insurancefirm.py index 1d0ff82..3e1824c 100644 --- a/insurancefirm.py +++ b/insurancefirm.py @@ -11,13 +11,13 @@ class InsuranceFirm(MetaInsuranceOrg): # QUERY: now abce is gone can all of these inits become proper __init__s? # In fact, can we do away with genericagent.py entirely? - def init(self, simulation_parameters, agent_parameters): + def __init__(self, simulation_parameters, agent_parameters): """Constructor method. Accepts arguments Signature is identical to constructor method of parent class. Constructor calls parent constructor and only overwrites boolean indicators of insurer and reinsurer role of the object.""" - super(InsuranceFirm, self).init(simulation_parameters, agent_parameters) + super(InsuranceFirm, self).__init__(simulation_parameters, agent_parameters) self.is_insurer = True self.is_reinsurer = False diff --git a/insurancesimulation.py b/insurancesimulation.py index b3c4506..65a3aa5 100644 --- a/insurancesimulation.py +++ b/insurancesimulation.py @@ -831,6 +831,7 @@ def solicit_insurance_requests(self, id, cash, insurer): for risk in insurer.risks_kept: risks_to_be_sent.append(risk) + # QUERY: what actuall is insurancefirm.risks_kept? insurer.risks_kept = [] np.random.shuffle(risks_to_be_sent) diff --git a/isleconfig.py b/isleconfig.py index 4ca35c1..01a1a91 100644 --- a/isleconfig.py +++ b/isleconfig.py @@ -26,7 +26,7 @@ "mean_contract_runtime": 12, "contract_runtime_halfspread": 2, "default_contract_payment_period": 3, - "max_time": 1000, + "max_time": 200, "money_supply": 2000000000, "event_time_mean_separation": 100 / 3.0, "expire_immediately": False, # QUERY: What does this mean? diff --git a/metainsuranceorg.py b/metainsuranceorg.py index 93dca55..f201fdf 100644 --- a/metainsuranceorg.py +++ b/metainsuranceorg.py @@ -21,8 +21,8 @@ def get_mean_std(x): return m, np.sqrt(variance / len(x)) -class MetaInsuranceOrg(GenericAgent): - def init(self, simulation_parameters, agent_parameters): +class MetaInsuranceOrg: + def __init__(self, simulation_parameters, agent_parameters): self.simulation = simulation_parameters["simulation"] self.simulation_parameters = simulation_parameters self.contract_runtime_dist = scipy.stats.randint( @@ -145,9 +145,8 @@ def init(self, simulation_parameters, agent_parameters): ] self.market_permanency_counter = 0 - def iterate( - self, time - ): # TODO: split function so that only the sequence of events remains here and everything else is in separate methods + def iterate(self, time): + # TODO: split up so that only the sequence of events remains here and everything else is in separate methods """obtain investments yield""" self.obtain_yield(time) @@ -166,23 +165,24 @@ def iterate( self.make_reinsurance_claims(time) - """mature contracts""" - if isleconfig.verbose: - print("Number of underwritten contracts ", len(self.underwritten_contracts)) - maturing = [ - contract - for contract in self.underwritten_contracts - if contract.expiration <= time - ] - for contract in maturing: - self.underwritten_contracts.remove(contract) - contract.mature(time) - contracts_dissolved = len(maturing) + contracts_dissolved = self.mature_contracts(time) """effect payments from contracts""" for contract in self.underwritten_contracts: contract.check_payment_due(time) + self.collect_process_evaluate_risks(time, contracts_dissolved) + + # """adjust liquidity, borrow or invest""" # Not implemented + # pass + + self.market_permanency(time) + + self.roll_over(time) + + self.estimated_var() + + def collect_process_evaluate_risks(self, time, contracts_dissolved): if self.operational: """request risks to be considered for underwriting in the next period and collect those for this period""" @@ -219,16 +219,16 @@ def iterate( """deal with non-proportional risks first as they must evaluate each request separatly, then with proportional ones""" - [ - reinrisks_per_categ, - number_reinrisks_categ, - ] = self.risks_reinrisks_organizer( + # Here the new reinrisks are organized by category. + reinrisks_per_categ, number_reinrisks_categ = self.risks_reinrisks_organizer( new_nonproportional_risks - ) # Here the new reinrisks are organized by category. + ) + assert self.recursion_limit > 0 for repetition in range(self.recursion_limit): - # TODO: find an efficient way to stop the recursion if there are no more risks to accept or if it is + # TODO: find an efficient way to stop the loop if there are no more risks to accept or if it is # not accepting any more over several iterations. + # Here we process all the new reinrisks in order to keep the portfolio as balanced as possible. former_reinrisks_per_categ = copy.copy(reinrisks_per_categ) [ reinrisks_per_categ, @@ -236,15 +236,17 @@ def iterate( ] = self.process_newrisks_reinsurer( reinrisks_per_categ, number_reinrisks_categ, time ) - # Here we process all the new reinrisks in order to keep the portfolio as balanced as possible. - if ( - former_reinrisks_per_categ == reinrisks_per_categ - ): # Stop condition implemented. Might solve the previous TODO. - break - self.simulation.return_reinrisks(not_accepted_reinrisks) + # QUERY: I moved this into the loop - was this correct? + # The loop only runs once in my tests, what needs tweaking to have firms not accept risks? + self.simulation.return_reinrisks(not_accepted_reinrisks) - # QUERY: it's typically dangerous to compare floats with !=, is it okay in this case? + if former_reinrisks_per_categ == reinrisks_per_categ: + # Stop condition implemented. Might solve the previous TODO. + break + + # QUERY: it's typically dangerous to compare floats with !=, is it okay in this case? Probably, since + # no arithmetic is done underwritten_risks = [ { "value": contract.value, @@ -260,7 +262,7 @@ def iterate( ] """obtain risk model evaluation (VaR) for underwriting decisions and for capacity specific decisions""" - # TODO: Enable reinsurance shares other tan 0.0 and 1.0 + # TODO: Enable reinsurance shares other than 0.0 and 1.0 expected_profit, acceptable_by_category, cash_left_by_categ, var_per_risk_per_categ, self.excess_capital = self.riskmodel.evaluate( underwritten_risks, self.cash ) @@ -271,11 +273,7 @@ def iterate( max_var_by_categ = self.cash - self.excess_capital self.adjust_capacity_target(max_var_by_categ) actual_capacity = self.increase_capacity(time, max_var_by_categ) - # seek reinsurance - # if self.is_insurer: - # # TODO: Why should only insurers be able to get reinsurance (not reinsurers)? (Technically, it should work) --> OBSOLETE - # self.ask_reinsurance(time) - # # TODO: make independent of insurer/reinsurer, but change this to different deductable values + """handle capital market interactions: capital history, dividends""" self.cash_last_periods = [self.cash] + self.cash_last_periods[:3] self.adjust_dividends(time, actual_capacity) @@ -299,22 +297,25 @@ def iterate( ) acceptable_by_category = np.int64(np.round(acceptable_by_category)) + # Here the new risks are organized by category. [risks_per_categ, number_risks_categ] = self.risks_reinrisks_organizer( new_risks - ) # Here the new risks are organized by category. + ) - for repetition in range( - self.recursion_limit - ): # TODO: find an efficient way to stop the recursion if there are no more risks to accept or if it is not accepting any more over several iterations. + for repetition in range(self.recursion_limit): + # TODO: find an efficient way to stop the recursion if there are no more risks to accept or if it is not accepting any more over several iterations. former_risks_per_categ = copy.copy(risks_per_categ) - [risks_per_categ, not_accepted_risks] = self.process_newrisks_insurer( + # Here we process all the new risks in order to keep the portfolio as balanced as possible. + risks_per_categ, not_accepted_risks = self.process_newrisks_insurer( risks_per_categ, number_risks_categ, acceptable_by_category, var_per_risk_per_categ, cash_left_by_categ, time, - ) # Here we process all the new risks in order to keep the portfolio as balanced as possible. + ) + # QUERY: As above, moved inside loop + self.simulation.return_risks(not_accepted_risks) if ( former_risks_per_categ == risks_per_categ ): # Stop condition implemented. Might solve the previous TODO. @@ -322,17 +323,6 @@ def iterate( # return unacceptables # print(self.id, " now has ", len(self.underwritten_contracts), " & returns ", len(not_accepted_risks)) - self.simulation.return_risks(not_accepted_risks) - - # not implemented - # """adjust liquidity, borrow or invest""" - # pass - - self.market_permanency(time) - - self.roll_over(time) - - self.estimated_var() def enter_illiquidity(self, time): """Enter_illiquidity Method. @@ -467,6 +457,20 @@ def obtain_yield(self, time): # This interest rate should be taken from self.simulation with a getter method self.simulation.receive_obligation(amount, self, time, "yields") + def mature_contracts(self, time): + # matures all contracts that have expired, returns the number of contracts that matured + if isleconfig.verbose: + print("Number of underwritten contracts ", len(self.underwritten_contracts)) + maturing = [ + contract + for contract in self.underwritten_contracts + if contract.expiration <= time + ] + for contract in maturing: + self.underwritten_contracts.remove(contract) + contract.mature(time) + return len(maturing) + def get_cash(self): return self.cash @@ -478,6 +482,11 @@ def logme(self): self.log("underwritten_contracts", self.underwritten_contracts) self.log("operational", self.operational) + def log(self, *args): + raise NotImplementedError( + "The log method should have been overridden by the subclass" + ) + def number_underwritten_contracts(self): return len(self.underwritten_contracts) @@ -523,19 +532,19 @@ def estimated_var(self): else: self.var_counter_per_risk = 0 - def increase_capacity(self): + def increase_capacity(self, time, var_by_category): raise NotImplementedError( "Method is not implemented in MetaInsuranceOrg, just in inheriting InsuranceFirm instances" ) - def adjust_dividend(self, time): + def adjust_dividends(self, time, actual_capacity): raise NotImplementedError( - "Method not implemented. adjust_dividend method should be implemented in inheriting classes" + "Method not implemented. adjust_dividends method should be implemented in inheriting classes" ) def adjust_capacity_target(self, time): raise NotImplementedError( - "Method not implemented. adjust_dividend method should be implemented in inheriting classes" + "Method not implemented. adjust_capacity_target method should be implemented in inheriting classes" ) def risks_reinrisks_organizer(self, new_risks): @@ -573,7 +582,7 @@ def balanced_portfolio(self, risk, cash_left_by_categ, var_per_risk): if risk.get("insurancetype") == "excess-of-loss": percentage_value_at_risk = self.riskmodel.getPPF( - categ_id=risk["category"], tailSize=self.riskmodel.var_tail_prob + categ_id=risk["category"], tail_size=self.riskmodel.var_tail_prob ) expected_damage = ( percentage_value_at_risk @@ -905,3 +914,9 @@ def roll_over(self, time): ): if reinrisk is not None: self.reinrisks_kept.append(reinrisk) + + def make_reinsurance_claims(self, time): + raise NotImplementedError( + "MetaInsuranceOrg does not implement make_reinsurance_claims, " + "it should have been overridden" + ) diff --git a/reinsurancefirm.py b/reinsurancefirm.py index ac2564f..8e188bb 100644 --- a/reinsurancefirm.py +++ b/reinsurancefirm.py @@ -6,12 +6,12 @@ class ReinsuranceFirm(InsuranceFirm): """ReinsuranceFirm class. Inherits from InsuranceFirm.""" - def init(self, simulation_parameters, agent_parameters): + def __init__(self, simulation_parameters, agent_parameters): """Constructor method. Accepts arguments Signature is identical to constructor method of parent class. Constructor calls parent constructor and only overwrites boolean indicators of insurer and reinsurer role of the object.""" - super(ReinsuranceFirm, self).init(simulation_parameters, agent_parameters) + super(ReinsuranceFirm, self).__init__(simulation_parameters, agent_parameters) self.is_insurer = False self.is_reinsurer = True diff --git a/riskmodel.py b/riskmodel.py index 22d183a..6ac799f 100644 --- a/riskmodel.py +++ b/riskmodel.py @@ -40,15 +40,16 @@ def __init__( # self.inaccuracy = np.random.uniform(9/10., 10/9., size=self.category_number) self.inaccuracy = inaccuracy - def getPPF(self, categ_id, tailSize): + def getPPF(self, categ_id, tail_size): """Method for getting quantile function of the damage distribution (value at risk) by category. Positional arguments: categ_id integer: category tailSize (float >=0, <=1): quantile Returns value-at-risk.""" - return self.damage_distribution[categ_id].ppf(1 - tailSize) + return self.damage_distribution[categ_id].ppf(1 - tail_size) - def get_categ_risks(self, risks, categ_id): + @staticmethod + def get_categ_risks(risks, categ_id): # categ_risks2 = [risk for risk in risks if risk["category"]==categ_id] categ_risks = [] for risk in risks: @@ -60,10 +61,10 @@ def get_categ_risks(self, risks, categ_id): def compute_expectation(self, categ_risks, categ_id): # TODO: more intuitive name? # average_exposure2 = np.mean([risk["excess"]-risk["deductible"] for risk in categ_risks]) # - ##average_risk_factor = np.mean([risk["risk_factor"] for risk in categ_risks]) + # average_risk_factor = np.mean([risk["risk_factor"] for risk in categ_risks]) # average_risk_factor2 = self.inaccuracy[categ_id] * np.mean([risk["risk_factor"] for risk in categ_risks]) # - ## compute expected profits from category + # """compute expected profits from category""" # mean_runtime2 = np.mean([risk["runtime"] for risk in categ_risks]) exposures = [] @@ -76,7 +77,9 @@ def compute_expectation(self, categ_risks, categ_id): # TODO: more intuitive na runtimes.append(risk["runtime"]) average_exposure = np.mean(exposures) average_risk_factor = self.inaccuracy[categ_id] * np.mean(risk_factors) + mean_runtime = np.mean(runtimes) + # assert average_exposure == average_exposure2 # assert average_risk_factor == average_risk_factor2 # assert mean_runtime == mean_runtime2 @@ -129,7 +132,7 @@ def evaluate_proportional(self, risks, cash): # compute value at risk var_per_risk = ( - self.getPPF(categ_id=categ_id, tailSize=self.var_tail_prob) + self.getPPF(categ_id=categ_id, tail_size=self.var_tail_prob) * average_risk_factor * average_exposure * self.margin_of_safety @@ -220,7 +223,7 @@ def evaluate_excess_of_loss(self, risks, cash, offered_risk=None): # TODO: allow for different risk distributions for different categories # TODO: factor in risk_factors percentage_value_at_risk = self.getPPF( - categ_id=categ_id, tailSize=self.var_tail_prob + categ_id=categ_id, tail_size=self.var_tail_prob ) # compute liquidity requirements from existing contracts diff --git a/setup.py b/setup.py index a7739ff..514a637 100644 --- a/setup.py +++ b/setup.py @@ -32,7 +32,7 @@ def __init__(self): self.max_time = self.simulation_parameters["max_time"] self.no_categories = self.simulation_parameters["no_categories"] - """set distribution""" + """set distribution""" # TODO: this should be a parameter self.non_truncated = scipy.stats.pareto( b=2, loc=0, scale=0.25 ) # It is assumed that the damages of the catastrophes are drawn from a truncated Pareto distribution. @@ -48,32 +48,23 @@ def __init__(self): self.random_seed = [] self.general_rc_event_schedule = [] self.general_rc_event_damage = [] + self.filepath = "risk_event_schedules.islestore" + self.overwrite = False + self.replications = None - def schedule( - self, replications - ): # This method returns the lists of schedule times and damages for an ensemble of replications of the model. The argument (replications) is the number of replications. - - general_rc_event_schedule = ( - [] - ) # In this list will be stored the lists of schedule times of catastrophes for an ensemble of simulations of the model. ([[Schedule 1], [Schedule 2], [Schedule 3],...,[Schedule N]]) - general_rc_event_damage = ( - [] - ) # In this list will be stored the lists of schedule damages of catastrophes for an ensemble of simulations of the model. ([[Schedule 1], [Schedule 2], [Schedule 3],...,[Schedule N]]) - + def schedule(self, replications): for i in range(replications): - rc_event_schedule = ( - [] - ) # In this list will be stored the lists of times when there will be catastrophes for every category of the model during a single run. ([[times for C1],[times for C2],[times for C3],[times for C4]]) - rc_event_damage = ( - [] - ) # In this list will be stored the lists of catastrophe damages for every category of the model during a single run. ([[damages for C1],[damages for C2],[damages for C3],[damages for C4]]) + # In this list will be stored the lists of times when there will be catastrophes for every category of the + # model during a single run. ([[times for C1],[times for C2],[times for C3],[times for C4]]) + rc_event_schedule = [] + # In this list will be stored the lists of catastrophe damages for every category of the model during a + # single run. ([[damages for C1],[damages for C2],[damages for C3],[damages for C4]]) + rc_event_damage = [] for j in range(self.no_categories): - event_schedule = ( - [] - ) # In this list will be stored the times when there will be a catastrophe related to a particular category. - event_damage = ( - [] - ) # In this list will be stored the damages of a catastrophe related to a particular category. + # In this list will be stored the times when there will be a catastrophe in a particular category. + event_schedule = [] + # In this list will be stored the damages of a catastrophe related to a particular category. + event_damage = [] total = 0 while total < self.max_time: separation_time = self.cat_separation_distribution.rvs() @@ -89,9 +80,9 @@ def schedule( return self.general_rc_event_schedule, self.general_rc_event_damage - def seeds( - self, replications - ): # This method returns the seeds required for an ensemble of replications of the model. The argument (replications) is the number of replications. + def seeds(self, replications): + # This method sets (and returns) the seeds required for an ensemble of replications of the model. + # The argument (replications) is the number of replications. """draw random variates for random seeds""" for i in range(replications): np_seed, random_seed = scipy.stats.randint.rvs(0, 2 ** 32 - 1, size=2) @@ -100,11 +91,18 @@ def seeds( return self.np_seed, self.random_seed - def store( - self, replications - ): # This method stores in a file the the schedules and random seeds required for an ensemble of replications of the model. The argument (replications) is the number of replications. + def store(self): + # This method stores in a file the the schedules and random seeds required for an ensemble of replications of + # the model. The number of replications is calculated from the length of the exisiting values. # With the information stored it is possible to replicate the entire behavior of the ensemble at a later time. event_schedules = [] + assert ( + len(self.np_seed) + == len(self.random_seed) + == len(self.general_rc_event_damage) + == len(self.general_rc_event_schedule) + ) + replications = len(self.np_seed) for i in range(replications): """pack to dict""" @@ -118,40 +116,96 @@ def store( """ ensure that logging directory exists""" if not os.path.isdir("data"): - assert not os.path.exists( - "data" - ), "./data exists as regular file. This filename is required for the logging and event schedule directory" + if os.path.exists("data"): + raise Exception( + "./data exists as regular file. " + "This filename is required for the logging and event schedule directory" + ) os.makedirs("data") - """Save as both pickle and txt""" - with open("./data/risk_event_schedules.pkl", "wb") as wfile: - pickle.dump(event_schedules, wfile, protocol=pickle.HIGHEST_PROTOCOL) - - with open("./data/risk_event_schedules.txt", "w") as wfile: - for rep_schedule in event_schedules: - wfile.write( - str(rep_schedule) - .replace("\n", "") - .replace("array", "np.array") - .replace("uint32", "np.uint32") - + "\n" + # If we are avoiding overwriting, check if the file to write to exist + if not self.overwrite: + if os.path.exists("data/" + self.filepath): + raise ValueError( + f"File {'./data/' + self.filepath} already exists and we are not overwriting." ) - def obtain_ensemble( - self, replications - ): # This method returns all the information (schedules and seeds) required to run an ensemble of simulations of the model. Since it also stores the information in a file it will be possible to replicate the ensemble at a later time. The argument (replications) is the number of replications. - # This method will be called either form ensemble.py or start.py - [general_rc_event_schedule, general_rc_event_damage] = self.schedule( - replications - ) - - [np_seeds, random_seeds] = self.seeds(replications) + """Save the initial values""" + with open("./data/" + self.filepath, "wb") as wfile: + pickle.dump(event_schedules, wfile, protocol=pickle.HIGHEST_PROTOCOL) - self.store(replications) + # with open("./data/risk_event_schedules.txt", "w") as wfile: # QUERY: what's this for? + # for rep_schedule in event_schedules: + # wfile.write( + # str(rep_schedule) + # .replace("\n", "") + # .replace("array", "np.array") + # .replace("uint32", "np.uint32") + # + "\n" + # ) + + def recall(self): + assert ( + self.np_seed + == self.random_seed + == self.general_rc_event_schedule + == self.general_rc_event_damage + == [] + ) + with open("./data/" + self.filepath, "rb") as rfile: + event_schedules = pickle.load(rfile) + self.replications = len(event_schedules) + for initial_values in event_schedules: + self.np_seed.append(initial_values["np_seed"]) + self.random_seed.append(initial_values["random_seed"]) + self.general_rc_event_schedule.append(initial_values["event_times"]) + self.general_rc_event_damage.append(initial_values["event_damages"]) + self.simulation_parameters["no_categories"] = initial_values[ + "num_categories" + ] + + def obtain_ensemble(self, replications, filepath, overwrite): + # This method returns all the information (schedules and seeds) required to run an ensemble of simulations of + # the model. Since it also stores the information in a file it will be possible to replicate the ensemble at a + # later time. The argument (replications) is the number of replications. + # This method will be called either form ensemble.py or start.py + if filepath is not None: + self.filepath = self.to_filename(filepath) + self.overwrite = overwrite + if not isleconfig.replicating: + # We are writing to the file given + self.replications = replications + if filepath is None and not self.overwrite: + print("No explicit path given, automatically overwriting default path") + self.overwrite = True + self.schedule(replications) + self.seeds(replications) + + self.store() + else: + # We are reading from the file given + if filepath is not None: + self.recall() + if replications != self.replications: + raise ValueError( + f"Found {self.replications} replications in given file, expected {replications}." + ) + else: + # Could read from default file, seems like a bad idea though. + raise ValueError( + "Simulation is set to replicate but no replicid has been given" + ) return ( - general_rc_event_schedule, - general_rc_event_damage, - np_seeds, - random_seeds, + self.general_rc_event_schedule, + self.general_rc_event_damage, + self.np_seed, + self.random_seed, ) + + @staticmethod + def to_filename(filepath): + if len(filepath) >= 10 and filepath[-10:] == ".islestore": + return filepath + else: + return filepath + ".islestore" diff --git a/start.py b/start.py index c4c208a..3097641 100644 --- a/start.py +++ b/start.py @@ -19,7 +19,8 @@ import calibrationscore simulation_parameters = isleconfig.simulation_parameters -replic_ID = None +filepath = None +overwrite = False override_no_riskmodels = False # ensure that logging directory exists @@ -39,6 +40,7 @@ def main( np_seed, random_seed, save_iter, + replic_ID, requested_logs=None, ): np.random.seed(np_seed) @@ -136,9 +138,8 @@ def main( # finish simulation, write logs simulation.finalize() - return simulation.obtain_log( - requested_logs - ) # It is required to return this list to download all the data generated by a single run of the model from the cloud. + # It is required to return this list to download all the data generated by a single run of the model from the cloud. + return simulation.obtain_log(requested_logs) # save function @@ -168,7 +169,9 @@ def save_simulation(t, sim, sim_param, exit_now=False): """ use argparse to handle command line arguments""" parser = argparse.ArgumentParser(description="Model the Insurance sector") - parser.add_argument("--abce", action="store_true", help="[REMOVED] use abce") + parser.add_argument( + "--abce", action="store_true", help="[REMOVED] ABCE no longer supported" + ) parser.add_argument( "--oneriskmodel", action="store_true", @@ -178,18 +181,29 @@ def save_simulation(t, sim, sim_param, exit_now=False): "--riskmodels", type=int, choices=[1, 2, 3, 4], - help="allow overriding the number of riskmodels from standard config (with 1 or other numbers)", + help="allow overriding the number of riskmodels from standard config (with 1 or other numbers)." + " Overrides --oneriskmodel", ) + parser.add_argument("--replicid", type=int, help="[REMOVED], use -f (--file)") parser.add_argument( - "--replicid", - type=int, - help="if replication ID is given, pass this to the simulation so that the risk profile can be restored", + "-f", + "--file", + action="store", + help="the file to store the initial randomness in. Will be stored in ./data and appended with .islestore " + "(if it is not already)", ) parser.add_argument( + "-r", "--replicating", action="store_true", help="if this is a simulation run designed to replicate another, override the config file parameter", ) + parser.add_argument( + "-o", + "--overwrite", + action="store_true", + help="allows overwriting of the file specified by -f", + ) parser.add_argument( "--randomseed", type=float, help="allow setting of numpy random seed" ) @@ -223,13 +237,14 @@ def save_simulation(t, sim, sim_param, exit_now=False): override_no_riskmodels = 1 if args.riskmodels: override_no_riskmodels = args.riskmodels - if args.replicid is not None: # TODO: this is broken, must be fixed or removed - replic_ID = args.replicid + if args.replicid is not None: # TODO: track down all uses of replicid + raise ValueError("--replicid is no longer supported, use --file") + if args.file: + filepath = args.file + if args.overwrite: + overwrite = True if args.replicating: isleconfig.replicating = True - assert ( - replic_ID is not None - ), "Error: Replication requires a replication ID to identify run to be replicated" if args.randomseed: randomseed = args.randomseed seed = int(randomseed) @@ -252,25 +267,28 @@ def save_simulation(t, sim, sim_param, exit_now=False): from setup import SetupSim setup = SetupSim() # Here the setup for the simulation is done. + [ general_rc_event_schedule, general_rc_event_damage, np_seeds, random_seeds, - ] = setup.obtain_ensemble( - 1 - ) # Only one ensemble. This part will only be run locally (laptop). + ] = setup.obtain_ensemble(1, filepath, overwrite) + # Only one ensemble. This part will only be run locally (laptop). # Run the main program + # Note that we pass the filepath as the replic_ID log = main( simulation_parameters, general_rc_event_schedule[0], general_rc_event_damage[0], np_seeds[0], random_seeds[0], + filepath, save_iter, ) + replic_ID = filepath """ Restore the log at the end of the single simulation run for saving and for potential further study """ is_background = (not isleconfig.force_foreground) and ( isleconfig.replicating or (replic_ID in locals()) From 1a01d781e20cdfc2a3466f802ade8d1d48484a5f Mon Sep 17 00:00:00 2001 From: Chris Hughes Date: Fri, 5 Jul 2019 16:52:24 +0100 Subject: [PATCH 010/125] Minor performance improvements --- catbond.py | 2 +- genericagent.py | 8 ----- insurancefirm.py | 2 -- insurancesimulation.py | 21 ++++++------ isleconfig.py | 2 +- metainsurancecontract.py | 58 ++++++++++++++++----------------- metainsuranceorg.py | 69 ++++++++++++++++++---------------------- 7 files changed, 72 insertions(+), 90 deletions(-) delete mode 100644 genericagent.py diff --git a/catbond.py b/catbond.py index 724b9ed..1f65c85 100644 --- a/catbond.py +++ b/catbond.py @@ -70,7 +70,7 @@ def iterate(self, time): if self.operational: self.pay_dividends(time) - # self.estimated_var() # cannot compute VaR for catbond as catbond does not have a riskmodel + # self.estimate_var() # cannot compute VaR for catbond as catbond does not have a riskmodel def set_owner(self, owner): self.owner = owner diff --git a/genericagent.py b/genericagent.py deleted file mode 100644 index 4985372..0000000 --- a/genericagent.py +++ /dev/null @@ -1,8 +0,0 @@ -class GenericAgent: - def __init__(self, *args, **kwargs): - self.init(*args, **kwargs) - - def init(*args, **kwargs): - assert ( - False - ), "Error: GenericAgent init method should have been overridden but was not." diff --git a/insurancefirm.py b/insurancefirm.py index 3e1824c..1b9e50f 100644 --- a/insurancefirm.py +++ b/insurancefirm.py @@ -9,8 +9,6 @@ class InsuranceFirm(MetaInsuranceOrg): """ReinsuranceFirm class. Inherits from MetaInsuranceFirm.""" - # QUERY: now abce is gone can all of these inits become proper __init__s? - # In fact, can we do away with genericagent.py entirely? def __init__(self, simulation_parameters, agent_parameters): """Constructor method. Accepts arguments diff --git a/insurancesimulation.py b/insurancesimulation.py index 65a3aa5..bb388ce 100644 --- a/insurancesimulation.py +++ b/insurancesimulation.py @@ -15,6 +15,8 @@ class InsuranceSimulation: + # Note: if we ever want to have a larger number of categories we should use numpy arrays for all of the + # "by_categ"-type variables and then where possible do numpy operations instead of iterating over them. def __init__( self, override_no_riskmodels, @@ -626,14 +628,15 @@ def receive_obligation(self, amount, recipient, due_time, purpose): self.obligations.append(obligation) def effect_payments(self, time): - due = [item for item in self.obligations if item["due_time"] <= time] - # print("SIMULATION obligations: ", len(self.obligations), " of which are due: ", len(due)) - self.obligations = [ - item for item in self.obligations if item["due_time"] > time - ] - sum_due = sum([item["amount"] for item in due]) - for obligation in due: - self.pay(obligation) + if self.get_operational(): + due = [item for item in self.obligations if item["due_time"] <= time] + # print("SIMULATION obligations: ", len(self.obligations), " of which are due: ", len(due)) + self.obligations = [ + item for item in self.obligations if item["due_time"] > time + ] + # sum_due = sum([item["amount"] for item in due]) + for obligation in due: + self.pay(obligation) def pay(self, obligation): amount = obligation["amount"] @@ -641,7 +644,7 @@ def pay(self, obligation): purpose = obligation["purpose"] if not self.money_supply > amount: warnings.warn("Something wrong: economy out of money", RuntimeWarning) - if self.get_operational() and recipient.get_operational(): + if recipient.get_operational(): self.money_supply -= amount recipient.receive(amount) diff --git a/isleconfig.py b/isleconfig.py index 01a1a91..4ca35c1 100644 --- a/isleconfig.py +++ b/isleconfig.py @@ -26,7 +26,7 @@ "mean_contract_runtime": 12, "contract_runtime_halfspread": 2, "default_contract_payment_period": 3, - "max_time": 200, + "max_time": 1000, "money_supply": 2000000000, "event_time_mean_separation": 100 / 3.0, "expire_immediately": False, # QUERY: What does this mean? diff --git a/metainsurancecontract.py b/metainsurancecontract.py index 0954f07..d79aa52 100644 --- a/metainsurancecontract.py +++ b/metainsurancecontract.py @@ -61,30 +61,20 @@ def __init__( # set deductible from argument, risk property or default value, whichever first is not None default_deductible_fraction = 0.0 - deductible_fraction_generator = ( - item - for item in [ - deductible_fraction, - properties.get("deductible_fraction"), - default_deductible_fraction, - ] - if item is not None - ) - self.deductible_fraction = next(deductible_fraction_generator) + if deductible_fraction is not None: + self.deductible_fraction = deductible_fraction + else: + self.deductible_fraction = properties.get("deductible_fraction", default_deductible_fraction) + self.deductible = self.deductible_fraction * self.value # set excess from argument, risk property or default value, whichever first is not None default_excess_fraction = 1.0 - excess_fraction_generator = ( - item - for item in [ - excess_fraction, - properties.get("excess_fraction"), - default_excess_fraction, - ] - if item is not None - ) - self.excess_fraction = next(excess_fraction_generator) + if excess_fraction is not None: + self.excess_fraction = excess_fraction + else: + self.excess_fraction = properties.get("excess_fraction", default_excess_fraction) + self.excess = self.excess_fraction * self.value self.reinsurance = reinsurance @@ -94,21 +84,27 @@ def __init__( # self.is_reinsurancecontract = False # setup payment schedule - # total_premium = premium * (self.excess - self.deductible) # TODO: excess and deductible should not be considered linearily in premium computation; this should be shifted to the (re)insurer who supplies the premium as argument to the contract's constructor method + # TODO: excess and deductible should not be considered linearily in premium computation; this should be + # shifted to the (re)insurer who supplies the premium as argument to the contract's constructor method + # total_premium = premium * (self.excess - self.deductible) total_premium = premium * self.value self.periodized_premium = total_premium / self.runtime + + # N.B.: payment times and values are in reverse, so the earliest time is at the end! This is because popping + # items off the end of lists is much easier than popping them off the start. self.payment_times = [ - time + i for i in range(runtime) if i % payment_period == 0 + time + i for i in range(runtime-1, -1, -1) if i % payment_period == 0 ] - self.payment_values = total_premium * ( - np.ones(len(self.payment_times)) / len(self.payment_times) - ) + # self.payment_values = total_premium * ( + # np.ones(len(self.payment_times)) / len(self.payment_times) + # ) + self.payment_values = [total_premium/len(self.payment_times)] * len(self.payment_times) ## Create obligation for premium payment # self.property_holder.receive_obligation(premium * (self.excess - self.deductible), self.insurer, time, 'premium') # Embed contract in reinsurance network, if applicable - if self.contract is not None: + if self.contract: self.contract.reinsure( reinsurer=self.insurer, reinsurance_share=properties["reinsurance_share"], @@ -119,16 +115,16 @@ def __init__( self.roll_over_flag = 0 def check_payment_due(self, time): - if len(self.payment_times) > 0 and time >= self.payment_times[0]: + if len(self.payment_times) > 0 and time >= self.payment_times[-1]: # Create obligation for premium payment - # self.property_holder.receive_obligation(premium * (self.excess - self.deductible), self.insurer, time, 'premium') + # value was premium * (self.excess - self.deductible) self.property_holder.receive_obligation( - self.payment_values[0], self.insurer, time, "premium" + self.payment_values[-1], self.insurer, time, "premium" ) # Remove current payment from payment schedule - self.payment_times = self.payment_times[1:] - self.payment_values = self.payment_values[1:] + del self.payment_times[-1] + del self.payment_values[-1] def get_and_reset_current_claim(self): current_claim = self.current_claim diff --git a/metainsuranceorg.py b/metainsuranceorg.py index f201fdf..6ef1704 100644 --- a/metainsuranceorg.py +++ b/metainsuranceorg.py @@ -2,23 +2,24 @@ import numpy as np import scipy.stats import copy +import math from insurancecontract import InsuranceContract from reinsurancecontract import ReinsuranceContract from riskmodel import RiskModel import sys, pdb import uuid -from genericagent import GenericAgent - def get_mean(x): return sum(x) / len(x) def get_mean_std(x): + # At the moment this is always called with a no_category length array + # I have tested the numpy versions of this, they are slower for small arrays but much, much faster for large ones m = get_mean(x) - variance = sum((val - m) ** 2 for val in x) - return m, np.sqrt(variance / len(x)) + std = math.sqrt(sum((val - m) ** 2 for val in x)) / len(x) + return m, std class MetaInsuranceOrg: @@ -180,7 +181,7 @@ def iterate(self, time): self.roll_over(time) - self.estimated_var() + self.estimate_var() def collect_process_evaluate_risks(self, time, contracts_dissolved): if self.operational: @@ -499,7 +500,7 @@ def get_profitslosses(self): def get_underwritten_contracts(self): return self.underwritten_contracts - def estimated_var(self): + def estimate_var(self): self.counter_category = np.zeros(self.simulation_no_risk_categories) self.var_category = np.zeros(self.simulation_no_risk_categories) @@ -510,22 +511,14 @@ def estimated_var(self): if self.operational: for contract in self.underwritten_contracts: - self.counter_category[contract.category] = ( - self.counter_category[contract.category] + 1 - ) - self.var_category[contract.category] = ( - self.var_category[contract.category] + contract.initial_VaR - ) + self.counter_category[contract.category] += 1 + self.var_category[contract.category] += contract.initial_VaR for category in range(len(self.counter_category)): - self.var_counter = ( - self.var_counter - + self.counter_category[category] - * self.riskmodel.inaccuracy[category] - ) - self.var_sum = self.var_sum + self.var_category[category] + self.var_counter += self.counter_category[category] * self.riskmodel.inaccuracy[category] + self.var_sum += self.var_category[category] - if not sum(self.counter_category) == 0: + if sum(self.counter_category) != 0: self.var_counter_per_risk = self.var_counter / sum( self.counter_category ) @@ -572,9 +565,9 @@ def balanced_portfolio(self, risk, cash_left_by_categ, var_per_risk): # This method decides whether the portfolio is balanced enough to accept a new risk or not. # If it is balanced enough return True, otherwise False. # This method also returns the cash available per category independently the risk is accepted or not. - cash_reserved_by_categ = ( - self.cash - cash_left_by_categ - ) # Here it is computed the cash already reserved by category + + # Compute the cash already reserved by category + cash_reserved_by_categ = self.cash - cash_left_by_categ _, std_pre = get_mean_std(cash_reserved_by_categ) @@ -597,27 +590,25 @@ def balanced_portfolio(self, risk, cash_left_by_categ, var_per_risk): # record liquidity requirement and apply margin of safety for liquidity requirement - cash_reserved_by_categ_store[risk["category"]] += ( - expected_claim * self.riskmodel.margin_of_safety - ) # Here it is computed how the cash reserved by category would change if the new reinsurance risk was accepted + # Compute how the cash reserved by category would change if the new reinsurance risk was accepted + cash_reserved_by_categ_store[risk["category"]] += expected_claim * self.riskmodel.margin_of_safety else: - cash_reserved_by_categ_store[risk["category"]] += var_per_risk[ - risk["category"] - ] # Here it is computed how the cash reserved by category would change if the new insurance risk was accepted + # Compute how the cash reserved by category would change if the new insurance risk was accepted + cash_reserved_by_categ_store[risk["category"]] += var_per_risk[risk["category"]] - mean, std_post = get_mean_std( - cash_reserved_by_categ_store - ) # Here it is computed the mean, std of the cash reserved by category after the new risk of reinrisk is accepted + # Compute the mean, std of the cash reserved by category after the new risk of reinrisk is accepted + mean, std_post = get_mean_std(cash_reserved_by_categ_store) total_cash_reserved_by_categ_post = sum(cash_reserved_by_categ_store) if (std_post * total_cash_reserved_by_categ_post / self.cash) <= ( self.balance_ratio * mean - ) or std_post < std_pre: # The new risk is accepted is the standard deviation is reduced or the cash reserved by category is very well balanced. (std_post) <= (self.balance_ratio * mean) - for i in range( - len(cash_left_by_categ) - ): # The balance condition is not taken into account if the cash reserve is far away from the limit. (total_cash_employed_by_categ_post/self.cash <<< 1) + ) or std_post < std_pre: + # The new risk is accepted if the standard deviation is reduced or the cash reserved by category is very + # well balanced. (std_post) <= (self.balance_ratio * mean) + for i in range(len(cash_left_by_categ)): + # The balance condition is not taken into account if the cash reserve is far away from the limit. (total_cash_employed_by_categ_post/self.cash <<< 1) cash_left_by_categ[i] = self.cash - cash_reserved_by_categ_store[i] return True, cash_left_by_categ @@ -662,6 +653,7 @@ def process_newrisks_reinsurer( # TODO: change riskmodel.evaluate() to accept new risk to be evaluated and # to account for existing non-proportional risks correctly -> DONE. if accept: + # TODO: rename this to per_value_premium in insurancecontract.py to avoid confusion per_value_reinsurance_premium = ( self.np_reinsurance_premium_share * risk_to_insure["periodized_total_premium"] @@ -671,12 +663,13 @@ def process_newrisks_reinsurer( / self.simulation.get_market_premium() ) / risk_to_insure["value"] - ) # TODO: rename this to per_value_premium in insurancecontract.py to avoid confusion - [condition, cash_left_by_categ] = self.balanced_portfolio( - risk_to_insure, cash_left_by_categ, None ) # Here it is check whether the portfolio is balanced or not if the reinrisk # (risk_to_insure) is underwritten. Return True if it is balanced. False otherwise. + condition, cash_left_by_categ = self.balanced_portfolio( + risk_to_insure, cash_left_by_categ, None + ) + if condition: contract = ReinsuranceContract( self, From 83f3cc612f323533ae77d1fd2d276da053066969 Mon Sep 17 00:00:00 2001 From: KloskaT Date: Fri, 5 Jul 2019 18:01:20 +0100 Subject: [PATCH 011/125] Added docstring to all functions in metainsuranceorg. Also created function (get_newrisks_by_type) for organising new risks (start to cleaning up iterate function). Removed some imports from start.py. --- insurancesimulation.py | 14 ++- metainsuranceorg.py | 221 +++++++++++++++++++++++++++++++++-------- start.py | 4 - 3 files changed, 189 insertions(+), 50 deletions(-) diff --git a/insurancesimulation.py b/insurancesimulation.py index fb95b94..e7cb4bc 100644 --- a/insurancesimulation.py +++ b/insurancesimulation.py @@ -597,6 +597,12 @@ def get_reinsurance_premium(self, np_reinsurance_deductible_fraction): return self.reinsurance_market_premium * (1. - max_reduction * np_reinsurance_deductible_fraction) def get_cat_bond_price(self, np_reinsurance_deductible_fraction): + """Method to calculate and return catbond price. If catbonds are not desired will return infinity so no catbonds + will be issued. Otherwise calculates based on reinsurance market premium, catbond premium, deductible fraction. + Accepts: + np_reinsurance_deductible_fraction: Type Integer + Returns: + Calculated catbond price.""" # TODO: implement function dependent on total capital in cat bonds and on deductible () # TODO: make max_reduction and max_CB_surcharge into simulation_parameters ? if self.catbonds_off: @@ -802,8 +808,12 @@ def record_claims(self, claims): self.cumulative_claims += claims def log(self): - """Method to log if a background run or not dependant on parameters force_foreground and if the run is - replicating or not.""" + """Method to save the data of the simulation. + No accepted values + No return values + Calls logger instance to save all the data of the simulation to a file, has to return if background run or + not for replicating instances. This depends on parameters force_foreground and if the run is replicating + or not.""" self.logger.save_log(self.background_run) def compute_market_diffvar(self): diff --git a/metainsuranceorg.py b/metainsuranceorg.py index f44c594..211a329 100644 --- a/metainsuranceorg.py +++ b/metainsuranceorg.py @@ -22,6 +22,12 @@ def get_mean_std(x): class MetaInsuranceOrg(GenericAgent): def init(self, simulation_parameters, agent_parameters): + """Constructor method. + Accepts: + Simulation_parameters: Type DataDict + agent_parameters: Type DataDict + Constructor creates general instance of an insurance company which is inherited by the reinsurance and + insurance firm classes. Initialises all necessary values provided by config file.""" self.simulation = simulation_parameters['simulation'] self.simulation_parameters = simulation_parameters self.contract_runtime_dist = scipy.stats.randint(simulation_parameters["mean_contract_runtime"] - \ @@ -100,8 +106,15 @@ def init(self, simulation_parameters, agent_parameters): self.market_permanency_counter = 0 def iterate(self, time): # TODO: split function so that only the sequence of events remains here and everything else is in separate methods - - """obtain investments yield""" + """Method that iterates each firm by one time step. + Accepts: + Time: Type Integer + No return value + For each time step this method obtains every firms interest payments, pays obligations, claim reinsurance, + matures necessary contracts. Check condition for operational firms (as never removed) so only operational + firms receive new risks to evaluate, pay dividends, adjust capacity.""" + + """Obtain interest generated by cash""" self.obtain_yield(time) """realize due payments""" @@ -116,8 +129,8 @@ def iterate(self, time): # TODO: split function so that only the sequence print("Number of underwritten contracts ", len(self.underwritten_contracts)) maturing = [contract for contract in self.underwritten_contracts if contract.expiration <= time] for contract in maturing: - self.underwritten_contracts.remove(contract) - contract.mature(time) + self.underwritten_contracts.remove(contract) + contract.mature(time) contracts_dissolved = len(maturing) """effect payments from contracts""" @@ -126,6 +139,9 @@ def iterate(self, time): # TODO: split function so that only the sequence if self.operational: """request risks to be considered for underwriting in the next period and collect those for this period""" + new_nonproportional_risks, new_risks = self.get_newrisks_by_type(contracts_dissolved) + + """ new_risks = [] if self.is_insurer: new_risks += self.simulation.solicit_insurance_requests(self.id, self.cash, self) @@ -138,6 +154,7 @@ def iterate(self, time): # TODO: split function so that only the sequence new_nonproportional_risks = [risk for risk in new_risks if risk.get("insurancetype")=='excess-of-loss' and risk["owner"] is not self] new_risks = [risk for risk in new_risks if risk.get("insurancetype") in ['proportional', None] and risk["owner"] is not self] + """ """deal with non-proportional risks first as they must evaluate each request separatly, then with proportional ones""" [reinrisks_per_categ, number_reinrisks_categ] = self.risks_reinrisks_organizer(new_nonproportional_risks) #Here the new reinrisks are organized by category. @@ -200,11 +217,11 @@ def iterate(self, time): # TODO: split function so that only the sequence adjust liquidity, borrow or invest pass""" - self.market_permanency(time) + self.market_permanency(time) - self.roll_over(time) + self.roll_over(time) - self.estimated_var() + self.estimated_var() def enter_illiquidity(self, time): """Enter_illiquidity Method. @@ -272,10 +289,24 @@ def dissolve(self, time, record): self.operational = False def receive_obligation(self, amount, recipient, due_time, purpose): + """Method for receiving obligations that the firm will have to pay. + Accepts: + amount: Type integer, how much will be payed + recipient: Type Class instance, who will be payed + due_time: Type Integer, what time value they will be payed + purpose: Type string, why they are being payed + No return value + Adds obligation (Type DataDict) to list of obligations owed by the firm.""" obligation = {"amount": amount, "recipient": recipient, "due_time": due_time, "purpose": purpose} self.obligations.append(obligation) def effect_payments(self, time): + """Method for checking if any payments are due. + Accepts: + time: Type Integer + No return value + Method checks firms list of obligations to see if ay are due for this time, then pays them. If the firm + does not have enough cash then it enters illiquity, leaves the market, and matures all contracts.""" due = [item for item in self.obligations if item["due_time"]<=time] self.obligations = [item for item in self.obligations if item["due_time"]>time] sum_due = sum([item["amount"] for item in due]) @@ -290,6 +321,11 @@ def effect_payments(self, time): self.pay(obligation) def pay(self, obligation): + """Method to pay other class instances. + Accepts: + Obligation: Type DataDict + No return value + Method removes value payed from the agents cash and adds it to recipient agents cash.""" amount = obligation["amount"] recipient = obligation["recipient"] purpose = obligation["purpose"] @@ -300,45 +336,64 @@ def pay(self, obligation): recipient.receive(amount) def receive(self, amount): - """Method to accept cash payments.""" + """Method to accept cash payments. + Accepts: + amount: Type Integer + No return value""" self.cash += amount self.profits_losses += amount def pay_dividends(self, time): + """Method to receive dividend obligation. + Accepts: + time: Type integer + No return value + If firm has positive profits will pay percentage of them as dividends. Currently pays to simulation.""" self.receive_obligation(self.per_period_dividend, self.owner, time, 'dividend') def obtain_yield(self, time): + """Method to calculate interest generated by agents total cash + Accepts: + time: Type integer + No return value""" amount = self.cash * self.interest_rate # TODO: agent should not award her own interest. This interest rate should be taken from self.simulation with a getter method self.simulation.receive_obligation(amount, self, time, 'yields') def get_cash(self): + """Method to return agents cash. Only used to calculate total sum of capital to recalculate market premium + each iteration. + No accepted values. + No return values.""" return self.cash def get_excess_capital(self): + """Method to get agents excess capital. Only used for saving data. Called by simulation. + No Accepted values. + Returns agents excess capital""" return self.excess_capital - def logme(self): - self.log('cash', self.cash) - self.log('underwritten_contracts', self.underwritten_contracts) - self.log('operational', self.operational) - - def len_underwritten_contracts(self): - return len(self.underwritten_contracts) - - def get_operational(self): - return self.operational - def get_profitslosses(self): + """Method to get agents profit or loss. Only used for saving data. Called by simulation. + No Accepted values. + Returns agents profits/losses""" return self.profits_losses - def get_underwritten_contracts(self): - return self.underwritten_contracts + def get_operational(self): + """Method to return boolean of if agent is operational. Only used as check for payments. + No accepted values + Returns Boolean""" + return self.operational def get_pointer(self): + """Method to get pointer. Returns self so renduant? Called only by resume.py""" return self def estimated_var(self): - + """Method to estimate Value at Risk. + No Accepted arguments. + No return values + Calculates value at risk per category and overall, based on underwritten contracts initial value at risk. + Assigns it to agent instance. Called at the end of each agents iteration cycle.""" self.counter_category = np.zeros(self.simulation_no_risk_categories) self.var_category = np.zeros(self.simulation_no_risk_categories) @@ -349,39 +404,79 @@ def estimated_var(self): if self.operational: for contract in self.underwritten_contracts: - self.counter_category[contract.category] = self.counter_category[contract.category] + 1 - self.var_category[contract.category] = self.var_category[contract.category] + contract.initial_VaR + self.counter_category[contract.category] += 1 + self.var_category[contract.category] += contract.initial_VaR for category in range(len(self.counter_category)): - self.var_counter = self.var_counter + self.counter_category[category] * self.riskmodel.inaccuracy[category] - self.var_sum = self.var_sum + self.var_category[category] + self.var_counter += self.counter_category[category] * self.riskmodel.inaccuracy[category] + self.var_sum += + self.var_category[category] if not sum(self.counter_category) == 0: self.var_counter_per_risk = self.var_counter / sum(self.counter_category) else: self.var_counter_per_risk = 0 + def get_newrisks_by_type(self, contracts_dissolved): + """Method for soliciting new risks from insurance simulation then organising them based if non-proportional + or not. + Accepts: + contracts_dissolved: list of contracts dissolved, needed for verbose condition. + Returns: + new_non_proportional_risks: Type list of DataDicts. + new_risks: Type list of DataDicts.""" + new_risks = [] + if self.is_insurer: + new_risks += self.simulation.solicit_insurance_requests(self.id, self.cash, self) + if self.is_reinsurer: + new_risks += self.simulation.solicit_reinsurance_requests(self.id, self.cash, self) + contracts_offered = len(new_risks) + if isleconfig.verbose and contracts_offered < 2 * contracts_dissolved: + print("Something wrong; agent {0:d} receives too few new contracts {1:d} <= {2:d}".format( + self.id, contracts_offered, 2 * contracts_dissolved)) + + new_nonproportional_risks = [risk for risk in new_risks if + risk.get("insurancetype") == 'excess-of-loss' and risk["owner"] is not self] + new_risks = [risk for risk in new_risks if + risk.get("insurancetype") in ['proportional', None] and risk["owner"] is not self] + return new_nonproportional_risks, new_risks + def risks_reinrisks_organizer(self, new_risks): # - """This method organizes the new risks received by the insurer (or reinsurer)""" + """This method organizes the new risks received by the insurer (or reinsurer) by category. + Accepts: + new_risks: Type list of DataDicts + Returns: + risks_per_catgegory: Type list of categories, each contains risks originating from that category. + number_risks_categ: Type list, elements are integers of total risks in each category + """ - risks_per_categ = [[] for x in range(self.simulation_parameters["no_categories"])] #This method organizes the new risks received by the insurer (or reinsurer) by category in the nested list "risks_per_categ". - number_risks_categ = [[] for x in range(self.simulation_parameters["no_categories"])] #This method also counts the new risks received by the insurer (or reinsurer) by category in the list "number_risks_categ". + risks_per_categ = [[] for x in range(self.simulation_parameters["no_categories"])] + number_risks_categ = [[] for x in range(self.simulation_parameters["no_categories"])] for categ_id in range(self.simulation_parameters["no_categories"]): risks_per_categ[categ_id] = [risk for risk in new_risks if risk["category"] == categ_id] number_risks_categ[categ_id] = len(risks_per_categ[categ_id]) - return risks_per_categ, number_risks_categ #The method returns both risks_per_categ and risks_per_categ. + return risks_per_categ, number_risks_categ + + def balanced_portfolio(self, risk, cash_left_by_categ, var_per_risk): + """This method decides whether the portfolio is balanced enough to accept a new risk or not. If it is balanced + enough return True otherwise False. This method also returns the cash available per category independently + the risk is accepted or not. + Accepts: + risk: Type DataDict + cash_left_by_category: Type List, contains list of available cash per category + var_per_risk: Type list of integers contains VaR for each category defined in getPPF from riskmodel.py + Returns: + Boolean + cash_left_by_categ: Type list of integers""" - def balanced_portfolio(self, risk, cash_left_by_categ, var_per_risk): #This method decides whether the portfolio is balanced enough to accept a new risk or not. If it is balanced enough return True otherwise False. - #This method also returns the cash available per category independently the risk is accepted or not. - cash_reserved_by_categ = self.cash - cash_left_by_categ #Here it is computed the cash already reserved by category + cash_reserved_by_categ = self.cash - cash_left_by_categ # Calculates the cash already reserved by category _, std_pre = get_mean_std(cash_reserved_by_categ) cash_reserved_by_categ_store = np.copy(cash_reserved_by_categ) - if risk.get("insurancetype")=='excess-of-loss': + if risk.get("insurancetype") == 'excess-of-loss': percentage_value_at_risk = self.riskmodel.getPPF(categ_id=risk["category"], tailSize=self.riskmodel.var_tail_prob) expected_damage = percentage_value_at_risk * risk["value"] * risk["risk_factor"] \ * self.riskmodel.inaccuracy[risk["category"]] @@ -410,8 +505,17 @@ def balanced_portfolio(self, risk, cash_left_by_categ, var_per_risk): #This meth return False, cash_left_by_categ def process_newrisks_reinsurer(self, reinrisks_per_categ, number_reinrisks_categ, time): - """This method processes one by one the reinrisks contained in reinrisks_per_categ in order to decide whether they should be underwritten or not. - It is done in this way to maintain the portfolio as balanced as possible. For that reason we process risk[C1], risk[C2], risk[C3], risk[C4], risk[C1], risk[C2], ... and so forth.""" + """Method to decide if new risks are underwritten for the reinsurance firm. + Accepts: + reinrisks_per_categ: Type List of lists containing new reinsurance risks. + number_reinrisks_per_categ: Type List of integers, contains number of new reinsurance risks per category. + time: Type integer + No return values. + This method processes one by one the reinrisks contained in reinrisks_per_categ in order to decide whether + they should be underwritten or not. It is done in this way to maintain the portfolio as balanced as possible. + For that reason we process risk[C1], risk[C2], risk[C3], risk[C4], risk[C1], risk[C2], ... and so forth. If + risks are accepted then a contract is written.""" + for iterion in range(max(number_reinrisks_categ)): for categ_id in range(self.simulation_parameters["no_categories"]): #Here we take only one risk per category at a time to achieve risk[C1], risk[C2], risk[C3], risk[C4], risk[C1], risk[C2], ... if possible. if iterion < number_reinrisks_categ[categ_id] and reinrisks_per_categ[categ_id][iterion] is not None: @@ -453,16 +557,31 @@ def process_newrisks_reinsurer(self, reinrisks_per_categ, number_reinrisks_categ return reinrisks_per_categ, not_accepted_reinrisks - def process_newrisks_insurer(self, risks_per_categ, number_risks_categ, acceptable_by_category, var_per_risk_per_categ, cash_left_by_categ, time): #This method processes one by one the risks contained in risks_per_categ in order to decide whether they should be underwritten or not. - #It is done in this way to maintain the portfolio as balanced as possible. For that reason we process risk[C1], risk[C2], risk[C3], risk[C4], risk[C1], risk[C2], ... and so forth. + def process_newrisks_insurer(self, risks_per_categ, number_risks_categ, acceptable_by_category, var_per_risk_per_categ, cash_left_by_categ, time): + """Method to decide if new risks are underwritten for the insurance firm. + Accepts: + risks_per_categ: Type List of lists containing new risks. + number_risks_categ: Type List of integers, contains number of new risks per category. + acceptable_per_category: + var_per_risk_per_categ: Type list of integers contains VaR for each category defined in getPPF. + cash_left_by_categ: Type List, contains list of available cash per category + time: Type integer. + Returns: + risks_per_categ: Type list of list, same as above however with None where contracts were accepted. + not_accepted_risks: Type List of DataDicts + This method processes one by one the reinrisks contained in reinrisks_per_categ in order to decide whether + they should be underwritten or not. It is done in this way to maintain the portfolio as balanced as possible. + For that reason we process risk[C1], risk[C2], risk[C3], risk[C4], risk[C1], risk[C2], ... and so forth. If + risks are accepted then a contract is written.""" + _cached_rvs = self.contract_runtime_dist.rvs() for iter in range(max(number_risks_categ)): - for categ_id in range(len(acceptable_by_category)): #Here we take only one risk per category at a time to achieve risk[C1], risk[C2], risk[C3], risk[C4], risk[C1], risk[C2], ... if possible. + for categ_id in range(len(acceptable_by_category)): if iter < number_risks_categ[categ_id] and acceptable_by_category[categ_id] > 0 and \ risks_per_categ[categ_id][iter] is not None: risk_to_insure = risks_per_categ[categ_id][iter] if risk_to_insure.get("contract") is not None and risk_to_insure[ - "contract"].expiration > time: # risk_to_insure["contract"]: # required to rule out contracts that have exploded in the meantime + "contract"].expiration > time: # required to rule out contracts that have exploded in the meantime [condition, cash_left_by_categ] = self.balanced_portfolio(risk_to_insure, cash_left_by_categ, None) #Here it is check whether the portfolio is balanced or not if the reinrisk (risk_to_insure) is underwritten. Return True if it is balanced. False otherwise. if condition: contract = ReinsuranceContract(self, risk_to_insure, time, \ @@ -499,8 +618,15 @@ def process_newrisks_insurer(self, risks_per_categ, number_risks_categ, acceptab return risks_per_categ, not_accepted_risks - def market_permanency(self, time): #This method determines whether an insurer or reinsurer stays in the market. If it has very few risks underwritten or too much cash left for TOO LONG it eventually leaves the market. - # If it has very few risks underwritten it cannot balance the portfolio so it makes sense to leave the market. + def market_permanency(self, time): + """Method determining if firm stays in market. + Accepts: + Time: Type Integer + No return values. + This method determines whether an insurer or reinsurer stays in the market. If it has very few risks + underwritten or too much cash left for TOO LONG it eventually leaves the market. If it has very few risks + underwritten it cannot balance the portfolio so it makes sense to leave the market."""# + if not self.simulation_parameters["market_permanency_off"]: cash_left_by_categ = np.asarray(self.cash_left_by_categ) @@ -533,14 +659,21 @@ def market_permanency(self, time): #This method determines whether an insure if self.market_permanency_counter >= self.simulation_parameters["reinsurance_permanency_time_constraint"]: # Here we determine how much is too long. self.market_exit(time) - def register_claim(self, claim): #This method records in insurancesimulation.py every claim made. It is called either from insurancecontract.py or reinsurancecontract.py respectively. + def register_claim(self, claim): + """Method to register claims. + Accepts: + claim: Type Integer, value of claim. + No return values. + This method records in insurancesimulation.py every claim made. It is called either from insurancecontract.py + or reinsurancecontract.py respectively.""" self.simulation.record_claims(claim) def reset_pl(self): """Reset_pl Method. Accepts no arguments: No return value. - Reset the profits and losses variable of each firm at the beginning of every iteration. It has to be run in insurancesimulation.py at the beginning of the iterate method""" + Reset the profits and losses variable of each firm at the beginning of every iteration. It has to be run in + insurancesimulation.py at the beginning of the iterate method""" self.profits_losses = 0 def roll_over(self, time): diff --git a/start.py b/start.py index 5c82008..c5b2925 100644 --- a/start.py +++ b/start.py @@ -1,14 +1,10 @@ """import common packages and necessary classes""" import numpy as np -import scipy.stats -import math -import sys, pdb import argparse import os import pickle import hashlib import random -import copy import importlib import isleconfig from insurancesimulation import InsuranceSimulation From 829c001bf58a9351006650ff8961d4aa19039cdc Mon Sep 17 00:00:00 2001 From: Chris Hughes Date: Mon, 8 Jul 2019 10:09:36 +0100 Subject: [PATCH 012/125] More minor performance improvements --- distributiontruncated.py | 7 +++-- insurancefirm.py | 2 +- isleconfig.py | 2 +- metainsurancecontract.py | 3 +- metainsuranceorg.py | 62 +++++++++++++++++++++------------------- riskmodel.py | 37 +++++++----------------- 6 files changed, 52 insertions(+), 61 deletions(-) diff --git a/distributiontruncated.py b/distributiontruncated.py index 7ab297a..3688234 100644 --- a/distributiontruncated.py +++ b/distributiontruncated.py @@ -6,7 +6,7 @@ class TruncatedDistWrapper: - def __init__(self, dist, lower_bound=0, upper_bound=1): + def __init__(self, dist, lower_bound=0., upper_bound=1.): self.dist = dist self.normalizing_factor = dist.cdf(upper_bound) - dist.cdf(lower_bound) self.lower_bound = lower_bound @@ -61,9 +61,12 @@ def rvs(self, size=1): sample = np.append(sample, self.rvs(size - len(sample))) return sample[:size] + # Cache could be replaced with a simple if is None cache, might offer a small performance gain. + # Also this could be a read-only @property, but then again so could a lot of things. + @functools.lru_cache(maxsize=1) def mean(self): mean_estimate, mean_error = scipy.integrate.quad( - lambda Y: Y * self.pdf(Y), self.lower_bound, self.upper_bound + lambda x: x * self.pdf(x), self.lower_bound, self.upper_bound ) return mean_estimate diff --git a/insurancefirm.py b/insurancefirm.py index 1b9e50f..9ee3949 100644 --- a/insurancefirm.py +++ b/insurancefirm.py @@ -350,7 +350,7 @@ def make_reinsurance_claims(self, time): categ_id, claims, is_proportional = contract.get_and_reset_current_claim() if is_proportional: claims_this_turn[categ_id] += claims - if contract.reincontract is not None: + if contract.reincontract: contract.reincontract.explode(time, claims) for categ_id in range(self.simulation_no_risk_categories): diff --git a/isleconfig.py b/isleconfig.py index 4ca35c1..b835ca1 100644 --- a/isleconfig.py +++ b/isleconfig.py @@ -26,7 +26,7 @@ "mean_contract_runtime": 12, "contract_runtime_halfspread": 2, "default_contract_payment_period": 3, - "max_time": 1000, + "max_time": 500, "money_supply": 2000000000, "event_time_mean_separation": 100 / 3.0, "expire_immediately": False, # QUERY: What does this mean? diff --git a/metainsurancecontract.py b/metainsurancecontract.py index d79aa52..a914d2e 100644 --- a/metainsurancecontract.py +++ b/metainsurancecontract.py @@ -58,8 +58,7 @@ def __init__( self.terminating = False self.current_claim = 0 self.initial_VaR = initial_var - - # set deductible from argument, risk property or default value, whichever first is not None + # set deductible from argument, risk property or default value, whichever first is not None default_deductible_fraction = 0.0 if deductible_fraction is not None: self.deductible_fraction = deductible_fraction diff --git a/metainsuranceorg.py b/metainsuranceorg.py index 6ef1704..e504645 100644 --- a/metainsuranceorg.py +++ b/metainsuranceorg.py @@ -10,6 +10,7 @@ import uuid +# Can't use caching, as arrays are mutable and thus not hashable def get_mean(x): return sum(x) / len(x) @@ -17,6 +18,8 @@ def get_mean(x): def get_mean_std(x): # At the moment this is always called with a no_category length array # I have tested the numpy versions of this, they are slower for small arrays but much, much faster for large ones + # If we ever let no_category be much larger, might want to use np for this bit + print(x) m = get_mean(x) std = math.sqrt(sum((val - m) ** 2 for val in x)) / len(x) return m, std @@ -140,10 +143,8 @@ def __init__(self, simulation_parameters, agent_parameters): self.reinrisks_kept = [] self.balance_ratio = simulation_parameters["insurers_balance_ratio"] self.recursion_limit = simulation_parameters["insurers_recursion_limit"] - # TODO: Check that this shouldn't have to sum to self.cash - self.cash_left_by_categ = [ - self.cash for i in range(self.simulation_parameters["no_categories"]) - ] + # QUERY: Should this have to sum to self.cash + self.cash_left_by_categ = self.cash * np.ones(self.simulation_parameters["no_categories"]) self.market_permanency_counter = 0 def iterate(self, time): @@ -880,33 +881,36 @@ def roll_over(self, time): for contract in self.underwritten_contracts if contract.expiration == time + 1 ] - - if self.is_insurer is True: - for contract in maturing_next: - contract.roll_over_flag = 1 - if ( - np.random.uniform(0, 1, 1) - > self.simulation_parameters["insurance_retention"] - ): - self.simulation.return_risks( - [contract.risk_data] - ) # TODO: This is not a retention, so the roll_over_flag might be confusing in this case - else: - self.risks_kept.append(contract.risk_data) - - if self.is_reinsurer is True: - for reincontract in maturing_next: - if reincontract.property_holder.operational: - reincontract.roll_over_flag = 1 - reinrisk = reincontract.property_holder.create_reinrisk( - time, reincontract.category - ) + # QUERY: Is it true to say that no firm underwrites both insurance and reinsurance? + # Generate all the rvs at the start + if maturing_next: + uniform_rvs = np.nditer(np.random.uniform(size=len(maturing_next))) + if self.is_insurer is True: + for contract in maturing_next: + contract.roll_over_flag = 1 if ( - np.random.uniform(0, 1, 1) - < self.simulation_parameters["reinsurance_retention"] + next(uniform_rvs) + > self.simulation_parameters["insurance_retention"] ): - if reinrisk is not None: - self.reinrisks_kept.append(reinrisk) + self.simulation.return_risks( + [contract.risk_data] + ) # TODO: This is not a retention, so the roll_over_flag might be confusing in this case + else: + self.risks_kept.append(contract.risk_data) + + if self.is_reinsurer is True: + for reincontract in maturing_next: + if reincontract.property_holder.operational: + reincontract.roll_over_flag = 1 + reinrisk = reincontract.property_holder.create_reinrisk( + time, reincontract.category + ) + if ( + next(uniform_rvs) + < self.simulation_parameters["reinsurance_retention"] + ): + if reinrisk is not None: + self.reinrisks_kept.append(reinrisk) def make_reinsurance_claims(self, time): raise NotImplementedError( diff --git a/riskmodel.py b/riskmodel.py index 6ac799f..00ba213 100644 --- a/riskmodel.py +++ b/riskmodel.py @@ -50,39 +50,24 @@ def getPPF(self, categ_id, tail_size): @staticmethod def get_categ_risks(risks, categ_id): - # categ_risks2 = [risk for risk in risks if risk["category"]==categ_id] - categ_risks = [] - for risk in risks: - if risk["category"] == categ_id: - categ_risks.append(risk) - # assert categ_risks == categ_risks2 + # List comprehension is faster + categ_risks = [risk for risk in risks if risk["category"] == categ_id] return categ_risks def compute_expectation(self, categ_risks, categ_id): # TODO: more intuitive name? - # average_exposure2 = np.mean([risk["excess"]-risk["deductible"] for risk in categ_risks]) - # - # average_risk_factor = np.mean([risk["risk_factor"] for risk in categ_risks]) - # average_risk_factor2 = self.inaccuracy[categ_id] * np.mean([risk["risk_factor"] for risk in categ_risks]) - # - # """compute expected profits from category""" - # mean_runtime2 = np.mean([risk["runtime"] for risk in categ_risks]) - - exposures = [] - risk_factors = [] - runtimes = [] - for risk in categ_risks: + + exposures = np.zeros(len(categ_risks)) + risk_factors = np.zeros(len(categ_risks)) + runtimes = np.zeros(len(categ_risks)) + for i, risk in enumerate(categ_risks): # TODO: factor in excess instead of value? - exposures.append(risk["value"] - risk["deductible"]) - risk_factors.append(risk["risk_factor"]) - runtimes.append(risk["runtime"]) + exposures[i] = risk["value"] - risk["deductible"] + risk_factors[i] = risk["risk_factor"] + runtimes[i] = risk["runtime"] average_exposure = np.mean(exposures) average_risk_factor = self.inaccuracy[categ_id] * np.mean(risk_factors) - mean_runtime = np.mean(runtimes) - - # assert average_exposure == average_exposure2 - # assert average_risk_factor == average_risk_factor2 - # assert mean_runtime == mean_runtime2 + # mean_runtime = np.mean(runtimes) if self.expire_immediately: incr_expected_profits = -1 From facfeba64518ed1e08de0b7aba38292fb9d777d4 Mon Sep 17 00:00:00 2001 From: Chris Hughes Date: Mon, 8 Jul 2019 14:22:45 +0100 Subject: [PATCH 013/125] Made the get_mean_std function take a tuple as an argument to enable caching (since tuples are hashable). Tested, lead to a small performance increase. Also did misc cleanup etc. --- distributionreinsurance.py | 7 +++++- distributiontruncated.py | 10 ++++---- insurancesimulation.py | 7 ++---- isleconfig.py | 21 ++++++++++------ metainsurancecontract.py | 16 ++++++++---- metainsuranceorg.py | 51 ++++++++++++++++++++++++-------------- reinsurancecontract.py | 45 +++++++++++++++++---------------- riskmodel.py | 25 +++++++++++-------- 8 files changed, 108 insertions(+), 74 deletions(-) diff --git a/distributionreinsurance.py b/distributionreinsurance.py index 8d9dcb2..beeff7f 100644 --- a/distributionreinsurance.py +++ b/distributionreinsurance.py @@ -3,9 +3,11 @@ from math import ceil import scipy import pdb +import functools class ReinsuranceDistWrapper: + # QUERY: Is this the distribution of the risk when excess of loss reinsurance is applied? def __init__(self, dist, lower_bound=None, upper_bound=None): assert lower_bound is not None or upper_bound is not None self.dist = dist @@ -18,6 +20,7 @@ def __init__(self, dist, lower_bound=None, upper_bound=None): assert self.upper_bound > self.lower_bound self.redistributed_share = dist.cdf(upper_bound) - dist.cdf(lower_bound) + @functools.lru_cache(maxsize=512) def pdf(self, x): x = np.array(x, ndmin=1) r = map( @@ -33,6 +36,7 @@ def pdf(self, x): r = float(r) return r + @functools.lru_cache(maxsize=512) def cdf(self, x): x = np.array(x, ndmin=1) r = map( @@ -46,6 +50,7 @@ def cdf(self, x): r = float(r) return r + @functools.lru_cache(maxsize=512) def ppf(self, x): x = np.array(x, ndmin=1) assert (x >= 0).all() and (x <= 1).all() @@ -83,7 +88,7 @@ def rvs(self, size=1): lower_bound=0.9, upper_bound=1.1, dist=non_truncated ) - x = np.linspace(non_truncated.ppf(0.01), non_truncated.ppf(0.99), 100) + x1 = np.linspace(non_truncated.ppf(0.01), non_truncated.ppf(0.99), 100) x2 = np.linspace(truncated.ppf(0.01), truncated.ppf(0.99), 100) # pdb.set_trace() diff --git a/distributiontruncated.py b/distributiontruncated.py index 3688234..28c030f 100644 --- a/distributiontruncated.py +++ b/distributiontruncated.py @@ -6,7 +6,7 @@ class TruncatedDistWrapper: - def __init__(self, dist, lower_bound=0., upper_bound=1.): + def __init__(self, dist, lower_bound=0.0, upper_bound=1.0): self.dist = dist self.normalizing_factor = dist.cdf(upper_bound) - dist.cdf(lower_bound) self.lower_bound = lower_bound @@ -53,15 +53,15 @@ def ppf(self, x): ) def rvs(self, size=1): + # Sample RVs from the original distribution and then throw out the ones that are outside the bounds. init_sample_size = int(ceil(size / self.normalizing_factor * 1.1)) sample = self.dist.rvs(size=init_sample_size) - sample = sample[sample >= self.lower_bound] - sample = sample[sample <= self.upper_bound] + sample = np.logical_and(self.lower_bound <= sample, sample <= self.upper_bound) while len(sample) < size: sample = np.append(sample, self.rvs(size - len(sample))) return sample[:size] - # Cache could be replaced with a simple if is None cache, might offer a small performance gain. + # Cache could be replaced with a simple "if is None" cache, might offer a small performance gain. # Also this could be a read-only @property, but then again so could a lot of things. @functools.lru_cache(maxsize=1) def mean(self): @@ -77,7 +77,7 @@ def mean(self): lower_bound=0.55, upper_bound=1.0, dist=non_truncated ) - x = np.linspace(non_truncated.ppf(0.01), non_truncated.ppf(0.99), 100) + x1 = np.linspace(non_truncated.ppf(0.01), non_truncated.ppf(0.99), 100) x2 = np.linspace(truncated.ppf(0.01), truncated.ppf(0.99), 100) print(truncated.mean()) diff --git a/insurancesimulation.py b/insurancesimulation.py index bb388ce..8ee1d9a 100644 --- a/insurancesimulation.py +++ b/insurancesimulation.py @@ -450,11 +450,8 @@ def iterate(self, t): self.simulation_parameters["no_categories"] ) - for ( - insurer - ) in ( - self.insurancefirms - ): # TODO: this and the next look like they could be cleaner + # TODO: this and the next look like they could be cleaner + for insurer in self.insurancefirms: if insurer.operational: for i in range(len(self.inaccuracy)): if insurer.riskmodel.inaccuracy == self.inaccuracy[i]: diff --git a/isleconfig.py b/isleconfig.py index b835ca1..785bbfb 100644 --- a/isleconfig.py +++ b/isleconfig.py @@ -17,7 +17,8 @@ "riskmodel_margin_of_safety": 2, # values >=1; factor of additional liquidity beyond value at risk "margin_increase": 0, - # This parameter modifies the margin of safety depending on the number of risks models available in the market. When is 0 all risk models have the same margin of safety. + # "margin_increase" modifies the margin of safety depending on the number of risks models available in the market. + # When it is 0 all risk models have the same margin of safety. "value_at_risk_tail_probability": 0.005, # values <1, >0, usually close to 0; tail probability at which the value at risk is taken by the risk models "norm_profit_markup": 0.15, @@ -26,10 +27,10 @@ "mean_contract_runtime": 12, "contract_runtime_halfspread": 2, "default_contract_payment_period": 3, - "max_time": 500, + "max_time": 1000, "money_supply": 2000000000, "event_time_mean_separation": 100 / 3.0, - "expire_immediately": False, # QUERY: What does this mean? + "expire_immediately": False, "risk_factors_present": False, "risk_factor_lower_bound": 0.4, "risk_factor_upper_bound": 0.6, @@ -53,14 +54,18 @@ "reinsurance_retention": 1, # Ratio of reinsurance contracts retained every iteration. # Premium sensitivity parameters "premium_sensitivity": 5, - # This parameter represents how sensitive is the variation of the insurance premium with respect of the capital of the market. Higher means more sensitive. + # This parameter represents how sensitive is the variation of the insurance premium with respect of the capital + # of the market. Higher means more sensitive. "reinpremium_sensitivity": 6, - # This parameter represents how sensitive is the variation of the reinsurance premium with respect of the capital of the market. Higher means more sensitive. + # This parameter represents how sensitive is the variation of the reinsurance premium with respect of the capital + # of the market. Higher means more sensitive. # Balanced portfolio parameters "insurers_balance_ratio": 0.1, - # This ratio represents how low we want to keep the standard deviation of the cash reserved below the mean for insurers. Lower means more balanced. + # This ratio represents how low we want to keep the standard deviation of the cash reserved below the mean for + # insurers. Lower means more balanced. "reinsurers_balance_ratio": 20, - # This ratio represents how low we want to keep the standard deviation of the cash reserved below the mean for reinsurers. Lower means more balanced. (Deactivated for the moment) + # This ratio represents how low we want to keep the standard deviation of the cash reserved below the mean for + # reinsurers. Lower means more balanced. (Deactivated for the moment) "insurers_recursion_limit": 50, # Intensity of the recursion algorithm to balance the portfolio of risks for insurers. "reinsurers_recursion_limit": 10, @@ -79,7 +84,7 @@ "reinsurance_permanency_contracts_limit": 2, # If reinsurers stay for too long under this limit of contracts they deccide to leave the market. "reinsurance_permanency_ratio_limit": 0.8, - # If reinsurers stay for too long under this limit they deccide to leave the market because they have too much capital. + # If reinsurers stay for too long under this limit they decide to leave the market because they have too much capital. "reinsurance_permanency_time_constraint": 48, # This parameter defines the period that the reinsurers wait if they have few capital or few contract before leaving the market. # Insurance and Reinsurance deductibles diff --git a/metainsurancecontract.py b/metainsurancecontract.py index a914d2e..68cb984 100644 --- a/metainsurancecontract.py +++ b/metainsurancecontract.py @@ -58,12 +58,14 @@ def __init__( self.terminating = False self.current_claim = 0 self.initial_VaR = initial_var - # set deductible from argument, risk property or default value, whichever first is not None + # set deductible from argument, risk property or default value, whichever first is not None default_deductible_fraction = 0.0 if deductible_fraction is not None: self.deductible_fraction = deductible_fraction else: - self.deductible_fraction = properties.get("deductible_fraction", default_deductible_fraction) + self.deductible_fraction = properties.get( + "deductible_fraction", default_deductible_fraction + ) self.deductible = self.deductible_fraction * self.value @@ -72,7 +74,9 @@ def __init__( if excess_fraction is not None: self.excess_fraction = excess_fraction else: - self.excess_fraction = properties.get("excess_fraction", default_excess_fraction) + self.excess_fraction = properties.get( + "excess_fraction", default_excess_fraction + ) self.excess = self.excess_fraction * self.value @@ -92,12 +96,14 @@ def __init__( # N.B.: payment times and values are in reverse, so the earliest time is at the end! This is because popping # items off the end of lists is much easier than popping them off the start. self.payment_times = [ - time + i for i in range(runtime-1, -1, -1) if i % payment_period == 0 + time + i for i in range(runtime - 1, -1, -1) if i % payment_period == 0 ] # self.payment_values = total_premium * ( # np.ones(len(self.payment_times)) / len(self.payment_times) # ) - self.payment_values = [total_premium/len(self.payment_times)] * len(self.payment_times) + self.payment_values = [total_premium / len(self.payment_times)] * len( + self.payment_times + ) ## Create obligation for premium payment # self.property_holder.receive_obligation(premium * (self.excess - self.deductible), self.insurer, time, 'premium') diff --git a/metainsuranceorg.py b/metainsuranceorg.py index e504645..178d562 100644 --- a/metainsuranceorg.py +++ b/metainsuranceorg.py @@ -8,18 +8,19 @@ from riskmodel import RiskModel import sys, pdb import uuid +import functools -# Can't use caching, as arrays are mutable and thus not hashable def get_mean(x): return sum(x) / len(x) +# A quick check tells me that we don't need a very large cache for this, as it only tends to repeat a couple of times. +@functools.lru_cache(maxsize=16) def get_mean_std(x): # At the moment this is always called with a no_category length array # I have tested the numpy versions of this, they are slower for small arrays but much, much faster for large ones # If we ever let no_category be much larger, might want to use np for this bit - print(x) m = get_mean(x) std = math.sqrt(sum((val - m) ** 2 for val in x)) / len(x) return m, std @@ -144,7 +145,9 @@ def __init__(self, simulation_parameters, agent_parameters): self.balance_ratio = simulation_parameters["insurers_balance_ratio"] self.recursion_limit = simulation_parameters["insurers_recursion_limit"] # QUERY: Should this have to sum to self.cash - self.cash_left_by_categ = self.cash * np.ones(self.simulation_parameters["no_categories"]) + self.cash_left_by_categ = self.cash * np.ones( + self.simulation_parameters["no_categories"] + ) self.market_permanency_counter = 0 def iterate(self, time): @@ -516,7 +519,10 @@ def estimate_var(self): self.var_category[contract.category] += contract.initial_VaR for category in range(len(self.counter_category)): - self.var_counter += self.counter_category[category] * self.riskmodel.inaccuracy[category] + self.var_counter += ( + self.counter_category[category] + * self.riskmodel.inaccuracy[category] + ) self.var_sum += self.var_category[category] if sum(self.counter_category) != 0: @@ -570,12 +576,12 @@ def balanced_portfolio(self, risk, cash_left_by_categ, var_per_risk): # Compute the cash already reserved by category cash_reserved_by_categ = self.cash - cash_left_by_categ - _, std_pre = get_mean_std(cash_reserved_by_categ) + _, std_pre = get_mean_std(tuple(cash_reserved_by_categ)) cash_reserved_by_categ_store = np.copy(cash_reserved_by_categ) if risk.get("insurancetype") == "excess-of-loss": - percentage_value_at_risk = self.riskmodel.getPPF( + percentage_value_at_risk = self.riskmodel.get_ppf( categ_id=risk["category"], tail_size=self.riskmodel.var_tail_prob ) expected_damage = ( @@ -592,14 +598,18 @@ def balanced_portfolio(self, risk, cash_left_by_categ, var_per_risk): # record liquidity requirement and apply margin of safety for liquidity requirement # Compute how the cash reserved by category would change if the new reinsurance risk was accepted - cash_reserved_by_categ_store[risk["category"]] += expected_claim * self.riskmodel.margin_of_safety + cash_reserved_by_categ_store[risk["category"]] += ( + expected_claim * self.riskmodel.margin_of_safety + ) else: # Compute how the cash reserved by category would change if the new insurance risk was accepted - cash_reserved_by_categ_store[risk["category"]] += var_per_risk[risk["category"]] + cash_reserved_by_categ_store[risk["category"]] += var_per_risk[ + risk["category"] + ] # Compute the mean, std of the cash reserved by category after the new risk of reinrisk is accepted - mean, std_post = get_mean_std(cash_reserved_by_categ_store) + mean, std_post = get_mean_std(tuple(cash_reserved_by_categ_store)) total_cash_reserved_by_categ_post = sum(cash_reserved_by_categ_store) @@ -710,25 +720,28 @@ def process_newrisks_insurer( # be underwritten or not. It is done in this way to maintain the portfolio as balanced as possible. For that # reason we process risk[C1], risk[C2], risk[C3], risk[C4], risk[C1], risk[C2], ... and so forth. _cached_rvs = self.contract_runtime_dist.rvs() - for iter in range(max(number_risks_categ)): + for risk_index in range(max(number_risks_categ)): for categ_id in range(len(acceptable_by_category)): # Here we take only one risk per category at a time to achieve risk[C1], risk[C2], risk[C3], # risk[C4], risk[C1], risk[C2], ... if possible. + + # First check that we are actually going to hit a risk if ( - iter < number_risks_categ[categ_id] + risk_index < number_risks_categ[categ_id] and acceptable_by_category[categ_id] > 0 - and risks_per_categ[categ_id][iter] is not None + and risks_per_categ[categ_id][risk_index] is not None ): - risk_to_insure = risks_per_categ[categ_id][iter] + risk_to_insure = risks_per_categ[categ_id][risk_index] if ( - risk_to_insure.get("contract") is not None + "contract" in risk_to_insure and risk_to_insure["contract"].expiration > time ): - # risk_to_insure["contract"]: # required to rule out contracts that have exploded in the meantime + # In this case the risk being inspected already has a contract, so we are deciding whether to + # give reinsurance for it # QUERY: is this correct? [condition, cash_left_by_categ] = self.balanced_portfolio( risk_to_insure, cash_left_by_categ, None ) - # Here it is check whether the portfolio is balanced or not if the reinrisk (risk_to_insure) is + # Here we check whether the portfolio is balanced or not if the reinrisk (risk_to_insure) is # underwritten. Return True if it is balanced. False otherwise. if condition: contract = ReinsuranceContract( @@ -744,13 +757,15 @@ def process_newrisks_insurer( ) self.underwritten_contracts.append(contract) self.cash_left_by_categ = cash_left_by_categ - risks_per_categ[categ_id][iter] = None + risks_per_categ[categ_id][risk_index] = None # TODO: move this to insurancecontract (ca. line 14) -> DONE # TODO: do not write into other object's properties, use setter -> DONE else: [condition, cash_left_by_categ] = self.balanced_portfolio( risk_to_insure, cash_left_by_categ, var_per_risk_per_categ ) + # In this case there is no contact currently associated with the risk, so we decide whether + # to insure it # Here it is check whether the portfolio is balanced or not if the risk (risk_to_insure) is # underwritten. Return True if it is balanced. False otherwise. if condition: @@ -768,7 +783,7 @@ def process_newrisks_insurer( ) self.underwritten_contracts.append(contract) self.cash_left_by_categ = cash_left_by_categ - risks_per_categ[categ_id][iter] = None + risks_per_categ[categ_id][risk_index] = None acceptable_by_category[categ_id] -= 1 # TODO: allow different values per risk (i.e. sum over value (and reinsurance_share) or # exposure instead of counting) diff --git a/reinsurancecontract.py b/reinsurancecontract.py index 8bb90c1..8060477 100644 --- a/reinsurancecontract.py +++ b/reinsurancecontract.py @@ -65,32 +65,35 @@ def explode(self, time, damage_extent=None): Method marks the contract for termination. """ - if self.insurancetype == "excess-of-loss" and damage_extent > self.deductible: - claim = min(self.excess, damage_extent) - self.deductible - self.insurer.receive_obligation(claim, self.property_holder, time, "claim") - else: # TODO: should this be elif == "proportional"? - claim = min(self.excess, damage_extent) - self.deductible - self.insurer.receive_obligation( - claim, self.property_holder, time + 1, "claim" - ) - # Reinsurer pays as soon as possible. - - self.insurer.register_claim( - claim - ) # Every reinsurance claim made is immediately registered. - if self.expire_immediately: - self.current_claim += ( - self.contract.claim - ) # TODO: should proportional reinsurance claims be subject to excess_of_loss retrocession? - # If so, reorganize more straightforwardly + # QUERY: What is the difference? Also, what happens if damage_extent = None? + if damage_extent > self.deductible: + # QUERY: Changed this, for the better? + if self.insurancetype == "excess-of-loss": + claim = min(self.excess, damage_extent) - self.deductible + self.insurer.receive_obligation( + claim, self.property_holder, time, "claim" + ) + elif self.insurancetype == "proportional": + claim = min(self.excess, damage_extent) - self.deductible + self.insurer.receive_obligation( + claim, self.property_holder, time + 1, "claim" + ) + # Reinsurer pays as soon as possible. + self.insurer.register_claim( + claim + ) # Every reinsurance claim made is immediately registered. + if self.expire_immediately: + self.current_claim += self.contract.claim + # TODO: should proportional reinsurance claims be subject to excess_of_loss retrocession? + # If so, reorganize more straightforwardly - self.expiration = time - # self.terminating = True + self.expiration = time + # self.terminating = True def mature(self, time): """Mature method. Accepts arguments - time: Tyoe integer. The current time. + time: Type integer. The current time. No return value. Removes any reinsurance functions this contract has and terminates any reinsurance contracts for this contract.""" diff --git a/riskmodel.py b/riskmodel.py index 00ba213..0cd0a39 100644 --- a/riskmodel.py +++ b/riskmodel.py @@ -23,6 +23,7 @@ def __init__( ): self.cat_separation_distribution = cat_separation_distribution self.norm_premium = norm_premium + # QUERY: Whis is this passed as an argument and then ignored? self.var_tail_prob = 0.02 self.expire_immediately = expire_immediately self.category_number = category_number @@ -40,7 +41,7 @@ def __init__( # self.inaccuracy = np.random.uniform(9/10., 10/9., size=self.category_number) self.inaccuracy = inaccuracy - def getPPF(self, categ_id, tail_size): + def get_ppf(self, categ_id, tail_size): """Method for getting quantile function of the damage distribution (value at risk) by category. Positional arguments: categ_id integer: category @@ -50,7 +51,7 @@ def getPPF(self, categ_id, tail_size): @staticmethod def get_categ_risks(risks, categ_id): - # List comprehension is faster + # List comprehension is slightly faster than repeated appending categ_risks = [risk for risk in risks if risk["category"] == categ_id] return categ_risks @@ -117,7 +118,7 @@ def evaluate_proportional(self, risks, cash): # compute value at risk var_per_risk = ( - self.getPPF(categ_id=categ_id, tail_size=self.var_tail_prob) + self.get_ppf(categ_id=categ_id, tail_size=self.var_tail_prob) * average_risk_factor * average_exposure * self.margin_of_safety @@ -127,7 +128,7 @@ def evaluate_proportional(self, risks, cash): necessary_liquidity += ( var_per_risk * self.margin_of_safety * len(categ_risks) ) - # print("RISKMODEL: ", self.getPPF(categ_id=categ_id, tailSize=0.01) * average_risk_factor * average_exposure, " = PPF(0.01) * ", average_risk_factor, " * ", average_exposure, " vs. cash: ", cash[categ_id], "TOTAL_RISK_IN_CATEG: ", self.getPPF(categ_id=categ_id, tailSize=0.01) * average_risk_factor * average_exposure * len(categ_risks)) + # print("RISKMODEL: ", self.get_ppf(categ_id=categ_id, tailSize=0.01) * average_risk_factor * average_exposure, " = PPF(0.01) * ", average_risk_factor, " * ", average_exposure, " vs. cash: ", cash[categ_id], "TOTAL_RISK_IN_CATEG: ", self.get_ppf(categ_id=categ_id, tailSize=0.01) * average_risk_factor * average_exposure * len(categ_risks)) if isleconfig.verbose: print(self.inaccuracy) print( @@ -142,10 +143,10 @@ def evaluate_proportional(self, risks, cash): "TOTAL_RISK_IN_CATEG: ", var_per_risk * len(categ_risks), ) - # print("RISKMODEL: ", self.getPPF(categ_id=categ_id, tailSize=0.05) * average_risk_factor * average_exposure, " = PPF(0.05) * ", average_risk_factor, " * ", average_exposure, " vs. cash: ", cash[categ_id], "TOTAL_RISK_IN_CATEG: ", self.getPPF(categ_id=categ_id, tailSize=0.05) * average_risk_factor * average_exposure * len(categ_risks)) - # print("RISKMODEL: ", self.getPPF(categ_id=categ_id, tailSize=0.1) * average_risk_factor * average_exposure, " = PPF(0.1) * ", average_risk_factor, " * ", average_exposure, " vs. cash: ", cash[categ_id], "TOTAL_RISK_IN_CATEG: ", self.getPPF(categ_id=categ_id, tailSize=0.1) * average_risk_factor * average_exposure * len(categ_risks)) - # print("RISKMODEL: ", self.getPPF(categ_id=categ_id, tailSize=0.25) * average_risk_factor * average_exposure, " = PPF(0.25) * ", average_risk_factor, " * ", average_exposure, " vs. cash: ", cash[categ_id], "TOTAL_RISK_IN_CATEG: ", self.getPPF(categ_id=categ_id, tailSize=0.25) * average_risk_factor * average_exposure * len(categ_risks)) - # print("RISKMODEL: ", self.getPPF(categ_id=categ_id, tailSize=0.5) * average_risk_factor * average_exposure, " = PPF(0.5) * ", average_risk_factor, " * ", average_exposure, " vs. cash: ", cash[categ_id], "TOTAL_RISK_IN_CATEG: ", self.getPPF(categ_id=categ_id, tailSize=0.5) * average_risk_factor * average_exposure * len(categ_risks)) + # print("RISKMODEL: ", self.get_ppf(categ_id=categ_id, tailSize=0.05) * average_risk_factor * average_exposure, " = PPF(0.05) * ", average_risk_factor, " * ", average_exposure, " vs. cash: ", cash[categ_id], "TOTAL_RISK_IN_CATEG: ", self.get_ppf(categ_id=categ_id, tailSize=0.05) * average_risk_factor * average_exposure * len(categ_risks)) + # print("RISKMODEL: ", self.get_ppf(categ_id=categ_id, tailSize=0.1) * average_risk_factor * average_exposure, " = PPF(0.1) * ", average_risk_factor, " * ", average_exposure, " vs. cash: ", cash[categ_id], "TOTAL_RISK_IN_CATEG: ", self.get_ppf(categ_id=categ_id, tailSize=0.1) * average_risk_factor * average_exposure * len(categ_risks)) + # print("RISKMODEL: ", self.get_ppf(categ_id=categ_id, tailSize=0.25) * average_risk_factor * average_exposure, " = PPF(0.25) * ", average_risk_factor, " * ", average_exposure, " vs. cash: ", cash[categ_id], "TOTAL_RISK_IN_CATEG: ", self.get_ppf(categ_id=categ_id, tailSize=0.25) * average_risk_factor * average_exposure * len(categ_risks)) + # print("RISKMODEL: ", self.get_ppf(categ_id=categ_id, tailSize=0.5) * average_risk_factor * average_exposure, " = PPF(0.5) * ", average_risk_factor, " * ", average_exposure, " vs. cash: ", cash[categ_id], "TOTAL_RISK_IN_CATEG: ", self.get_ppf(categ_id=categ_id, tailSize=0.5) * average_risk_factor * average_exposure * len(categ_risks)) # if cash[categ_id] < 0: # pdb.set_trace() try: @@ -160,7 +161,8 @@ def evaluate_proportional(self, risks, cash): cash_left_by_category[categ_id] = cash_left var_per_risk_per_categ[categ_id] = var_per_risk - # TODO: expected profits should only be returned once the expire_immediately == False case is fixed; the else-clause conditional statement should then be raised to unconditional + # TODO: expected profits should only be returned once the expire_immediately == False case is fixed; + # the else-clause conditional statement should then be raised to unconditional if expected_profits < 0: expected_profits = None else: @@ -173,8 +175,9 @@ def evaluate_proportional(self, risks, cash): max_cash_by_categ = max(cash_left_by_category) floored_cash_by_categ = cash_left_by_category.copy() floored_cash_by_categ[floored_cash_by_categ < 0] = 0 - remaining_acceptable_by_category_old = remaining_acceptable_by_category.copy() + # remaining_acceptable_by_category_old = remaining_acceptable_by_category.copy() for categ_id in range(self.category_number): + # QUERY: Where does this come from? remaining_acceptable_by_category[categ_id] = math.floor( remaining_acceptable_by_category[categ_id] * pow(floored_cash_by_categ[categ_id] / max_cash_by_categ, 5) @@ -207,7 +210,7 @@ def evaluate_excess_of_loss(self, risks, cash, offered_risk=None): # TODO: allow for different risk distributions for different categories # TODO: factor in risk_factors - percentage_value_at_risk = self.getPPF( + percentage_value_at_risk = self.get_ppf( categ_id=categ_id, tail_size=self.var_tail_prob ) From b9ab7e45163574178ca68515c7cfb68389a8bf3f Mon Sep 17 00:00:00 2001 From: KloskaT Date: Mon, 8 Jul 2019 17:30:09 +0100 Subject: [PATCH 014/125] Added comments, cleaned up remnants of parent functions. --- catbond.py | 234 ++++++++--------------------------------------------- 1 file changed, 35 insertions(+), 199 deletions(-) diff --git a/catbond.py b/catbond.py index 4740497..62e02f5 100644 --- a/catbond.py +++ b/catbond.py @@ -11,7 +11,13 @@ class CatBond(MetaInsuranceOrg): - def init(self, simulation, per_period_premium, owner, interest_rate = 0): # do we need simulation parameters + def __init__(self, simulation, per_period_premium, owner): + """Initialising methods. + Accepts: + simulation: Type class + per_period_premium: Type decimal + owner: Type class + This initialised the catbond class instance, inheriting methods from MetaInsuranceOrg.""" self.simulation = simulation self.id = 0 self.underwritten_contracts = [] @@ -21,67 +27,15 @@ def init(self, simulation, per_period_premium, owner, interest_rate = 0): # do self.operational = True self.owner = owner self.per_period_dividend = per_period_premium - self.interest_rate = interest_rate # TODO: shift obtain_yield method to insurancesimulation, thereby making it unnecessary to drag parameters like self.interest_rate from instance to instance and from class to class - #self.simulation_no_risk_categories = self.simulation.simulation_parameters["no_categories"] - - # TODO: change start and InsuranceSimulation so that it iterates CatBonds - #old parent class init, cat bond class should be much smaller - def parent_init(self, simulation_parameters, agent_parameters): - #def init(self, simulation_parameters, agent_parameters): - self.simulation = simulation_parameters['simulation'] - self.simulation_parameters = simulation_parameters - self.contract_runtime_dist = scipy.stats.randint(simulation_parameters["mean_contract_runtime"] - \ - simulation_parameters["contract_runtime_halfspread"], simulation_parameters["mean_contract_runtime"] \ - + simulation_parameters["contract_runtime_halfspread"] + 1) - self.default_contract_payment_period = simulation_parameters["default_contract_payment_period"] - self.id = agent_parameters['id'] - self.cash = agent_parameters['initial_cash'] - self.premium = agent_parameters["norm_premium"] - self.profit_target = agent_parameters['profit_target'] - self.acceptance_threshold = agent_parameters['initial_acceptance_threshold'] # 0.5 - self.acceptance_threshold_friction = agent_parameters['acceptance_threshold_friction'] # 0.9 #1.0 to switch off - self.interest_rate = agent_parameters["interest_rate"] - self.reinsurance_limit = agent_parameters["reinsurance_limit"] - self.simulation_no_risk_categories = simulation_parameters["no_categories"] - self.simulation_reinsurance_type = simulation_parameters["simulation_reinsurance_type"] - - rm_config = agent_parameters['riskmodel_config'] - self.riskmodel = RiskModel(damage_distribution=rm_config["damage_distribution"], \ - expire_immediately=rm_config["expire_immediately"], \ - cat_separation_distribution=rm_config["cat_separation_distribution"], \ - norm_premium=rm_config["norm_premium"], \ - category_number=rm_config["no_categories"], \ - init_average_exposure=rm_config["risk_value_mean"], \ - init_average_risk_factor=rm_config["risk_factor_mean"], \ - init_profit_estimate=rm_config["norm_profit_markup"], \ - margin_of_safety=rm_config["margin_of_safety"], \ - var_tail_prob=rm_config["var_tail_prob"], \ - inaccuracy=rm_config["inaccuracy_by_categ"]) - - self.category_reinsurance = [None for i in range(self.simulation_no_risk_categories)] - if self.simulation_reinsurance_type == 'non-proportional': - self.np_reinsurance_deductible_fraction = simulation_parameters["default_non-proportional_reinsurance_deductible"] - self.np_reinsurance_excess_fraction = simulation_parameters["default_non-proportional_reinsurance_excess"] - self.np_reinsurance_premium_share = simulation_parameters["default_non-proportional_reinsurance_premium_share"] - self.obligations = [] - self.underwritten_contracts = [] - #self.reinsurance_contracts = [] - self.operational = True - self.is_insurer = True - self.is_reinsurer = False - - """set up risk value estimate variables""" - self.var_counter = 0 # sum over risk model inaccuracies for all contracts - self.var_counter_per_risk = 0 # average risk model inaccuracy across contracts - self.var_sum = 0 # sum over initial VaR for all contracts - self.counter_category = np.zeros(self.simulation_no_risk_categories) # var_counter disaggregated by category - self.var_category = np.zeros(self.simulation_no_risk_categories) # var_sum disaggregated by category def iterate(self, time): - """obtain investments yield""" - self.obtain_yield(time) - - """realize due payments""" + """Method to perform CatBond duties for each time iteration. + Accepts: + time: Type Integer + No return values + For each time iteration this is called from insurancesimulation to perform duties: interest payments, + pay obligations, mature the contract if ended, make payments.""" + self.simulation.bank.award_interest(self, self.cash) self.effect_payments(time) if isleconfig.verbose: print(time, ":", self.id, len(self.underwritten_contracts), self.cash, self.operational) @@ -92,161 +46,43 @@ def iterate(self, time): for contract in maturing: self.underwritten_contracts.remove(contract) contract.mature(time) - contracts_dissolved = len(maturing) - """effect payments from contracts""" [contract.check_payment_due(time) for contract in self.underwritten_contracts] if self.underwritten_contracts == []: - self.mature_bond() #TODO: mature_bond method should check if operational - + self.mature_bond() else: #TODO: dividend should only be payed according to pre-arranged schedule, and only if no risk events have materialized so far if self.operational: self.pay_dividends(time) - - #self.estimated_var() # cannot compute VaR for catbond as catbond does not have a riskmodel - - #old parent class iterate, cat bond class should be much smaller - def parent_iterate(self, time): # TODO: split function so that only the sequence of events remains here and everything else is in separate methods - """obtain investments yield""" - self.obtain_yield(time) - - """realize due payments""" - self.effect_payments(time) - if isleconfig.verbose: - print(time, ":", self.id, len(self.underwritten_contracts), self.cash, self.operational) - - self.make_reinsurance_claims(time) - - """mature contracts""" - if isleconfig.verbose: - print("Number of underwritten contracts ", len(self.underwritten_contracts)) - maturing = [contract for contract in self.underwritten_contracts if contract.expiration <= time] - for contract in maturing: - self.underwritten_contracts.remove(contract) - contract.mature(time) - contracts_dissolved = len(maturing) - - """effect payments from contracts""" - [contract.check_payment_due(time) for contract in self.underwritten_contracts] - - if self.operational: - - """request risks to be considered for underwriting in the next period and collect those for this period""" - new_risks = [] - if self.is_insurer: - new_risks += self.simulation.solicit_insurance_requests(self.id, self.cash) - if self.is_reinsurer: - new_risks += self.simulation.solicit_reinsurance_requests(self.id, self.cash) - contracts_offered = len(new_risks) - try: - assert contracts_offered > 2 * contracts_dissolved - except: - print("Something wrong; agent {0:d} receives too few new contracts {1:d} <= {2:d}".format(self.id, contracts_offered, 2*contracts_dissolved), file=sys.stderr) - #print(self.id, " has ", len(self.underwritten_contracts), " & receives ", contracts_offered, " & lost ", contracts_dissolved) - - new_nonproportional_risks = [risk for risk in new_risks if risk.get("insurancetype")=='excess-of-loss' and risk["owner"] is not self] - new_risks = [risk for risk in new_risks if risk.get("insurancetype") in ['proportional', None] and risk["owner"] is not self] - - underwritten_risks = [{"value": contract.value, "category": contract.category, \ - "risk_factor": contract.risk_factor, "deductible": contract.deductible, \ - "excess": contract.excess, "insurancetype": contract.insurancetype, \ - "runtime": contract.runtime} for contract in self.underwritten_contracts if contract.reinsurance_share != 1.0] - - """deal with non-proportional risks first as they must evaluate each request separatly, then with proportional ones""" - for risk in new_nonproportional_risks: - accept, var_this_risk = self.riskmodel.evaluate(underwritten_risks, self.cash, risk) # TODO: change riskmodel.evaluate() to accept new risk to be evaluated and to account for existing non-proportional risks correctly -> DONE. - if accept: - per_value_reinsurance_premium = self.np_reinsurance_premium_share * risk["periodized_total_premium"] * risk["runtime"] / risk["value"] #TODO: rename this to per_value_premium in insurancecontract.py to avoid confusion - contract = ReinsuranceContract(self, risk, time, per_value_reinsurance_premium, risk["runtime"], \ - self.default_contract_payment_period, \ - expire_immediately=self.simulation_parameters["expire_immediately"], \ - initial_VaR=var_this_risk, \ - insurancetype=risk["insurancetype"]) # TODO: implement excess of loss for reinsurance contracts - self.underwritten_contracts.append(contract) - #pass # TODO: write this nonproportional risk acceptance decision section based on commented code in the lines above this -> DONE. - - """make underwriting decisions, category-wise""" - # TODO: Enable reinsurance shares other tan 0.0 and 1.0 - expected_profit, acceptable_by_category, var_per_risk_per_categ = self.riskmodel.evaluate(underwritten_risks, self.cash) - - #if expected_profit * 1./self.cash < self.profit_target: - # self.acceptance_threshold = ((self.acceptance_threshold - .4) * 5. * self.acceptance_threshold_friction) / 5. + .4 - #else: - # self.acceptance_threshold = (1 - self.acceptance_threshold_friction * (1 - (self.acceptance_threshold - .4) * 5.)) / 5. + .4 - - growth_limit = max(50, 2 * len(self.underwritten_contracts) + contracts_dissolved) - if sum(acceptable_by_category) > growth_limit: - acceptable_by_category = np.asarray(acceptable_by_category) - acceptable_by_category = acceptable_by_category * growth_limit / sum(acceptable_by_category) - acceptable_by_category = np.int64(np.round(acceptable_by_category)) - - not_accepted_risks = [] - for categ_id in range(len(acceptable_by_category)): - categ_risks = [risk for risk in new_risks if risk["category"] == categ_id] - new_risks = [risk for risk in new_risks if risk["category"] != categ_id] - categ_risks = sorted(categ_risks, key = lambda risk: risk["risk_factor"]) - i = 0 - if isleconfig.verbose: - print("InsuranceFirm underwrote: ", len(self.underwritten_contracts), " will accept: ", acceptable_by_category[categ_id], " out of ", len(categ_risks), "acceptance threshold: ", self.acceptance_threshold) - while (acceptable_by_category[categ_id] > 0 and len(categ_risks) > i): #\ - #and categ_risks[i]["risk_factor"] < self.acceptance_threshold): - if categ_risks[i].get("contract") is not None: #categ_risks[i]["reinsurance"]: - if categ_risks[i]["contract"].expiration > time: # required to rule out contracts that have exploded in the meantime - #print("ACCEPTING", categ_risks[i]["contract"].expiration, categ_risks[i]["expiration"], categ_risks[i]["identifier"], categ_risks[i].get("contract").terminating) - contract = ReinsuranceContract(self, categ_risks[i], time, \ - self.simulation.get_market_premium(), categ_risks[i]["expiration"] - time, \ - self.default_contract_payment_period, \ - expire_immediately=self.simulation_parameters["expire_immediately"], ) - self.underwritten_contracts.append(contract) - #categ_risks[i]["contract"].reincontract = contract - # TODO: move this to insurancecontract (ca. line 14) -> DONE - # TODO: do not write into other object's properties, use setter -> DONE - - assert categ_risks[i]["contract"].expiration >= contract.expiration, "Reinsurancecontract lasts longer than insurancecontract: {0:d}>{1:d} (EXPIRATION2: {2:d} Time: {3:d})".format(contract.expiration, categ_risks[i]["contract"].expiration, categ_risks[i]["expiration"], time) - #else: - # pass - else: - contract = InsuranceContract(self, categ_risks[i], time, self.simulation.get_market_premium(), \ - self.contract_runtime_dist.rvs(), \ - self.default_contract_payment_period, \ - expire_immediately=self.simulation_parameters["expire_immediately"], \ - initial_VaR = var_per_risk_per_categ[categ_id]) - self.underwritten_contracts.append(contract) - acceptable_by_category[categ_id] -= 1 # TODO: allow different values per risk (i.e. sum over value (and reinsurance_share) or exposure instead of counting) - i += 1 - - not_accepted_risks += categ_risks[i:] - not_accepted_risks = [risk for risk in not_accepted_risks if risk.get("contract") is None] - - # seek reinsurance - if self.is_insurer: - # TODO: Why should only insurers be able to get reinsurance (not reinsurers)? (Technically, it should work) - self.ask_reinsurance(time) - - # return unacceptables - #print(self.id, " now has ", len(self.underwritten_contracts), " & returns ", len(not_accepted_risks)) - self.simulation.return_risks(not_accepted_risks) - - #not implemented - #"""adjust liquidity, borrow or invest""" - #pass - - self.estimated_var() def set_owner(self, owner): + """Method to set owner of the Cat Bond. + Accepts: + owner: Type class + No return values.""" self.owner = owner if isleconfig.verbose: print("SOLD") - #pdb.set_trace() def set_contract(self, contract): + """Method to record new instances of CatBonds. + Accepts: + owner: Type class + No return values + Only one contract is ever added to the list of underwritten contracts as each CatBond is a contract itself.""" self.underwritten_contracts.append(contract) def mature_bond(self): - obligation = {"amount": self.cash, "recipient": self.simulation, "due_time": 1, "purpose": 'mature'} - self.pay(obligation) - self.simulation.delete_agents("catbond", [self]) - self.operational = False + """Method to mature CatBond. + No accepted values + No return values + When the catbond contract matures this is called which pays the value of the catbond to the simulation, and is + then deleted from the list of agents.""" + if self.operational: + obligation = {"amount": self.cash, "recipient": self.simulation, "due_time": 1, "purpose": 'mature'} + self.pay(obligation) + self.simulation.delete_agents("catbond", [self]) + self.operational = False + else: print('CatBond is not operational so cannot mature') From 919537bdbf65905d8e4f33085aa10eab067bd6d8 Mon Sep 17 00:00:00 2001 From: KloskaT Date: Mon, 8 Jul 2019 17:31:09 +0100 Subject: [PATCH 015/125] Added comments. Removed some redundant lines. --- metainsurancecontract.py | 21 +++++++++++++-------- 1 file changed, 13 insertions(+), 8 deletions(-) diff --git a/metainsurancecontract.py b/metainsurancecontract.py index 139d2b7..c464e33 100644 --- a/metainsurancecontract.py +++ b/metainsurancecontract.py @@ -59,19 +59,13 @@ def __init__(self, insurer, properties, time, premium, runtime, payment_period, self.reinsurer = None self.reincontract = None self.reinsurance_share = None - #self.is_reinsurancecontract = False # setup payment schedule - #total_premium = premium * (self.excess - self.deductible) # TODO: excess and deductible should not be considered linearily in premium computation; this should be shifted to the (re)insurer who supplies the premium as argument to the contract's constructor method total_premium = premium * self.value self.periodized_premium = total_premium / self.runtime self.payment_times = [time + i for i in range(runtime) if i % payment_period == 0] self.payment_values = total_premium * (np.ones(len(self.payment_times)) / len(self.payment_times)) - - ## Create obligation for premium payment - #self.property_holder.receive_obligation(premium * (self.excess - self.deductible), self.insurer, time, 'premium') - - # Embed contract in reinsurance network, if applicable + if self.contract is not None: self.contract.reinsure(reinsurer=self.insurer, reinsurance_share=properties["reinsurance_share"], \ reincontract=self) @@ -80,9 +74,13 @@ def __init__(self, insurer, properties, time, premium, runtime, payment_period, self.roll_over_flag = 0 def check_payment_due(self, time): + """Method to check if a contract payment is due. + Accepts: + time: Type integer + No return values. + This method checks if a scheduled premium payment is due, pays it to the insurer, and removes from schedule.""" if len(self.payment_times) > 0 and time >= self.payment_times[0]: # Create obligation for premium payment - #self.property_holder.receive_obligation(premium * (self.excess - self.deductible), self.insurer, time, 'premium') self.property_holder.receive_obligation(self.payment_values[0], self.insurer, time, 'premium') # Remove current payment from payment schedule @@ -90,6 +88,13 @@ def check_payment_due(self, time): self.payment_values = self.payment_values[1:] def get_and_reset_current_claim(self): + """Method to return and reset claim. + No accepted values + Returns: + self.category: Type integer. Which category the contracted risk is in. + current_claim: Type decimal + self.insurancetype == proportional: Type Boolean. Returns True if insurance is proportional and vice versa. + This method retuns the current claim, then resets it, and also indicates the type of insurance.""" current_claim = self.current_claim self.current_claim = 0 return self.category, current_claim, (self.insurancetype == "proportional") From e95bd39dc8bd94fe6c23ea31e7a3ffab8cbee014 Mon Sep 17 00:00:00 2001 From: KloskaT Date: Mon, 8 Jul 2019 17:32:39 +0100 Subject: [PATCH 016/125] Created file and class that acts as central bank in awarding agents interest. --- centralbank.py | 62 ++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 62 insertions(+) create mode 100644 centralbank.py diff --git a/centralbank.py b/centralbank.py new file mode 100644 index 0000000..bf903ae --- /dev/null +++ b/centralbank.py @@ -0,0 +1,62 @@ +from isleconfig import simulation_parameters + + +class CentralBank: + def __init__(self): + """Constructor Method. + No accepted arguments. + Constructs the CentralBank class. This class is currently only used to award interest payments.""" + self.interest_rate = simulation_parameters['interest_rate'] + self.inflation_target = 0.02 + self.actual_inflation = 0 + self.onemonth_CPI = 0 + self.twelvemonth_CPI = 0 + self.feedback_counter = 0 + self.prices_list = [] + + def set_interest_rate(self): + """Method to set the interest rate + No accepted arguments + No return values + This method is meant to set interest rates dependant on prices however insurance firms have little effect on + interest rates therefore is not used and needs work if to be used.""" + if self.actual_inflation > self.inflation_target: + if self.feedback_counter > 4: + self.interest_rate += 0.0001 + self.feedback_counter = 0 + else: + self.feedback_counter += 1 + elif self.actual_inflation < -0.01: + if self.feedback_counter > 4: + if self.interest_rate > 0.0001: + self.interest_rate -= 0.0001 + self.feedback_counter = 0 + else: + self.feedback_counter += 1 + else: + self.feedback_counter = 0 + print(self.interest_rate) + + def award_interest(self, firm, total_cash): + """Method to award interest. + Accepts: + firm: Type class, the agent that is to be awarded interest. + total_cash: Type decimal + This method takes an agents cash and awards it an interest payment on the cash.""" + interest_payment = total_cash * self.interest_rate + firm.receive(interest_payment) + + def calculate_inflation(self, current_price, time): + """Method to calculate inflation in insurance prices. + Accepts: + current_price: Type decimal + time: Type integer + This method is designed to calculate both the percentage change in insurance price last 1 and 12 months as an + estimate of inflation. This is to help calculate how insurance rates should be set. Currently unused.""" + self.prices_list.append(current_price) + if time < 13: + self.actual_inflation = self.inflation_target + else: + self.onemonth_CPI = (current_price - self.prices_list[-2])/self.prices_list[-2] + self.twelvemonth_CPI = (current_price - self.prices_list[-13])/self.prices_list[-13] + self.actual_inflation = self.twelvemonth_CPI From 53810ad57affa42b1df4e4de269c9981fe4d09ec Mon Sep 17 00:00:00 2001 From: Chris Hughes Date: Tue, 9 Jul 2019 10:10:05 +0100 Subject: [PATCH 017/125] Mostly just refactoring/comments --- ensemble.py | 53 +++++++++++++++------------------ insurancesimulation.py | 47 +++++++++++++++-------------- logger.py | 13 +++++--- metainsuranceorg.py | 19 +++++------- riskmodel.py | 12 ++++---- setup.py => setup_simulation.py | 8 ++--- start.py | 20 +++++++------ utils.py | 21 +++++++++++++ visualisation.py | 17 ++++++++--- 9 files changed, 119 insertions(+), 91 deletions(-) rename setup.py => setup_simulation.py (97%) create mode 100644 utils.py diff --git a/ensemble.py b/ensemble.py index b35ed7e..bc5fe90 100644 --- a/ensemble.py +++ b/ensemble.py @@ -12,7 +12,7 @@ import listify import isleconfig from distributiontruncated import TruncatedDistWrapper -from setup import SetupSim +from setup_simulation import SetupSim from sandman2.api import operation, Session @@ -89,7 +89,7 @@ def rake(hostname): directory = os.getcwd() + dir_prefix try: # Here it is checked whether the directory to collect the results exists or not. If not it is created. os.stat(directory) - except: + except FileNotFoundError: os.mkdir(directory) """Clear old dict saving files (*_history_logs.dat)""" @@ -99,34 +99,29 @@ def rake(hostname): os.remove(filename) """Setup of the simulations""" - - setup = SetupSim() # Here the setup for the simulation is done. + # Here the setup for the simulation is done. + # Since this script is used to carry out simulations in the cloud will usually have more than 1 replication. + setup = SetupSim() [ general_rc_event_schedule, general_rc_event_damage, np_seeds, random_seeds, - ] = setup.obtain_ensemble( - replications - ) # Since this script is used to carry out simulations in the cloud will usually have more than 1 replication.. - save_iter = ( - isleconfig.simulation_parameters["max_time"] + 2 - ) # never save simulation state in ensemble runs (resuming is impossible anyway) - - for ( - i - ) in ( - riskmodels - ): # In this loop the parameters, schedules and random seeds for every run are prepared. Different risk models will be run with the same schedule, damage size and random seed for a fair comparison. - - simulation_parameters = copy.copy( - parameters - ) # Here the parameters used for the simulation are loaded. Clone is needed otherwise all the runs will be carried out with the last number of thee loop. - simulation_parameters[ - "no_riskmodels" - ] = ( - i - ) # Since we want to obtain ensembles for different number of risk models, we vary here the number of risks models. + ] = setup.obtain_ensemble(replications) + # never save simulation state in ensemble runs (resuming is impossible anyway) + save_iter = isleconfig.simulation_parameters["max_time"] + 2 + + for i in riskmodels: + # In this loop the parameters, schedules and random seeds for every run are prepared. Different risk models will + # be run with the same schedule, damage size and random seed for a fair comparison. + + # Here the parameters used for the simulation are loaded. Clone is needed otherwise all the runs will be carried + # out with the last number of the loop. + simulation_parameters = copy.copy(parameters) + # Since we want to obtain ensembles for different number of risk models, we vary the number of risks models. + simulation_parameters["no_riskmodels"] = i + # Here is assembled each job with the corresponding: simulation parameters, time events, damage events, seeds, + # simulation state save interval (never, i.e. longer than max_time), and list of requested logs. job = [ m( simulation_parameters, @@ -138,16 +133,15 @@ def rake(hostname): list(requested_logs.keys()), ) for x in range(replications) - ] # Here is assembled each job with the corresponding: simulation parameters, time events, damage events, seeds, simulation state save interval (never, i.e. longer than max_time), and list of requested logs. + ] jobs.append(job) # All jobs are collected in the jobs list. """Here the jobs are submitted""" with Session(host=hostname, default_cb_to_stdout=True) as sess: - for ( - job - ) in jobs: # If there are 4 risk models jobs will be a list with 4 elements. + for job in jobs: + # If there are 4 risk models jobs will be a list with 4 elements. """Run simulation and obtain result""" result = sess.submit(job) @@ -188,6 +182,7 @@ def rake(hostname): + requested_logs[name] ) + # TODO: write to the files one at a time with a 'with ... as ... :' for name in logfile_dict: wfiles_dict[name] = open(logfile_dict[name], "w") diff --git a/insurancesimulation.py b/insurancesimulation.py index 8ee1d9a..1e7579b 100644 --- a/insurancesimulation.py +++ b/insurancesimulation.py @@ -12,6 +12,7 @@ import copy import logger import warnings +import utils class InsuranceSimulation: @@ -63,15 +64,11 @@ def __init__( loc=self.risk_factor_lower_bound, scale=self.risk_factor_spread ) else: - self.risk_factor_distribution = scipy.stats.uniform(loc=1.0, scale=0) - # TODO: figure out a better way of implementing a constant rv + self.risk_factor_distribution = utils.constant(loc=1.0) # self.risk_value_distribution = scipy.stats.uniform(loc=100, scale=9900) - self.risk_value_distribution = scipy.stats.uniform(loc=1000, scale=0) + self.risk_value_distribution = utils.constant(loc=1000) risk_factor_mean = self.risk_factor_distribution.mean() - # unfortunately scipy.stats.mean is not well-defined if scale = 0 - if np.isnan(risk_factor_mean): - risk_factor_mean = self.risk_factor_distribution.rvs() # set initial market price (normalized, i.e. must be multiplied by value or excess-deductible) if self.simulation_parameters["expire_immediately"]: @@ -125,11 +122,8 @@ def __init__( # set up risks risk_value_mean = self.risk_value_distribution.mean() - if np.isnan(risk_value_mean): - # unfortunately scipy.stats.mean is not well-defined if scale = 0 - risk_value_mean = self.risk_value_distribution.rvs() - # QUERY: I'm stumped, what is the risk factor distribution? + # QUERY: What are risk factors? Are "risk_factor" values other than one meaningful at present? rrisk_factors = self.risk_factor_distribution.rvs( size=self.simulation_parameters["no_risks"] ) @@ -195,11 +189,16 @@ def __init__( ] # prepare setting up agents (to be done from start.py) + # QUERY: What is agent_parameters["insurancefirm"] meant to be? Is it a list of the parameters for the existing + # firms (why can't we just get that from the instances of InsuranceFirm) or a list of the *possible* parameter + # values for insurance firms (in which case why does it have the length it does)? self.agent_parameters = {"insurancefirm": [], "reinsurancefirm": []} self.insurer_id_counter = 0 # TODO: collapse the following two loops into one generic one? for i in range(simulation_parameters["no_insurancefirms"]): + # Set up the parameters for each insurance firm + # Determine the level of reinsurance the firm will aim for? #QUERY: is that what this is? if simulation_parameters["static_non-proportional_reinsurance_levels"]: insurance_reinsurance_level = simulation_parameters[ "default_non-proportional_reinsurance_deductible" @@ -209,7 +208,9 @@ def __init__( simulation_parameters["insurance_reinsurance_levels_lower_bound"], simulation_parameters["insurance_reinsurance_levels_upper_bound"], ) - # The initial set of insurers are approximately uniformly distributed over the possible risk models + + # The initial set of insurers are cycle over the possible risk models + # QUERY: Why don't they pick random risk models? riskmodel_config = risk_model_configurations[ i % len(risk_model_configurations) ] @@ -324,12 +325,15 @@ def __init__( self.simulation_parameters["no_categories"] ) - # QUERY: Now abce is gone can we merge all of the agent creation into here out of start.py? + def add_agents(self, agent_class, agent_class_string): + pass + def build_agents( self, agent_class, agent_class_string, parameters, agent_parameters ): # assert agent_parameters == self.agent_parameters[agent_class_string] # #assert fits only the initial creation of agents, not later additions # TODO: fix + assert parameters == self.simulation_parameters agents = [] for ap in agent_parameters: agents.append(agent_class(parameters, ap)) @@ -416,8 +420,8 @@ def iterate(self, t): ) # Schedules of catastrophes and damages must me generated at the same time. self.inflict_peril(categ_id=categ_id, damage=damage_extent, t=t) self.rc_event_damage[categ_id] = self.rc_event_damage[categ_id][1:] - # TODO: Ideally don't want to be taking from the beginning of lists, - # consider having soonest events at the end of the list. + # TODO: Ideally don't want to be taking from the beginning of lists, consider having soonest events at + # the end of the list. Probably fine though, only happens once per iteration else: if isleconfig.verbose: print("Next peril ", self.rc_event_schedule[categ_id]) @@ -563,22 +567,18 @@ def save_data(self): current_log[ "cumulative_unrecovered_claims" ] = self.cumulative_unrecovered_claims - current_log[ - "cumulative_claims" - ] = self.cumulative_claims # Log the cumulative claims received so far. + # Log the cumulative claims received so far. + current_log["cumulative_claims"] = self.cumulative_claims """ add agent-level data to dict""" current_log["insurance_firms_cash"] = insurance_firms current_log["reinsurance_firms_cash"] = reinsurance_firms current_log["market_diffvar"] = self.compute_market_diffvar() - current_log["individual_contracts"] = [] - individual_contracts_no = [ + current_log["individual_contracts"] = [ len(insurancefirm.underwritten_contracts) for insurancefirm in self.insurancefirms ] - for i in range(len(individual_contracts_no)): - current_log["individual_contracts"].append(individual_contracts_no[i]) """ call to Logger object """ self.logger.record_data(current_log) @@ -794,7 +794,7 @@ def get_reinsurance_premium(self, np_reinsurance_deductible_fraction): if self.reinsurance_off: return float("inf") max_reduction = 0.1 - # QUERY: why is this this way? + # QUERY: why is this this way? Why no, say, 1.0 - min(max_reduction * np_reinsurance_deductible_fraction)? return self.reinsurance_market_premium * ( 1.0 - max_reduction * np_reinsurance_deductible_fraction ) @@ -806,6 +806,7 @@ def get_cat_bond_price(self, np_reinsurance_deductible_fraction): return float("inf") max_reduction = 0.9 max_cat_bond_surcharge = 0.5 + # QUERY: again, what does max_reduction represent? return self.reinsurance_market_premium * ( 1.0 + max_cat_bond_surcharge @@ -824,7 +825,7 @@ def get_reinrisks(self): np.random.shuffle(self.reinrisks) return self.reinrisks - def solicit_insurance_requests(self, id, cash, insurer): + def solicit_insurance_requests(self, insurer_id, cash, insurer): risks_to_be_sent = self.risks[: int(self.insurers_weights[insurer.id])] self.risks = self.risks[int(self.insurers_weights[insurer.id]) :] diff --git a/logger.py b/logger.py index 60f14d3..ddb7472 100644 --- a/logger.py +++ b/logger.py @@ -42,9 +42,11 @@ def __init__( """Variables pertaining to insurance sector""" # TODO: should we not have `cumulative_bankruptcies` and - # `cumulative_market_exits` for both insurance firms and reinsurance firms? - # `cumulative_claims`: Here are stored the total cumulative claims received - # by the whole insurance sector until a certain time. + # `cumulative_market_exits` for both insurance firms and reinsurance firms? + # `cumulative_claims`: Here are stored the total cumulative claims received + # by the whole insurance sector until a certain time. + + # Why not just write a list? insurance_sector = ( "total_cash total_excess_capital total_profitslosses " "total_contracts total_operational cumulative_bankruptcies " @@ -92,13 +94,16 @@ def record_data(self, data_dict): ) def obtain_log( - self, requested_logs=LOG_DEFAULT + self, requested_logs ): # This function allows to return in a list all the data generated by the model. There is no other way to transfer it back from the cloud. """Method to transfer entire log (self.history_log as well as risk event schedule). This is used to transfer the log to master core from work cores in ensemble runs in the cloud. No arguments. Returns list (listified dict).""" + # LOG_DEFAULT is a list and thus mutable, so we don't want to have it as a default value + if requested_logs is None: + requested_logs = LOG_DEFAULT """Include environment variables (number of risk models and risk event schedule)""" self.history_logs["number_riskmodels"] = self.number_riskmodels self.history_logs["rc_event_damage_initial"] = self.rc_event_damage_initial diff --git a/metainsuranceorg.py b/metainsuranceorg.py index 178d562..abdfcee 100644 --- a/metainsuranceorg.py +++ b/metainsuranceorg.py @@ -550,12 +550,10 @@ def adjust_capacity_target(self, time): def risks_reinrisks_organizer(self, new_risks): # This method organizes the new risks received by the insurer (or reinsurer) - risks_per_categ = [ - [] for x in range(self.simulation_parameters["no_categories"]) - ] # This method organizes the new risks received by the insurer (or reinsurer) by category in the nested list "risks_per_categ". - number_risks_categ = [ - [] for x in range(self.simulation_parameters["no_categories"]) - ] # This method also counts the new risks received by the insurer (or reinsurer) by category in the list "number_risks_categ". + # This method organizes the new risks received by category in the nested list "risks_per_categ". + risks_per_categ = [[]] * self.simulation_parameters["no_categories"] + # This method also counts the new risks received by category in the list "number_risks_categ". + number_risks_categ = [0] * self.simulation_parameters["no_categories"] for categ_id in range(self.simulation_parameters["no_categories"]): risks_per_categ[categ_id] = [ @@ -563,10 +561,8 @@ def risks_reinrisks_organizer(self, new_risks): ] number_risks_categ[categ_id] = len(risks_per_categ[categ_id]) - return ( - risks_per_categ, - number_risks_categ, - ) # The method returns both risks_per_categ and number_risks_categ. + # The method returns both risks_per_categ and number_risks_categ. + return risks_per_categ, number_risks_categ def balanced_portfolio(self, risk, cash_left_by_categ, var_per_risk): # This method decides whether the portfolio is balanced enough to accept a new risk or not. @@ -619,7 +615,8 @@ def balanced_portfolio(self, risk, cash_left_by_categ, var_per_risk): # The new risk is accepted if the standard deviation is reduced or the cash reserved by category is very # well balanced. (std_post) <= (self.balance_ratio * mean) for i in range(len(cash_left_by_categ)): - # The balance condition is not taken into account if the cash reserve is far away from the limit. (total_cash_employed_by_categ_post/self.cash <<< 1) + # The balance condition is not taken into account if the cash reserve is far away from the limit. + # (total_cash_employed_by_categ_post/self.cash <<< 1) cash_left_by_categ[i] = self.cash - cash_reserved_by_categ_store[i] return True, cash_left_by_categ diff --git a/riskmodel.py b/riskmodel.py index 0cd0a39..33a4451 100644 --- a/riskmodel.py +++ b/riskmodel.py @@ -149,13 +149,11 @@ def evaluate_proportional(self, risks, cash): # print("RISKMODEL: ", self.get_ppf(categ_id=categ_id, tailSize=0.5) * average_risk_factor * average_exposure, " = PPF(0.5) * ", average_risk_factor, " * ", average_exposure, " vs. cash: ", cash[categ_id], "TOTAL_RISK_IN_CATEG: ", self.get_ppf(categ_id=categ_id, tailSize=0.5) * average_risk_factor * average_exposure * len(categ_risks)) # if cash[categ_id] < 0: # pdb.set_trace() - try: - acceptable = int(math.floor(cash[categ_id] / var_per_risk)) - remaining = acceptable - len(categ_risks) - cash_left = cash[categ_id] - len(categ_risks) * var_per_risk - except: - print(sys.exc_info()) - pdb.set_trace() + + acceptable = int(math.floor(cash[categ_id] / var_per_risk)) + remaining = acceptable - len(categ_risks) + cash_left = cash[categ_id] - len(categ_risks) * var_per_risk + acceptable_by_category.append(acceptable) remaining_acceptable_by_category.append(remaining) cash_left_by_category[categ_id] = cash_left diff --git a/setup.py b/setup_simulation.py similarity index 97% rename from setup.py rename to setup_simulation.py index 514a637..e10f237 100644 --- a/setup.py +++ b/setup_simulation.py @@ -134,7 +134,7 @@ def store(self): with open("./data/" + self.filepath, "wb") as wfile: pickle.dump(event_schedules, wfile, protocol=pickle.HIGHEST_PROTOCOL) - # with open("./data/risk_event_schedules.txt", "w") as wfile: # QUERY: what's this for? + # with open("./data/risk_event_schedules.txt", "w") as wfile: # QUERY: what's this for? Why do we need the txt? # for rep_schedule in event_schedules: # wfile.write( # str(rep_schedule) @@ -164,7 +164,7 @@ def recall(self): "num_categories" ] - def obtain_ensemble(self, replications, filepath, overwrite): + def obtain_ensemble(self, replications, filepath=None, overwrite=False): # This method returns all the information (schedules and seeds) required to run an ensemble of simulations of # the model. Since it also stores the information in a file it will be possible to replicate the ensemble at a # later time. The argument (replications) is the number of replications. @@ -173,7 +173,7 @@ def obtain_ensemble(self, replications, filepath, overwrite): self.filepath = self.to_filename(filepath) self.overwrite = overwrite if not isleconfig.replicating: - # We are writing to the file given + # Not replicating another run, so we are writing to the file given self.replications = replications if filepath is None and not self.overwrite: print("No explicit path given, automatically overwriting default path") @@ -183,7 +183,7 @@ def obtain_ensemble(self, replications, filepath, overwrite): self.store() else: - # We are reading from the file given + # Replicating anothe run, so we are reading from the file given if filepath is not None: self.recall() if replications != self.replications: diff --git a/start.py b/start.py index 3097641..8474a9f 100644 --- a/start.py +++ b/start.py @@ -82,15 +82,15 @@ def main( if simulation.insurance_firm_enters_market(agent_type="InsuranceFirm"): parameters = [ np.random.choice(simulation.agent_parameters["insurancefirm"]) - ] # Which of these should be used? + ] # QUERY Which of these should be used? parameters = [ simulation.agent_parameters["insurancefirm"][ simulation.insurance_entry_index() ] ] - # As far as I can tell, there are only {no_riskmodels} distinct values for parameters, why does - # simulation.agent_parameters["insurancefirm"] need to have length {no_insurancefirms}? - # Also why do the new insurers always use the least popular risk model? + # QUERY: As far as I can tell, there are only {no_riskmodels} distinct values for parameters, why does + # simulation.agent_parameters["insurancefirm"] need to have length {no_insurancefirms}? + # Also why do the new insurers always use the least popular risk model? parameters[0]["id"] = simulation.get_unique_insurer_id() new_insurance_firm = simulation.build_agents( insurancefirm.InsuranceFirm, @@ -106,7 +106,7 @@ def main( np.random.choice(simulation.agent_parameters["reinsurancefirm"]) ] # The reinsurance firms do just pick a random riskmodel when they are created. It is weighted by the initial - # distribution, I think # TODO: is this right? + # distribution, I think # QUERY: is this right? parameters[0]["initial_cash"] = simulation.reinsurance_capital_entry() # Since the value of the reinrisks varies overtime it makes sense that the market entry of reinsures # depends on those values. The method world.reinsurance_capital_entry() determines the capital @@ -190,13 +190,15 @@ def save_simulation(t, sim, sim_param, exit_now=False): "--file", action="store", help="the file to store the initial randomness in. Will be stored in ./data and appended with .islestore " - "(if it is not already)", + "(if it is not already). The default filepath is ./data/risk_event_schedules.islestore, which will be " + "overwritten event if --overwrite is not passed!", ) parser.add_argument( "-r", "--replicating", action="store_true", - help="if this is a simulation run designed to replicate another, override the config file parameter", + help="if this is a simulation run designed to replicate another, override the config file parameter. " + "You probably want to specify the --file to read from.", ) parser.add_argument( "-o", @@ -237,7 +239,7 @@ def save_simulation(t, sim, sim_param, exit_now=False): override_no_riskmodels = 1 if args.riskmodels: override_no_riskmodels = args.riskmodels - if args.replicid is not None: # TODO: track down all uses of replicid + if args.replicid: # TODO: track down all uses of replicid raise ValueError("--replicid is no longer supported, use --file") if args.file: filepath = args.file @@ -264,7 +266,7 @@ def save_simulation(t, sim, sim_param, exit_now=False): else: save_iter = 200 - from setup import SetupSim + from setup_simulation import SetupSim setup = SetupSim() # Here the setup for the simulation is done. diff --git a/utils.py b/utils.py new file mode 100644 index 0000000..92cad72 --- /dev/null +++ b/utils.py @@ -0,0 +1,21 @@ +from scipy import stats +import numpy as np + + +class constant_gen(stats.rv_continuous): + def _pdf(self, x, *args): + a = np.float_(x == 0) + a[a == 1.0] = np.float_("inf") + return a + + def _cdf(self, x, *args): + return np.float_(x >= 0) + + def _rvs(self, *args): + if self._size is None: + return 0.0 + else: + return np.zeros(shape=self._size) + + +constant = constant_gen(name="constant") diff --git a/visualisation.py b/visualisation.py index ec949d5..5235f5f 100644 --- a/visualisation.py +++ b/visualisation.py @@ -135,8 +135,11 @@ def insurer_time_series( fig=None, title="Insurer", colour="black", - percentiles=[25, 75], + percentiles=None, ): + # Default values shouldn't be mutable + if percentiles is None: + percentiles = [25, 75] # runs should be a list of the indexes you want included in the ensemble for consideration if runs: data = [self.history_logs_list[x] for x in runs] @@ -215,8 +218,10 @@ def reinsurer_time_series( fig=None, title="Reinsurer", colour="black", - percentiles=[25, 75], + percentiles=None, ): + if percentiles is None: + percentiles = [25, 75] # runs should be a list of the indexes you want included in the ensemble for consideration if runs: data = [self.history_logs_list[x] for x in runs] @@ -367,15 +372,19 @@ def __init__(self, vis_list, colour_list): self.vis_list = vis_list self.colour_list = colour_list - def create_insurer_timeseries(self, fig=None, axlst=None, percentiles=[25, 75]): + def create_insurer_timeseries(self, fig=None, axlst=None, percentiles=None): # create the time series for each object in turn and superpose them? + if percentiles is None: + percentiles = [25, 75] fig = axlst = None for vis, colour in zip(self.vis_list, self.colour_list): (fig, axlst) = vis.insurer_time_series( fig=fig, axlst=axlst, colour=colour, percentiles=percentiles ) - def create_reinsurer_timeseries(self, fig=None, axlst=None, percentiles=[25, 75]): + def create_reinsurer_timeseries(self, fig=None, axlst=None, percentiles=None): + if percentiles is None: + percentiles = [25, 75] # create the time series for each object in turn and superpose them? fig = axlst = None for vis, colour in zip(self.vis_list, self.colour_list): From c87453f1b51210068c97531872215b17bf6d48ea Mon Sep 17 00:00:00 2001 From: KloskaT Date: Tue, 9 Jul 2019 13:27:32 +0100 Subject: [PATCH 018/125] Added docstring to all methods. Merged create_reinrisk into ask_reinsurance_non_proportional_by_category as they were almost the same. --- insurancefirm.py | 194 +++++++++++++++++++++++++++++++++++------------ 1 file changed, 144 insertions(+), 50 deletions(-) diff --git a/insurancefirm.py b/insurancefirm.py index a20fa1c..da75298 100644 --- a/insurancefirm.py +++ b/insurancefirm.py @@ -19,15 +19,27 @@ def init(self, simulation_parameters, agent_parameters): self.is_reinsurer = False def adjust_dividends(self, time, actual_capacity): + """Method to adjust dividends firm pays to investors. + Accepts: + time: Type Integer. Not used. + actual_capacity: Type Decimal. + No return values. + Method is called from MetaInsuranceOrg iterate method between evaluating reinsurance and insurance risks to + calculate dividend to be payed if the firm has made profit and has achieved capital targets.""" #TODO: Implement algorithm from flowchart - profits = self.get_profitslosses() + profits = self.profits_losses self.per_period_dividend = max(0, self.dividend_share_of_profits * profits) # max function ensures that no negative dividends are paid - #if profits < 0: # no dividends when losses are written - # self.per_period_dividend = 0 if actual_capacity < self.capacity_target: # no dividends if firm misses capital target self.per_period_dividend = 0 def get_reinsurance_VaR_estimate(self, max_var): + """Method to estimate the VaR if another reinsurance contract were to be taken. + Accepts: + max_var: Type Decimal. Max value at risk + Returns: + reinsurance_VaR_estimate: Type Decimal. + This method takes the max VaR and mulitiplies it by a factor that estimates the VaR if another reinsurance + contract was to be taken. Called by the adjust_target_capacity and get_capacity methods.""" reinsurance_factor_estimate = (sum([ 1 for categ_id in range(self.simulation_no_risk_categories) \ if (self.category_reinsurance[categ_id] is None)]) \ * 1. / self.simulation_no_risk_categories) \ @@ -36,6 +48,12 @@ def get_reinsurance_VaR_estimate(self, max_var): return reinsurance_VaR_estimate def adjust_capacity_target(self, max_var): + """Method to adjust capacity target. + Accepts: + max_var: Type Decimal. + No return values. + This method decides to increase/decrease the capacity target dependant on if the ratio of capacity target to max + VaR is above/below a predetermined limit.""" reinsurance_VaR_estimate = self.get_reinsurance_VaR_estimate(max_var) capacity_target_var_ratio_estimate = (self.capacity_target + reinsurance_VaR_estimate) * 1. / (max_var + reinsurance_VaR_estimate) if capacity_target_var_ratio_estimate > self.capacity_target_increment_threshold: @@ -45,16 +63,33 @@ def adjust_capacity_target(self, max_var): return def get_capacity(self, max_var): + """Method to get capacity of firm. + Accepts: + max_var: Type Decimal. + Returns: + self.cash (+ reinsurance_VaR_estimate): Type Decimal. + This method is called by increase_capacity to get the real capacity of the firm. If the firm has enough money to + cover its max value at risk then its capacity is its cash + the reinsurance VaR estimate, otherwise the firm is + recovering from some losses and so capacity is just cash.""" if max_var < self.cash: # ensure presence of sufficiently much cash to cover VaR reinsurance_VaR_estimate = self.get_reinsurance_VaR_estimate(max_var) return self.cash + reinsurance_VaR_estimate - # else: # (This point is only reached when insurer is in severe financial difficulty. Ensure insurer recovers complete coverage.) - return self.cash + return self.cash # Ensure insurer recovers complete coverage. def increase_capacity(self, time, max_var): - '''This is implemented for non-proportional reinsurance only. Otherwise the price comparison is not meaningful. Assert non-proportional mode.''' + """Method to increase the capacity of the firm. + Accepts: + time: Type Integer. + max_var: Type Decimal. + Returns: + capacity: Type Decimal. + This method is called from the main iterate method in metainsuranceorg and gets prices for cat bonds and + reinsurance then checks if each category needs it. Passes a random category and the prices to the + increase_capacity_by_category method. If a firms capacity is above its target then it will only issue one if the + market premium is above its average premium, otherwise firm is 'forced' to get a catbond or reinsurance. Only + implemented for non-proportional(excess of loss) reinsurance. Only issues one reinsurance or catbond per + iteration unless not enough capacity to meet target.""" assert self.simulation_reinsurance_type == 'non-proportional' - '''get prices''' reinsurance_price = self.simulation.get_reinsurance_premium(self.np_reinsurance_deductible_fraction) cat_bond_price = self.simulation.get_cat_bond_price(self.np_reinsurance_deductible_fraction) capacity = None @@ -65,7 +100,7 @@ def increase_capacity(self, time, max_var): while len(categ_ids) >= 1: categ_id = categ_ids.pop() capacity = self.get_capacity(max_var) - if self.capacity_target < capacity: # just one per iteration, unless capital target is unmatched + if self.capacity_target < capacity: if self.increase_capacity_by_category(time, categ_id, reinsurance_price=reinsurance_price, cat_bond_price=cat_bond_price, force=False): categ_ids = [] else: @@ -76,6 +111,17 @@ def increase_capacity(self, time, max_var): return capacity def increase_capacity_by_category(self, time, categ_id, reinsurance_price, cat_bond_price, force=False): + """Method to increase capacity. Only called by increase_capacity. + Accepts: + time: Type Integer> + categ_id: Type integer. + reinsurance_price: Type Decimal. + cat_bond_price: Type Decimal. + force: Type Boolean. Forces firm to get reinsurance/catbond or not. + Returns Boolean to stop loop if firm has enough capacity. + This method is given a category and prices of reinsurance/catbonds and will issue whichever one is cheaper to a + firm for the given category. This is forced if firm does not have enough capacity to meet target otherwise will + only issue if market premium is greater than firms average premium.""" if isleconfig.verbose: print("IF {0:d} increasing capacity in period {1:d}, cat bond price: {2:f}, reinsurance premium {3:f}".format(self.id, time, cat_bond_price, reinsurance_price)) if not force: @@ -95,6 +141,11 @@ def increase_capacity_by_category(self, time, categ_id, reinsurance_price, cat_b return True def get_average_premium(self, categ_id): + """Method to calculate and return the firms average premium for all currently underwritten contracts. + Accepts: + categ_id: Type Integer. + Returns: + premium payments left/total value of contracts: Type Decimal""" weighted_premium_sum = 0 total_weight = 0 for contract in self.underwritten_contracts: @@ -107,6 +158,11 @@ def get_average_premium(self, categ_id): return weighted_premium_sum * 1.0 / total_weight def ask_reinsurance(self, time): + """Method called specifically to call relevant reinsurance function for simulations reinsurance type. Only + non-proportional type is used as this is the one mainly used in reality. + Accepts: + time: Type Integer. + No return values.""" if self.simulation_reinsurance_type == 'proportional': self.ask_reinsurance_proportional() elif self.simulation_reinsurance_type == 'non-proportional': @@ -116,13 +172,11 @@ def ask_reinsurance(self, time): def ask_reinsurance_non_proportional(self, time): """ Method for requesting excess of loss reinsurance for all underwritten contracts by category. - The method calculates the combined valur at risk. With a probability it then creates a combined + The method calculates the combined value at risk. With a probability it then creates a combined reinsurance risk that may then be underwritten by a reinsurance firm. Arguments: time: integer - Returns None. - - """ + Returns None.""" """Evaluate by risk category""" for categ_id in range(self.simulation_no_risk_categories): """Seek reinsurance only with probability 10% if not already reinsured""" # TODO: find a more generic way to decide whether to request reinsurance for category in this period @@ -130,6 +184,16 @@ def ask_reinsurance_non_proportional(self, time): self.ask_reinsurance_non_proportional_by_category(time, categ_id) def characterize_underwritten_risks_by_category(self, time, categ_id): + """Method to characterise associated risks in a given category in terms of value, number, avg risk factor, and + total premium per iteration. + Accepts: + time: Type Integer. Not used.. + categ_id: Type Integer. The given category for characterising risks. + Returns: + total_value: Type Decimal. Total value of all contracts in the category. + avg_risk_facotr: Type Decimal. Avg risk factor of all contracted risks in category. + number_risks: Type Integer. Total number of contracted risks in category. + periodised_total_premium: Total value per month of all contracts premium payments.""" total_value = 0 avg_risk_factor = 0 number_risks = 0 @@ -144,8 +208,19 @@ def characterize_underwritten_risks_by_category(self, time, categ_id): avg_risk_factor /= number_risks return total_value, avg_risk_factor, number_risks, periodized_total_premium - def ask_reinsurance_non_proportional_by_category(self, time, categ_id): - """Proceed with creation of reinsurance risk only if category is not empty.""" + def ask_reinsurance_non_proportional_by_category(self, time, categ_id, purpose='newrisk'): + """Method to create a reinsurance risk for a given category for firm that calls it. Called from increase_ + capacity_by_category, ask_reinsurance_non_proportional, and roll_over in metainsuranceorg. + Accepts: + time: Type Integer. + categ_id: Type Integer. + purpose: Type String. Needed for when called from roll_over method as the risk is then returned. + Returns: + risk: Type DataDict. Only returned when method used for roll_over. + This method is given a category, then characterises all the underwritten risks in that category for the firm + and, assuming firms has underwritten risks in category, creates new reinsurance risk with values based on firms + existing underwritten risks. If the method was called to create a new risks then it is appended to list of + 'reinrisks', otherwise used for creating the risk when a reinsurance contract rolls over.""" total_value, avg_risk_factor, number_risks, periodized_total_premium = self.characterize_underwritten_risks_by_category(time, categ_id) if number_risks > 0: risk = {"value": total_value, "category": categ_id, "owner": self, @@ -155,10 +230,17 @@ def ask_reinsurance_non_proportional_by_category(self, time, categ_id): "excess_fraction": self.np_reinsurance_excess_fraction, "periodized_total_premium": periodized_total_premium, "runtime": 12, "expiration": time + 12, "risk_factor": avg_risk_factor} # TODO: make runtime into a parameter - - self.simulation.append_reinrisks(risk) + if purpose == 'newrisk': + self.simulation.append_reinrisks(risk) + elif purpose == 'rollover': + return risk + elif number_risks == 0 and purpose == 'rollover': + return None def ask_reinsurance_proportional(self): + """Method to create proportional reinsurance risk. Not used in code as not really used in reality. + No accepted values. + NO return values.""" nonreinsured = [] for contract in self.underwritten_contracts: if contract.reincontract == None: @@ -183,18 +265,39 @@ def ask_reinsurance_proportional(self): break def add_reinsurance(self, category, excess_fraction, deductible_fraction, contract): + """Method called by reinsurancecontract to add the reinsurance contract to the firms counter for the given + category, normally used so only one reinsurance contract is issued per category at a time. + Accepts: + category: Type Integer. + excess_fraction: Type Decimal. Value of excess. + deductible_fraction: Type Decimal. Value of deductible. + contract: Type Class. Reinsurance contract issued to firm. + No return values.""" self.riskmodel.add_reinsurance(category, excess_fraction, deductible_fraction, contract) self.category_reinsurance[category] = contract def delete_reinsurance(self, category, excess_fraction, deductible_fraction, contract): + """Method called by reinsurancecontract to delete the reinsurance contract from the firms counter for the given + category, used so that another reinsurance contract can be issued for that category if needed. + Accepts: + category: Type Integer. + excess_fraction: Type Decimal. Value of excess. + deductible_fraction: Type Decimal. Value of deductible. + contract: Type Class. Reinsurance contract issued to firm. + No return values.""" self.riskmodel.delete_reinsurance(category, excess_fraction, deductible_fraction, contract) self.category_reinsurance[category] = None - def issue_cat_bond(self, time, categ_id, per_value_per_period_premium = 0): - # premium is for usual reinsurance contracts paid using per value market premium - # for the quasi-contract for the cat bond, nothing is paid, everything is already paid at the beginning. - #per_value_reinsurance_premium = self.np_reinsurance_premium_share * risk["periodized_total_premium"] * risk["runtime"] / risk["value"] #TODO: rename this to per_value_premium in insurancecontract.py to avoid confusion - """ create catbond """ + def issue_cat_bond(self, time, categ_id, per_value_per_period_premium=0): + """Method to issue cat bond to given firm for given category. + Accepts: + time: Type Integer. + categ_id: Type Integer. + per_value_per_period_premium: Type Integer. + No return values. + Method is only called by increase_capacity_by_category method when CatBond prices are lower than reinsurance. It + then creates the CatBond as a quasi-reinsurance contract that is paid for immediately (by simulation) with no + premium payments.""" total_value, avg_risk_factor, number_risks, periodized_total_premium = self.characterize_underwritten_risks_by_category(time, categ_id) if number_risks > 0: risk = {"value": total_value, "category": categ_id, "owner": self, @@ -206,29 +309,29 @@ def issue_cat_bond(self, time, categ_id, per_value_per_period_premium = 0): "expiration": time + 12, "risk_factor": avg_risk_factor} # TODO: make runtime into a parameter _, _, var_this_risk, _ = self.riskmodel.evaluate([], self.cash, risk) per_period_premium = per_value_per_period_premium * risk["value"] - total_premium = sum([per_period_premium * ((1/(1+self.interest_rate))**i) for i in range(risk["runtime"])]) # TODO: or is it range(1, risk["runtime"]+1)? - catbond = CatBond(self.simulation, per_period_premium, self.interest_rate) # TODO: shift obtain_yield method to insurancesimulation, thereby making it unnecessary to drag parameters like self.interest_rate from instance to instance and from class to class + total_premium = sum([per_period_premium * ((1/(1+self.interest_rate))**i) for i in range(risk["runtime"])]) - """add contract; contract is a quasi-reinsurance contract""" - contract = ReinsuranceContract(catbond, risk, time, 0, risk["runtime"], \ - self.default_contract_payment_period, \ - expire_immediately=self.simulation_parameters["expire_immediately"], \ - initial_VaR=var_this_risk, \ - insurancetype=risk["insurancetype"]) - # per_value_reinsurance_premium = 0 because the insurance firm does not continue to make payments to the cat bond. Only once. + catbond = CatBond(self.simulation, per_period_premium, self.simulation) + contract = ReinsuranceContract(catbond, risk, time, 0, risk["runtime"], + self.default_contract_payment_period, \ + expire_immediately=self.simulation_parameters["expire_immediately"], \ + initial_VaR=var_this_risk, insurancetype=risk["insurancetype"]) catbond.set_contract(contract) - """sell cat bond (to self.simulation)""" self.simulation.receive_obligation(var_this_risk, self, time, 'bond') - catbond.set_owner(self.simulation) """hand cash over to cat bond such that var_this_risk is covered""" obligation = {"amount": var_this_risk + total_premium, "recipient": catbond, "due_time": time, "purpose": 'bond'} - self.pay(obligation) #TODO: is var_this_risk the correct amount? - """register catbond""" + self.pay(obligation) self.simulation.accept_agents("catbond", [catbond], time=time) - def make_reinsurance_claims(self,time): - """collect and effect reinsurance claims""" + def make_reinsurance_claims(self, time): + """Method to make reinsurance claims. + Accepts: + time: Type Integer. + No return values. + This method calculates the total amount of claims this iteration per category, and explodes (see reinsurance + contracts) any reinsurance contracts present for one of the contracts (currently always zero). Then, for a + category with reinsurance and claims, the applicable reinsurance contract is exploded.""" # TODO: reorganize this with risk category ledgers # TODO: Put facultative insurance claims here claims_this_turn = np.zeros(self.simulation_no_risk_categories) @@ -244,6 +347,11 @@ def make_reinsurance_claims(self,time): self.category_reinsurance[categ_id].explode(time, claims_this_turn[categ_id]) def get_excess_of_loss_reinsurance(self): + """Method to return list containing the reinsurance for each category interms of the reinsurer, value of + contract and category. Only used for network visualisation. + No accepted values. + Returns: + reinsurance: Type list of DataDicts.""" reinsurance = [] for categ_id in range(self.simulation_no_risk_categories): if self.category_reinsurance[categ_id] is not None: @@ -254,17 +362,3 @@ def get_excess_of_loss_reinsurance(self): reinsurance.append(reinsurance_contract) return reinsurance - def create_reinrisk(self, time, categ_id): - """Proceed with creation of reinsurance risk only if category is not empty.""" - total_value, avg_risk_factor, number_risks, periodized_total_premium = self.characterize_underwritten_risks_by_category(time, categ_id) - if number_risks > 0: - risk = {"value": total_value, "category": categ_id, "owner": self, - #"identifier": uuid.uuid1(), - "insurancetype": 'excess-of-loss', "number_risks": number_risks, - "deductible_fraction": self.np_reinsurance_deductible_fraction, - "excess_fraction": self.np_reinsurance_excess_fraction, - "periodized_total_premium": periodized_total_premium, "runtime": 12, - "expiration": time + 12, "risk_factor": avg_risk_factor} # TODO: make runtime into a parameter - return risk - else: - return None From 882d0bf169f115b903e8f0aa05f7dd8aeda0db01 Mon Sep 17 00:00:00 2001 From: Chris Hughes Date: Tue, 9 Jul 2019 14:24:28 +0100 Subject: [PATCH 019/125] Apply black.py to all python files. Just a code formatter, zero change to compiled bytecode. Done so simplify diffs with future commits. --- calibration_conditions.py | 134 ++- calibrationscore.py | 33 +- catbond.py | 385 +++++--- compute_profits_losses_from_cash.py | 8 +- condition_aux.py | 131 ++- distribution_wrapper_test.py | 16 +- distributionreinsurance.py | 64 +- distributiontruncated.py | 51 +- ensemble.py | 197 +++-- genericagent.py | 9 +- genericagentabce.py | 1 + insurancecontract.py | 45 +- insurancefirm.py | 329 ++++--- insurancesimulation.py | 836 ++++++++++++------ isleconfig.py | 146 +-- listify.py | 16 +- logger.py | 125 +-- metainsurancecontract.py | 110 ++- metainsuranceorg.py | 736 ++++++++++----- metaplotter.py | 272 ++++-- metaplotter_pl_timescale.py | 261 ++++-- ...lotter_pl_timescale_additional_measures.py | 314 +++++-- plotter.py | 42 +- plotter_pl_timescale.py | 40 +- reinsurancecontract.py | 86 +- reinsurancefirm.py | 4 +- resume.py | 165 ++-- riskmodel.py | 265 ++++-- setup.py | 96 +- start.py | 256 ++++-- visualisation.py | 378 ++++++-- visualization_network.py | 88 +- 32 files changed, 3946 insertions(+), 1693 deletions(-) diff --git a/calibration_conditions.py b/calibration_conditions.py index 33bc6f7..94a8980 100644 --- a/calibration_conditions.py +++ b/calibration_conditions.py @@ -31,100 +31,158 @@ import condition_aux import isleconfig + def condition_stationary_state_cash(logobj): """Stationarity test for total cash""" - return condition_aux.condition_stationary_state(logobj.history_logs['total_cash']) - + return condition_aux.condition_stationary_state(logobj.history_logs["total_cash"]) + + def condition_stationary_state_excess_capital(logobj): """Stationarity test for total excess capital""" - return condition_aux.condition_stationary_state(logobj.history_logs['total_excess_capital']) + return condition_aux.condition_stationary_state( + logobj.history_logs["total_excess_capital"] + ) + def condition_stationary_state_profits_losses(logobj): """Stationarity test for total profits and losses""" - return condition_aux.condition_stationary_state(logobj.history_logs['total_profitslosses']) + return condition_aux.condition_stationary_state( + logobj.history_logs["total_profitslosses"] + ) + def condition_stationary_state_contracts(logobj): """Stationarity test for total number of contracts""" - return condition_aux.condition_stationary_state(logobj.history_logs['total_contracts']) + return condition_aux.condition_stationary_state( + logobj.history_logs["total_contracts"] + ) + def condition_stationary_state_rein_cash(logobj): """Stationarity test for total cash (reinsurers)""" - return condition_aux.condition_stationary_state(logobj.history_logs['total_reincash']) + return condition_aux.condition_stationary_state( + logobj.history_logs["total_reincash"] + ) + def condition_stationary_state_rein_excess_capital(logobj): """Stationarity test for total excess capital (reinsurers)""" - return condition_aux.condition_stationary_state(logobj.history_logs['total_reinexcess_capital']) + return condition_aux.condition_stationary_state( + logobj.history_logs["total_reinexcess_capital"] + ) + def condition_stationary_state_rein_profits_losses(logobj): """Stationarity test for total profits and losses (reinsurers)""" - return condition_aux.condition_stationary_state(logobj.history_logs['total_reinprofitslosses']) + return condition_aux.condition_stationary_state( + logobj.history_logs["total_reinprofitslosses"] + ) + def condition_stationary_state_rein_contracts(logobj): """Stationarity test for total number of reinsured contracts""" - return condition_aux.condition_stationary_state(logobj.history_logs['total_reincontracts']) + return condition_aux.condition_stationary_state( + logobj.history_logs["total_reincontracts"] + ) + def condition_stationary_state_market_premium(logobj): """Stationarity test for insurance market premium""" - return condition_aux.condition_stationary_state(logobj.history_logs['market_premium']) + return condition_aux.condition_stationary_state( + logobj.history_logs["market_premium"] + ) + def condition_stationary_state_rein_market_premium(logobj): """Stationarity test for reinsurance market premium""" - return condition_aux.condition_stationary_state(logobj.history_logs['market_reinpremium']) + return condition_aux.condition_stationary_state( + logobj.history_logs["market_reinpremium"] + ) -def condition_defaults_insurance(logobj): # TODO: develop this into a non-binary measure + +def condition_defaults_insurance( + logobj +): # TODO: develop this into a non-binary measure """Test for number of insurance bankruptcies (non zero, not all insurers)""" - #series = logobj.history_logs['total_operational'] - #if series[-1] != 0 and any(series[i]-series[i-1] < 0 for i in range(1,len(series))): - opseries = [logobj.history_logs["insurance_firms_cash"][-1][i][2] for i in \ - range(len(logobj.history_logs["insurance_firms_cash"][-1]))] + # series = logobj.history_logs['total_operational'] + # if series[-1] != 0 and any(series[i]-series[i-1] < 0 for i in range(1,len(series))): + opseries = [ + logobj.history_logs["insurance_firms_cash"][-1][i][2] + for i in range(len(logobj.history_logs["insurance_firms_cash"][-1])) + ] if any(opseries) and not all(opseries): return 1 else: return 0 -def condition_defaults_reinsurance(logobj): # TODO: develop this into a non-binary measure + +def condition_defaults_reinsurance( + logobj +): # TODO: develop this into a non-binary measure """Test for number of reinsurance bankruptcies (non zero, not all reinsurers)""" - #series = logobj.history_logs['total_reinoperational'] - #if series[-1] != 0 and any(series[i]-series[i-1] < 0 for i in range(1,len(series))): - opseries = [logobj.history_logs["reinsurance_firms_cash"][-1][i][2] for i in \ - range(len(logobj.history_logs["reinsurance_firms_cash"][-1]))] + # series = logobj.history_logs['total_reinoperational'] + # if series[-1] != 0 and any(series[i]-series[i-1] < 0 for i in range(1,len(series))): + opseries = [ + logobj.history_logs["reinsurance_firms_cash"][-1][i][2] + for i in range(len(logobj.history_logs["reinsurance_firms_cash"][-1])) + ] if any(opseries) and not all(opseries): return 1 else: return 0 + def condition_insurance_coverage(logobj): """Test for insurance coverage close to 100%""" - return logobj.history_logs['total_contracts'][-1] * 1. / isleconfig.simulation_parameters["no_risks"] + return ( + logobj.history_logs["total_contracts"][-1] + * 1.0 + / isleconfig.simulation_parameters["no_risks"] + ) + def condition_reinsurance_coverage(logobj, minimum=0.6): """Test for reinsurance coverage close to some minimum that may be less than 100% (default 60%)""" - score = logobj.history_logs['total_reincontracts'][-1] * 1. / (minimum * logobj.history_logs['total_contracts'][-1]) - score = 1 if score>1 else score + score = ( + logobj.history_logs["total_reincontracts"][-1] + * 1.0 + / (minimum * logobj.history_logs["total_contracts"][-1]) + ) + score = 1 if score > 1 else score return score -def condition_insurance_firm_dist(logobj): + +def condition_insurance_firm_dist(logobj): """Empirical calibration test for insurance firm size (total assets; cash)""" """filter operational firms""" - #dist = [logobj.history_logs["insurance_firms_cash"][-1][i][0] for i in range(len(logobj.history_logs["insurance_firms_cash"])) if \ + # dist = [logobj.history_logs["insurance_firms_cash"][-1][i][0] for i in range(len(logobj.history_logs["insurance_firms_cash"])) if \ # logobj.history_logs["insurance_firms_cash"][-1][i][0] > isleconfig.simulation_parameters["cash_permanency_limit"]] - dist = [logobj.history_logs["insurance_firms_cash"][-1][i][0] for i in \ - range(len(logobj.history_logs["insurance_firms_cash"][-1])) if \ - logobj.history_logs["insurance_firms_cash"][-1][i][2]] + dist = [ + logobj.history_logs["insurance_firms_cash"][-1][i][0] + for i in range(len(logobj.history_logs["insurance_firms_cash"][-1])) + if logobj.history_logs["insurance_firms_cash"][-1][i][2] + ] """run two-sided KS test""" - KS_statistic, p_value = stats.ks_2samp(condition_aux.scaler(condition_aux.insurance_firm_sizes_empirical_2017), - condition_aux.scaler(dist)) + KS_statistic, p_value = stats.ks_2samp( + condition_aux.scaler(condition_aux.insurance_firm_sizes_empirical_2017), + condition_aux.scaler(dist), + ) return p_value -def condition_reinsurance_firm_dist(logobj): + +def condition_reinsurance_firm_dist(logobj): """Empirical calibration test for reinsurance firm size (total assets; cash)""" """filter operational firms""" - #dist = [logobj.history_logs["reinsurance_firms_cash"][-1][i][0] for i in range(len(logobj.history_logs["reinsurance_firms_cash"])) if \ + # dist = [logobj.history_logs["reinsurance_firms_cash"][-1][i][0] for i in range(len(logobj.history_logs["reinsurance_firms_cash"])) if \ # logobj.history_logs["reinsurance_firms_cash"][-1][i][0] > isleconfig.simulation_parameters["cash_permanency_limit"]] - dist = [logobj.history_logs["reinsurance_firms_cash"][-1][i][0] for i in \ - range(len(logobj.history_logs["reinsurance_firms_cash"][-1])) if - logobj.history_logs["reinsurance_firms_cash"][-1][i][2]] + dist = [ + logobj.history_logs["reinsurance_firms_cash"][-1][i][0] + for i in range(len(logobj.history_logs["reinsurance_firms_cash"][-1])) + if logobj.history_logs["reinsurance_firms_cash"][-1][i][2] + ] """run two-sided KS test""" - KS_statistic, p_value = stats.ks_2samp(condition_aux.scaler(condition_aux.reinsurance_firm_sizes_empirical_2017), - condition_aux.scaler(dist)) + KS_statistic, p_value = stats.ks_2samp( + condition_aux.scaler(condition_aux.reinsurance_firm_sizes_empirical_2017), + condition_aux.scaler(dist), + ) return p_value diff --git a/calibrationscore.py b/calibrationscore.py index 32d870c..b0a86a6 100644 --- a/calibrationscore.py +++ b/calibrationscore.py @@ -5,9 +5,10 @@ from inspect import getmembers, isfunction import numpy as np -import calibration_conditions # Test functions +import calibration_conditions # Test functions -class CalibrationScore(): + +class CalibrationScore: def __init__(self, L): """Constructor method. Arguments: @@ -17,31 +18,41 @@ def __init__(self, L): """Assert sanity of log and save log.""" assert isinstance(L, logger.Logger) self.logger = L - + """Prepare list of calibration tests from calibration_conditions.py""" - self.conditions = [f for f in getmembers(calibration_conditions) if isfunction(f[1])] - + self.conditions = [ + f for f in getmembers(calibration_conditions) if isfunction(f[1]) + ] + """Prepare calibration score variable.""" self.calibration_score = None - + def test_all(self): """Method to test all calibration tests. No arguments. Returns combined calibration score as float \in [0,1].""" - + """Compute score components""" - scores = {condition[0]: condition[1](self.logger) for condition in self.conditions} + scores = { + condition[0]: condition[1](self.logger) for condition in self.conditions + } """Print components""" print("\n") for cond_name, score in scores.items(): print("{0:47s}: {1:8f}".format(cond_name, score)) """Compute combined score""" - self.calibration_score = self.combine_scores(np.array([*scores.values()], dtype=object)) + self.calibration_score = self.combine_scores( + np.array([*scores.values()], dtype=object) + ) """Print combined score""" - print("\n Total calibration score: {0:8f}".format(self.calibration_score)) + print( + "\n Total calibration score: {0:8f}".format( + self.calibration_score + ) + ) """Return""" return self.calibration_score - + def combine_scores(self, slist): """Method to combine calibration score components. Combination is additive (mean). Change the function for other combination methods (multiplicative or minimum). diff --git a/catbond.py b/catbond.py index 1445011..9f7ee68 100644 --- a/catbond.py +++ b/catbond.py @@ -1,4 +1,3 @@ - import isleconfig import numpy as np import scipy.stats @@ -9,8 +8,11 @@ import sys, pdb import uuid + class CatBond(MetaInsuranceOrg): - def init(self, simulation, per_period_premium, owner, interest_rate = 0): # do we need simulation parameters + def init( + self, simulation, per_period_premium, owner, interest_rate=0 + ): # do we need simulation parameters self.simulation = simulation self.id = 0 self.underwritten_contracts = [] @@ -20,61 +22,89 @@ def init(self, simulation, per_period_premium, owner, interest_rate = 0): # do self.operational = True self.owner = owner self.per_period_dividend = per_period_premium - self.interest_rate = interest_rate # TODO: shift obtain_yield method to insurancesimulation, thereby making it unnecessary to drag parameters like self.interest_rate from instance to instance and from class to class - #self.simulation_no_risk_categories = self.simulation.simulation_parameters["no_categories"] - + self.interest_rate = ( + interest_rate + ) # TODO: shift obtain_yield method to insurancesimulation, thereby making it unnecessary to drag parameters like self.interest_rate from instance to instance and from class to class + # self.simulation_no_risk_categories = self.simulation.simulation_parameters["no_categories"] + # TODO: change start and InsuranceSimulation so that it iterates CatBonds - #old parent class init, cat bond class should be much smaller + # old parent class init, cat bond class should be much smaller def parent_init(self, simulation_parameters, agent_parameters): - #def init(self, simulation_parameters, agent_parameters): - self.simulation = simulation_parameters['simulation'] + # def init(self, simulation_parameters, agent_parameters): + self.simulation = simulation_parameters["simulation"] self.simulation_parameters = simulation_parameters - self.contract_runtime_dist = scipy.stats.randint(simulation_parameters["mean_contract_runtime"] - \ - simulation_parameters["contract_runtime_halfspread"], simulation_parameters["mean_contract_runtime"] \ - + simulation_parameters["contract_runtime_halfspread"] + 1) - self.default_contract_payment_period = simulation_parameters["default_contract_payment_period"] - self.id = agent_parameters['id'] - self.cash = agent_parameters['initial_cash'] + self.contract_runtime_dist = scipy.stats.randint( + simulation_parameters["mean_contract_runtime"] + - simulation_parameters["contract_runtime_halfspread"], + simulation_parameters["mean_contract_runtime"] + + simulation_parameters["contract_runtime_halfspread"] + + 1, + ) + self.default_contract_payment_period = simulation_parameters[ + "default_contract_payment_period" + ] + self.id = agent_parameters["id"] + self.cash = agent_parameters["initial_cash"] self.premium = agent_parameters["norm_premium"] - self.profit_target = agent_parameters['profit_target'] - self.acceptance_threshold = agent_parameters['initial_acceptance_threshold'] # 0.5 - self.acceptance_threshold_friction = agent_parameters['acceptance_threshold_friction'] # 0.9 #1.0 to switch off + self.profit_target = agent_parameters["profit_target"] + self.acceptance_threshold = agent_parameters[ + "initial_acceptance_threshold" + ] # 0.5 + self.acceptance_threshold_friction = agent_parameters[ + "acceptance_threshold_friction" + ] # 0.9 #1.0 to switch off self.interest_rate = agent_parameters["interest_rate"] self.reinsurance_limit = agent_parameters["reinsurance_limit"] self.simulation_no_risk_categories = simulation_parameters["no_categories"] - self.simulation_reinsurance_type = simulation_parameters["simulation_reinsurance_type"] - - rm_config = agent_parameters['riskmodel_config'] - self.riskmodel = RiskModel(damage_distribution=rm_config["damage_distribution"], \ - expire_immediately=rm_config["expire_immediately"], \ - cat_separation_distribution=rm_config["cat_separation_distribution"], \ - norm_premium=rm_config["norm_premium"], \ - category_number=rm_config["no_categories"], \ - init_average_exposure=rm_config["risk_value_mean"], \ - init_average_risk_factor=rm_config["risk_factor_mean"], \ - init_profit_estimate=rm_config["norm_profit_markup"], \ - margin_of_safety=rm_config["margin_of_safety"], \ - var_tail_prob=rm_config["var_tail_prob"], \ - inaccuracy=rm_config["inaccuracy_by_categ"]) - - self.category_reinsurance = [None for i in range(self.simulation_no_risk_categories)] - if self.simulation_reinsurance_type == 'non-proportional': - self.np_reinsurance_deductible_fraction = simulation_parameters["default_non-proportional_reinsurance_deductible"] - self.np_reinsurance_excess_fraction = simulation_parameters["default_non-proportional_reinsurance_excess"] - self.np_reinsurance_premium_share = simulation_parameters["default_non-proportional_reinsurance_premium_share"] + self.simulation_reinsurance_type = simulation_parameters[ + "simulation_reinsurance_type" + ] + + rm_config = agent_parameters["riskmodel_config"] + self.riskmodel = RiskModel( + damage_distribution=rm_config["damage_distribution"], + expire_immediately=rm_config["expire_immediately"], + cat_separation_distribution=rm_config["cat_separation_distribution"], + norm_premium=rm_config["norm_premium"], + category_number=rm_config["no_categories"], + init_average_exposure=rm_config["risk_value_mean"], + init_average_risk_factor=rm_config["risk_factor_mean"], + init_profit_estimate=rm_config["norm_profit_markup"], + margin_of_safety=rm_config["margin_of_safety"], + var_tail_prob=rm_config["var_tail_prob"], + inaccuracy=rm_config["inaccuracy_by_categ"], + ) + + self.category_reinsurance = [ + None for i in range(self.simulation_no_risk_categories) + ] + if self.simulation_reinsurance_type == "non-proportional": + self.np_reinsurance_deductible_fraction = simulation_parameters[ + "default_non-proportional_reinsurance_deductible" + ] + self.np_reinsurance_excess_fraction = simulation_parameters[ + "default_non-proportional_reinsurance_excess" + ] + self.np_reinsurance_premium_share = simulation_parameters[ + "default_non-proportional_reinsurance_premium_share" + ] self.obligations = [] self.underwritten_contracts = [] - #self.reinsurance_contracts = [] + # self.reinsurance_contracts = [] self.operational = True self.is_insurer = True self.is_reinsurer = False - + """set up risk value estimate variables""" - self.var_counter = 0 # sum over risk model inaccuracies for all contracts - self.var_counter_per_risk = 0 # average risk model inaccuracy across contracts - self.var_sum = 0 # sum over initial VaR for all contracts - self.counter_category = np.zeros(self.simulation_no_risk_categories) # var_counter disaggregated by category - self.var_category = np.zeros(self.simulation_no_risk_categories) # var_sum disaggregated by category + self.var_counter = 0 # sum over risk model inaccuracies for all contracts + self.var_counter_per_risk = 0 # average risk model inaccuracy across contracts + self.var_sum = 0 # sum over initial VaR for all contracts + self.counter_category = np.zeros( + self.simulation_no_risk_categories + ) # var_counter disaggregated by category + self.var_category = np.zeros( + self.simulation_no_risk_categories + ) # var_sum disaggregated by category def iterate(self, time): """obtain investments yield""" @@ -83,11 +113,22 @@ def iterate(self, time): """realize due payments""" self.effect_payments(time) if isleconfig.verbose: - print(time, ":", self.id, len(self.underwritten_contracts), self.cash, self.operational) - + print( + time, + ":", + self.id, + len(self.underwritten_contracts), + self.cash, + self.operational, + ) + """mature contracts""" print("Number of underwritten contracts ", len(self.underwritten_contracts)) - maturing = [contract for contract in self.underwritten_contracts if contract.expiration <= time] + maturing = [ + contract + for contract in self.underwritten_contracts + if contract.expiration <= time + ] for contract in maturing: self.underwritten_contracts.remove(contract) contract.mature(time) @@ -95,32 +136,45 @@ def iterate(self, time): """effect payments from contracts""" [contract.check_payment_due(time) for contract in self.underwritten_contracts] - + if self.underwritten_contracts == []: - self.mature_bond() #TODO: mature_bond method should check if operational - - else: #TODO: dividend should only be payed according to pre-arranged schedule, and only if no risk events have materialized so far + self.mature_bond() # TODO: mature_bond method should check if operational + + else: # TODO: dividend should only be payed according to pre-arranged schedule, and only if no risk events have materialized so far if self.operational: self.pay_dividends(time) - #self.estimated_var() # cannot compute VaR for catbond as catbond does not have a riskmodel - - #old parent class iterate, cat bond class should be much smaller - def parent_iterate(self, time): # TODO: split function so that only the sequence of events remains here and everything else is in separate methods + # self.estimated_var() # cannot compute VaR for catbond as catbond does not have a riskmodel + + # old parent class iterate, cat bond class should be much smaller + def parent_iterate( + self, time + ): # TODO: split function so that only the sequence of events remains here and everything else is in separate methods """obtain investments yield""" self.obtain_yield(time) """realize due payments""" self.effect_payments(time) if isleconfig.verbose: - print(time, ":", self.id, len(self.underwritten_contracts), self.cash, self.operational) + print( + time, + ":", + self.id, + len(self.underwritten_contracts), + self.cash, + self.operational, + ) self.make_reinsurance_claims(time) """mature contracts""" if isleconfig.verbose: print("Number of underwritten contracts ", len(self.underwritten_contracts)) - maturing = [contract for contract in self.underwritten_contracts if contract.expiration <= time] + maturing = [ + contract + for contract in self.underwritten_contracts + if contract.expiration <= time + ] for contract in maturing: self.underwritten_contracts.remove(contract) contract.mature(time) @@ -134,89 +188,183 @@ def parent_iterate(self, time): # TODO: split function so that only the s """request risks to be considered for underwriting in the next period and collect those for this period""" new_risks = [] if self.is_insurer: - new_risks += self.simulation.solicit_insurance_requests(self.id, self.cash) + new_risks += self.simulation.solicit_insurance_requests( + self.id, self.cash + ) if self.is_reinsurer: - new_risks += self.simulation.solicit_reinsurance_requests(self.id, self.cash) + new_risks += self.simulation.solicit_reinsurance_requests( + self.id, self.cash + ) contracts_offered = len(new_risks) try: assert contracts_offered > 2 * contracts_dissolved except: - print("Something wrong; agent {0:d} receives too few new contracts {1:d} <= {2:d}".format(self.id, contracts_offered, 2*contracts_dissolved), file=sys.stderr) - #print(self.id, " has ", len(self.underwritten_contracts), " & receives ", contracts_offered, " & lost ", contracts_dissolved) - - new_nonproportional_risks = [risk for risk in new_risks if risk.get("insurancetype")=='excess-of-loss' and risk["owner"] is not self] - new_risks = [risk for risk in new_risks if risk.get("insurancetype") in ['proportional', None] and risk["owner"] is not self] - - underwritten_risks = [{"value": contract.value, "category": contract.category, \ - "risk_factor": contract.risk_factor, "deductible": contract.deductible, \ - "excess": contract.excess, "insurancetype": contract.insurancetype, \ - "runtime": contract.runtime} for contract in self.underwritten_contracts if contract.reinsurance_share != 1.0] - + print( + "Something wrong; agent {0:d} receives too few new contracts {1:d} <= {2:d}".format( + self.id, contracts_offered, 2 * contracts_dissolved + ), + file=sys.stderr, + ) + # print(self.id, " has ", len(self.underwritten_contracts), " & receives ", contracts_offered, " & lost ", contracts_dissolved) + + new_nonproportional_risks = [ + risk + for risk in new_risks + if risk.get("insurancetype") == "excess-of-loss" + and risk["owner"] is not self + ] + new_risks = [ + risk + for risk in new_risks + if risk.get("insurancetype") in ["proportional", None] + and risk["owner"] is not self + ] + + underwritten_risks = [ + { + "value": contract.value, + "category": contract.category, + "risk_factor": contract.risk_factor, + "deductible": contract.deductible, + "excess": contract.excess, + "insurancetype": contract.insurancetype, + "runtime": contract.runtime, + } + for contract in self.underwritten_contracts + if contract.reinsurance_share != 1.0 + ] + """deal with non-proportional risks first as they must evaluate each request separatly, then with proportional ones""" for risk in new_nonproportional_risks: - accept, var_this_risk = self.riskmodel.evaluate(underwritten_risks, self.cash, risk) # TODO: change riskmodel.evaluate() to accept new risk to be evaluated and to account for existing non-proportional risks correctly -> DONE. + accept, var_this_risk = self.riskmodel.evaluate( + underwritten_risks, self.cash, risk + ) # TODO: change riskmodel.evaluate() to accept new risk to be evaluated and to account for existing non-proportional risks correctly -> DONE. if accept: - per_value_reinsurance_premium = self.np_reinsurance_premium_share * risk["periodized_total_premium"] * risk["runtime"] / risk["value"] #TODO: rename this to per_value_premium in insurancecontract.py to avoid confusion - contract = ReinsuranceContract(self, risk, time, per_value_reinsurance_premium, risk["runtime"], \ - self.default_contract_payment_period, \ - expire_immediately=self.simulation_parameters["expire_immediately"], \ - initial_VaR=var_this_risk, \ - insurancetype=risk["insurancetype"]) # TODO: implement excess of loss for reinsurance contracts + per_value_reinsurance_premium = ( + self.np_reinsurance_premium_share + * risk["periodized_total_premium"] + * risk["runtime"] + / risk["value"] + ) # TODO: rename this to per_value_premium in insurancecontract.py to avoid confusion + contract = ReinsuranceContract( + self, + risk, + time, + per_value_reinsurance_premium, + risk["runtime"], + self.default_contract_payment_period, + expire_immediately=self.simulation_parameters[ + "expire_immediately" + ], + initial_VaR=var_this_risk, + insurancetype=risk["insurancetype"], + ) # TODO: implement excess of loss for reinsurance contracts self.underwritten_contracts.append(contract) - #pass # TODO: write this nonproportional risk acceptance decision section based on commented code in the lines above this -> DONE. - + # pass # TODO: write this nonproportional risk acceptance decision section based on commented code in the lines above this -> DONE. + """make underwriting decisions, category-wise""" # TODO: Enable reinsurance shares other tan 0.0 and 1.0 - expected_profit, acceptable_by_category, var_per_risk_per_categ = self.riskmodel.evaluate(underwritten_risks, self.cash) + expected_profit, acceptable_by_category, var_per_risk_per_categ = self.riskmodel.evaluate( + underwritten_risks, self.cash + ) - #if expected_profit * 1./self.cash < self.profit_target: + # if expected_profit * 1./self.cash < self.profit_target: # self.acceptance_threshold = ((self.acceptance_threshold - .4) * 5. * self.acceptance_threshold_friction) / 5. + .4 - #else: + # else: # self.acceptance_threshold = (1 - self.acceptance_threshold_friction * (1 - (self.acceptance_threshold - .4) * 5.)) / 5. + .4 - growth_limit = max(50, 2 * len(self.underwritten_contracts) + contracts_dissolved) + growth_limit = max( + 50, 2 * len(self.underwritten_contracts) + contracts_dissolved + ) if sum(acceptable_by_category) > growth_limit: acceptable_by_category = np.asarray(acceptable_by_category) - acceptable_by_category = acceptable_by_category * growth_limit / sum(acceptable_by_category) + acceptable_by_category = ( + acceptable_by_category * growth_limit / sum(acceptable_by_category) + ) acceptable_by_category = np.int64(np.round(acceptable_by_category)) not_accepted_risks = [] for categ_id in range(len(acceptable_by_category)): - categ_risks = [risk for risk in new_risks if risk["category"] == categ_id] + categ_risks = [ + risk for risk in new_risks if risk["category"] == categ_id + ] new_risks = [risk for risk in new_risks if risk["category"] != categ_id] - categ_risks = sorted(categ_risks, key = lambda risk: risk["risk_factor"]) + categ_risks = sorted(categ_risks, key=lambda risk: risk["risk_factor"]) i = 0 if isleconfig.verbose: - print("InsuranceFirm underwrote: ", len(self.underwritten_contracts), " will accept: ", acceptable_by_category[categ_id], " out of ", len(categ_risks), "acceptance threshold: ", self.acceptance_threshold) - while (acceptable_by_category[categ_id] > 0 and len(categ_risks) > i): #\ - #and categ_risks[i]["risk_factor"] < self.acceptance_threshold): - if categ_risks[i].get("contract") is not None: #categ_risks[i]["reinsurance"]: - if categ_risks[i]["contract"].expiration > time: # required to rule out contracts that have exploded in the meantime - #print("ACCEPTING", categ_risks[i]["contract"].expiration, categ_risks[i]["expiration"], categ_risks[i]["identifier"], categ_risks[i].get("contract").terminating) - contract = ReinsuranceContract(self, categ_risks[i], time, \ - self.simulation.get_market_premium(), categ_risks[i]["expiration"] - time, \ - self.default_contract_payment_period, \ - expire_immediately=self.simulation_parameters["expire_immediately"], ) + print( + "InsuranceFirm underwrote: ", + len(self.underwritten_contracts), + " will accept: ", + acceptable_by_category[categ_id], + " out of ", + len(categ_risks), + "acceptance threshold: ", + self.acceptance_threshold, + ) + while ( + acceptable_by_category[categ_id] > 0 and len(categ_risks) > i + ): # \ + # and categ_risks[i]["risk_factor"] < self.acceptance_threshold): + if ( + categ_risks[i].get("contract") is not None + ): # categ_risks[i]["reinsurance"]: + if ( + categ_risks[i]["contract"].expiration > time + ): # required to rule out contracts that have exploded in the meantime + # print("ACCEPTING", categ_risks[i]["contract"].expiration, categ_risks[i]["expiration"], categ_risks[i]["identifier"], categ_risks[i].get("contract").terminating) + contract = ReinsuranceContract( + self, + categ_risks[i], + time, + self.simulation.get_market_premium(), + categ_risks[i]["expiration"] - time, + self.default_contract_payment_period, + expire_immediately=self.simulation_parameters[ + "expire_immediately" + ], + ) self.underwritten_contracts.append(contract) - #categ_risks[i]["contract"].reincontract = contract + # categ_risks[i]["contract"].reincontract = contract # TODO: move this to insurancecontract (ca. line 14) -> DONE # TODO: do not write into other object's properties, use setter -> DONE - assert categ_risks[i]["contract"].expiration >= contract.expiration, "Reinsurancecontract lasts longer than insurancecontract: {0:d}>{1:d} (EXPIRATION2: {2:d} Time: {3:d})".format(contract.expiration, categ_risks[i]["contract"].expiration, categ_risks[i]["expiration"], time) - #else: + assert ( + categ_risks[i]["contract"].expiration + >= contract.expiration + ), "Reinsurancecontract lasts longer than insurancecontract: {0:d}>{1:d} (EXPIRATION2: {2:d} Time: {3:d})".format( + contract.expiration, + categ_risks[i]["contract"].expiration, + categ_risks[i]["expiration"], + time, + ) + # else: # pass else: - contract = InsuranceContract(self, categ_risks[i], time, self.simulation.get_market_premium(), \ - self.contract_runtime_dist.rvs(), \ - self.default_contract_payment_period, \ - expire_immediately=self.simulation_parameters["expire_immediately"], \ - initial_VaR = var_per_risk_per_categ[categ_id]) + contract = InsuranceContract( + self, + categ_risks[i], + time, + self.simulation.get_market_premium(), + self.contract_runtime_dist.rvs(), + self.default_contract_payment_period, + expire_immediately=self.simulation_parameters[ + "expire_immediately" + ], + initial_VaR=var_per_risk_per_categ[categ_id], + ) self.underwritten_contracts.append(contract) - acceptable_by_category[categ_id] -= 1 # TODO: allow different values per risk (i.e. sum over value (and reinsurance_share) or exposure instead of counting) + acceptable_by_category[ + categ_id + ] -= ( + 1 + ) # TODO: allow different values per risk (i.e. sum over value (and reinsurance_share) or exposure instead of counting) i += 1 not_accepted_risks += categ_risks[i:] - not_accepted_risks = [risk for risk in not_accepted_risks if risk.get("contract") is None] + not_accepted_risks = [ + risk for risk in not_accepted_risks if risk.get("contract") is None + ] # seek reinsurance if self.is_insurer: @@ -224,28 +372,31 @@ def parent_iterate(self, time): # TODO: split function so that only the s self.ask_reinsurance(time) # return unacceptables - #print(self.id, " now has ", len(self.underwritten_contracts), " & returns ", len(not_accepted_risks)) + # print(self.id, " now has ", len(self.underwritten_contracts), " & returns ", len(not_accepted_risks)) self.simulation.return_risks(not_accepted_risks) - #not implemented - #"""adjust liquidity, borrow or invest""" - #pass + # not implemented + # """adjust liquidity, borrow or invest""" + # pass self.estimated_var() - + def set_owner(self, owner): self.owner = owner if isleconfig.verbose: print("SOLD") - #pdb.set_trace() - + # pdb.set_trace() + def set_contract(self, contract): self.underwritten_contracts.append(contract) - + def mature_bond(self): - obligation = {"amount": self.cash, "recipient": self.simulation, "due_time": 1, "purpose": 'mature'} + obligation = { + "amount": self.cash, + "recipient": self.simulation, + "due_time": 1, + "purpose": "mature", + } self.pay(obligation) self.simulation.delete_agents("catbond", [self]) self.operational = False - - diff --git a/compute_profits_losses_from_cash.py b/compute_profits_losses_from_cash.py index ed6691a..865f749 100644 --- a/compute_profits_losses_from_cash.py +++ b/compute_profits_losses_from_cash.py @@ -9,11 +9,9 @@ infile.close() filename = "data/" + r + "_" + ft + "profitslosses.dat" outfile = open(filename, "w") - + for series in data: - outputdata = [series[i]-series[i-1] for i in range(1, len(series))] + outputdata = [series[i] - series[i - 1] for i in range(1, len(series))] outfile.write(str(outputdata) + "\n") - - outfile.close() - + outfile.close() diff --git a/condition_aux.py b/condition_aux.py index 9ffab5b..eefc0f5 100644 --- a/condition_aux.py +++ b/condition_aux.py @@ -6,20 +6,98 @@ """Data""" """Bloomberg size data for US firms""" -insurance_firm_sizes_empirical_2017 = [42.4701, 108.0418, 110.2641, 114.437, 130.2988, 133.674, 146.438, 152.3354, - 239.032, 337.689, 375.914, 376.988, 395.859, 436.191, 482.503, 585.824, 667.849, - 842.264, 894.848, 896.227, 904.873, 1231.126, 1357.016, 1454.999, 1518.236, - 1665.859, 1681.94, 1737.9198, 1771.21, 1807.279, 1989.742, 2059.921, 2385.485, - 2756.695, 2947.244, 3014.3, 3659.2, 3840.1, 4183.431, 4929.197, 5101.323, - 5224.622, 5900.881, 7686.431, 8376.2, 8439.743, 8764.0, 9095.0, 11198.34, - 14433.0, 15469.6, 19403.5, 21843.0, 23192.374, 24299.917, 25218.63, 31843.0, - 32051.658, 32805.016, 38701.2, 56567.0, 60658.0, 79586.0, 103483.0, 112422.0, - 167022.0, 225260.0, 498301.0, 702095.0] -reinsurance_firm_sizes_empirical_2017 = [396.898, 627.808, 6644.189, 15226.131, 25384.317, 23591.792, 3357.393, - 13606.422, 4671.794, 614.121, 60514.818, 24760.177, 2001.669, 182.2, 12906.4] +insurance_firm_sizes_empirical_2017 = [ + 42.4701, + 108.0418, + 110.2641, + 114.437, + 130.2988, + 133.674, + 146.438, + 152.3354, + 239.032, + 337.689, + 375.914, + 376.988, + 395.859, + 436.191, + 482.503, + 585.824, + 667.849, + 842.264, + 894.848, + 896.227, + 904.873, + 1231.126, + 1357.016, + 1454.999, + 1518.236, + 1665.859, + 1681.94, + 1737.9198, + 1771.21, + 1807.279, + 1989.742, + 2059.921, + 2385.485, + 2756.695, + 2947.244, + 3014.3, + 3659.2, + 3840.1, + 4183.431, + 4929.197, + 5101.323, + 5224.622, + 5900.881, + 7686.431, + 8376.2, + 8439.743, + 8764.0, + 9095.0, + 11198.34, + 14433.0, + 15469.6, + 19403.5, + 21843.0, + 23192.374, + 24299.917, + 25218.63, + 31843.0, + 32051.658, + 32805.016, + 38701.2, + 56567.0, + 60658.0, + 79586.0, + 103483.0, + 112422.0, + 167022.0, + 225260.0, + 498301.0, + 702095.0, +] +reinsurance_firm_sizes_empirical_2017 = [ + 396.898, + 627.808, + 6644.189, + 15226.131, + 25384.317, + 23591.792, + 3357.393, + 13606.422, + 4671.794, + 614.121, + 60514.818, + 24760.177, + 2001.669, + 182.2, + 12906.4, +] """Functions""" + def condition_stationary_state(series): """Stationarity test function for time series. Tests if the mean of the last 25% of the time series is within 1-2 standard deviation of the mean of the middle section (between 25% and 75% of the time series). The first @@ -29,24 +107,29 @@ def condition_stationary_state(series): Returns: Calibration score between 0 and 1. Is 1 if last 25% are within one standard deviation, between 0 and 1 if they are between 1 and 2 standard deviations, 0 otherwise.""" - + """Compute means and standard deviation""" - mean_reference = np.mean(series[int(len(series)*.25):int(len(series)*.75)]) - std_reference = np.std(series[int(len(series)*.25):int(len(series)*.75)]) - mean_test = np.mean(series[int(len(series)*.75):int(len(series)*1.)]) - + mean_reference = np.mean(series[int(len(series) * 0.25) : int(len(series) * 0.75)]) + std_reference = np.std(series[int(len(series) * 0.25) : int(len(series) * 0.75)]) + mean_test = np.mean(series[int(len(series) * 0.75) : int(len(series) * 1.0)]) + """Compute score""" score = 1 + (np.abs(mean_test - mean_reference) - std_reference) / std_reference - score = 1 if score>1 else score - score = 0 if score<0 else score - + score = 1 if score > 1 else score + score = 0 if score < 0 else score + """Set score to one if standard deviation is zero""" - if score == np.nan and np.std(series[int(len(series)*.25):int(len(series)*.75)]) == 0: + if ( + score == np.nan + and np.std(series[int(len(series) * 0.25) : int(len(series) * 0.75)]) == 0 + ): score = 1 return score - -def scaler(series): # TODO: find a better way to scale heavy-tailed distributions than to use standard score scaling on logs + +def scaler( + series +): # TODO: find a better way to scale heavy-tailed distributions than to use standard score scaling on logs """Function to do a standard score scaling of the log of a heavy-tailed distribution. This is used to calibrate distributions where the unit is not important (distributions of sizes of firms e.g.). This would be perfectly appropriate for lognormal distributions, but should work reasonably well for calibration of other heavy-tailed @@ -56,10 +139,10 @@ def scaler(series): # TODO: find a better way to scale heavy-tailed distribution Returns: Calibratied series.""" series = np.asarray(series) - assert (series>1).all() + assert (series > 1).all() logseries = np.log(series) mean = np.mean(logseries) std = np.std(logseries) - z = (logseries - mean)/std + z = (logseries - mean) / std newseries = np.exp(z) return newseries diff --git a/distribution_wrapper_test.py b/distribution_wrapper_test.py index b4bfa97..81734f9 100644 --- a/distribution_wrapper_test.py +++ b/distribution_wrapper_test.py @@ -5,14 +5,20 @@ import pdb non_truncated_dist = scipy.stats.pareto(b=2, loc=0, scale=0.5) -truncated_dist = TruncatedDistWrapper(lower_bound=0.6, upper_bound=1., dist=non_truncated_dist) -reinsurance_dist = ReinsuranceDistWrapper(lower_bound=0.85, upper_bound=0.95, dist=truncated_dist) +truncated_dist = TruncatedDistWrapper( + lower_bound=0.6, upper_bound=1.0, dist=non_truncated_dist +) +reinsurance_dist = ReinsuranceDistWrapper( + lower_bound=0.85, upper_bound=0.95, dist=truncated_dist +) x1 = np.linspace(non_truncated_dist.ppf(0.01), non_truncated_dist.ppf(0.99), 100) -x2 = np.linspace(truncated_dist.ppf(0.01), truncated_dist.ppf(1.), 100) -x3 = np.linspace(reinsurance_dist.ppf(0.01), reinsurance_dist.ppf(1.), 100) +x2 = np.linspace(truncated_dist.ppf(0.01), truncated_dist.ppf(1.0), 100) +x3 = np.linspace(reinsurance_dist.ppf(0.01), reinsurance_dist.ppf(1.0), 100) x_val_1 = reinsurance_dist.lower_bound -x_val_2 = truncated_dist.upper_bound - (reinsurance_dist.upper_bound - reinsurance_dist.lower_bound) +x_val_2 = truncated_dist.upper_bound - ( + reinsurance_dist.upper_bound - reinsurance_dist.lower_bound +) x_val_3 = reinsurance_dist.upper_bound x_val_4 = truncated_dist.upper_bound diff --git a/distributionreinsurance.py b/distributionreinsurance.py index fd85eb3..8d9dcb2 100644 --- a/distributionreinsurance.py +++ b/distributionreinsurance.py @@ -4,7 +4,8 @@ import scipy import pdb -class ReinsuranceDistWrapper(): + +class ReinsuranceDistWrapper: def __init__(self, dist, lower_bound=None, upper_bound=None): assert lower_bound is not None or upper_bound is not None self.dist = dist @@ -17,57 +18,72 @@ def __init__(self, dist, lower_bound=None, upper_bound=None): assert self.upper_bound > self.lower_bound self.redistributed_share = dist.cdf(upper_bound) - dist.cdf(lower_bound) - def pdf(self, x): x = np.array(x, ndmin=1) - r = map(lambda Y: self.dist.pdf(Y) if Y < self.lower_bound \ - else np.inf if Y==self.lower_bound \ - else self.dist.pdf(Y + self.upper_bound - self.lower_bound), x) + r = map( + lambda Y: self.dist.pdf(Y) + if Y < self.lower_bound + else np.inf + if Y == self.lower_bound + else self.dist.pdf(Y + self.upper_bound - self.lower_bound), + x, + ) r = np.array(list(r)) if len(r.flatten()) == 1: r = float(r) return r - + def cdf(self, x): x = np.array(x, ndmin=1) - r = map(lambda Y: self.dist.cdf(Y) if Y < self.lower_bound \ - else self.dist.cdf(Y + self.upper_bound - self.lower_bound), x) + r = map( + lambda Y: self.dist.cdf(Y) + if Y < self.lower_bound + else self.dist.cdf(Y + self.upper_bound - self.lower_bound), + x, + ) r = np.array(list(r)) if len(r.flatten()) == 1: r = float(r) return r - + def ppf(self, x): x = np.array(x, ndmin=1) assert (x >= 0).all() and (x <= 1).all() - r = map(lambda Y: self.dist.ppf(Y) if Y <= self.dist.cdf(self.lower_bound) \ - else self.dist.ppf(self.dist.cdf(self.lower_bound)) if Y <= self.dist.cdf(self.upper_bound) \ - else self.dist.ppf(Y) - self.upper_bound + self.lower_bound, x) + r = map( + lambda Y: self.dist.ppf(Y) + if Y <= self.dist.cdf(self.lower_bound) + else self.dist.ppf(self.dist.cdf(self.lower_bound)) + if Y <= self.dist.cdf(self.upper_bound) + else self.dist.ppf(Y) - self.upper_bound + self.lower_bound, + x, + ) r = np.array(list(r)) if len(r.flatten()) == 1: r = float(r) return r - + def rvs(self, size=1): sample = self.dist.rvs(size=size) - sample1 = sample[sample<=self.lower_bound] - sample2 = sample[sample>self.lower_bound] - sample3 = sample2[sample2>=self.upper_bound] - sample2 = sample2[sample2 self.lower_bound] + sample3 = sample2[sample2 >= self.upper_bound] + sample2 = sample2[sample2 < self.upper_bound] + sample2 = np.ones(len(sample2)) * self.lower_bound - sample3 = sample3 -self.upper_bound + self.lower_bound - - sample = np.append(np.append(sample1,sample2),sample3) + sample3 = sample3 - self.upper_bound + self.lower_bound + + sample = np.append(np.append(sample1, sample2), sample3) return sample[:size] if __name__ == "__main__": non_truncated = scipy.stats.pareto(b=2, loc=0, scale=0.5) - #truncated = ReinsuranceDistWrapper(lower_bound=0, upper_bound=1, dist=non_truncated) - truncated = ReinsuranceDistWrapper(lower_bound=0.9, upper_bound=1.1, dist=non_truncated) + # truncated = ReinsuranceDistWrapper(lower_bound=0, upper_bound=1, dist=non_truncated) + truncated = ReinsuranceDistWrapper( + lower_bound=0.9, upper_bound=1.1, dist=non_truncated + ) x = np.linspace(non_truncated.ppf(0.01), non_truncated.ppf(0.99), 100) x2 = np.linspace(truncated.ppf(0.01), truncated.ppf(0.99), 100) - #pdb.set_trace() + # pdb.set_trace() diff --git a/distributiontruncated.py b/distributiontruncated.py index d862d8d..18b8552 100644 --- a/distributiontruncated.py +++ b/distributiontruncated.py @@ -3,55 +3,74 @@ from math import ceil import scipy.integrate -class TruncatedDistWrapper(): + +class TruncatedDistWrapper: def __init__(self, dist, lower_bound=0, upper_bound=1): self.dist = dist self.normalizing_factor = dist.cdf(upper_bound) - dist.cdf(lower_bound) self.lower_bound = lower_bound self.upper_bound = upper_bound assert self.upper_bound > self.lower_bound - + def pdf(self, x): x = np.array(x, ndmin=1) - r = map(lambda Y: self.dist.pdf(Y) / self.normalizing_factor \ - if (Y >= self.lower_bound and Y <= self.upper_bound) else 0, x) + r = map( + lambda Y: self.dist.pdf(Y) / self.normalizing_factor + if (Y >= self.lower_bound and Y <= self.upper_bound) + else 0, + x, + ) r = np.array(list(r)) if len(r.flatten()) == 1: r = float(r) return r - + def cdf(self, x): x = np.array(x, ndmin=1) - r = map(lambda Y: 0 if Y < self.lower_bound else 1 if Y > self.upper_bound \ - else (self.dist.cdf(Y) - self.dist.cdf(self.lower_bound))/ self.normalizing_factor, x) + r = map( + lambda Y: 0 + if Y < self.lower_bound + else 1 + if Y > self.upper_bound + else (self.dist.cdf(Y) - self.dist.cdf(self.lower_bound)) + / self.normalizing_factor, + x, + ) r = np.array(list(r)) if len(r.flatten()) == 1: r = float(r) return r - + def ppf(self, x): x = np.array(x, ndmin=1) assert (x >= 0).all() and (x <= 1).all() - return self.dist.ppf(x * self.normalizing_factor + self.dist.cdf(self.lower_bound)) + return self.dist.ppf( + x * self.normalizing_factor + self.dist.cdf(self.lower_bound) + ) def rvs(self, size=1): init_sample_size = int(ceil(size / self.normalizing_factor * 1.1)) sample = self.dist.rvs(size=init_sample_size) - sample = sample[sample>=self.lower_bound] - sample = sample[sample<=self.upper_bound] + sample = sample[sample >= self.lower_bound] + sample = sample[sample <= self.upper_bound] while len(sample) < size: sample = np.append(sample, self.rvs(size - len(sample))) - return sample[:size] - + return sample[:size] + def mean(self): - mean_estimate, mean_error = scipy.integrate.quad(lambda Y: Y*self.pdf(Y), self.lower_bound, self.upper_bound) + mean_estimate, mean_error = scipy.integrate.quad( + lambda Y: Y * self.pdf(Y), self.lower_bound, self.upper_bound + ) return mean_estimate + if __name__ == "__main__": non_truncated = scipy.stats.pareto(b=2, loc=0, scale=0.5) - truncated = TruncatedDistWrapper(lower_bound=0.55, upper_bound=1., dist=non_truncated) + truncated = TruncatedDistWrapper( + lower_bound=0.55, upper_bound=1.0, dist=non_truncated + ) x = np.linspace(non_truncated.ppf(0.01), non_truncated.ppf(0.99), 100) x2 = np.linspace(truncated.ppf(0.01), truncated.ppf(0.99), 100) - + print(truncated.mean()) diff --git a/ensemble.py b/ensemble.py index 92c3d49..4af37b7 100644 --- a/ensemble.py +++ b/ensemble.py @@ -1,6 +1,6 @@ -#This script allows to launch an ensemble of simulations for different number of risks models. -#It can be run locally if no argument is passed when called from the terminal. -#It can be run in the cloud if it is passed as argument the server that will be used. +# This script allows to launch an ensemble of simulations for different number of risks models. +# It can be run locally if no argument is passed when called from the terminal. +# It can be run in the cloud if it is passed as argument the server that will be used. import sys import random import os @@ -16,11 +16,10 @@ from sandman2.api import operation, Session - @operation def agg(*outputs): # do nothing - return outputs + return outputs def rake(hostname): @@ -29,63 +28,67 @@ def rake(hostname): """Configuration of the ensemble""" - replications = 70 #Number of replications to be carried out for each configuration. Usually one risk model, two risk models, three risk models, four risk models. + replications = ( + 70 + ) # Number of replications to be carried out for each configuration. Usually one risk model, two risk models, three risk models, four risk models. model = start.main - m = operation(model, include_modules = True) + m = operation(model, include_modules=True) - riskmodels = [1,2,3,4] #The number of risk models that will be used. + riskmodels = [1, 2, 3, 4] # The number of risk models that will be used. parameters = isleconfig.simulation_parameters - nums = {'1': 'one', - '2': 'two', - '3': 'three', - '4': 'four', - '5': 'five', - '6': 'six', - '7': 'seven', - '8': 'eight', - '9': 'nine'} + nums = { + "1": "one", + "2": "two", + "3": "three", + "4": "four", + "5": "five", + "6": "six", + "7": "seven", + "8": "eight", + "9": "nine", + } """Configure the return values and corresponding file suffixes where they should be saved""" - requested_logs = {'total_cash': '_cash.dat', - 'total_excess_capital': '_excess_capital.dat', - 'total_profitslosses': '_profitslosses.dat', - 'total_contracts': '_contracts.dat', - 'total_operational': '_operational.dat', - 'total_reincash': '_reincash.dat', - 'total_reinexcess_capital': '_reinexcess_capital.dat', - 'total_reinprofitslosses': '_reinprofitslosses.dat', - 'total_reincontracts': '_reincontracts.dat', - 'total_reinoperational': '_reinoperational.dat', - 'total_catbondsoperational': '_total_catbondsoperational.dat', - 'market_premium': '_premium.dat', - 'market_reinpremium': '_reinpremium.dat', - 'cumulative_bankruptcies': '_cumulative_bankruptcies.dat', - 'cumulative_market_exits': '_cumulative_market_exits', # TODO: correct filename - 'cumulative_unrecovered_claims': '_cumulative_unrecovered_claims.dat', - 'cumulative_claims': '_cumulative_claims.dat', - 'insurance_firms_cash': '_insurance_firms_cash.dat', - 'reinsurance_firms_cash': '_reinsurance_firms_cash.dat', - 'market_diffvar': '_market_diffvar.dat', - 'rc_event_schedule_initial': '_rc_event_schedule.dat', - 'rc_event_damage_initial': '_rc_event_damage.dat', - 'number_riskmodels': '_number_riskmodels.dat' - } - + requested_logs = { + "total_cash": "_cash.dat", + "total_excess_capital": "_excess_capital.dat", + "total_profitslosses": "_profitslosses.dat", + "total_contracts": "_contracts.dat", + "total_operational": "_operational.dat", + "total_reincash": "_reincash.dat", + "total_reinexcess_capital": "_reinexcess_capital.dat", + "total_reinprofitslosses": "_reinprofitslosses.dat", + "total_reincontracts": "_reincontracts.dat", + "total_reinoperational": "_reinoperational.dat", + "total_catbondsoperational": "_total_catbondsoperational.dat", + "market_premium": "_premium.dat", + "market_reinpremium": "_reinpremium.dat", + "cumulative_bankruptcies": "_cumulative_bankruptcies.dat", + "cumulative_market_exits": "_cumulative_market_exits", # TODO: correct filename + "cumulative_unrecovered_claims": "_cumulative_unrecovered_claims.dat", + "cumulative_claims": "_cumulative_claims.dat", + "insurance_firms_cash": "_insurance_firms_cash.dat", + "reinsurance_firms_cash": "_reinsurance_firms_cash.dat", + "market_diffvar": "_market_diffvar.dat", + "rc_event_schedule_initial": "_rc_event_schedule.dat", + "rc_event_damage_initial": "_rc_event_damage.dat", + "number_riskmodels": "_number_riskmodels.dat", + } + if isleconfig.slim_log: - for name in ['insurance_firms_cash', 'reinsurance_firms_cash']: + for name in ["insurance_firms_cash", "reinsurance_firms_cash"]: del requested_logs[name] - + assert "number_riskmodels" in requested_logs - """Configure log directory and ensure that the directory exists""" dir_prefix = "/data/" directory = os.getcwd() + dir_prefix - try: #Here it is checked whether the directory to collect the results exists or not. If not it is created. + try: # Here it is checked whether the directory to collect the results exists or not. If not it is created. os.stat(directory) except: os.mkdir(directory) @@ -95,78 +98,124 @@ def rake(hostname): filename = os.getcwd() + dir_prefix + nums[str(i)] + "_history_logs.dat" if os.path.exists(filename): os.remove(filename) - """Setup of the simulations""" - setup = SetupSim() #Here the setup for the simulation is done. - [general_rc_event_schedule, general_rc_event_damage, np_seeds, random_seeds] = setup.obtain_ensemble(replications) #Since this script is used to carry out simulations in the cloud will usually have more than 1 replication.. - save_iter = isleconfig.simulation_parameters["max_time"] + 2 # never save simulation state in ensemble runs (resuming is impossible anyway) - - for i in riskmodels: #In this loop the parameters, schedules and random seeds for every run are prepared. Different risk models will be run with the same schedule, damage size and random seed for a fair comparison. - - simulation_parameters = copy.copy(parameters) #Here the parameters used for the simulation are loaded. Clone is needed otherwise all the runs will be carried out with the last number of thee loop. - simulation_parameters["no_riskmodels"] = i #Since we want to obtain ensembles for different number of risk models, we vary here the number of risks models. - job = [m(simulation_parameters, general_rc_event_schedule[x], general_rc_event_damage[x], np_seeds[x], random_seeds[x], save_iter, list(requested_logs.keys())) for x in range(replications)] #Here is assembled each job with the corresponding: simulation parameters, time events, damage events, seeds, simulation state save interval (never, i.e. longer than max_time), and list of requested logs. - jobs.append(job) #All jobs are collected in the jobs list. + setup = SetupSim() # Here the setup for the simulation is done. + [ + general_rc_event_schedule, + general_rc_event_damage, + np_seeds, + random_seeds, + ] = setup.obtain_ensemble( + replications + ) # Since this script is used to carry out simulations in the cloud will usually have more than 1 replication.. + save_iter = ( + isleconfig.simulation_parameters["max_time"] + 2 + ) # never save simulation state in ensemble runs (resuming is impossible anyway) + + for ( + i + ) in ( + riskmodels + ): # In this loop the parameters, schedules and random seeds for every run are prepared. Different risk models will be run with the same schedule, damage size and random seed for a fair comparison. + + simulation_parameters = copy.copy( + parameters + ) # Here the parameters used for the simulation are loaded. Clone is needed otherwise all the runs will be carried out with the last number of thee loop. + simulation_parameters[ + "no_riskmodels" + ] = ( + i + ) # Since we want to obtain ensembles for different number of risk models, we vary here the number of risks models. + job = [ + m( + simulation_parameters, + general_rc_event_schedule[x], + general_rc_event_damage[x], + np_seeds[x], + random_seeds[x], + save_iter, + list(requested_logs.keys()), + ) + for x in range(replications) + ] # Here is assembled each job with the corresponding: simulation parameters, time events, damage events, seeds, simulation state save interval (never, i.e. longer than max_time), and list of requested logs. + jobs.append(job) # All jobs are collected in the jobs list. """Here the jobs are submitted""" with Session(host=hostname, default_cb_to_stdout=True) as sess: - for job in jobs: #If there are 4 risk models jobs will be a list with 4 elements. - + for ( + job + ) in jobs: # If there are 4 risk models jobs will be a list with 4 elements. + """Run simulation and obtain result""" result = sess.submit(job) - - + """find number of riskmodels from log""" delistified_result = [listify.delistify(list(res)) for res in result] - #nrmidx = result[0][-1].index("number_riskmodels") - #nrm = result[0][nrmidx] + # nrmidx = result[0][-1].index("number_riskmodels") + # nrm = result[0][nrmidx] nrm = delistified_result[0]["number_riskmodels"] """These are the files created to collect the results""" wfiles_dict = {} logfile_dict = {} - + for name in requested_logs.keys(): if "rc_event" in name or "number_riskmodels" in name: - logfile_dict[name] = os.getcwd() + dir_prefix + "check_" + str(nums[str(nrm)]) + requested_logs[name] + logfile_dict[name] = ( + os.getcwd() + + dir_prefix + + "check_" + + str(nums[str(nrm)]) + + requested_logs[name] + ) elif "firms_cash" in name: - logfile_dict[name] = os.getcwd() + dir_prefix + "record_" + str(nums[str(nrm)]) + requested_logs[name] + logfile_dict[name] = ( + os.getcwd() + + dir_prefix + + "record_" + + str(nums[str(nrm)]) + + requested_logs[name] + ) else: - logfile_dict[name] = os.getcwd() + dir_prefix + str(nums[str(nrm)]) + requested_logs[name] + logfile_dict[name] = ( + os.getcwd() + + dir_prefix + + str(nums[str(nrm)]) + + requested_logs[name] + ) for name in logfile_dict: wfiles_dict[name] = open(logfile_dict[name], "w") - """Recreate logger object locally and save logs""" - + """Create local object""" L = logger.Logger() for i in range(len(job)): """Populate logger object with logs obtained from remote simulation run""" L.restore_logger_object(list(result[i])) - + """Save logs as dict (to _history_logs.dat)""" L.save_log(True) - + """Save logs as indivitual files""" for name in logfile_dict: wfiles_dict[name].write(str(delistified_result[i][name]) + "\n") - + """Once the data is stored in disk the files are closed""" for name in logfile_dict: wfiles_dict[name].close() del wfiles_dict[name] -if __name__ == '__main__': +if __name__ == "__main__": host = None if len(sys.argv) > 1: - host = sys.argv[1] #The server is passed as an argument. + host = sys.argv[1] # The server is passed as an argument. rake(host) diff --git a/genericagent.py b/genericagent.py index 8e58efe..4985372 100644 --- a/genericagent.py +++ b/genericagent.py @@ -1,7 +1,8 @@ - -class GenericAgent(): +class GenericAgent: def __init__(self, *args, **kwargs): self.init(*args, **kwargs) - + def init(*args, **kwargs): - assert False, "Error: GenericAgent init method should have been overridden but was not." + assert ( + False + ), "Error: GenericAgent init method should have been overridden but was not." diff --git a/genericagentabce.py b/genericagentabce.py index c0eb884..28d9531 100644 --- a/genericagentabce.py +++ b/genericagentabce.py @@ -1,4 +1,5 @@ import abce + class GenericAgent(abce.Agent): pass diff --git a/insurancecontract.py b/insurancecontract.py index d331a8d..55a8fc3 100644 --- a/insurancecontract.py +++ b/insurancecontract.py @@ -11,11 +11,35 @@ class InsuranceContract(MetaInsuranceContract): The signature of this class' constructor is the same as that of the InsuranceContract constructor. The class has two methods (explode, mature) that overwrite methods in InsuranceContract.""" - def __init__(self, insurer, properties, time, premium, runtime, payment_period, expire_immediately, initial_VaR=0.,\ - insurancetype="proportional", deductible_fraction=None, excess_fraction=None, reinsurance=0): - super(InsuranceContract, self).__init__(insurer, properties, time, premium, runtime, payment_period, \ - expire_immediately, initial_VaR, insurancetype, deductible_fraction, - excess_fraction, reinsurance) + def __init__( + self, + insurer, + properties, + time, + premium, + runtime, + payment_period, + expire_immediately, + initial_VaR=0.0, + insurancetype="proportional", + deductible_fraction=None, + excess_fraction=None, + reinsurance=0, + ): + super(InsuranceContract, self).__init__( + insurer, + properties, + time, + premium, + runtime, + payment_period, + expire_immediately, + initial_VaR, + insurancetype, + deductible_fraction, + excess_fraction, + reinsurance, + ) self.risk_data = properties @@ -34,10 +58,14 @@ def explode(self, time, uniform_value, damage_extent): if uniform_value < self.risk_factor: # if True: claim = min(self.excess, damage_extent * self.value) - self.deductible - self.insurer.register_claim(claim) #Every insurance claim made is immediately registered. + self.insurer.register_claim( + claim + ) # Every insurance claim made is immediately registered. self.current_claim += claim - self.insurer.receive_obligation(claim, self.property_holder, time + 2, 'claim') + self.insurer.receive_obligation( + claim, self.property_holder, time + 2, "claim" + ) # Insurer pays one time step after reinsurer to avoid bankruptcy. # TODO: Is this realistic? Change this? if self.expire_immediately: @@ -51,10 +79,9 @@ def mature(self, time): No return value. Returns risk to simulation as contract terminates. Calls terminate_reinsurance to dissolve any reinsurance contracts.""" - #self.terminating = True + # self.terminating = True self.terminate_reinsurance(time) if not self.roll_over_flag: self.property_holder.return_risks([self.risk_data]) - diff --git a/insurancefirm.py b/insurancefirm.py index 61d5746..be7fdd9 100644 --- a/insurancefirm.py +++ b/insurancefirm.py @@ -4,9 +4,11 @@ from reinsurancecontract import ReinsuranceContract import isleconfig + class InsuranceFirm(MetaInsuranceOrg): """ReinsuranceFirm class. Inherits from InsuranceFirm.""" + def init(self, simulation_parameters, agent_parameters): """Constructor method. Accepts arguments @@ -18,78 +20,131 @@ def init(self, simulation_parameters, agent_parameters): self.is_reinsurer = False def adjust_dividends(self, time, actual_capacity): - #TODO: Implement algorithm from flowchart + # TODO: Implement algorithm from flowchart profits = self.get_profitslosses() - self.per_period_dividend = max(0, self.dividend_share_of_profits * profits) # max function ensures that no negative dividends are paid - #if profits < 0: # no dividends when losses are written + self.per_period_dividend = max( + 0, self.dividend_share_of_profits * profits + ) # max function ensures that no negative dividends are paid + # if profits < 0: # no dividends when losses are written # self.per_period_dividend = 0 - if actual_capacity < self.capacity_target: # no dividends if firm misses capital target + if ( + actual_capacity < self.capacity_target + ): # no dividends if firm misses capital target self.per_period_dividend = 0 def get_reinsurance_VaR_estimate(self, max_var): - reinsurance_factor_estimate = (sum([ 1 for categ_id in range(self.simulation_no_risk_categories) \ - if (self.category_reinsurance[categ_id] is None)]) \ - * 1. / self.simulation_no_risk_categories) \ - * (1. - self.np_reinsurance_deductible_fraction) - reinsurance_VaR_estimate = max_var * (1. + reinsurance_factor_estimate) + reinsurance_factor_estimate = ( + sum( + [ + 1 + for categ_id in range(self.simulation_no_risk_categories) + if (self.category_reinsurance[categ_id] is None) + ] + ) + * 1.0 + / self.simulation_no_risk_categories + ) * (1.0 - self.np_reinsurance_deductible_fraction) + reinsurance_VaR_estimate = max_var * (1.0 + reinsurance_factor_estimate) return reinsurance_VaR_estimate - + def adjust_capacity_target(self, max_var): reinsurance_VaR_estimate = self.get_reinsurance_VaR_estimate(max_var) - capacity_target_var_ratio_estimate = (self.capacity_target + reinsurance_VaR_estimate) * 1. / (max_var + reinsurance_VaR_estimate) - if capacity_target_var_ratio_estimate > self.capacity_target_increment_threshold: + capacity_target_var_ratio_estimate = ( + (self.capacity_target + reinsurance_VaR_estimate) + * 1.0 + / (max_var + reinsurance_VaR_estimate) + ) + if ( + capacity_target_var_ratio_estimate + > self.capacity_target_increment_threshold + ): self.capacity_target *= self.capacity_target_increment_factor - elif capacity_target_var_ratio_estimate < self.capacity_target_decrement_threshold: + elif ( + capacity_target_var_ratio_estimate + < self.capacity_target_decrement_threshold + ): self.capacity_target *= self.capacity_target_decrement_factor - return + return def get_capacity(self, max_var): - if max_var < self.cash: # ensure presence of sufficiently much cash to cover VaR + if ( + max_var < self.cash + ): # ensure presence of sufficiently much cash to cover VaR reinsurance_VaR_estimate = self.get_reinsurance_VaR_estimate(max_var) return self.cash + reinsurance_VaR_estimate # else: # (This point is only reached when insurer is in severe financial difficulty. Ensure insurer recovers complete coverage.) return self.cash def increase_capacity(self, time, max_var): - '''This is implemented for non-proportional reinsurance only. Otherwise the price comparison is not meaningful. Assert non-proportional mode.''' - assert self.simulation_reinsurance_type == 'non-proportional' - '''get prices''' - reinsurance_price = self.simulation.get_reinsurance_premium(self.np_reinsurance_deductible_fraction) - cat_bond_price = self.simulation.get_cat_bond_price(self.np_reinsurance_deductible_fraction) + """This is implemented for non-proportional reinsurance only. Otherwise the price comparison is not meaningful. Assert non-proportional mode.""" + assert self.simulation_reinsurance_type == "non-proportional" + """get prices""" + reinsurance_price = self.simulation.get_reinsurance_premium( + self.np_reinsurance_deductible_fraction + ) + cat_bond_price = self.simulation.get_cat_bond_price( + self.np_reinsurance_deductible_fraction + ) capacity = None - if not reinsurance_price == cat_bond_price == float('inf'): - categ_ids = [ categ_id for categ_id in range(self.simulation_no_risk_categories) if (self.category_reinsurance[categ_id] is None)] + if not reinsurance_price == cat_bond_price == float("inf"): + categ_ids = [ + categ_id + for categ_id in range(self.simulation_no_risk_categories) + if (self.category_reinsurance[categ_id] is None) + ] if len(categ_ids) > 1: np.random.shuffle(categ_ids) - while len(categ_ids) >= 1: + while len(categ_ids) >= 1: categ_id = categ_ids.pop() capacity = self.get_capacity(max_var) - if self.capacity_target < capacity: # just one per iteration, unless capital target is unmatched - if self.increase_capacity_by_category(time, categ_id, reinsurance_price=reinsurance_price, cat_bond_price=cat_bond_price, force=False): + if ( + self.capacity_target < capacity + ): # just one per iteration, unless capital target is unmatched + if self.increase_capacity_by_category( + time, + categ_id, + reinsurance_price=reinsurance_price, + cat_bond_price=cat_bond_price, + force=False, + ): categ_ids = [] else: - self.increase_capacity_by_category(time, categ_id, reinsurance_price=reinsurance_price, cat_bond_price=cat_bond_price, force=True) + self.increase_capacity_by_category( + time, + categ_id, + reinsurance_price=reinsurance_price, + cat_bond_price=cat_bond_price, + force=True, + ) # capacity is returned in order not to recompute more often than necessary - if capacity is None: + if capacity is None: capacity = self.get_capacity(max_var) - return capacity + return capacity - def increase_capacity_by_category(self, time, categ_id, reinsurance_price, cat_bond_price, force=False): + def increase_capacity_by_category( + self, time, categ_id, reinsurance_price, cat_bond_price, force=False + ): if isleconfig.verbose: - print("IF {0:d} increasing capacity in period {1:d}, cat bond price: {2:f}, reinsurance premium {3:f}".format(self.id, time, cat_bond_price, reinsurance_price)) + print( + "IF {0:d} increasing capacity in period {1:d}, cat bond price: {2:f}, reinsurance premium {3:f}".format( + self.id, time, cat_bond_price, reinsurance_price + ) + ) if not force: actual_premium = self.get_average_premium(categ_id) possible_premium = self.simulation.get_market_premium() if actual_premium >= possible_premium: return False - '''on the basis of prices decide for obtaining reinsurance or for issuing cat bond''' + """on the basis of prices decide for obtaining reinsurance or for issuing cat bond""" if reinsurance_price > cat_bond_price: if isleconfig.verbose: print("IF {0:d} issuing Cat bond in period {1:d}".format(self.id, time)) self.issue_cat_bond(time, categ_id) else: if isleconfig.verbose: - print("IF {0:d} getting reinsurance in period {1:d}".format(self.id, time)) + print( + "IF {0:d} getting reinsurance in period {1:d}".format(self.id, time) + ) self.ask_reinsurance_non_proportional_by_category(time, categ_id) return True @@ -102,13 +157,13 @@ def get_average_premium(self, categ_id): contract_premium = contract.periodized_premium * contract.runtime weighted_premium_sum += contract_premium if total_weight == 0: - return 0 # will prevent any attempt to reinsure empty categories + return 0 # will prevent any attempt to reinsure empty categories return weighted_premium_sum * 1.0 / total_weight - + def ask_reinsurance(self, time): - if self.simulation_reinsurance_type == 'proportional': + if self.simulation_reinsurance_type == "proportional": self.ask_reinsurance_proportional() - elif self.simulation_reinsurance_type == 'non-proportional': + elif self.simulation_reinsurance_type == "non-proportional": self.ask_reinsurance_non_proportional(time) else: assert False, "Undefined reinsurance type" @@ -125,7 +180,7 @@ def ask_reinsurance_non_proportional(self, time): """Evaluate by risk category""" for categ_id in range(self.simulation_no_risk_categories): """Seek reinsurance only with probability 10% if not already reinsured""" # TODO: find a more generic way to decide whether to request reinsurance for category in this period - if (self.category_reinsurance[categ_id] is None): + if self.category_reinsurance[categ_id] is None: self.ask_reinsurance_non_proportional_by_category(time, categ_id) def characterize_underwritten_risks_by_category(self, time, categ_id): @@ -139,22 +194,30 @@ def characterize_underwritten_risks_by_category(self, time, categ_id): avg_risk_factor += contract.risk_factor number_risks += 1 periodized_total_premium += contract.periodized_premium - if number_risks > 0: + if number_risks > 0: avg_risk_factor /= number_risks return total_value, avg_risk_factor, number_risks, periodized_total_premium - def ask_reinsurance_non_proportional_by_category(self, time, categ_id): """Proceed with creation of reinsurance risk only if category is not empty.""" - total_value, avg_risk_factor, number_risks, periodized_total_premium = self.characterize_underwritten_risks_by_category(time, categ_id) - if number_risks > 0: - risk = {"value": total_value, "category": categ_id, "owner": self, - #"identifier": uuid.uuid1(), - "insurancetype": 'excess-of-loss', "number_risks": number_risks, - "deductible_fraction": self.np_reinsurance_deductible_fraction, - "excess_fraction": self.np_reinsurance_excess_fraction, - "periodized_total_premium": periodized_total_premium, "runtime": 12, - "expiration": time + 12, "risk_factor": avg_risk_factor} # TODO: make runtime into a parameter + total_value, avg_risk_factor, number_risks, periodized_total_premium = self.characterize_underwritten_risks_by_category( + time, categ_id + ) + if number_risks > 0: + risk = { + "value": total_value, + "category": categ_id, + "owner": self, + # "identifier": uuid.uuid1(), + "insurancetype": "excess-of-loss", + "number_risks": number_risks, + "deductible_fraction": self.np_reinsurance_deductible_fraction, + "excess_fraction": self.np_reinsurance_excess_fraction, + "periodized_total_premium": periodized_total_premium, + "runtime": 12, + "expiration": time + 12, + "risk_factor": avg_risk_factor, + } # TODO: make runtime into a parameter self.simulation.append_reinrisks(risk) @@ -164,83 +227,125 @@ def ask_reinsurance_proportional(self): if contract.reincontract == None: nonreinsured.append(contract) - #nonreinsured_b = [contract + # nonreinsured_b = [contract # for contract in self.underwritten_contracts # if contract.reincontract == None] # - #try: + # try: # assert nonreinsured == nonreinsured_b - #except: + # except: # pdb.set_trace() nonreinsured.reverse() - if len(nonreinsured) >= (1 - self.reinsurance_limit) * len(self.underwritten_contracts): + if len(nonreinsured) >= (1 - self.reinsurance_limit) * len( + self.underwritten_contracts + ): counter = 0 - limitrein = len(nonreinsured) - (1 - self.reinsurance_limit) * len(self.underwritten_contracts) + limitrein = len(nonreinsured) - (1 - self.reinsurance_limit) * len( + self.underwritten_contracts + ) for contract in nonreinsured: if counter < limitrein: - risk = {"value": contract.value, "category": contract.category, "owner": self, - #"identifier": uuid.uuid1(), - "reinsurance_share": 1., - "expiration": contract.expiration, "contract": contract, - "risk_factor": contract.risk_factor} + risk = { + "value": contract.value, + "category": contract.category, + "owner": self, + # "identifier": uuid.uuid1(), + "reinsurance_share": 1.0, + "expiration": contract.expiration, + "contract": contract, + "risk_factor": contract.risk_factor, + } - #print("CREATING", risk["expiration"], contract.expiration, risk["contract"].expiration, risk["identifier"]) + # print("CREATING", risk["expiration"], contract.expiration, risk["contract"].expiration, risk["identifier"]) self.simulation.append_reinrisks(risk) counter += 1 else: break def add_reinsurance(self, category, excess_fraction, deductible_fraction, contract): - self.riskmodel.add_reinsurance(category, excess_fraction, deductible_fraction, contract) + self.riskmodel.add_reinsurance( + category, excess_fraction, deductible_fraction, contract + ) self.category_reinsurance[category] = contract - #pass + # pass - def delete_reinsurance(self, category, excess_fraction, deductible_fraction, contract): - self.riskmodel.delete_reinsurance(category, excess_fraction, deductible_fraction, contract) + def delete_reinsurance( + self, category, excess_fraction, deductible_fraction, contract + ): + self.riskmodel.delete_reinsurance( + category, excess_fraction, deductible_fraction, contract + ) self.category_reinsurance[category] = None - #pass - - def issue_cat_bond(self, time, categ_id, per_value_per_period_premium = 0): + # pass + + def issue_cat_bond(self, time, categ_id, per_value_per_period_premium=0): # premium is for usual reinsurance contracts paid using per value market premium # for the quasi-contract for the cat bond, nothing is paid, everything is already paid at the beginning. - #per_value_reinsurance_premium = self.np_reinsurance_premium_share * risk["periodized_total_premium"] * risk["runtime"] / risk["value"] #TODO: rename this to per_value_premium in insurancecontract.py to avoid confusion + # per_value_reinsurance_premium = self.np_reinsurance_premium_share * risk["periodized_total_premium"] * risk["runtime"] / risk["value"] #TODO: rename this to per_value_premium in insurancecontract.py to avoid confusion """ create catbond """ - total_value, avg_risk_factor, number_risks, periodized_total_premium = self.characterize_underwritten_risks_by_category(time, categ_id) + total_value, avg_risk_factor, number_risks, periodized_total_premium = self.characterize_underwritten_risks_by_category( + time, categ_id + ) if number_risks > 0: - risk = {"value": total_value, "category": categ_id, "owner": self, - #"identifier": uuid.uuid1(), - "insurancetype": 'excess-of-loss', "number_risks": number_risks, - "deductible_fraction": self.np_reinsurance_deductible_fraction, - "excess_fraction": self.np_reinsurance_excess_fraction, - "periodized_total_premium": 0, "runtime": 12, - "expiration": time + 12, "risk_factor": avg_risk_factor} # TODO: make runtime into a parameter + risk = { + "value": total_value, + "category": categ_id, + "owner": self, + # "identifier": uuid.uuid1(), + "insurancetype": "excess-of-loss", + "number_risks": number_risks, + "deductible_fraction": self.np_reinsurance_deductible_fraction, + "excess_fraction": self.np_reinsurance_excess_fraction, + "periodized_total_premium": 0, + "runtime": 12, + "expiration": time + 12, + "risk_factor": avg_risk_factor, + } # TODO: make runtime into a parameter _, _, var_this_risk, _ = self.riskmodel.evaluate([], self.cash, risk) per_period_premium = per_value_per_period_premium * risk["value"] - total_premium = sum([per_period_premium * ((1/(1+self.interest_rate))**i) for i in range(risk["runtime"])]) # TODO: or is it range(1, risk["runtime"]+1)? - #catbond = CatBond(self.simulation, per_period_premium) - catbond = CatBond(self.simulation, per_period_premium, self.interest_rate) # TODO: shift obtain_yield method to insurancesimulation, thereby making it unnecessary to drag parameters like self.interest_rate from instance to instance and from class to class + total_premium = sum( + [ + per_period_premium * ((1 / (1 + self.interest_rate)) ** i) + for i in range(risk["runtime"]) + ] + ) # TODO: or is it range(1, risk["runtime"]+1)? + # catbond = CatBond(self.simulation, per_period_premium) + catbond = CatBond( + self.simulation, per_period_premium, self.interest_rate + ) # TODO: shift obtain_yield method to insurancesimulation, thereby making it unnecessary to drag parameters like self.interest_rate from instance to instance and from class to class """add contract; contract is a quasi-reinsurance contract""" - contract = ReinsuranceContract(catbond, risk, time, 0, risk["runtime"], \ - self.default_contract_payment_period, \ - expire_immediately=self.simulation_parameters["expire_immediately"], \ - initial_VaR=var_this_risk, \ - insurancetype=risk["insurancetype"]) + contract = ReinsuranceContract( + catbond, + risk, + time, + 0, + risk["runtime"], + self.default_contract_payment_period, + expire_immediately=self.simulation_parameters["expire_immediately"], + initial_VaR=var_this_risk, + insurancetype=risk["insurancetype"], + ) # per_value_reinsurance_premium = 0 because the insurance firm does not continue to make payments to the cat bond. Only once. catbond.set_contract(contract) """sell cat bond (to self.simulation)""" - self.simulation.receive_obligation(var_this_risk, self, time, 'bond') + self.simulation.receive_obligation(var_this_risk, self, time, "bond") catbond.set_owner(self.simulation) """hand cash over to cat bond such that var_this_risk is covered""" - obligation = {"amount": var_this_risk + total_premium, "recipient": catbond, "due_time": time, "purpose": 'bond'} - self.pay(obligation) #TODO: is var_this_risk the correct amount? + obligation = { + "amount": var_this_risk + total_premium, + "recipient": catbond, + "due_time": time, + "purpose": "bond", + } + self.pay(obligation) # TODO: is var_this_risk the correct amount? """register catbond""" self.simulation.accept_agents("catbond", [catbond], time=time) - def make_reinsurance_claims(self,time): + def make_reinsurance_claims(self, time): """collect and effect reinsurance claims""" # TODO: reorganize this with risk category ledgers # TODO: Put facultative insurance claims here @@ -249,35 +354,53 @@ def make_reinsurance_claims(self,time): categ_id, claims, is_proportional = contract.get_and_reset_current_claim() if is_proportional: claims_this_turn[categ_id] += claims - if (contract.reincontract != None): + if contract.reincontract != None: contract.reincontract.explode(time, claims) for categ_id in range(self.simulation_no_risk_categories): - if claims_this_turn[categ_id] > 0 and self.category_reinsurance[categ_id] is not None: - self.category_reinsurance[categ_id].explode(time, claims_this_turn[categ_id]) + if ( + claims_this_turn[categ_id] > 0 + and self.category_reinsurance[categ_id] is not None + ): + self.category_reinsurance[categ_id].explode( + time, claims_this_turn[categ_id] + ) def get_excess_of_loss_reinsurance(self): reinsurance = [] for categ_id in range(self.simulation_no_risk_categories): if self.category_reinsurance[categ_id] is not None: - reinsurance_contract = {} - reinsurance_contract["reinsurer"] = self.category_reinsurance[categ_id].insurer - reinsurance_contract["value"] = self.category_reinsurance[categ_id].value - reinsurance_contract["category"] = categ_id - reinsurance.append(reinsurance_contract) + reinsurance_contract = {} + reinsurance_contract["reinsurer"] = self.category_reinsurance[ + categ_id + ].insurer + reinsurance_contract["value"] = self.category_reinsurance[ + categ_id + ].value + reinsurance_contract["category"] = categ_id + reinsurance.append(reinsurance_contract) return reinsurance def create_reinrisk(self, time, categ_id): """Proceed with creation of reinsurance risk only if category is not empty.""" - total_value, avg_risk_factor, number_risks, periodized_total_premium = self.characterize_underwritten_risks_by_category(time, categ_id) + total_value, avg_risk_factor, number_risks, periodized_total_premium = self.characterize_underwritten_risks_by_category( + time, categ_id + ) if number_risks > 0: - risk = {"value": total_value, "category": categ_id, "owner": self, - #"identifier": uuid.uuid1(), - "insurancetype": 'excess-of-loss', "number_risks": number_risks, - "deductible_fraction": self.np_reinsurance_deductible_fraction, - "excess_fraction": self.np_reinsurance_excess_fraction, - "periodized_total_premium": periodized_total_premium, "runtime": 12, - "expiration": time + 12, "risk_factor": avg_risk_factor} # TODO: make runtime into a parameter + risk = { + "value": total_value, + "category": categ_id, + "owner": self, + # "identifier": uuid.uuid1(), + "insurancetype": "excess-of-loss", + "number_risks": number_risks, + "deductible_fraction": self.np_reinsurance_deductible_fraction, + "excess_fraction": self.np_reinsurance_excess_fraction, + "periodized_total_premium": periodized_total_premium, + "runtime": 12, + "expiration": time + 12, + "risk_factor": avg_risk_factor, + } # TODO: make runtime into a parameter return risk else: return None diff --git a/insurancesimulation.py b/insurancesimulation.py index f7383d8..1da5736 100644 --- a/insurancesimulation.py +++ b/insurancesimulation.py @@ -1,5 +1,6 @@ from insurancefirm import InsuranceFirm -#from riskmodel import RiskModel + +# from riskmodel import RiskModel from reinsurancefirm import ReinsuranceFirm from distributiontruncated import TruncatedDistWrapper import numpy as np @@ -16,65 +17,93 @@ if isleconfig.use_abce: import abce - #print("abce imported") -#else: -# print("abce not imported") + # print("abce imported") +# else: +# print("abce not imported") -class InsuranceSimulation(): - def __init__(self, override_no_riskmodels, replic_ID, simulation_parameters, rc_event_schedule, rc_event_damage): +class InsuranceSimulation: + def __init__( + self, + override_no_riskmodels, + replic_ID, + simulation_parameters, + rc_event_schedule, + rc_event_damage, + ): # override one-riskmodel case (this is to ensure all other parameters are truly identical for comparison runs) if override_no_riskmodels: simulation_parameters["no_riskmodels"] = override_no_riskmodels self.number_riskmodels = simulation_parameters["no_riskmodels"] - + # save parameters if (replic_ID is None) or (isleconfig.force_foreground): - self.background_run = False + self.background_run = False else: self.background_run = True self.replic_ID = replic_ID self.simulation_parameters = simulation_parameters # unpack parameters, set up environment (distributions etc.) - + # damage distribution # TODO: control damage distribution via parameters, not directly - #self.damage_distribution = scipy.stats.uniform(loc=0, scale=1) + # self.damage_distribution = scipy.stats.uniform(loc=0, scale=1) non_truncated = scipy.stats.pareto(b=2, loc=0, scale=0.25) - self.damage_distribution = TruncatedDistWrapper(lower_bound=0.25, upper_bound=1., dist=non_truncated) - + self.damage_distribution = TruncatedDistWrapper( + lower_bound=0.25, upper_bound=1.0, dist=non_truncated + ) + # remaining parameters self.catbonds_off = simulation_parameters["catbonds_off"] self.reinsurance_off = simulation_parameters["reinsurance_off"] - self.cat_separation_distribution = scipy.stats.expon(0, simulation_parameters["event_time_mean_separation"]) + self.cat_separation_distribution = scipy.stats.expon( + 0, simulation_parameters["event_time_mean_separation"] + ) self.risk_factor_lower_bound = simulation_parameters["risk_factor_lower_bound"] - self.risk_factor_spread = simulation_parameters["risk_factor_upper_bound"] - simulation_parameters["risk_factor_lower_bound"] - self.risk_factor_distribution = scipy.stats.uniform(loc=self.risk_factor_lower_bound, scale=self.risk_factor_spread) + self.risk_factor_spread = ( + simulation_parameters["risk_factor_upper_bound"] + - simulation_parameters["risk_factor_lower_bound"] + ) + self.risk_factor_distribution = scipy.stats.uniform( + loc=self.risk_factor_lower_bound, scale=self.risk_factor_spread + ) if not simulation_parameters["risk_factors_present"]: self.risk_factor_distribution = scipy.stats.uniform(loc=1.0, scale=0) - #self.risk_value_distribution = scipy.stats.uniform(loc=100, scale=9900) + # self.risk_value_distribution = scipy.stats.uniform(loc=100, scale=9900) self.risk_value_distribution = scipy.stats.uniform(loc=1000, scale=0) risk_factor_mean = self.risk_factor_distribution.mean() - if np.isnan(risk_factor_mean): # unfortunately scipy.stats.mean is not well-defined if scale = 0 + if np.isnan( + risk_factor_mean + ): # unfortunately scipy.stats.mean is not well-defined if scale = 0 risk_factor_mean = self.risk_factor_distribution.rvs() # set initial market price (normalized, i.e. must be multiplied by value or excess-deductible) if self.simulation_parameters["expire_immediately"]: assert self.cat_separation_distribution.dist.name == "expon" - expected_damage_frequency = 1 - scipy.stats.poisson(1 / self.simulation_parameters["event_time_mean_separation"] * \ - self.simulation_parameters["mean_contract_runtime"]).pmf(0) + expected_damage_frequency = 1 - scipy.stats.poisson( + 1 + / self.simulation_parameters["event_time_mean_separation"] + * self.simulation_parameters["mean_contract_runtime"] + ).pmf(0) else: - expected_damage_frequency = self.simulation_parameters["mean_contract_runtime"] / \ - self.cat_separation_distribution.mean() - self.norm_premium = expected_damage_frequency * self.damage_distribution.mean() * \ - risk_factor_mean * \ - (1 + self.simulation_parameters["norm_profit_markup"]) + expected_damage_frequency = ( + self.simulation_parameters["mean_contract_runtime"] + / self.cat_separation_distribution.mean() + ) + self.norm_premium = ( + expected_damage_frequency + * self.damage_distribution.mean() + * risk_factor_mean + * (1 + self.simulation_parameters["norm_profit_markup"]) + ) self.market_premium = self.norm_premium - self.reinsurance_market_premium = self.market_premium # TODO: is this problematic as initial value? (later it is recomputed in every iteration) + self.reinsurance_market_premium = ( + self.market_premium + ) # TODO: is this problematic as initial value? (later it is recomputed in every iteration) self.total_no_risks = simulation_parameters["no_risks"] # set up monetary system (should instead be with the customers, if customers are modeled explicitly) @@ -85,142 +114,237 @@ def __init__(self, override_no_riskmodels, replic_ID, simulation_parameters, rc_ self.riskcategories = list(range(self.simulation_parameters["no_categories"])) self.rc_event_schedule = [] self.rc_event_damage = [] - self.rc_event_schedule_initial = [] #For debugging (cloud debugging) purposes is good to store the initial schedule of catastrophes - self.rc_event_damage_initial = [] #and damages that will be use in a single run of the model. - - if rc_event_schedule is not None and rc_event_damage is not None: #If we have schedules pass as arguments we used them. + self.rc_event_schedule_initial = ( + [] + ) # For debugging (cloud debugging) purposes is good to store the initial schedule of catastrophes + self.rc_event_damage_initial = ( + [] + ) # and damages that will be use in a single run of the model. + + if ( + rc_event_schedule is not None and rc_event_damage is not None + ): # If we have schedules pass as arguments we used them. self.rc_event_schedule = copy.copy(rc_event_schedule) self.rc_event_schedule_initial = copy.copy(rc_event_schedule) self.rc_event_damage = copy.copy(rc_event_damage) self.rc_event_damage_initial = copy.copy(rc_event_damage) - else: #Otherwise the schedules and damages are generated. + else: # Otherwise the schedules and damages are generated. self.setup_risk_categories_caller() - # set up risks risk_value_mean = self.risk_value_distribution.mean() - if np.isnan(risk_value_mean): # unfortunately scipy.stats.mean is not well-defined if scale = 0 + if np.isnan( + risk_value_mean + ): # unfortunately scipy.stats.mean is not well-defined if scale = 0 risk_value_mean = self.risk_value_distribution.rvs() - rrisk_factors = self.risk_factor_distribution.rvs(size=self.simulation_parameters["no_risks"]) - rvalues = self.risk_value_distribution.rvs(size=self.simulation_parameters["no_risks"]) - rcategories = np.random.randint(0, self.simulation_parameters["no_categories"], size=self.simulation_parameters["no_risks"]) - self.risks = [{"risk_factor": rrisk_factors[i], "value": rvalues[i], "category": rcategories[i], "owner": self} for i in range(self.simulation_parameters["no_risks"])] - - self.risks_counter = [0,0,0,0] + rrisk_factors = self.risk_factor_distribution.rvs( + size=self.simulation_parameters["no_risks"] + ) + rvalues = self.risk_value_distribution.rvs( + size=self.simulation_parameters["no_risks"] + ) + rcategories = np.random.randint( + 0, + self.simulation_parameters["no_categories"], + size=self.simulation_parameters["no_risks"], + ) + self.risks = [ + { + "risk_factor": rrisk_factors[i], + "value": rvalues[i], + "category": rcategories[i], + "owner": self, + } + for i in range(self.simulation_parameters["no_risks"]) + ] + + self.risks_counter = [0, 0, 0, 0] for item in self.risks: - self.risks_counter[item["category"]] = self.risks_counter[item["category"]] + 1 - + self.risks_counter[item["category"]] = ( + self.risks_counter[item["category"]] + 1 + ) # set up risk models - #inaccuracy = [[(1./self.simulation_parameters["riskmodel_inaccuracy_parameter"] if (i + j) % 2 == 0 \ + # inaccuracy = [[(1./self.simulation_parameters["riskmodel_inaccuracy_parameter"] if (i + j) % 2 == 0 \ # else self.simulation_parameters["riskmodel_inaccuracy_parameter"]) \ # for i in range(self.simulation_parameters["no_categories"])] \ # for j in range(self.simulation_parameters["no_riskmodels"])] - self.inaccuracy = self.get_all_riskmodel_combinations(self.simulation_parameters["no_categories"], self.simulation_parameters["riskmodel_inaccuracy_parameter"]) - - self.inaccuracy = random.sample(self.inaccuracy, self.simulation_parameters["no_riskmodels"]) - - risk_model_configurations = [{"damage_distribution": self.damage_distribution, - "expire_immediately": self.simulation_parameters["expire_immediately"], - "cat_separation_distribution": self.cat_separation_distribution, - "norm_premium": self.norm_premium, - "no_categories": self.simulation_parameters["no_categories"], - "risk_value_mean": risk_value_mean, - "risk_factor_mean": risk_factor_mean, - "norm_profit_markup": self.simulation_parameters["norm_profit_markup"], - "margin_of_safety": self.simulation_parameters["riskmodel_margin_of_safety"], - "var_tail_prob": self.simulation_parameters["value_at_risk_tail_probability"], - "inaccuracy_by_categ": self.inaccuracy[i]} \ - for i in range(self.simulation_parameters["no_riskmodels"])] - + self.inaccuracy = self.get_all_riskmodel_combinations( + self.simulation_parameters["no_categories"], + self.simulation_parameters["riskmodel_inaccuracy_parameter"], + ) + + self.inaccuracy = random.sample( + self.inaccuracy, self.simulation_parameters["no_riskmodels"] + ) + + risk_model_configurations = [ + { + "damage_distribution": self.damage_distribution, + "expire_immediately": self.simulation_parameters["expire_immediately"], + "cat_separation_distribution": self.cat_separation_distribution, + "norm_premium": self.norm_premium, + "no_categories": self.simulation_parameters["no_categories"], + "risk_value_mean": risk_value_mean, + "risk_factor_mean": risk_factor_mean, + "norm_profit_markup": self.simulation_parameters["norm_profit_markup"], + "margin_of_safety": self.simulation_parameters[ + "riskmodel_margin_of_safety" + ], + "var_tail_prob": self.simulation_parameters[ + "value_at_risk_tail_probability" + ], + "inaccuracy_by_categ": self.inaccuracy[i], + } + for i in range(self.simulation_parameters["no_riskmodels"]) + ] + # prepare setting up agents (to be done from start.py) - self.agent_parameters = {"insurancefirm": [], "reinsurance": []} # TODO: rename reinsurance -> reinsurancefirm (also in start.py and below in method accept_agents + self.agent_parameters = { + "insurancefirm": [], + "reinsurance": [], + } # TODO: rename reinsurance -> reinsurancefirm (also in start.py and below in method accept_agents self.insurer_id_counter = 0 # TODO: collapse the following two loops into one generic one? for i in range(simulation_parameters["no_insurancefirms"]): - if simulation_parameters['static_non-proportional_reinsurance_levels']: - insurance_reinsurance_level = simulation_parameters["default_non-proportional_reinsurance_deductible"] + if simulation_parameters["static_non-proportional_reinsurance_levels"]: + insurance_reinsurance_level = simulation_parameters[ + "default_non-proportional_reinsurance_deductible" + ] else: - insurance_reinsurance_level = np.random.uniform(simulation_parameters["insurance_reinsurance_levels_lower_bound"], simulation_parameters["insurance_reinsurance_levels_upper_bound"]) - - riskmodel_config = risk_model_configurations[i % len(risk_model_configurations)] - self.agent_parameters["insurancefirm"].append({'id': self.get_unique_insurer_id(), 'initial_cash': simulation_parameters["initial_agent_cash"], - 'riskmodel_config': riskmodel_config, 'norm_premium': self.norm_premium, - 'profit_target': simulation_parameters["norm_profit_markup"], - 'initial_acceptance_threshold': simulation_parameters["initial_acceptance_threshold"], - 'acceptance_threshold_friction': simulation_parameters["acceptance_threshold_friction"], - 'reinsurance_limit': simulation_parameters["reinsurance_limit"], - 'non-proportional_reinsurance_level': insurance_reinsurance_level, - 'capacity_target_decrement_threshold': simulation_parameters['capacity_target_decrement_threshold'], - 'capacity_target_increment_threshold': simulation_parameters['capacity_target_increment_threshold'], - 'capacity_target_decrement_factor': simulation_parameters['capacity_target_decrement_factor'], - 'capacity_target_increment_factor': simulation_parameters['capacity_target_increment_factor'], - 'interest_rate': simulation_parameters["interest_rate"]}) + insurance_reinsurance_level = np.random.uniform( + simulation_parameters["insurance_reinsurance_levels_lower_bound"], + simulation_parameters["insurance_reinsurance_levels_upper_bound"], + ) + + riskmodel_config = risk_model_configurations[ + i % len(risk_model_configurations) + ] + self.agent_parameters["insurancefirm"].append( + { + "id": self.get_unique_insurer_id(), + "initial_cash": simulation_parameters["initial_agent_cash"], + "riskmodel_config": riskmodel_config, + "norm_premium": self.norm_premium, + "profit_target": simulation_parameters["norm_profit_markup"], + "initial_acceptance_threshold": simulation_parameters[ + "initial_acceptance_threshold" + ], + "acceptance_threshold_friction": simulation_parameters[ + "acceptance_threshold_friction" + ], + "reinsurance_limit": simulation_parameters["reinsurance_limit"], + "non-proportional_reinsurance_level": insurance_reinsurance_level, + "capacity_target_decrement_threshold": simulation_parameters[ + "capacity_target_decrement_threshold" + ], + "capacity_target_increment_threshold": simulation_parameters[ + "capacity_target_increment_threshold" + ], + "capacity_target_decrement_factor": simulation_parameters[ + "capacity_target_decrement_factor" + ], + "capacity_target_increment_factor": simulation_parameters[ + "capacity_target_increment_factor" + ], + "interest_rate": simulation_parameters["interest_rate"], + } + ) self.reinsurer_id_counter = 0 for i in range(simulation_parameters["no_reinsurancefirms"]): - if simulation_parameters['static_non-proportional_reinsurance_levels']: - reinsurance_reinsurance_level = simulation_parameters["default_non-proportional_reinsurance_deductible"] + if simulation_parameters["static_non-proportional_reinsurance_levels"]: + reinsurance_reinsurance_level = simulation_parameters[ + "default_non-proportional_reinsurance_deductible" + ] else: - reinsurance_reinsurance_level = np.random.uniform(simulation_parameters["reinsurance_reinsurance_levels_lower_bound"], simulation_parameters["reinsurance_reinsurance_levels_upper_bound"]) - - riskmodel_config = risk_model_configurations[i % len(risk_model_configurations)] - self.agent_parameters["reinsurance"].append({'id': self.get_unique_reinsurer_id(), 'initial_cash': simulation_parameters["initial_reinagent_cash"], - 'riskmodel_config': riskmodel_config, 'norm_premium': self.norm_premium, - 'profit_target': simulation_parameters["norm_profit_markup"], - 'initial_acceptance_threshold': simulation_parameters["initial_acceptance_threshold"], - 'acceptance_threshold_friction': simulation_parameters["acceptance_threshold_friction"], - 'reinsurance_limit': simulation_parameters["reinsurance_limit"], - 'non-proportional_reinsurance_level': reinsurance_reinsurance_level, - 'capacity_target_decrement_threshold': simulation_parameters['capacity_target_decrement_threshold'], - 'capacity_target_increment_threshold': simulation_parameters['capacity_target_increment_threshold'], - 'capacity_target_decrement_factor': simulation_parameters['capacity_target_decrement_factor'], - 'capacity_target_increment_factor': simulation_parameters['capacity_target_increment_factor'], - 'interest_rate': simulation_parameters["interest_rate"]}) - + reinsurance_reinsurance_level = np.random.uniform( + simulation_parameters["reinsurance_reinsurance_levels_lower_bound"], + simulation_parameters["reinsurance_reinsurance_levels_upper_bound"], + ) + + riskmodel_config = risk_model_configurations[ + i % len(risk_model_configurations) + ] + self.agent_parameters["reinsurance"].append( + { + "id": self.get_unique_reinsurer_id(), + "initial_cash": simulation_parameters["initial_reinagent_cash"], + "riskmodel_config": riskmodel_config, + "norm_premium": self.norm_premium, + "profit_target": simulation_parameters["norm_profit_markup"], + "initial_acceptance_threshold": simulation_parameters[ + "initial_acceptance_threshold" + ], + "acceptance_threshold_friction": simulation_parameters[ + "acceptance_threshold_friction" + ], + "reinsurance_limit": simulation_parameters["reinsurance_limit"], + "non-proportional_reinsurance_level": reinsurance_reinsurance_level, + "capacity_target_decrement_threshold": simulation_parameters[ + "capacity_target_decrement_threshold" + ], + "capacity_target_increment_threshold": simulation_parameters[ + "capacity_target_increment_threshold" + ], + "capacity_target_decrement_factor": simulation_parameters[ + "capacity_target_decrement_factor" + ], + "capacity_target_increment_factor": simulation_parameters[ + "capacity_target_increment_factor" + ], + "interest_rate": simulation_parameters["interest_rate"], + } + ) + # set up remaining list variables - + # agent lists self.reinsurancefirms = [] self.insurancefirms = [] self.catbonds = [] - + # lists of agent weights self.insurers_weights = {} self.reinsurers_weights = {} - # list of reinsurance risks offered for underwriting self.reinrisks = [] self.not_accepted_reinrisks = [] - + # cumulative variables for history and logging self.cumulative_bankruptcies = 0 self.cumulative_market_exits = 0 self.cumulative_unrecovered_claims = 0.0 self.cumulative_claims = 0.0 - + # lists for logging history - self.logger = logger.Logger(no_riskmodels=simulation_parameters["no_riskmodels"], - rc_event_schedule_initial=self.rc_event_schedule_initial, - rc_event_damage_initial=self.rc_event_damage_initial) - - self.insurance_models_counter = np.zeros(self.simulation_parameters["no_categories"]) - self.reinsurance_models_counter = np.zeros(self.simulation_parameters["no_categories"]) - - - - def build_agents(self, agent_class, agent_class_string, parameters, agent_parameters): - #assert agent_parameters == self.agent_parameters[agent_class_string] #assert fits only the initial creation of agents, not later additions # TODO: fix + self.logger = logger.Logger( + no_riskmodels=simulation_parameters["no_riskmodels"], + rc_event_schedule_initial=self.rc_event_schedule_initial, + rc_event_damage_initial=self.rc_event_damage_initial, + ) + + self.insurance_models_counter = np.zeros( + self.simulation_parameters["no_categories"] + ) + self.reinsurance_models_counter = np.zeros( + self.simulation_parameters["no_categories"] + ) + + def build_agents( + self, agent_class, agent_class_string, parameters, agent_parameters + ): + # assert agent_parameters == self.agent_parameters[agent_class_string] #assert fits only the initial creation of agents, not later additions # TODO: fix agents = [] for ap in agent_parameters: agents.append(agent_class(parameters, ap)) return agents - + def accept_agents(self, agent_class_string, agents, agent_group=None, time=0): # TODO: fix agent id's for late entrants (both firms and catbonds) if agent_class_string == "insurancefirm": @@ -251,17 +375,21 @@ def accept_agents(self, agent_class_string, agents, agent_group=None, time=0): self.catbonds += agents except: print(sys.exc_info()) - pdb.set_trace() + pdb.set_trace() else: - assert False, "Error: Unexpected agent class used {0:s}".format(agent_class_string) + assert False, "Error: Unexpected agent class used {0:s}".format( + agent_class_string + ) def delete_agents(self, agent_class_string, agents): if agent_class_string == "catbond": for agent in agents: self.catbonds.remove(agent) else: - assert False, "Trying to remove unremovable agent, type: {0:s}".format(agent_class_string) - + assert False, "Trying to remove unremovable agent, type: {0:s}".format( + agent_class_string + ) + def iterate(self, t): if isleconfig.verbose: @@ -272,16 +400,19 @@ def iterate(self, t): self.reset_pls() - # adjust market premiums - sum_capital = sum([agent.get_cash() for agent in self.insurancefirms]) #TODO: include reinsurancefirms + sum_capital = sum( + [agent.get_cash() for agent in self.insurancefirms] + ) # TODO: include reinsurancefirms self.adjust_market_premium(capital=sum_capital) - sum_capital = sum([agent.get_cash() for agent in self.reinsurancefirms]) # TODO: include reinsurancefirms + sum_capital = sum( + [agent.get_cash() for agent in self.reinsurancefirms] + ) # TODO: include reinsurancefirms self.adjust_reinsurance_market_premium(capital=sum_capital) # pay obligations self.effect_payments(t) - + # identify perils and effect claims for categ_id in range(len(self.rc_event_schedule)): try: @@ -289,53 +420,62 @@ def iterate(self, t): assert self.rc_event_schedule[categ_id][0] >= t except: print("Something wrong; past events not deleted", file=sys.stderr) - if len(self.rc_event_schedule[categ_id]) > 0 and self.rc_event_schedule[categ_id][0] == t: + if ( + len(self.rc_event_schedule[categ_id]) > 0 + and self.rc_event_schedule[categ_id][0] == t + ): self.rc_event_schedule[categ_id] = self.rc_event_schedule[categ_id][1:] - damage_extent = copy.copy(self.rc_event_damage[categ_id][0]) #Schedules of catastrophes and damages must me generated at the same time. - self.inflict_peril(categ_id=categ_id, damage=damage_extent, t=t)# TODO: consider splitting the following lines from this method and running it with nb.jit + damage_extent = copy.copy( + self.rc_event_damage[categ_id][0] + ) # Schedules of catastrophes and damages must me generated at the same time. + self.inflict_peril( + categ_id=categ_id, damage=damage_extent, t=t + ) # TODO: consider splitting the following lines from this method and running it with nb.jit self.rc_event_damage[categ_id] = self.rc_event_damage[categ_id][1:] else: if isleconfig.verbose: print("Next peril ", self.rc_event_schedule[categ_id]) - + # shuffle risks (insurance and reinsurance risks) self.shuffle_risks() # reset reinweights self.reset_reinsurance_weights() - + # iterate reinsurnace firm agents for reinagent in self.reinsurancefirms: reinagent.iterate(t) # TODO: is the following necessary for abce to work (log) properly? - #if isleconfig.use_abce: + # if isleconfig.use_abce: # self.reinsurancefirms_group.iterate(time=t) - #else: + # else: # for reinagent in self.reinsurancefirms: # reinagent.iterate(t) - + # remove all non-accepted reinsurance risks self.reinrisks = [] # reset weights self.reset_insurance_weights() - + # iterate insurance firm agents for agent in self.insurancefirms: agent.iterate(t) # TODO: is the following necessary for abce to work (log) properly? - #if isleconfig.use_abce: + # if isleconfig.use_abce: # self.insurancefirms_group.iterate(time=t) - #else: + # else: # for agent in self.insurancefirms: # agent.iterate(t) - - # iterate catbonds + + # iterate catbonds for agent in self.catbonds: agent.iterate(t) - self.insurance_models_counter = np.zeros(self.simulation_parameters["no_categories"]) + self.insurance_models_counter = np.zeros( + self.simulation_parameters["no_categories"] + ) for insurer in self.insurancefirms: for i in range(len(self.inaccuracy)): @@ -343,84 +483,137 @@ def iterate(self, t): if insurer.riskmodel.inaccuracy == self.inaccuracy[i]: self.insurance_models_counter[i] += 1 - self.reinsurance_models_counter = np.zeros(self.simulation_parameters["no_categories"]) + self.reinsurance_models_counter = np.zeros( + self.simulation_parameters["no_categories"] + ) for reinsurer in self.reinsurancefirms: for i in range(len(self.inaccuracy)): if reinsurer.operational: if reinsurer.riskmodel.inaccuracy == self.inaccuracy[i]: self.reinsurance_models_counter[i] += 1 - - #print(isleconfig.show_network) + + # print(isleconfig.show_network) # TODO: use network representation in a more generic way, perhaps only once at the end to characterize the network and use for calibration(?) if isleconfig.show_network and t % 40 == 0 and t > 0: - RN = visualization_network.ReinsuranceNetwork(self.insurancefirms, self.reinsurancefirms, self.catbonds) + RN = visualization_network.ReinsuranceNetwork( + self.insurancefirms, self.reinsurancefirms, self.catbonds + ) RN.compute_measures() RN.visualize() - - + def save_data(self): """Method to collect statistics about the current state of the simulation. Will pass these to the Logger object (self.logger) to be recorded. No arguments. Returns None.""" - + """ collect data """ - total_cash_no = sum([insurancefirm.cash for insurancefirm in self.insurancefirms]) - total_excess_capital = sum([insurancefirm.get_excess_capital() for insurancefirm in self.insurancefirms]) - total_profitslosses = sum([insurancefirm.get_profitslosses() for insurancefirm in self.insurancefirms]) - total_contracts_no = sum([len(insurancefirm.underwritten_contracts) for insurancefirm in self.insurancefirms]) - total_reincash_no = sum([reinsurancefirm.cash for reinsurancefirm in self.reinsurancefirms]) - total_reinexcess_capital = sum([reinsurancefirm.get_excess_capital() for reinsurancefirm in self.reinsurancefirms]) - total_reinprofitslosses = sum([reinsurancefirm.get_profitslosses() for reinsurancefirm in self.reinsurancefirms]) - total_reincontracts_no = sum([len(reinsurancefirm.underwritten_contracts) for reinsurancefirm in self.reinsurancefirms]) - operational_no = sum([insurancefirm.operational for insurancefirm in self.insurancefirms]) - reinoperational_no = sum([reinsurancefirm.operational for reinsurancefirm in self.reinsurancefirms]) + total_cash_no = sum( + [insurancefirm.cash for insurancefirm in self.insurancefirms] + ) + total_excess_capital = sum( + [ + insurancefirm.get_excess_capital() + for insurancefirm in self.insurancefirms + ] + ) + total_profitslosses = sum( + [insurancefirm.get_profitslosses() for insurancefirm in self.insurancefirms] + ) + total_contracts_no = sum( + [ + len(insurancefirm.underwritten_contracts) + for insurancefirm in self.insurancefirms + ] + ) + total_reincash_no = sum( + [reinsurancefirm.cash for reinsurancefirm in self.reinsurancefirms] + ) + total_reinexcess_capital = sum( + [ + reinsurancefirm.get_excess_capital() + for reinsurancefirm in self.reinsurancefirms + ] + ) + total_reinprofitslosses = sum( + [ + reinsurancefirm.get_profitslosses() + for reinsurancefirm in self.reinsurancefirms + ] + ) + total_reincontracts_no = sum( + [ + len(reinsurancefirm.underwritten_contracts) + for reinsurancefirm in self.reinsurancefirms + ] + ) + operational_no = sum( + [insurancefirm.operational for insurancefirm in self.insurancefirms] + ) + reinoperational_no = sum( + [reinsurancefirm.operational for reinsurancefirm in self.reinsurancefirms] + ) catbondsoperational_no = sum([catbond.operational for catbond in self.catbonds]) - + """ collect agent-level data """ - insurance_firms = [(insurancefirm.cash,insurancefirm.id,insurancefirm.operational) for insurancefirm in self.insurancefirms] - reinsurance_firms = [(reinsurancefirm.cash,reinsurancefirm.id,reinsurancefirm.operational) for reinsurancefirm in self.reinsurancefirms] - + insurance_firms = [ + (insurancefirm.cash, insurancefirm.id, insurancefirm.operational) + for insurancefirm in self.insurancefirms + ] + reinsurance_firms = [ + (reinsurancefirm.cash, reinsurancefirm.id, reinsurancefirm.operational) + for reinsurancefirm in self.reinsurancefirms + ] + """ prepare dict """ current_log = {} - current_log['total_cash'] = total_cash_no - current_log['total_excess_capital'] = total_excess_capital - current_log['total_profitslosses'] = total_profitslosses - current_log['total_contracts'] = total_contracts_no - current_log['total_operational'] = operational_no - current_log['total_reincash'] = total_reincash_no - current_log['total_reinexcess_capital'] = total_reinexcess_capital - current_log['total_reinprofitslosses'] = total_reinprofitslosses - current_log['total_reincontracts'] = total_reincontracts_no - current_log['total_reinoperational'] = reinoperational_no - current_log['total_catbondsoperational'] = catbondsoperational_no - current_log['market_premium'] = self.market_premium - current_log['market_reinpremium'] = self.reinsurance_market_premium - current_log['cumulative_bankruptcies'] = self.cumulative_bankruptcies - current_log['cumulative_market_exits'] = self.cumulative_market_exits - current_log['cumulative_unrecovered_claims'] = self.cumulative_unrecovered_claims - current_log['cumulative_claims'] = self.cumulative_claims #Log the cumulative claims received so far. - - """ add agent-level data to dict""" - current_log['insurance_firms_cash'] = insurance_firms - current_log['reinsurance_firms_cash'] = reinsurance_firms - current_log['market_diffvar'] = self.compute_market_diffvar() - - current_log['individual_contracts'] = [] - individual_contracts_no = [len(insurancefirm.underwritten_contracts) for insurancefirm in self.insurancefirms] + current_log["total_cash"] = total_cash_no + current_log["total_excess_capital"] = total_excess_capital + current_log["total_profitslosses"] = total_profitslosses + current_log["total_contracts"] = total_contracts_no + current_log["total_operational"] = operational_no + current_log["total_reincash"] = total_reincash_no + current_log["total_reinexcess_capital"] = total_reinexcess_capital + current_log["total_reinprofitslosses"] = total_reinprofitslosses + current_log["total_reincontracts"] = total_reincontracts_no + current_log["total_reinoperational"] = reinoperational_no + current_log["total_catbondsoperational"] = catbondsoperational_no + current_log["market_premium"] = self.market_premium + current_log["market_reinpremium"] = self.reinsurance_market_premium + current_log["cumulative_bankruptcies"] = self.cumulative_bankruptcies + current_log["cumulative_market_exits"] = self.cumulative_market_exits + current_log[ + "cumulative_unrecovered_claims" + ] = self.cumulative_unrecovered_claims + current_log[ + "cumulative_claims" + ] = self.cumulative_claims # Log the cumulative claims received so far. + + """ add agent-level data to dict""" + current_log["insurance_firms_cash"] = insurance_firms + current_log["reinsurance_firms_cash"] = reinsurance_firms + current_log["market_diffvar"] = self.compute_market_diffvar() + + current_log["individual_contracts"] = [] + individual_contracts_no = [ + len(insurancefirm.underwritten_contracts) + for insurancefirm in self.insurancefirms + ] for i in range(len(individual_contracts_no)): - current_log['individual_contracts'].append(individual_contracts_no[i]) + current_log["individual_contracts"].append(individual_contracts_no[i]) """ call to Logger object """ self.logger.record_data(current_log) - - def obtain_log(self, requested_logs=None): #This function allows to return in a list all the data generated by the model. There is no other way to transfer it back from the cloud. + + def obtain_log( + self, requested_logs=None + ): # This function allows to return in a list all the data generated by the model. There is no other way to transfer it back from the cloud. return self.logger.obtain_log(requested_logs) - + def advance_round(self, *args): pass - + def finalize(self, *args): """Function to handle oberations after the end of the simulation run. Currently empty. @@ -431,21 +624,38 @@ def finalize(self, *args): pass def inflict_peril(self, categ_id, damage, t): - affected_contracts = [contract for insurer in self.insurancefirms for contract in insurer.underwritten_contracts if contract.category == categ_id] + affected_contracts = [ + contract + for insurer in self.insurancefirms + for contract in insurer.underwritten_contracts + if contract.category == categ_id + ] if isleconfig.verbose: print("**** PERIL ", damage) - damagevalues = np.random.beta(1, 1./damage -1, size=self.risks_counter[categ_id]) + damagevalues = np.random.beta( + 1, 1.0 / damage - 1, size=self.risks_counter[categ_id] + ) uniformvalues = np.random.uniform(0, 1, size=self.risks_counter[categ_id]) - [contract.explode(t, uniformvalues[i], damagevalues[i]) for i, contract in enumerate(affected_contracts)] - + [ + contract.explode(t, uniformvalues[i], damagevalues[i]) + for i, contract in enumerate(affected_contracts) + ] + def receive_obligation(self, amount, recipient, due_time, purpose): - obligation = {"amount": amount, "recipient": recipient, "due_time": due_time, "purpose": purpose} + obligation = { + "amount": amount, + "recipient": recipient, + "due_time": due_time, + "purpose": purpose, + } self.obligations.append(obligation) def effect_payments(self, time): - due = [item for item in self.obligations if item["due_time"]<=time] - #print("SIMULATION obligations: ", len(self.obligations), " of which are due: ", len(due)) - self.obligations = [item for item in self.obligations if item["due_time"]>time] + due = [item for item in self.obligations if item["due_time"] <= time] + # print("SIMULATION obligations: ", len(self.obligations), " of which are due: ", len(due)) + self.obligations = [ + item for item in self.obligations if item["due_time"] > time + ] sum_due = sum([item["amount"] for item in due]) for obligation in due: self.pay(obligation) @@ -475,7 +685,11 @@ def reset_reinsurance_weights(self): self.not_accepted_reinrisks = [] - operational_reinfirms = [reinsurancefirm for reinsurancefirm in self.reinsurancefirms if reinsurancefirm.operational] + operational_reinfirms = [ + reinsurancefirm + for reinsurancefirm in self.reinsurancefirms + if reinsurancefirm.operational + ] operational_no = len(operational_reinfirms) @@ -488,8 +702,8 @@ def reset_reinsurance_weights(self): if operational_no > 0: - if reinrisks_no/operational_no > 1: - weights = reinrisks_no/operational_no + if reinrisks_no / operational_no > 1: + weights = reinrisks_no / operational_no for reinsurer in self.reinsurancefirms: self.reinsurers_weights[reinsurer.id] = math.floor(weights) else: @@ -501,9 +715,15 @@ def reset_reinsurance_weights(self): def reset_insurance_weights(self): - operational_no = sum([insurancefirm.operational for insurancefirm in self.insurancefirms]) + operational_no = sum( + [insurancefirm.operational for insurancefirm in self.insurancefirms] + ) - operational_firms = [insurancefirm for insurancefirm in self.insurancefirms if insurancefirm.operational] + operational_firms = [ + insurancefirm + for insurancefirm in self.insurancefirms + if insurancefirm.operational + ] risks_no = len(self.risks) @@ -514,8 +734,8 @@ def reset_insurance_weights(self): if operational_no > 0: - if risks_no/operational_no > 1: - weights = risks_no/operational_no + if risks_no / operational_no > 1: + weights = risks_no / operational_no for insurer in self.insurancefirms: self.insurers_weights[insurer.id] = math.floor(weights) else: @@ -536,10 +756,24 @@ def adjust_market_premium(self, capital): with the capital available in the insurance market and viceversa. The premium reduces until it reaches a minimum below which no insurer is willing to reduce further the price. This method is only called in the self.iterate() method of this class.""" - self.market_premium = self.norm_premium * (self.simulation_parameters["upper_price_limit"] - self.simulation_parameters["premium_sensitivity"] * capital / (self.simulation_parameters["initial_agent_cash"] * self.damage_distribution.mean() * self.simulation_parameters["no_risks"])) - if self.market_premium < self.norm_premium * self.simulation_parameters["lower_price_limit"]: - self.market_premium = self.norm_premium * self.simulation_parameters["lower_price_limit"] - + self.market_premium = self.norm_premium * ( + self.simulation_parameters["upper_price_limit"] + - self.simulation_parameters["premium_sensitivity"] + * capital + / ( + self.simulation_parameters["initial_agent_cash"] + * self.damage_distribution.mean() + * self.simulation_parameters["no_risks"] + ) + ) + if ( + self.market_premium + < self.norm_premium * self.simulation_parameters["lower_price_limit"] + ): + self.market_premium = ( + self.norm_premium * self.simulation_parameters["lower_price_limit"] + ) + def adjust_reinsurance_market_premium(self, capital): """Adjust_market_premium Method. Accepts arguments @@ -549,9 +783,23 @@ def adjust_reinsurance_market_premium(self, capital): with the capital available in the reinsurance market and viceversa. The premium reduces until it reaches a minimum below which no reinsurer is willing to reduce further the price. This method is only called in the self.iterate() method of this class.""" - self.reinsurance_market_premium = self.norm_premium * (self.simulation_parameters["upper_price_limit"] - self.simulation_parameters["reinpremium_sensitivity"] * capital / (self.simulation_parameters["initial_agent_cash"] * self.damage_distribution.mean() * self.simulation_parameters["no_risks"])) - if self.reinsurance_market_premium < self.norm_premium * self.simulation_parameters["lower_price_limit"]: - self.reinsurance_market_premium = self.norm_premium * self.simulation_parameters["lower_price_limit"] + self.reinsurance_market_premium = self.norm_premium * ( + self.simulation_parameters["upper_price_limit"] + - self.simulation_parameters["reinpremium_sensitivity"] + * capital + / ( + self.simulation_parameters["initial_agent_cash"] + * self.damage_distribution.mean() + * self.simulation_parameters["no_risks"] + ) + ) + if ( + self.reinsurance_market_premium + < self.norm_premium * self.simulation_parameters["lower_price_limit"] + ): + self.reinsurance_market_premium = ( + self.norm_premium * self.simulation_parameters["lower_price_limit"] + ) def get_market_premium(self): """Get_market_premium Method. @@ -574,25 +822,29 @@ def get_reinsurance_premium(self, np_reinsurance_deductible_fraction): # TODO: make premiums dependend on the deductible per value (np_reinsurance_deductible_fraction) -> DONE. # TODO: make max_reduction into simulation_parameter ? if self.reinsurance_off: - return float('inf') + return float("inf") max_reduction = 0.1 - return self.reinsurance_market_premium * (1. - max_reduction * np_reinsurance_deductible_fraction) - + return self.reinsurance_market_premium * ( + 1.0 - max_reduction * np_reinsurance_deductible_fraction + ) + def get_cat_bond_price(self, np_reinsurance_deductible_fraction): # TODO: implement function dependent on total capital in cat bonds and on deductible () # TODO: make max_reduction and max_CB_surcharge into simulation_parameters ? if self.catbonds_off: - return float('inf') + return float("inf") max_reduction = 0.9 - max_CB_surcharge = 0.5 - return self.reinsurance_market_premium * (1. + max_CB_surcharge - max_reduction * np_reinsurance_deductible_fraction) - + max_CB_surcharge = 0.5 + return self.reinsurance_market_premium * ( + 1.0 + max_CB_surcharge - max_reduction * np_reinsurance_deductible_fraction + ) + def append_reinrisks(self, item): - if(len(item) > 0): + if len(item) > 0: self.reinrisks.append(item) - def remove_reinrisks(self,risko): - if(risko != None): + def remove_reinrisks(self, risko): + if risko != None: self.reinrisks.remove(risko) def get_reinrisks(self): @@ -601,8 +853,8 @@ def get_reinrisks(self): def solicit_insurance_requests(self, id, cash, insurer): - risks_to_be_sent = self.risks[:int(self.insurers_weights[insurer.id])] - self.risks = self.risks[int(self.insurers_weights[insurer.id]):] + risks_to_be_sent = self.risks[: int(self.insurers_weights[insurer.id])] + self.risks = self.risks[int(self.insurers_weights[insurer.id]) :] for risk in insurer.risks_kept: risks_to_be_sent.append(risk) @@ -613,8 +865,10 @@ def solicit_insurance_requests(self, id, cash, insurer): return risks_to_be_sent def solicit_reinsurance_requests(self, id, cash, reinsurer): - reinrisks_to_be_sent = self.reinrisks[:int(self.reinsurers_weights[reinsurer.id])] - self.reinrisks = self.reinrisks[int(self.reinsurers_weights[reinsurer.id]):] + reinrisks_to_be_sent = self.reinrisks[ + : int(self.reinsurers_weights[reinsurer.id]) + ] + self.reinrisks = self.reinrisks[int(self.reinsurers_weights[reinsurer.id]) :] for reinrisk in reinsurer.reinrisks_kept: reinrisks_to_be_sent.append(reinrisk) @@ -634,8 +888,10 @@ def return_reinrisks(self, not_accepted_risks): def get_all_riskmodel_combinations(self, n, rm_factor): riskmodels = [] for i in range(self.simulation_parameters["no_categories"]): - riskmodel_combination = rm_factor * np.ones(self.simulation_parameters["no_categories"]) - riskmodel_combination[i] = 1/rm_factor + riskmodel_combination = rm_factor * np.ones( + self.simulation_parameters["no_categories"] + ) + riskmodel_combination[i] = 1 / rm_factor riskmodels.append(riskmodel_combination.tolist()) return riskmodels @@ -644,20 +900,26 @@ def setup_risk_categories(self): event_schedule = [] event_damage = [] total = 0 - while (total < self.simulation_parameters["max_time"]): + while total < self.simulation_parameters["max_time"]: separation_time = self.cat_separation_distribution.rvs() total += int(math.ceil(separation_time)) if total < self.simulation_parameters["max_time"]: event_schedule.append(total) - event_damage.append(self.damage_distribution.rvs()) #Schedules of catastrophes and damages must me generated at the same time. Reason: replication across different risk models. + event_damage.append( + self.damage_distribution.rvs() + ) # Schedules of catastrophes and damages must me generated at the same time. Reason: replication across different risk models. self.rc_event_schedule.append(event_schedule) self.rc_event_damage.append(event_damage) - self.rc_event_schedule_initial = copy.copy(self.rc_event_damage) #For debugging (cloud debugging) purposes is good to store the initial schedule of catastrophes - self.rc_event_damage_initial = copy.copy(self.rc_event_damage) #and damages that will be use in a single run of the model. + self.rc_event_schedule_initial = copy.copy( + self.rc_event_damage + ) # For debugging (cloud debugging) purposes is good to store the initial schedule of catastrophes + self.rc_event_damage_initial = copy.copy( + self.rc_event_damage + ) # and damages that will be use in a single run of the model. def setup_risk_categories_caller(self): - #if self.background_run: + # if self.background_run: if self.replic_ID is not None: if isleconfig.replicating: self.restore_state_and_risk_categories() @@ -670,44 +932,66 @@ def setup_risk_categories_caller(self): def save_state_and_risk_categories(self): # save numpy Mersenne Twister state mersennetwoster_randomseed = str(np.random.get_state()) - mersennetwoster_randomseed = mersennetwoster_randomseed.replace("\n","").replace("array", "np.array").replace("uint32", "np.uint32") - wfile = open("data/replication_randomseed.dat","a") - wfile.write(mersennetwoster_randomseed+"\n") + mersennetwoster_randomseed = ( + mersennetwoster_randomseed.replace("\n", "") + .replace("array", "np.array") + .replace("uint32", "np.uint32") + ) + wfile = open("data/replication_randomseed.dat", "a") + wfile.write(mersennetwoster_randomseed + "\n") wfile.close() # save event schedule - wfile = open("data/replication_rc_event_schedule.dat","a") - wfile.write(str(self.rc_event_schedule)+"\n") + wfile = open("data/replication_rc_event_schedule.dat", "a") + wfile.write(str(self.rc_event_schedule) + "\n") wfile.close() - + def restore_state_and_risk_categories(self): - rfile = open("data/replication_rc_event_schedule.dat","r") + rfile = open("data/replication_rc_event_schedule.dat", "r") found = False for i, line in enumerate(rfile): - #print(i, self.replic_ID) + # print(i, self.replic_ID) if i == self.replic_ID: self.rc_event_schedule = eval(line) found = True rfile.close() - assert found, "rc event schedule for current replication ID number {0:d} not found in data file. Exiting.".format(self.replic_ID) - rfile = open("data/replication_randomseed.dat","r") + assert ( + found + ), "rc event schedule for current replication ID number {0:d} not found in data file. Exiting.".format( + self.replic_ID + ) + rfile = open("data/replication_randomseed.dat", "r") found = False for i, line in enumerate(rfile): - #print(i, self.replic_ID) + # print(i, self.replic_ID) if i == self.replic_ID: mersennetwister_randomseed = eval(line) found = True rfile.close() np.random.set_state(mersennetwister_randomseed) - assert found, "mersennetwister randomseed for current replication ID number {0:d} not found in data file. Exiting.".format(self.replic_ID) - - def insurance_firm_market_entry(self, prob=-1, agent_type="InsuranceFirm"): # TODO: replace method name with a more descriptive one + assert ( + found + ), "mersennetwister randomseed for current replication ID number {0:d} not found in data file. Exiting.".format( + self.replic_ID + ) + + def insurance_firm_market_entry( + self, prob=-1, agent_type="InsuranceFirm" + ): # TODO: replace method name with a more descriptive one if prob == -1: if agent_type == "InsuranceFirm": - prob = self.simulation_parameters["insurance_firm_market_entry_probability"] + prob = self.simulation_parameters[ + "insurance_firm_market_entry_probability" + ] elif agent_type == "ReinsuranceFirm": - prob = self.simulation_parameters["reinsurance_firm_market_entry_probability"] + prob = self.simulation_parameters[ + "reinsurance_firm_market_entry_probability" + ] else: - assert False, "Unknown agent type. Simulation requested to create agent of type {0:s}".format(agent_type) + assert ( + False + ), "Unknown agent type. Simulation requested to create agent of type {0:s}".format( + agent_type + ) if np.random.random() < prob: return True else: @@ -732,12 +1016,14 @@ def record_market_exit(self): def record_unrecovered_claims(self, loss): self.cumulative_unrecovered_claims += loss - def record_claims(self, claims): #This method records every claim made to insurers and reinsurers. It is called from both insurers and reinsurers (metainsuranceorg.py). + def record_claims( + self, claims + ): # This method records every claim made to insurers and reinsurers. It is called from both insurers and reinsurers (metainsuranceorg.py). self.cumulative_claims += claims - + def log(self): self.logger.save_log(self.background_run) - + def compute_market_diffvar(self): varsfirms = [] @@ -765,20 +1051,27 @@ def compute_market_diffvar(self): totalreal = totalreal + sum(varsreinfirms) totaldiff = totalina - totalreal - + return totaldiff - #self.history_logs['market_diffvar'].append(totaldiff) + # self.history_logs['market_diffvar'].append(totaldiff) def count_underwritten_and_reinsured_risks_by_category(self): underwritten_risks = 0 reinsured_risks = 0 - underwritten_per_category = np.zeros(self.simulation_parameters["no_categories"]) + underwritten_per_category = np.zeros( + self.simulation_parameters["no_categories"] + ) reinsured_per_category = np.zeros(self.simulation_parameters["no_categories"]) for firm in self.insurancefirms: if firm.operational: underwritten_by_category += firm.counter_category - if self.simulation_parameters["simulation_reinsurance_type"] == "non-proportional": - reinsured_per_category += firm.counter_category * firm.category_reinsurance + if ( + self.simulation_parameters["simulation_reinsurance_type"] + == "non-proportional" + ): + reinsured_per_category += ( + firm.counter_category * firm.category_reinsurance + ) if self.simulation_parameters["simulation_reinsurance_type"] == "proportional": for firm in self.insurancefirms: if firm.operational: @@ -795,28 +1088,44 @@ def get_unique_reinsurer_id(self): return current_id def insurance_entry_index(self): - return self.insurance_models_counter[0:self.simulation_parameters["no_riskmodels"]].argmin() + return self.insurance_models_counter[ + 0 : self.simulation_parameters["no_riskmodels"] + ].argmin() def reinsurance_entry_index(self): - return self.reinsurance_models_counter[0:self.simulation_parameters["no_riskmodels"]].argmin() + return self.reinsurance_models_counter[ + 0 : self.simulation_parameters["no_riskmodels"] + ].argmin() def get_operational(self): return True - def reinsurance_capital_entry(self): #This method determines the capital market entry of reinsurers. It is only run in start.py. + def reinsurance_capital_entry( + self + ): # This method determines the capital market entry of reinsurers. It is only run in start.py. capital_per_non_re_cat = [] for reinrisk in self.not_accepted_reinrisks: - capital_per_non_re_cat.append(reinrisk["value"]) #It takes all the values of the reinsurance risks NOT REINSURED. - - if len(capital_per_non_re_cat) > 0: #We only perform this action if there are reinsurance contracts that has not been reinsured in the last period of time. - capital_per_non_re_cat = np.random.choice(capital_per_non_re_cat, 10) #Only 10 values sampled randomly are considered. (Too low?) - entry = max(capital_per_non_re_cat) #For market entry the maximum of the sample is considered. - entry = 2 * entry #The capital market entry of those values will be the double of the maximum. - else: #Otherwise the default reinsurance cash market entry is considered. + capital_per_non_re_cat.append( + reinrisk["value"] + ) # It takes all the values of the reinsurance risks NOT REINSURED. + + if ( + len(capital_per_non_re_cat) > 0 + ): # We only perform this action if there are reinsurance contracts that has not been reinsured in the last period of time. + capital_per_non_re_cat = np.random.choice( + capital_per_non_re_cat, 10 + ) # Only 10 values sampled randomly are considered. (Too low?) + entry = max( + capital_per_non_re_cat + ) # For market entry the maximum of the sample is considered. + entry = ( + 2 * entry + ) # The capital market entry of those values will be the double of the maximum. + else: # Otherwise the default reinsurance cash market entry is considered. entry = self.simulation_parameters["initial_reinagent_cash"] - return entry #The capital market entry is returned. + return entry # The capital market entry is returned. def reset_pls(self): """Reset_pls Method. @@ -831,4 +1140,3 @@ def reset_pls(self): for catbond in self.catbonds: catbond.reset_pl() - diff --git a/isleconfig.py b/isleconfig.py index 2885c21..7bd66ae 100644 --- a/isleconfig.py +++ b/isleconfig.py @@ -4,76 +4,78 @@ force_foreground = False verbose = False showprogress = False -show_network = False # Should network be visualized? This should be False by default, to be overridden by commandline arguments -slim_log = True # Should logs be small in ensemble runs (only aggregated level data)? - -simulation_parameters={"no_categories": 4, - "no_insurancefirms": 20, - "no_reinsurancefirms": 4, - "no_riskmodels": 2, - "riskmodel_inaccuracy_parameter": 2, # values >=1; inaccuracy higher with higher values - "riskmodel_margin_of_safety": 2, # values >=1; factor of additional liquidity beyond value at risk - "margin_increase": 0, # This parameter modifies the margin of safety depending on the number of risks models available in the market. When is 0 all risk models have the same margin of safety. - "value_at_risk_tail_probability": 0.005, # values <1, >0, usually close to 0; tail probability at which the value at risk is taken by the risk models - "norm_profit_markup": 0.15, - "rein_norm_profit_markup": 0.15, - "dividend_share_of_profits": 0.4, - "mean_contract_runtime": 12, - "contract_runtime_halfspread": 2, - "default_contract_payment_period": 3, - "max_time": 1000, - "money_supply": 2000000000, - "event_time_mean_separation": 100 / 3., - "expire_immediately": False, - "risk_factors_present": False, - "risk_factor_lower_bound": 0.4, - "risk_factor_upper_bound": 0.6, - "initial_acceptance_threshold": 0.5, - "acceptance_threshold_friction": 0.9, - "insurance_firm_market_entry_probability": 0.3, #0.02, - "reinsurance_firm_market_entry_probability": 0.05, #0.004, - "simulation_reinsurance_type": 'non-proportional', - "default_non-proportional_reinsurance_deductible": 0.3, - "default_non-proportional_reinsurance_excess": 1.0, - "default_non-proportional_reinsurance_premium_share": 0.3, - "static_non-proportional_reinsurance_levels": False, - "catbonds_off": True, - "reinsurance_off": False, - "capacity_target_decrement_threshold": 1.8, - "capacity_target_increment_threshold": 1.2, - "capacity_target_decrement_factor": 24/25., - "capacity_target_increment_factor": 25/24., - # Retention parameters - "insurance_retention": 0.85, # Ratio of insurance contracts retained every iteration. - "reinsurance_retention": 1, # Ratio of reinsurance contracts retained every iteration. - #Premium sensitivity parameters - "premium_sensitivity": 5, # This parameter represents how sensitive is the variation of the insurance premium with respect of the capital of the market. Higher means more sensitive. - "reinpremium_sensitivity": 6, # This parameter represents how sensitive is the variation of the reinsurance premium with respect of the capital of the market. Higher means more sensitive. - #Balanced portfolio parameters - "insurers_balance_ratio": 0.1, # This ratio represents how low we want to keep the standard deviation of the cash reserved below the mean for insurers. Lower means more balanced. - "reinsurers_balance_ratio": 20, # This ratio represents how low we want to keep the standard deviation of the cash reserved below the mean for reinsurers. Lower means more balanced. (Deactivated for the moment) - "insurers_recursion_limit": 50, # Intensity of the recursion algorithm to balance the portfolio of risks for insurers. - "reinsurers_recursion_limit": 10, # Intensity of the recursion algorithm to balance the portfolio of risks for reinsurers. - #Market permanency parameters - "market_permanency_off": False, # This parameter activates (deactivates) the following market permanency constraints. - "cash_permanency_limit": 100, # This parameter enforces the limit under which the firms leave the market because they cannot underwrite anything. - "insurance_permanency_contracts_limit": 4, # If insurers stay for too long under this limit of contracts they deccide to leave the market. - "insurance_permanency_ratio_limit": 0.6, # If insurers stay for too long under this limit they deccide to leave the market because they have too much capital. - "insurance_permanency_time_constraint": 24, # This parameter defines the period that the insurers wait if they have few capital or few contract before leaving the market. - "reinsurance_permanency_contracts_limit": 2, # If reinsurers stay for too long under this limit of contracts they deccide to leave the market. - "reinsurance_permanency_ratio_limit": 0.8, # If reinsurers stay for too long under this limit they deccide to leave the market because they have too much capital. - "reinsurance_permanency_time_constraint": 48, # This parameter defines the period that the reinsurers wait if they have few capital or few contract before leaving the market. - #Insurance and Reinsurance deductibles - "insurance_reinsurance_levels_lower_bound": 0.25, - "insurance_reinsurance_levels_upper_bound": 0.30, - "reinsurance_reinsurance_levels_lower_bound": 0.5, - "reinsurance_reinsurance_levels_upper_bound": 0.95, - "initial_agent_cash": 80000, - "initial_reinagent_cash": 2000000, - "interest_rate": 0.001, - "reinsurance_limit": 0.1, - "upper_price_limit": 1.2, - "lower_price_limit": 0.85, - "no_risks": 20000} - +show_network = ( + False +) # Should network be visualized? This should be False by default, to be overridden by commandline arguments +slim_log = True # Should logs be small in ensemble runs (only aggregated level data)? +simulation_parameters = { + "no_categories": 4, + "no_insurancefirms": 20, + "no_reinsurancefirms": 4, + "no_riskmodels": 2, + "riskmodel_inaccuracy_parameter": 2, # values >=1; inaccuracy higher with higher values + "riskmodel_margin_of_safety": 2, # values >=1; factor of additional liquidity beyond value at risk + "margin_increase": 0, # This parameter modifies the margin of safety depending on the number of risks models available in the market. When is 0 all risk models have the same margin of safety. + "value_at_risk_tail_probability": 0.005, # values <1, >0, usually close to 0; tail probability at which the value at risk is taken by the risk models + "norm_profit_markup": 0.15, + "rein_norm_profit_markup": 0.15, + "dividend_share_of_profits": 0.4, + "mean_contract_runtime": 12, + "contract_runtime_halfspread": 2, + "default_contract_payment_period": 3, + "max_time": 1000, + "money_supply": 2000000000, + "event_time_mean_separation": 100 / 3.0, + "expire_immediately": False, + "risk_factors_present": False, + "risk_factor_lower_bound": 0.4, + "risk_factor_upper_bound": 0.6, + "initial_acceptance_threshold": 0.5, + "acceptance_threshold_friction": 0.9, + "insurance_firm_market_entry_probability": 0.3, # 0.02, + "reinsurance_firm_market_entry_probability": 0.05, # 0.004, + "simulation_reinsurance_type": "non-proportional", + "default_non-proportional_reinsurance_deductible": 0.3, + "default_non-proportional_reinsurance_excess": 1.0, + "default_non-proportional_reinsurance_premium_share": 0.3, + "static_non-proportional_reinsurance_levels": False, + "catbonds_off": True, + "reinsurance_off": False, + "capacity_target_decrement_threshold": 1.8, + "capacity_target_increment_threshold": 1.2, + "capacity_target_decrement_factor": 24 / 25.0, + "capacity_target_increment_factor": 25 / 24.0, + # Retention parameters + "insurance_retention": 0.85, # Ratio of insurance contracts retained every iteration. + "reinsurance_retention": 1, # Ratio of reinsurance contracts retained every iteration. + # Premium sensitivity parameters + "premium_sensitivity": 5, # This parameter represents how sensitive is the variation of the insurance premium with respect of the capital of the market. Higher means more sensitive. + "reinpremium_sensitivity": 6, # This parameter represents how sensitive is the variation of the reinsurance premium with respect of the capital of the market. Higher means more sensitive. + # Balanced portfolio parameters + "insurers_balance_ratio": 0.1, # This ratio represents how low we want to keep the standard deviation of the cash reserved below the mean for insurers. Lower means more balanced. + "reinsurers_balance_ratio": 20, # This ratio represents how low we want to keep the standard deviation of the cash reserved below the mean for reinsurers. Lower means more balanced. (Deactivated for the moment) + "insurers_recursion_limit": 50, # Intensity of the recursion algorithm to balance the portfolio of risks for insurers. + "reinsurers_recursion_limit": 10, # Intensity of the recursion algorithm to balance the portfolio of risks for reinsurers. + # Market permanency parameters + "market_permanency_off": False, # This parameter activates (deactivates) the following market permanency constraints. + "cash_permanency_limit": 100, # This parameter enforces the limit under which the firms leave the market because they cannot underwrite anything. + "insurance_permanency_contracts_limit": 4, # If insurers stay for too long under this limit of contracts they deccide to leave the market. + "insurance_permanency_ratio_limit": 0.6, # If insurers stay for too long under this limit they deccide to leave the market because they have too much capital. + "insurance_permanency_time_constraint": 24, # This parameter defines the period that the insurers wait if they have few capital or few contract before leaving the market. + "reinsurance_permanency_contracts_limit": 2, # If reinsurers stay for too long under this limit of contracts they deccide to leave the market. + "reinsurance_permanency_ratio_limit": 0.8, # If reinsurers stay for too long under this limit they deccide to leave the market because they have too much capital. + "reinsurance_permanency_time_constraint": 48, # This parameter defines the period that the reinsurers wait if they have few capital or few contract before leaving the market. + # Insurance and Reinsurance deductibles + "insurance_reinsurance_levels_lower_bound": 0.25, + "insurance_reinsurance_levels_upper_bound": 0.30, + "reinsurance_reinsurance_levels_lower_bound": 0.5, + "reinsurance_reinsurance_levels_upper_bound": 0.95, + "initial_agent_cash": 80000, + "initial_reinagent_cash": 2000000, + "interest_rate": 0.001, + "reinsurance_limit": 0.1, + "upper_price_limit": 1.2, + "lower_price_limit": 0.85, + "no_risks": 20000, +} diff --git a/listify.py b/listify.py index 591e626..c410c21 100644 --- a/listify.py +++ b/listify.py @@ -1,6 +1,7 @@ """Auxiliary function to transform dicts into lists and back for transfer from cloud (sandman2) to local.""" + def listify(d): """Function to convert dict to list with keys in last list element. Arguments: @@ -8,16 +9,17 @@ def listify(d): Returns: list with dict values as elements [:-1] and dict keys as last element.""" - + """extract keys""" keys = list(d.keys()) - + """create list""" l = [d[key] for key in keys] l.append(keys) - + return l + def delistify(l): """Function to convert listified dict back to dict. Arguments: @@ -26,12 +28,12 @@ def delistify(l): dict keys as list in the last element. Returns: dict - The restored dict.""" - + """extract keys""" keys = l.pop() assert len(keys) == len(l) - + """create dict""" - d = {key: l[i] for i,key in enumerate(keys)} - + d = {key: l[i] for i, key in enumerate(keys)} + return d diff --git a/logger.py b/logger.py index 8fe928f..60f14d3 100644 --- a/logger.py +++ b/logger.py @@ -5,16 +5,22 @@ import listify LOG_DEFAULT = ( - 'total_cash total_excess_capital total_profitslosses total_contracts ' - 'total_operational total_reincash total_reinexcess_capital total_reinprofitslosses ' - 'total_reincontracts total_reinoperational total_catbondsoperational market_premium ' - 'market_reinpremium cumulative_bankruptcies cumulative_market_exits cumulative_unrecovered_claims ' - 'cumulative_claims insurance_firms_cash reinsurance_firms_cash market_diffvar ' - 'rc_event_schedule_initial rc_event_damage_initial number_riskmodels' -).split(' ') - -class Logger(): - def __init__(self, no_riskmodels=None, rc_event_schedule_initial=None, rc_event_damage_initial=None): + "total_cash total_excess_capital total_profitslosses total_contracts " + "total_operational total_reincash total_reinexcess_capital total_reinprofitslosses " + "total_reincontracts total_reinoperational total_catbondsoperational market_premium " + "market_reinpremium cumulative_bankruptcies cumulative_market_exits cumulative_unrecovered_claims " + "cumulative_claims insurance_firms_cash reinsurance_firms_cash market_diffvar " + "rc_event_schedule_initial rc_event_damage_initial number_riskmodels" +).split(" ") + + +class Logger: + def __init__( + self, + no_riskmodels=None, + rc_event_schedule_initial=None, + rc_event_damage_initial=None, + ): """Constructor. Prepares history_logs atribute as dict for the logs. Records initial event schedule of simulation run. Arguments @@ -22,82 +28,90 @@ def __init__(self, no_riskmodels=None, rc_event_schedule_initial=None, rc_event_ rc_event_schedule_initial: list of lists of int. Times of risk events by category rc_event_damage_initial: list of arrays (or lists) of float. Damage by peril for each category as share of total possible damage (maximum insured or excess). - Returns class instance.""" - + Returns class instance.""" + """Record number of riskmodels""" self.number_riskmodels = no_riskmodels - + """Record initial event schedule""" self.rc_event_schedule_initial = rc_event_schedule_initial self.rc_event_damage_initial = rc_event_damage_initial """Prepare history log dict""" self.history_logs = {} - + """Variables pertaining to insurance sector""" # TODO: should we not have `cumulative_bankruptcies` and # `cumulative_market_exits` for both insurance firms and reinsurance firms? # `cumulative_claims`: Here are stored the total cumulative claims received # by the whole insurance sector until a certain time. - insurance_sector = ('total_cash total_excess_capital total_profitslosses ' - 'total_contracts total_operational cumulative_bankruptcies ' - 'cumulative_market_exits cumulative_claims cumulative_unrecovered_claims').split(' ') + insurance_sector = ( + "total_cash total_excess_capital total_profitslosses " + "total_contracts total_operational cumulative_bankruptcies " + "cumulative_market_exits cumulative_claims cumulative_unrecovered_claims" + ).split(" ") for _v in insurance_sector: self.history_logs[_v] = [] - + """Variables pertaining to individual insurance firms""" - self.history_logs['individual_contracts'] = [] # TODO: Should there not be a similar record for reinsurance - self.history_logs['insurance_firms_cash'] = [] - + self.history_logs[ + "individual_contracts" + ] = [] # TODO: Should there not be a similar record for reinsurance + self.history_logs["insurance_firms_cash"] = [] + """Variables pertaining to reinsurance sector""" - self.history_logs['total_reincash'] = [] - self.history_logs['total_reinexcess_capital'] = [] - self.history_logs['total_reinprofitslosses'] = [] - self.history_logs['total_reincontracts'] = [] - self.history_logs['total_reinoperational'] = [] + self.history_logs["total_reincash"] = [] + self.history_logs["total_reinexcess_capital"] = [] + self.history_logs["total_reinprofitslosses"] = [] + self.history_logs["total_reincontracts"] = [] + self.history_logs["total_reinoperational"] = [] """Variables pertaining to individual reinsurance firms""" - self.history_logs['reinsurance_firms_cash'] = [] + self.history_logs["reinsurance_firms_cash"] = [] """Variables pertaining to cat bonds""" - self.history_logs['total_catbondsoperational'] = [] + self.history_logs["total_catbondsoperational"] = [] """Variables pertaining to premiums""" - self.history_logs['market_premium'] = [] - self.history_logs['market_reinpremium'] = [] - self.history_logs['market_diffvar'] = [] - + self.history_logs["market_premium"] = [] + self.history_logs["market_reinpremium"] = [] + self.history_logs["market_diffvar"] = [] + def record_data(self, data_dict): """Method to record data for one period Arguments data_dict: Type dict. Data with the same keys as are used in self.history_log(). - Returns None.""" + Returns None.""" for key in data_dict.keys(): if key != "individual_contracts": self.history_logs[key].append(data_dict[key]) else: for i in range(len(data_dict["individual_contracts"])): - self.history_logs['individual_contracts'][i].append(data_dict["individual_contracts"][i]) + self.history_logs["individual_contracts"][i].append( + data_dict["individual_contracts"][i] + ) - def obtain_log(self, requested_logs=LOG_DEFAULT): #This function allows to return in a list all the data generated by the model. There is no other way to transfer it back from the cloud. + def obtain_log( + self, requested_logs=LOG_DEFAULT + ): # This function allows to return in a list all the data generated by the model. There is no other way to transfer it back from the cloud. """Method to transfer entire log (self.history_log as well as risk event schedule). This is used to transfer the log to master core from work cores in ensemble runs in the cloud. No arguments. Returns list (listified dict).""" - + """Include environment variables (number of risk models and risk event schedule)""" self.history_logs["number_riskmodels"] = self.number_riskmodels self.history_logs["rc_event_damage_initial"] = self.rc_event_damage_initial self.history_logs["rc_event_schedule_initial"] = self.rc_event_schedule_initial - + """Parse logs to be returned""" if requested_logs == None: requested_logs = LOG_DEFAULT log = {name: self.history_logs[name] for name in requested_logs} - + """Convert to list and return""" return listify.listify(log) - + def restore_logger_object(self, log): """Method to restore logger object. A log can be restored later. It can also be restored on a different machine. This is useful in the case of ensemble runs to move the log to @@ -110,13 +124,17 @@ def restore_logger_object(self, log): """Restore dict""" log = listify.delistify(log) - + """Extract environment variables (number of risk models and risk event schedule)""" self.rc_event_schedule_initial = log["rc_event_schedule_initial"] self.rc_event_damage_initial = log["rc_event_damage_initial"] self.number_riskmodels = log["number_riskmodels"] - del log["rc_event_schedule_initial"], log["rc_event_damage_initial"], log["number_riskmodels"] - + del ( + log["rc_event_schedule_initial"], + log["rc_event_damage_initial"], + log["number_riskmodels"], + ) + """Restore history log""" self.history_logs = log @@ -126,18 +144,18 @@ def save_log(self, background_run): Arguments: background_run: Type bool. Is this an ensemble run (true) or not (false). Returns None.""" - + """Prepare writing tasks""" if background_run: to_log = self.replication_log_prepare() else: to_log = self.single_log_prepare() - + """Write to disk""" for filename, data, operation_character in to_log: with open(filename, operation_character) as wfile: wfile.write(str(data) + "\n") - + def replication_log_prepare(self): """Method to prepare writing tasks for ensemble run saving. No arguments @@ -150,7 +168,7 @@ def replication_log_prepare(self): to_log = [] to_log.append(("data/" + fpf + "_history_logs.dat", self.history_logs, "a")) return to_log - + def single_log_prepare(self): """Method to prepare writing tasks for single run saving. No arguments @@ -161,17 +179,18 @@ def single_log_prepare(self): to_log = [] to_log.append(("data/history_logs.dat", self.history_logs, "w")) return to_log - - def add_insurance_agent(self): + + def add_insurance_agent(self): """Method for adding an additional insurer agent to the history log. This is necessary to keep the number of individual insurance firm logs constant in time. No arguments. Returns None.""" - # TODO: should this not also be done for self.history_logs['insurance_firms_cash'] and + # TODO: should this not also be done for self.history_logs['insurance_firms_cash'] and # self.history_logs['reinsurance_firms_cash'] - if len(self.history_logs['individual_contracts']) > 0: - zeroes_to_append = list(np.zeros(len(self.history_logs['individual_contracts'][0]), dtype=int)) + if len(self.history_logs["individual_contracts"]) > 0: + zeroes_to_append = list( + np.zeros(len(self.history_logs["individual_contracts"][0]), dtype=int) + ) else: zeroes_to_append = [] - self.history_logs['individual_contracts'].append(zeroes_to_append) - + self.history_logs["individual_contracts"].append(zeroes_to_append) diff --git a/metainsurancecontract.py b/metainsurancecontract.py index 20d17bb..7df9cb7 100644 --- a/metainsurancecontract.py +++ b/metainsurancecontract.py @@ -1,9 +1,23 @@ import numpy as np import sys, pdb -class MetaInsuranceContract(): - def __init__(self, insurer, properties, time, premium, runtime, payment_period, expire_immediately, initial_VaR=0., \ - insurancetype="proportional", deductible_fraction=None, excess_fraction=None, reinsurance=0): + +class MetaInsuranceContract: + def __init__( + self, + insurer, + properties, + time, + premium, + runtime, + payment_period, + expire_immediately, + initial_VaR=0.0, + insurancetype="proportional", + deductible_fraction=None, + excess_fraction=None, + reinsurance=0, + ): """Constructor method. Accepts arguments insurer: Type InsuranceFirm. @@ -24,15 +38,19 @@ def __init__(self, insurer, properties, time, premium, runtime, payment_period, Creates InsuranceContract, saves parameters. Creates obligation for premium payment. Includes contract in reinsurance network if applicable (e.g. if this is a ReinsuranceContract).""" # TODO: argument reinsurance seems senseless; remove? - + # Save parameters self.insurer = insurer self.risk_factor = properties["risk_factor"] self.category = properties["category"] self.property_holder = properties["owner"] self.value = properties["value"] - self.contract = properties.get("contract") # will assign None if key does not exist - self.insurancetype = properties.get("insurancetype") if insurancetype is None else insurancetype + self.contract = properties.get( + "contract" + ) # will assign None if key does not exist + self.insurancetype = ( + properties.get("insurancetype") if insurancetype is None else insurancetype + ) self.runtime = runtime self.starttime = time self.expiration = runtime + time @@ -40,63 +58,83 @@ def __init__(self, insurer, properties, time, premium, runtime, payment_period, self.terminating = False self.current_claim = 0 self.initial_VaR = initial_VaR - - # set deductible from argument, risk property or default value, whichever first is not None + + # set deductible from argument, risk property or default value, whichever first is not None default_deductible_fraction = 0.0 - deductible_fraction_generator = (item for item in [deductible_fraction, properties.get("deductible_fraction"), \ - default_deductible_fraction] if item is not None) + deductible_fraction_generator = ( + item + for item in [ + deductible_fraction, + properties.get("deductible_fraction"), + default_deductible_fraction, + ] + if item is not None + ) self.deductible_fraction = next(deductible_fraction_generator) self.deductible = self.deductible_fraction * self.value - - # set excess from argument, risk property or default value, whichever first is not None + + # set excess from argument, risk property or default value, whichever first is not None default_excess_fraction = 1.0 - excess_fraction_generator = (item for item in [excess_fraction, properties.get("excess_fraction"), \ - default_excess_fraction] if item is not None) + excess_fraction_generator = ( + item + for item in [ + excess_fraction, + properties.get("excess_fraction"), + default_excess_fraction, + ] + if item is not None + ) self.excess_fraction = next(excess_fraction_generator) self.excess = self.excess_fraction * self.value - + self.reinsurance = reinsurance self.reinsurer = None self.reincontract = None self.reinsurance_share = None - #self.is_reinsurancecontract = False + # self.is_reinsurancecontract = False # setup payment schedule - #total_premium = premium * (self.excess - self.deductible) # TODO: excess and deductible should not be considered linearily in premium computation; this should be shifted to the (re)insurer who supplies the premium as argument to the contract's constructor method - total_premium = premium * self.value + # total_premium = premium * (self.excess - self.deductible) # TODO: excess and deductible should not be considered linearily in premium computation; this should be shifted to the (re)insurer who supplies the premium as argument to the contract's constructor method + total_premium = premium * self.value self.periodized_premium = total_premium / self.runtime - self.payment_times = [time + i for i in range(runtime) if i % payment_period == 0] - self.payment_values = total_premium * (np.ones(len(self.payment_times)) / len(self.payment_times)) - + self.payment_times = [ + time + i for i in range(runtime) if i % payment_period == 0 + ] + self.payment_values = total_premium * ( + np.ones(len(self.payment_times)) / len(self.payment_times) + ) + ## Create obligation for premium payment - #self.property_holder.receive_obligation(premium * (self.excess - self.deductible), self.insurer, time, 'premium') - + # self.property_holder.receive_obligation(premium * (self.excess - self.deductible), self.insurer, time, 'premium') + # Embed contract in reinsurance network, if applicable if self.contract is not None: - self.contract.reinsure(reinsurer=self.insurer, reinsurance_share=properties["reinsurance_share"], \ - reincontract=self) + self.contract.reinsure( + reinsurer=self.insurer, + reinsurance_share=properties["reinsurance_share"], + reincontract=self, + ) # This flag is set to 1, when the contract is about to expire and there is an attempt to roll it over. self.roll_over_flag = 0 - - def check_payment_due(self, time): if len(self.payment_times) > 0 and time >= self.payment_times[0]: # Create obligation for premium payment - #self.property_holder.receive_obligation(premium * (self.excess - self.deductible), self.insurer, time, 'premium') - self.property_holder.receive_obligation(self.payment_values[0], self.insurer, time, 'premium') - + # self.property_holder.receive_obligation(premium * (self.excess - self.deductible), self.insurer, time, 'premium') + self.property_holder.receive_obligation( + self.payment_values[0], self.insurer, time, "premium" + ) + # Remove current payment from payment schedule self.payment_times = self.payment_times[1:] self.payment_values = self.payment_values[1:] - + def get_and_reset_current_claim(self): current_claim = self.current_claim self.current_claim = 0 return self.category, current_claim, (self.insurancetype == "proportional") - def terminate_reinsurance(self, time): """Terminate reinsurance method. Accepts arguments @@ -105,7 +143,7 @@ def terminate_reinsurance(self, time): Causes any reinsurance contracts to be dissolved as the present contract terminates.""" if self.reincontract is not None: self.reincontract.dissolve(time) - + def dissolve(self, time): """Dissolve method. Accepts arguments @@ -126,9 +164,9 @@ def reinsure(self, reinsurer, reinsurance_share, reincontract): self.reinsurance = self.value * reinsurance_share self.reinsurance_share = reinsurance_share self.reincontract = reincontract - assert self.reinsurance_share in [None, 0.0, 1.0] - - def unreinsure(self): + assert self.reinsurance_share in [None, 0.0, 1.0] + + def unreinsure(self): """Unreinsurance Method. Accepts no arguments: No return value. diff --git a/metainsuranceorg.py b/metainsuranceorg.py index c0fa39e..f9ea1ec 100644 --- a/metainsuranceorg.py +++ b/metainsuranceorg.py @@ -1,4 +1,3 @@ - import isleconfig import numpy as np import scipy.stats @@ -11,99 +10,150 @@ if isleconfig.use_abce: from genericagentabce import GenericAgent - #print("abce imported") + + # print("abce imported") else: from genericagent import GenericAgent - #print("abce not imported") + + # print("abce not imported") + def get_mean(x): return sum(x) / len(x) + def get_mean_std(x): m = get_mean(x) variance = sum((val - m) ** 2 for val in x) return m, np.sqrt(variance / len(x)) + class MetaInsuranceOrg(GenericAgent): def init(self, simulation_parameters, agent_parameters): - self.simulation = simulation_parameters['simulation'] + self.simulation = simulation_parameters["simulation"] self.simulation_parameters = simulation_parameters - self.contract_runtime_dist = scipy.stats.randint(simulation_parameters["mean_contract_runtime"] - \ - simulation_parameters["contract_runtime_halfspread"], simulation_parameters["mean_contract_runtime"] \ - + simulation_parameters["contract_runtime_halfspread"] + 1) - self.default_contract_payment_period = simulation_parameters["default_contract_payment_period"] - self.id = agent_parameters['id'] - self.cash = agent_parameters['initial_cash'] + self.contract_runtime_dist = scipy.stats.randint( + simulation_parameters["mean_contract_runtime"] + - simulation_parameters["contract_runtime_halfspread"], + simulation_parameters["mean_contract_runtime"] + + simulation_parameters["contract_runtime_halfspread"] + + 1, + ) + self.default_contract_payment_period = simulation_parameters[ + "default_contract_payment_period" + ] + self.id = agent_parameters["id"] + self.cash = agent_parameters["initial_cash"] self.capacity_target = self.cash * 0.9 - self.capacity_target_decrement_threshold = agent_parameters['capacity_target_decrement_threshold'] - self.capacity_target_increment_threshold = agent_parameters['capacity_target_increment_threshold'] - self.capacity_target_decrement_factor = agent_parameters['capacity_target_decrement_factor'] - self.capacity_target_increment_factor = agent_parameters['capacity_target_increment_factor'] + self.capacity_target_decrement_threshold = agent_parameters[ + "capacity_target_decrement_threshold" + ] + self.capacity_target_increment_threshold = agent_parameters[ + "capacity_target_increment_threshold" + ] + self.capacity_target_decrement_factor = agent_parameters[ + "capacity_target_decrement_factor" + ] + self.capacity_target_increment_factor = agent_parameters[ + "capacity_target_increment_factor" + ] self.excess_capital = self.cash self.premium = agent_parameters["norm_premium"] - self.profit_target = agent_parameters['profit_target'] - self.acceptance_threshold = agent_parameters['initial_acceptance_threshold'] # 0.5 - self.acceptance_threshold_friction = agent_parameters['acceptance_threshold_friction'] # 0.9 #1.0 to switch off + self.profit_target = agent_parameters["profit_target"] + self.acceptance_threshold = agent_parameters[ + "initial_acceptance_threshold" + ] # 0.5 + self.acceptance_threshold_friction = agent_parameters[ + "acceptance_threshold_friction" + ] # 0.9 #1.0 to switch off self.interest_rate = agent_parameters["interest_rate"] self.reinsurance_limit = agent_parameters["reinsurance_limit"] self.simulation_no_risk_categories = simulation_parameters["no_categories"] - self.simulation_reinsurance_type = simulation_parameters["simulation_reinsurance_type"] - self.dividend_share_of_profits = simulation_parameters["dividend_share_of_profits"] - - self.owner = self.simulation # TODO: Make this into agent_parameter value? + self.simulation_reinsurance_type = simulation_parameters[ + "simulation_reinsurance_type" + ] + self.dividend_share_of_profits = simulation_parameters[ + "dividend_share_of_profits" + ] + + self.owner = self.simulation # TODO: Make this into agent_parameter value? self.per_period_dividend = 0 - self.cash_last_periods = list(np.zeros(4, dtype=int)*self.cash) - - rm_config = agent_parameters['riskmodel_config'] + self.cash_last_periods = list(np.zeros(4, dtype=int) * self.cash) + + rm_config = agent_parameters["riskmodel_config"] """Here we modify the margin of safety depending on the number of risks models available in the market. When is 0 all risk models have the same margin of safety. The reason for doing this is that with more risk models the firms tend to be closer to the max capacity""" - margin_of_safety_correction = (rm_config["margin_of_safety"] + (simulation_parameters["no_riskmodels"] - 1) * simulation_parameters["margin_increase"]) - - self.riskmodel = RiskModel(damage_distribution=rm_config["damage_distribution"], \ - expire_immediately=rm_config["expire_immediately"], \ - cat_separation_distribution=rm_config["cat_separation_distribution"], \ - norm_premium=rm_config["norm_premium"], \ - category_number=rm_config["no_categories"], \ - init_average_exposure=rm_config["risk_value_mean"], \ - init_average_risk_factor=rm_config["risk_factor_mean"], \ - init_profit_estimate=rm_config["norm_profit_markup"], \ - margin_of_safety=margin_of_safety_correction, \ - var_tail_prob=rm_config["var_tail_prob"], \ - inaccuracy=rm_config["inaccuracy_by_categ"]) - - self.category_reinsurance = [None for i in range(self.simulation_no_risk_categories)] - if self.simulation_reinsurance_type == 'non-proportional': - if agent_parameters['non-proportional_reinsurance_level'] is not None: - self.np_reinsurance_deductible_fraction = agent_parameters['non-proportional_reinsurance_level'] + margin_of_safety_correction = ( + rm_config["margin_of_safety"] + + (simulation_parameters["no_riskmodels"] - 1) + * simulation_parameters["margin_increase"] + ) + + self.riskmodel = RiskModel( + damage_distribution=rm_config["damage_distribution"], + expire_immediately=rm_config["expire_immediately"], + cat_separation_distribution=rm_config["cat_separation_distribution"], + norm_premium=rm_config["norm_premium"], + category_number=rm_config["no_categories"], + init_average_exposure=rm_config["risk_value_mean"], + init_average_risk_factor=rm_config["risk_factor_mean"], + init_profit_estimate=rm_config["norm_profit_markup"], + margin_of_safety=margin_of_safety_correction, + var_tail_prob=rm_config["var_tail_prob"], + inaccuracy=rm_config["inaccuracy_by_categ"], + ) + + self.category_reinsurance = [ + None for i in range(self.simulation_no_risk_categories) + ] + if self.simulation_reinsurance_type == "non-proportional": + if agent_parameters["non-proportional_reinsurance_level"] is not None: + self.np_reinsurance_deductible_fraction = agent_parameters[ + "non-proportional_reinsurance_level" + ] else: - self.np_reinsurance_deductible_fraction = simulation_parameters["default_non-proportional_reinsurance_deductible"] - self.np_reinsurance_excess_fraction = simulation_parameters["default_non-proportional_reinsurance_excess"] - self.np_reinsurance_premium_share = simulation_parameters["default_non-proportional_reinsurance_premium_share"] + self.np_reinsurance_deductible_fraction = simulation_parameters[ + "default_non-proportional_reinsurance_deductible" + ] + self.np_reinsurance_excess_fraction = simulation_parameters[ + "default_non-proportional_reinsurance_excess" + ] + self.np_reinsurance_premium_share = simulation_parameters[ + "default_non-proportional_reinsurance_premium_share" + ] self.obligations = [] self.underwritten_contracts = [] self.profits_losses = 0 - #self.reinsurance_contracts = [] + # self.reinsurance_contracts = [] self.operational = True self.is_insurer = True self.is_reinsurer = False - + """set up risk value estimate variables""" - self.var_counter = 0 # sum over risk model inaccuracies for all contracts - self.var_counter_per_risk = 0 # average risk model inaccuracy across contracts - self.var_sum = 0 # sum over initial VaR for all contracts - self.counter_category = np.zeros(self.simulation_no_risk_categories) # var_counter disaggregated by category - self.var_category = np.zeros(self.simulation_no_risk_categories) # var_sum disaggregated by category + self.var_counter = 0 # sum over risk model inaccuracies for all contracts + self.var_counter_per_risk = 0 # average risk model inaccuracy across contracts + self.var_sum = 0 # sum over initial VaR for all contracts + self.counter_category = np.zeros( + self.simulation_no_risk_categories + ) # var_counter disaggregated by category + self.var_category = np.zeros( + self.simulation_no_risk_categories + ) # var_sum disaggregated by category self.naccep = [] self.risks_kept = [] self.reinrisks_kept = [] - self.balance_ratio = simulation_parameters['insurers_balance_ratio'] - self.recursion_limit = simulation_parameters['insurers_recursion_limit'] - self.cash_left_by_categ = [self.cash for i in range(self.simulation_parameters["no_categories"])] + self.balance_ratio = simulation_parameters["insurers_balance_ratio"] + self.recursion_limit = simulation_parameters["insurers_recursion_limit"] + self.cash_left_by_categ = [ + self.cash for i in range(self.simulation_parameters["no_categories"]) + ] self.market_permanency_counter = 0 - def iterate(self, time): # TODO: split function so that only the sequence of events remains here and everything else is in separate methods + def iterate( + self, time + ): # TODO: split function so that only the sequence of events remains here and everything else is in separate methods """obtain investments yield""" self.obtain_yield(time) @@ -111,14 +161,25 @@ def iterate(self, time): # TODO: split function so that only the sequence """realize due payments""" self.effect_payments(time) if isleconfig.verbose: - print(time, ":", self.id, len(self.underwritten_contracts), self.cash, self.operational) + print( + time, + ":", + self.id, + len(self.underwritten_contracts), + self.cash, + self.operational, + ) self.make_reinsurance_claims(time) """mature contracts""" if isleconfig.verbose: print("Number of underwritten contracts ", len(self.underwritten_contracts)) - maturing = [contract for contract in self.underwritten_contracts if contract.expiration <= time] + maturing = [ + contract + for contract in self.underwritten_contracts + if contract.expiration <= time + ] for contract in maturing: self.underwritten_contracts.remove(contract) contract.mature(time) @@ -132,39 +193,80 @@ def iterate(self, time): # TODO: split function so that only the sequence """request risks to be considered for underwriting in the next period and collect those for this period""" new_risks = [] if self.is_insurer: - new_risks += self.simulation.solicit_insurance_requests(self.id, self.cash, self) + new_risks += self.simulation.solicit_insurance_requests( + self.id, self.cash, self + ) if self.is_reinsurer: - new_risks += self.simulation.solicit_reinsurance_requests(self.id, self.cash, self) + new_risks += self.simulation.solicit_reinsurance_requests( + self.id, self.cash, self + ) contracts_offered = len(new_risks) if isleconfig.verbose and contracts_offered < 2 * contracts_dissolved: - print("Something wrong; agent {0:d} receives too few new contracts {1:d} <= {2:d}".format( - self.id, contracts_offered, 2*contracts_dissolved)) - - new_nonproportional_risks = [risk for risk in new_risks if risk.get("insurancetype")=='excess-of-loss' and risk["owner"] is not self] - new_risks = [risk for risk in new_risks if risk.get("insurancetype") in ['proportional', None] and risk["owner"] is not self] - + print( + "Something wrong; agent {0:d} receives too few new contracts {1:d} <= {2:d}".format( + self.id, contracts_offered, 2 * contracts_dissolved + ) + ) + + new_nonproportional_risks = [ + risk + for risk in new_risks + if risk.get("insurancetype") == "excess-of-loss" + and risk["owner"] is not self + ] + new_risks = [ + risk + for risk in new_risks + if risk.get("insurancetype") in ["proportional", None] + and risk["owner"] is not self + ] + """deal with non-proportional risks first as they must evaluate each request separatly, then with proportional ones""" - [reinrisks_per_categ, number_reinrisks_categ] = self.risks_reinrisks_organizer(new_nonproportional_risks) #Here the new reinrisks are organized by category. + [ + reinrisks_per_categ, + number_reinrisks_categ, + ] = self.risks_reinrisks_organizer( + new_nonproportional_risks + ) # Here the new reinrisks are organized by category. - for repetition in range(self.recursion_limit): # TODO: find an efficient way to stop the recursion if there are no more risks to accept or if it is not accepting any more over several iterations. + for repetition in range( + self.recursion_limit + ): # TODO: find an efficient way to stop the recursion if there are no more risks to accept or if it is not accepting any more over several iterations. former_reinrisks_per_categ = copy.copy(reinrisks_per_categ) - [reinrisks_per_categ, not_accepted_reinrisks] = self.process_newrisks_reinsurer(reinrisks_per_categ, number_reinrisks_categ, time) #Here we process all the new reinrisks in order to keep the portfolio as balanced as possible. - if former_reinrisks_per_categ == reinrisks_per_categ: #Stop condition implemented. Might solve the previous TODO. + [ + reinrisks_per_categ, + not_accepted_reinrisks, + ] = self.process_newrisks_reinsurer( + reinrisks_per_categ, number_reinrisks_categ, time + ) # Here we process all the new reinrisks in order to keep the portfolio as balanced as possible. + if ( + former_reinrisks_per_categ == reinrisks_per_categ + ): # Stop condition implemented. Might solve the previous TODO. break self.simulation.return_reinrisks(not_accepted_reinrisks) - underwritten_risks = [{"value": contract.value, "category": contract.category, \ - "risk_factor": contract.risk_factor, "deductible": contract.deductible, \ - "excess": contract.excess, "insurancetype": contract.insurancetype, \ - "runtime": contract.runtime} for contract in self.underwritten_contracts if - contract.reinsurance_share != 1.0] + underwritten_risks = [ + { + "value": contract.value, + "category": contract.category, + "risk_factor": contract.risk_factor, + "deductible": contract.deductible, + "excess": contract.excess, + "insurancetype": contract.insurancetype, + "runtime": contract.runtime, + } + for contract in self.underwritten_contracts + if contract.reinsurance_share != 1.0 + ] """obtain risk model evaluation (VaR) for underwriting decisions and for capacity specific decisions""" # TODO: Enable reinsurance shares other tan 0.0 and 1.0 - expected_profit, acceptable_by_category, cash_left_by_categ, var_per_risk_per_categ, self.excess_capital = self.riskmodel.evaluate(underwritten_risks, self.cash) - # TODO: resolve insurance reinsurance inconsistency (insurer underwrite after capacity decisions, reinsurers before). + expected_profit, acceptable_by_category, cash_left_by_categ, var_per_risk_per_categ, self.excess_capital = self.riskmodel.evaluate( + underwritten_risks, self.cash + ) + # TODO: resolve insurance reinsurance inconsistency (insurer underwrite after capacity decisions, reinsurers before). # This is currently so because it minimizes the number of times we need to run self.riskmodel.evaluate(). # It would also be more consistent if excess capital would be updated at the end of the iteration. """handle adjusting capacity target and capacity""" @@ -172,7 +274,7 @@ def iterate(self, time): # TODO: split function so that only the sequence self.adjust_capacity_target(max_var_by_categ) actual_capacity = self.increase_capacity(time, max_var_by_categ) # seek reinsurance - #if self.is_insurer: + # if self.is_insurer: # # TODO: Why should only insurers be able to get reinsurance (not reinsurers)? (Technically, it should work) --> OBSOLETE # self.ask_reinsurance(time) # # TODO: make independent of insurer/reinsurer, but change this to different deductable values @@ -182,38 +284,56 @@ def iterate(self, time): # TODO: split function so that only the sequence self.pay_dividends(time) """make underwriting decisions, category-wise""" - #if expected_profit * 1./self.cash < self.profit_target: + # if expected_profit * 1./self.cash < self.profit_target: # self.acceptance_threshold = ((self.acceptance_threshold - .4) * 5. * self.acceptance_threshold_friction) / 5. + .4 - #else: + # else: # self.acceptance_threshold = (1 - self.acceptance_threshold_friction * (1 - (self.acceptance_threshold - .4) * 5.)) / 5. + .4 - growth_limit = max(50, 2 * len(self.underwritten_contracts) + contracts_dissolved) + growth_limit = max( + 50, 2 * len(self.underwritten_contracts) + contracts_dissolved + ) if sum(acceptable_by_category) > growth_limit: - acceptable_by_category = np.asarray(acceptable_by_category).astype(np.double) - acceptable_by_category = acceptable_by_category * growth_limit / sum(acceptable_by_category) + acceptable_by_category = np.asarray(acceptable_by_category).astype( + np.double + ) + acceptable_by_category = ( + acceptable_by_category * growth_limit / sum(acceptable_by_category) + ) acceptable_by_category = np.int64(np.round(acceptable_by_category)) - [risks_per_categ, number_risks_categ] = self.risks_reinrisks_organizer(new_risks) #Here the new risks are organized by category. + [risks_per_categ, number_risks_categ] = self.risks_reinrisks_organizer( + new_risks + ) # Here the new risks are organized by category. - for repetition in range(self.recursion_limit): # TODO: find an efficient way to stop the recursion if there are no more risks to accept or if it is not accepting any more over several iterations. + for repetition in range( + self.recursion_limit + ): # TODO: find an efficient way to stop the recursion if there are no more risks to accept or if it is not accepting any more over several iterations. former_risks_per_categ = copy.copy(risks_per_categ) - [risks_per_categ, not_accepted_risks] = self.process_newrisks_insurer(risks_per_categ, number_risks_categ, acceptable_by_category, - var_per_risk_per_categ, cash_left_by_categ, time) #Here we process all the new risks in order to keep the portfolio as balanced as possible. - if former_risks_per_categ == risks_per_categ: #Stop condition implemented. Might solve the previous TODO. + [risks_per_categ, not_accepted_risks] = self.process_newrisks_insurer( + risks_per_categ, + number_risks_categ, + acceptable_by_category, + var_per_risk_per_categ, + cash_left_by_categ, + time, + ) # Here we process all the new risks in order to keep the portfolio as balanced as possible. + if ( + former_risks_per_categ == risks_per_categ + ): # Stop condition implemented. Might solve the previous TODO. break # return unacceptables - #print(self.id, " now has ", len(self.underwritten_contracts), " & returns ", len(not_accepted_risks)) + # print(self.id, " now has ", len(self.underwritten_contracts), " & returns ", len(not_accepted_risks)) self.simulation.return_risks(not_accepted_risks) - #not implemented - #"""adjust liquidity, borrow or invest""" - #pass + # not implemented + # """adjust liquidity, borrow or invest""" + # pass self.market_permanency(time) self.roll_over(time) - + self.estimated_var() def enter_illiquidity(self, time): @@ -234,7 +354,7 @@ def enter_bankruptcy(self, time): This method is used when a firm does not have enough cash to pay all its obligations. It is only called from the method self.enter_illiquidity() which is only called from the method self.effect_payments(). This method dissolves the firm through the method self.dissolve().""" - self.dissolve(time, 'record_bankruptcy') + self.dissolve(time, "record_bankruptcy") def market_exit(self, time): """Market_exit Method. @@ -250,7 +370,7 @@ def market_exit(self, time): for obligation in due: self.pay(obligation) self.obligations = [] - self.dissolve(time, 'record_market_exit') + self.dissolve(time, "record_market_exit") def dissolve(self, time, record): """Dissolve Method. @@ -265,14 +385,27 @@ def dissolve(self, time, record): next iteration. Finally the type of dissolution is recorded and the operational state is set to false. Different class variables are reset during the process: self.risks_kept, self.reinrisks_kept, self.excess_capital and self.profits_losses.""" - [contract.dissolve(time) for contract in self.underwritten_contracts] # removing (dissolving) all risks immediately after bankruptcy (may not be realistic, they might instead be bought by another company) + [ + contract.dissolve(time) for contract in self.underwritten_contracts + ] # removing (dissolving) all risks immediately after bankruptcy (may not be realistic, they might instead be bought by another company) self.simulation.return_risks(self.risks_kept) self.risks_kept = [] self.reinrisks_kept = [] - obligation = {"amount": self.cash, "recipient": self.simulation, "due_time": time, "purpose": "Dissolution"} - self.pay(obligation) #This MUST be the last obligation before the dissolution of the firm. - self.excess_capital = 0 #Excess of capital is 0 after bankruptcy or market exit. - self.profits_losses = 0 #Profits and losses are 0 after bankruptcy or market exit. + obligation = { + "amount": self.cash, + "recipient": self.simulation, + "due_time": time, + "purpose": "Dissolution", + } + self.pay( + obligation + ) # This MUST be the last obligation before the dissolution of the firm. + self.excess_capital = ( + 0 + ) # Excess of capital is 0 after bankruptcy or market exit. + self.profits_losses = ( + 0 + ) # Profits and losses are 0 after bankruptcy or market exit. if self.operational: method_to_call = getattr(self.simulation, record) method_to_call() @@ -282,12 +415,19 @@ def dissolve(self, time, record): self.operational = False def receive_obligation(self, amount, recipient, due_time, purpose): - obligation = {"amount": amount, "recipient": recipient, "due_time": due_time, "purpose": purpose} + obligation = { + "amount": amount, + "recipient": recipient, + "due_time": due_time, + "purpose": purpose, + } self.obligations.append(obligation) def effect_payments(self, time): - due = [item for item in self.obligations if item["due_time"]<=time] - self.obligations = [item for item in self.obligations if item["due_time"]>time] + due = [item for item in self.obligations if item["due_time"] <= time] + self.obligations = [ + item for item in self.obligations if item["due_time"] > time + ] sum_due = sum([item["amount"] for item in due]) if sum_due > self.cash: self.obligations += due @@ -299,14 +439,13 @@ def effect_payments(self, time): for obligation in due: self.pay(obligation) - def pay(self, obligation): amount = obligation["amount"] recipient = obligation["recipient"] purpose = obligation["purpose"] if self.get_operational() and recipient.get_operational(): self.cash -= amount - if purpose is not 'dividend': + if purpose is not "dividend": self.profits_losses -= amount recipient.receive(amount) @@ -316,15 +455,19 @@ def receive(self, amount): self.profits_losses += amount def pay_dividends(self, time): - self.receive_obligation(self.per_period_dividend, self.owner, time, 'dividend') - + self.receive_obligation(self.per_period_dividend, self.owner, time, "dividend") + def obtain_yield(self, time): - amount = self.cash * self.interest_rate # TODO: agent should not award her own interest. This interest rate should be taken from self.simulation with a getter method - self.simulation.receive_obligation(amount, self, time, 'yields') - + amount = ( + self.cash * self.interest_rate + ) # TODO: agent should not award her own interest. This interest rate should be taken from self.simulation with a getter method + self.simulation.receive_obligation(amount, self, time, "yields") + def increase_capacity(self): - raise AttributeError( "Method is not implemented in MetaInsuranceOrg, just in inheriting InsuranceFirm instances" ) - + raise AttributeError( + "Method is not implemented in MetaInsuranceOrg, just in inheriting InsuranceFirm instances" + ) + def get_cash(self): return self.cash @@ -332,11 +475,11 @@ def get_excess_capital(self): return self.excess_capital def logme(self): - self.log('cash', self.cash) - self.log('underwritten_contracts', self.underwritten_contracts) - self.log('operational', self.operational) + self.log("cash", self.cash) + self.log("underwritten_contracts", self.underwritten_contracts) + self.log("operational", self.operational) - #def zeros(self): + # def zeros(self): # return 0 def len_underwritten_contracts(self): @@ -350,7 +493,7 @@ def get_profitslosses(self): def get_underwritten_contracts(self): return self.underwritten_contracts - + def get_pointer(self): return self @@ -362,69 +505,119 @@ def estimated_var(self): self.var_counter = 0 self.var_counter_per_risk = 0 self.var_sum = 0 - + if self.operational: for contract in self.underwritten_contracts: - self.counter_category[contract.category] = self.counter_category[contract.category] + 1 - self.var_category[contract.category] = self.var_category[contract.category] + contract.initial_VaR + self.counter_category[contract.category] = ( + self.counter_category[contract.category] + 1 + ) + self.var_category[contract.category] = ( + self.var_category[contract.category] + contract.initial_VaR + ) for category in range(len(self.counter_category)): - self.var_counter = self.var_counter + self.counter_category[category] * self.riskmodel.inaccuracy[category] + self.var_counter = ( + self.var_counter + + self.counter_category[category] + * self.riskmodel.inaccuracy[category] + ) self.var_sum = self.var_sum + self.var_category[category] if not sum(self.counter_category) == 0: - self.var_counter_per_risk = self.var_counter / sum(self.counter_category) + self.var_counter_per_risk = self.var_counter / sum( + self.counter_category + ) else: self.var_counter_per_risk = 0 def increase_capacity(self, time): - assert False, "Method not implemented. increase_capacity method should be implemented in inheriting classes" + assert ( + False + ), "Method not implemented. increase_capacity method should be implemented in inheriting classes" def adjust_dividend(self, time): - assert False, "Method not implemented. adjust_dividend method should be implemented in inheriting classes" - + assert ( + False + ), "Method not implemented. adjust_dividend method should be implemented in inheriting classes" + def adjust_capacity_target(self, time): - assert False, "Method not implemented. adjust_dividend method should be implemented in inheriting classes" + assert ( + False + ), "Method not implemented. adjust_dividend method should be implemented in inheriting classes" - def risks_reinrisks_organizer(self, new_risks): #This method organizes the new risks received by the insurer (or reinsurer) + def risks_reinrisks_organizer( + self, new_risks + ): # This method organizes the new risks received by the insurer (or reinsurer) - risks_per_categ = [[] for x in range(self.simulation_parameters["no_categories"])] #This method organizes the new risks received by the insurer (or reinsurer) by category in the nested list "risks_per_categ". - number_risks_categ = [[] for x in range(self.simulation_parameters["no_categories"])] #This method also counts the new risks received by the insurer (or reinsurer) by category in the list "number_risks_categ". + risks_per_categ = [ + [] for x in range(self.simulation_parameters["no_categories"]) + ] # This method organizes the new risks received by the insurer (or reinsurer) by category in the nested list "risks_per_categ". + number_risks_categ = [ + [] for x in range(self.simulation_parameters["no_categories"]) + ] # This method also counts the new risks received by the insurer (or reinsurer) by category in the list "number_risks_categ". for categ_id in range(self.simulation_parameters["no_categories"]): - risks_per_categ[categ_id] = [risk for risk in new_risks if risk["category"] == categ_id] + risks_per_categ[categ_id] = [ + risk for risk in new_risks if risk["category"] == categ_id + ] number_risks_categ[categ_id] = len(risks_per_categ[categ_id]) - return risks_per_categ, number_risks_categ #The method returns both risks_per_categ and risks_per_categ. + return ( + risks_per_categ, + number_risks_categ, + ) # The method returns both risks_per_categ and risks_per_categ. - def balanced_portfolio(self, risk, cash_left_by_categ, var_per_risk): #This method decides whether the portfolio is balanced enough to accept a new risk or not. If it is balanced enough return True otherwise False. - #This method also returns the cash available per category independently the risk is accepted or not. - cash_reserved_by_categ = self.cash - cash_left_by_categ #Here it is computed the cash already reserved by category + def balanced_portfolio( + self, risk, cash_left_by_categ, var_per_risk + ): # This method decides whether the portfolio is balanced enough to accept a new risk or not. If it is balanced enough return True otherwise False. + # This method also returns the cash available per category independently the risk is accepted or not. + cash_reserved_by_categ = ( + self.cash - cash_left_by_categ + ) # Here it is computed the cash already reserved by category _, std_pre = get_mean_std(cash_reserved_by_categ) cash_reserved_by_categ_store = np.copy(cash_reserved_by_categ) - if risk.get("insurancetype")=='excess-of-loss': - percentage_value_at_risk = self.riskmodel.getPPF(categ_id=risk["category"], tailSize=self.riskmodel.var_tail_prob) - expected_damage = percentage_value_at_risk * risk["value"] * risk["risk_factor"] \ - * self.riskmodel.inaccuracy[risk["category"]] - expected_claim = min(expected_damage, risk["value"] * risk["excess_fraction"]) - risk["value"] * risk["deductible_fraction"] + if risk.get("insurancetype") == "excess-of-loss": + percentage_value_at_risk = self.riskmodel.getPPF( + categ_id=risk["category"], tailSize=self.riskmodel.var_tail_prob + ) + expected_damage = ( + percentage_value_at_risk + * risk["value"] + * risk["risk_factor"] + * self.riskmodel.inaccuracy[risk["category"]] + ) + expected_claim = ( + min(expected_damage, risk["value"] * risk["excess_fraction"]) + - risk["value"] * risk["deductible_fraction"] + ) # record liquidity requirement and apply margin of safety for liquidity requirement - cash_reserved_by_categ_store[risk["category"]] += expected_claim * self.riskmodel.margin_of_safety #Here it is computed how the cash reserved by category would change if the new reinsurance risk was accepted + cash_reserved_by_categ_store[risk["category"]] += ( + expected_claim * self.riskmodel.margin_of_safety + ) # Here it is computed how the cash reserved by category would change if the new reinsurance risk was accepted else: - cash_reserved_by_categ_store[risk["category"]] += var_per_risk[risk["category"]] #Here it is computed how the cash reserved by category would change if the new insurance risk was accepted + cash_reserved_by_categ_store[risk["category"]] += var_per_risk[ + risk["category"] + ] # Here it is computed how the cash reserved by category would change if the new insurance risk was accepted - mean, std_post = get_mean_std(cash_reserved_by_categ_store) #Here it is computed the mean, std of the cash reserved by category after the new risk of reinrisk is accepted + mean, std_post = get_mean_std( + cash_reserved_by_categ_store + ) # Here it is computed the mean, std of the cash reserved by category after the new risk of reinrisk is accepted total_cash_reserved_by_categ_post = sum(cash_reserved_by_categ_store) - if (std_post * total_cash_reserved_by_categ_post/self.cash) <= (self.balance_ratio * mean) or std_post < std_pre: #The new risk is accepted is the standard deviation is reduced or the cash reserved by category is very well balanced. (std_post) <= (self.balance_ratio * mean) - for i in range(len(cash_left_by_categ)): #The balance condition is not taken into account if the cash reserve is far away from the limit. (total_cash_employed_by_categ_post/self.cash <<< 1) + if (std_post * total_cash_reserved_by_categ_post / self.cash) <= ( + self.balance_ratio * mean + ) or std_post < std_pre: # The new risk is accepted is the standard deviation is reduced or the cash reserved by category is very well balanced. (std_post) <= (self.balance_ratio * mean) + for i in range( + len(cash_left_by_categ) + ): # The balance condition is not taken into account if the cash reserve is far away from the limit. (total_cash_employed_by_categ_post/self.cash <<< 1) cash_left_by_categ[i] = self.cash - cash_reserved_by_categ_store[i] return True, cash_left_by_categ @@ -434,35 +627,63 @@ def balanced_portfolio(self, risk, cash_left_by_categ, var_per_risk): #This meth return False, cash_left_by_categ - def process_newrisks_reinsurer(self, reinrisks_per_categ, number_reinrisks_categ, time): #This method processes one by one the reinrisks contained in reinrisks_per_categ in order to decide whether they should be underwritten or not. - #It is done in this way to maintain the portfolio as balanced as possible. For that reason we process risk[C1], risk[C2], risk[C3], risk[C4], risk[C1], risk[C2], ... and so forth. + def process_newrisks_reinsurer( + self, reinrisks_per_categ, number_reinrisks_categ, time + ): # This method processes one by one the reinrisks contained in reinrisks_per_categ in order to decide whether they should be underwritten or not. + # It is done in this way to maintain the portfolio as balanced as possible. For that reason we process risk[C1], risk[C2], risk[C3], risk[C4], risk[C1], risk[C2], ... and so forth. for iterion in range(max(number_reinrisks_categ)): - for categ_id in range(self.simulation_parameters["no_categories"]): #Here we take only one risk per category at a time to achieve risk[C1], risk[C2], risk[C3], risk[C4], risk[C1], risk[C2], ... if possible. - if iterion < number_reinrisks_categ[categ_id] and reinrisks_per_categ[categ_id][iterion] is not None: + for categ_id in range( + self.simulation_parameters["no_categories"] + ): # Here we take only one risk per category at a time to achieve risk[C1], risk[C2], risk[C3], risk[C4], risk[C1], risk[C2], ... if possible. + if ( + iterion < number_reinrisks_categ[categ_id] + and reinrisks_per_categ[categ_id][iterion] is not None + ): risk_to_insure = reinrisks_per_categ[categ_id][iterion] - underwritten_risks = [{"value": contract.value, "category": contract.category, \ - "risk_factor": contract.risk_factor, - "deductible": contract.deductible, \ - "excess": contract.excess, "insurancetype": contract.insurancetype, \ - "runtime_left": (contract.expiration - time)} for contract in - self.underwritten_contracts if contract.insurancetype == "excess-of-loss"] + underwritten_risks = [ + { + "value": contract.value, + "category": contract.category, + "risk_factor": contract.risk_factor, + "deductible": contract.deductible, + "excess": contract.excess, + "insurancetype": contract.insurancetype, + "runtime_left": (contract.expiration - time), + } + for contract in self.underwritten_contracts + if contract.insurancetype == "excess-of-loss" + ] accept, cash_left_by_categ, var_this_risk, self.excess_capital = self.riskmodel.evaluate( - underwritten_risks, self.cash, - risk_to_insure) # TODO: change riskmodel.evaluate() to accept new risk to be evaluated and to account for existing non-proportional risks correctly -> DONE. + underwritten_risks, self.cash, risk_to_insure + ) # TODO: change riskmodel.evaluate() to accept new risk to be evaluated and to account for existing non-proportional risks correctly -> DONE. if accept: - per_value_reinsurance_premium = self.np_reinsurance_premium_share * risk_to_insure[ - "periodized_total_premium"] * risk_to_insure["runtime"] * (self.simulation.get_market_reinpremium()/self.simulation.get_market_premium()) / risk_to_insure[ - "value"] # TODO: rename this to per_value_premium in insurancecontract.py to avoid confusion - [condition, cash_left_by_categ] = self.balanced_portfolio(risk_to_insure, cash_left_by_categ, None) #Here it is check whether the portfolio is balanced or not if the reinrisk (risk_to_insure) is underwritten. Return True if it is balanced. False otherwise. + per_value_reinsurance_premium = ( + self.np_reinsurance_premium_share + * risk_to_insure["periodized_total_premium"] + * risk_to_insure["runtime"] + * ( + self.simulation.get_market_reinpremium() + / self.simulation.get_market_premium() + ) + / risk_to_insure["value"] + ) # TODO: rename this to per_value_premium in insurancecontract.py to avoid confusion + [condition, cash_left_by_categ] = self.balanced_portfolio( + risk_to_insure, cash_left_by_categ, None + ) # Here it is check whether the portfolio is balanced or not if the reinrisk (risk_to_insure) is underwritten. Return True if it is balanced. False otherwise. if condition: - contract = ReinsuranceContract(self, risk_to_insure, time, per_value_reinsurance_premium, - risk_to_insure["runtime"], \ - self.default_contract_payment_period, \ - expire_immediately=self.simulation_parameters[ - "expire_immediately"], \ - initial_VaR=var_this_risk, \ - insurancetype=risk_to_insure[ - "insurancetype"]) # TODO: implement excess of loss for reinsurance contracts + contract = ReinsuranceContract( + self, + risk_to_insure, + time, + per_value_reinsurance_premium, + risk_to_insure["runtime"], + self.default_contract_payment_period, + expire_immediately=self.simulation_parameters[ + "expire_immediately" + ], + initial_VaR=var_this_risk, + insurancetype=risk_to_insure["insurancetype"], + ) # TODO: implement excess of loss for reinsurance contracts self.underwritten_contracts.append(contract) self.cash_left_by_categ = cash_left_by_categ reinrisks_per_categ[categ_id][iterion] = None @@ -473,47 +694,78 @@ def process_newrisks_reinsurer(self, reinrisks_per_categ, number_reinrisks_categ if reinrisk is not None: not_accepted_reinrisks.append(reinrisk) - - return reinrisks_per_categ, not_accepted_reinrisks - def process_newrisks_insurer(self, risks_per_categ, number_risks_categ, acceptable_by_category, var_per_risk_per_categ, cash_left_by_categ, time): #This method processes one by one the risks contained in risks_per_categ in order to decide whether they should be underwritten or not. - #It is done in this way to maintain the portfolio as balanced as possible. For that reason we process risk[C1], risk[C2], risk[C3], risk[C4], risk[C1], risk[C2], ... and so forth. + def process_newrisks_insurer( + self, + risks_per_categ, + number_risks_categ, + acceptable_by_category, + var_per_risk_per_categ, + cash_left_by_categ, + time, + ): # This method processes one by one the risks contained in risks_per_categ in order to decide whether they should be underwritten or not. + # It is done in this way to maintain the portfolio as balanced as possible. For that reason we process risk[C1], risk[C2], risk[C3], risk[C4], risk[C1], risk[C2], ... and so forth. _cached_rvs = self.contract_runtime_dist.rvs() for iter in range(max(number_risks_categ)): - for categ_id in range(len(acceptable_by_category)): #Here we take only one risk per category at a time to achieve risk[C1], risk[C2], risk[C3], risk[C4], risk[C1], risk[C2], ... if possible. - if iter < number_risks_categ[categ_id] and acceptable_by_category[categ_id] > 0 and \ - risks_per_categ[categ_id][iter] is not None: + for categ_id in range( + len(acceptable_by_category) + ): # Here we take only one risk per category at a time to achieve risk[C1], risk[C2], risk[C3], risk[C4], risk[C1], risk[C2], ... if possible. + if ( + iter < number_risks_categ[categ_id] + and acceptable_by_category[categ_id] > 0 + and risks_per_categ[categ_id][iter] is not None + ): risk_to_insure = risks_per_categ[categ_id][iter] - if risk_to_insure.get("contract") is not None and risk_to_insure[ - "contract"].expiration > time: # risk_to_insure["contract"]: # required to rule out contracts that have exploded in the meantime - [condition, cash_left_by_categ] = self.balanced_portfolio(risk_to_insure, cash_left_by_categ, None) #Here it is check whether the portfolio is balanced or not if the reinrisk (risk_to_insure) is underwritten. Return True if it is balanced. False otherwise. + if ( + risk_to_insure.get("contract") is not None + and risk_to_insure["contract"].expiration > time + ): # risk_to_insure["contract"]: # required to rule out contracts that have exploded in the meantime + [condition, cash_left_by_categ] = self.balanced_portfolio( + risk_to_insure, cash_left_by_categ, None + ) # Here it is check whether the portfolio is balanced or not if the reinrisk (risk_to_insure) is underwritten. Return True if it is balanced. False otherwise. if condition: - contract = ReinsuranceContract(self, risk_to_insure, time, \ - self.simulation.get_reinsurance_market_premium(), - risk_to_insure["expiration"] - time, \ - self.default_contract_payment_period, \ - expire_immediately=self.simulation_parameters[ - "expire_immediately"], ) + contract = ReinsuranceContract( + self, + risk_to_insure, + time, + self.simulation.get_reinsurance_market_premium(), + risk_to_insure["expiration"] - time, + self.default_contract_payment_period, + expire_immediately=self.simulation_parameters[ + "expire_immediately" + ], + ) self.underwritten_contracts.append(contract) self.cash_left_by_categ = cash_left_by_categ risks_per_categ[categ_id][iter] = None # TODO: move this to insurancecontract (ca. line 14) -> DONE # TODO: do not write into other object's properties, use setter -> DONE else: - [condition, cash_left_by_categ] = self.balanced_portfolio(risk_to_insure, cash_left_by_categ, - var_per_risk_per_categ) #Here it is check whether the portfolio is balanced or not if the risk (risk_to_insure) is underwritten. Return True if it is balanced. False otherwise. + [condition, cash_left_by_categ] = self.balanced_portfolio( + risk_to_insure, cash_left_by_categ, var_per_risk_per_categ + ) # Here it is check whether the portfolio is balanced or not if the risk (risk_to_insure) is underwritten. Return True if it is balanced. False otherwise. if condition: - contract = InsuranceContract(self, risk_to_insure, time, self.simulation.get_market_premium(), \ - _cached_rvs, \ - self.default_contract_payment_period, \ - expire_immediately=self.simulation_parameters[ - "expire_immediately"], \ - initial_VaR=var_per_risk_per_categ[categ_id]) + contract = InsuranceContract( + self, + risk_to_insure, + time, + self.simulation.get_market_premium(), + _cached_rvs, + self.default_contract_payment_period, + expire_immediately=self.simulation_parameters[ + "expire_immediately" + ], + initial_VaR=var_per_risk_per_categ[categ_id], + ) self.underwritten_contracts.append(contract) self.cash_left_by_categ = cash_left_by_categ risks_per_categ[categ_id][iter] = None - acceptable_by_category[categ_id] -= 1 # TODO: allow different values per risk (i.e. sum over value (and reinsurance_share) or exposure instead of counting) + acceptable_by_category[ + categ_id + ] -= ( + 1 + ) # TODO: allow different values per risk (i.e. sum over value (and reinsurance_share) or exposure instead of counting) not_accepted_risks = [] for categ_id in range(len(acceptable_by_category)): @@ -523,42 +775,77 @@ def process_newrisks_insurer(self, risks_per_categ, number_risks_categ, acceptab return risks_per_categ, not_accepted_risks - - def market_permanency(self, time): #This method determines whether an insurer or reinsurer stays in the market. If it has very few risks underwritten or too much cash left for TOO LONG it eventually leaves the market. - # If it has very few risks underwritten it cannot balance the portfolio so it makes sense to leave the market. + def market_permanency( + self, time + ): # This method determines whether an insurer or reinsurer stays in the market. If it has very few risks underwritten or too much cash left for TOO LONG it eventually leaves the market. + # If it has very few risks underwritten it cannot balance the portfolio so it makes sense to leave the market. if not self.simulation_parameters["market_permanency_off"]: cash_left_by_categ = np.asarray(self.cash_left_by_categ) avg_cash_left = get_mean(cash_left_by_categ) - if self.cash < self.simulation_parameters["cash_permanency_limit"]: #If their level of cash is so low that they cannot underwrite anything they also leave the market. + if ( + self.cash < self.simulation_parameters["cash_permanency_limit"] + ): # If their level of cash is so low that they cannot underwrite anything they also leave the market. self.market_exit(time) else: if self.is_insurer: - if len(self.underwritten_contracts) < self.simulation_parameters["insurance_permanency_contracts_limit"] or avg_cash_left / self.cash > self.simulation_parameters["insurance_permanency_ratio_limit"]: - #Insurers leave the market if they have contracts under the limit or an excess capital over the limit for too long. + if ( + len(self.underwritten_contracts) + < self.simulation_parameters[ + "insurance_permanency_contracts_limit" + ] + or avg_cash_left / self.cash + > self.simulation_parameters["insurance_permanency_ratio_limit"] + ): + # Insurers leave the market if they have contracts under the limit or an excess capital over the limit for too long. self.market_permanency_counter += 1 else: - self.market_permanency_counter = 0 #All these limits maybe should be parameters in isleconfig.py - - if self.market_permanency_counter >= self.simulation_parameters["insurance_permanency_time_constraint"]: # Here we determine how much is too long. + self.market_permanency_counter = ( + 0 + ) # All these limits maybe should be parameters in isleconfig.py + + if ( + self.market_permanency_counter + >= self.simulation_parameters[ + "insurance_permanency_time_constraint" + ] + ): # Here we determine how much is too long. self.market_exit(time) if self.is_reinsurer: - if len(self.underwritten_contracts) < self.simulation_parameters["reinsurance_permanency_contracts_limit"] or avg_cash_left / self.cash > self.simulation_parameters["reinsurance_permanency_ratio_limit"]: - #Reinsurers leave the market if they have contracts under the limit or an excess capital over the limit for too long. - - self.market_permanency_counter += 1 #Insurers and reinsurers potentially have different reasons to leave the market. That's why the code is duplicated here. + if ( + len(self.underwritten_contracts) + < self.simulation_parameters[ + "reinsurance_permanency_contracts_limit" + ] + or avg_cash_left / self.cash + > self.simulation_parameters[ + "reinsurance_permanency_ratio_limit" + ] + ): + # Reinsurers leave the market if they have contracts under the limit or an excess capital over the limit for too long. + + self.market_permanency_counter += ( + 1 + ) # Insurers and reinsurers potentially have different reasons to leave the market. That's why the code is duplicated here. else: self.market_permanency_counter = 0 - if self.market_permanency_counter >= self.simulation_parameters["reinsurance_permanency_time_constraint"]: # Here we determine how much is too long. + if ( + self.market_permanency_counter + >= self.simulation_parameters[ + "reinsurance_permanency_time_constraint" + ] + ): # Here we determine how much is too long. self.market_exit(time) - def register_claim(self, claim): #This method records in insurancesimulation.py every claim made. It is called either from insurancecontract.py or reinsurancecontract.py respectively. + def register_claim( + self, claim + ): # This method records in insurancesimulation.py every claim made. It is called either from insurancecontract.py or reinsurancecontract.py respectively. self.simulation.record_claims(claim) def reset_pl(self): @@ -568,7 +855,7 @@ def reset_pl(self): Reset the profits and losses variable of each firm at the beginning of every iteration. It has to be run in insurancesimulation.py at the beginning of the iterate method""" self.profits_losses = 0 - def roll_over(self,time): + def roll_over(self, time): """Roll_over Method. Accepts arguments time: Type integer. The current time. No return value. @@ -581,13 +868,22 @@ def roll_over(self,time): are created and destroyed every iteration. The main reason to implemented this method is to avoid a lack of coverage that appears, if contracts are allowed to mature and are evaluated again the next iteration.""" - maturing_next = [contract for contract in self.underwritten_contracts if contract.expiration == time + 1] + maturing_next = [ + contract + for contract in self.underwritten_contracts + if contract.expiration == time + 1 + ] if self.is_insurer is True: for contract in maturing_next: contract.roll_over_flag = 1 - if np.random.uniform(0,1,1) > self.simulation_parameters["insurance_retention"]: - self.simulation.return_risks([contract.risk_data]) # TODO: This is not a retention, so the roll_over_flag might be confusing in this case + if ( + np.random.uniform(0, 1, 1) + > self.simulation_parameters["insurance_retention"] + ): + self.simulation.return_risks( + [contract.risk_data] + ) # TODO: This is not a retention, so the roll_over_flag might be confusing in this case else: self.risks_kept.append(contract.risk_data) @@ -595,14 +891,12 @@ def roll_over(self,time): for reincontract in maturing_next: if reincontract.property_holder.operational: reincontract.roll_over_flag = 1 - reinrisk = reincontract.property_holder.create_reinrisk(time, reincontract.category) - if np.random.uniform(0,1,1) < self.simulation_parameters["reinsurance_retention"]: + reinrisk = reincontract.property_holder.create_reinrisk( + time, reincontract.category + ) + if ( + np.random.uniform(0, 1, 1) + < self.simulation_parameters["reinsurance_retention"] + ): if reinrisk is not None: self.reinrisks_kept.append(reinrisk) - - - - - - - diff --git a/metaplotter.py b/metaplotter.py index 775b55e..858987e 100644 --- a/metaplotter.py +++ b/metaplotter.py @@ -5,11 +5,12 @@ import time import glob + def read_data(): # do not overwrite old pdfs - #if os.path.exists("data/fig_one_and_two_rm_comp.pdf"): + # if os.path.exists("data/fig_one_and_two_rm_comp.pdf"): # os.rename("data/fig_one_and_two_rm_comp.pdf", "data/fig_one_and_two_rm_comp_old_" + time.strftime('%Y_%b_%d_%H_%M') + ".pdf") - #if os.path.exists("data/fig_three_and_four_rm_comp.pdf"): + # if os.path.exists("data/fig_three_and_four_rm_comp.pdf"): # os.rename("data/fig_three_and_four_rm_comp.pdf", "data/fig_three_and_four_rm_comp_old_" + time.strftime('%Y_%b_%d_%H_%M') + ".pdf") upper_bound = 75 @@ -30,7 +31,12 @@ def read_data(): filenames_threes.sort() filenames_fours.sort() - assert len(filenames_ones) == len(filenames_twos) == len(filenames_threes) == len(filenames_fours) + assert ( + len(filenames_ones) + == len(filenames_twos) + == len(filenames_threes) + == len(filenames_fours) + ) all_filenames = filenames_ones + filenames_twos + filenames_threes + filenames_fours for filename in all_filenames: @@ -38,7 +44,7 @@ def read_data(): rfile = open(filename, "r") data = [eval(k) for k in rfile] rfile.close() - + # compute data series data_means = [] data_medians = [] @@ -53,21 +59,39 @@ def read_data(): data_medians = np.array(data_medians) data_q25 = np.array(data_q25) data_q75 = np.array(data_q75) - + # record data series timeseries_dict["mean"][filename] = data_means timeseries_dict["median"][filename] = data_medians timeseries_dict["quantile25"][filename] = data_q25 timeseries_dict["quantile75"][filename] = data_q75 return timeseries_dict - - -def plotting(output_label, timeseries_dict, riskmodelsetting1, riskmodelsetting2, series1, series2=None, additionalriskmodelsetting3=None, additionalriskmodelsetting4=None, plottype1="mean", plottype2="mean"): + +def plotting( + output_label, + timeseries_dict, + riskmodelsetting1, + riskmodelsetting2, + series1, + series2=None, + additionalriskmodelsetting3=None, + additionalriskmodelsetting4=None, + plottype1="mean", + plottype2="mean", +): # dictionaries colors = {"one": "red", "two": "blue", "three": "green", "four": "yellow"} - labels = {"contracts": "Contracts (Insurers)", "cash": "Liquidity (Insurers)", "operational": "Active Insurers", "premium": "Premium", "reincash": "Liquidity (Reinsurers)", "reincontracts": "Contracts (Reinsurers)", "reinoperational": "Active Reinsurers"} - + labels = { + "contracts": "Contracts (Insurers)", + "cash": "Liquidity (Insurers)", + "operational": "Active Insurers", + "premium": "Premium", + "reincash": "Liquidity (Reinsurers)", + "reincontracts": "Contracts (Reinsurers)", + "reinoperational": "Active Reinsurers", + } + # prepare labels, timeseries, etc. color1 = colors[riskmodelsetting1] color2 = colors[riskmodelsetting2] @@ -80,23 +104,33 @@ def plotting(output_label, timeseries_dict, riskmodelsetting1, riskmodelsetting2 plot_2_2 = "data/" + riskmodelsetting2 + "_" + series2 + ".dat" if additionalriskmodelsetting3 is not None: color3 = colors[additionalriskmodelsetting3] - label3 = str.upper(additionalriskmodelsetting3[0]) + additionalriskmodelsetting3[1:] + " riskmodels" + label3 = ( + str.upper(additionalriskmodelsetting3[0]) + + additionalriskmodelsetting3[1:] + + " riskmodels" + ) plot_1_3 = "data/" + additionalriskmodelsetting3 + "_" + series1 + ".dat" if series2 is not None: plot_2_3 = "data/" + additionalriskmodelsetting3 + "_" + series2 + ".dat" if additionalriskmodelsetting4 is not None: color4 = colors[additionalriskmodelsetting4] - label4 = str.upper(additionalriskmodelsetting4[0]) + additionalriskmodelsetting4[1:] + " riskmodels" + label4 = ( + str.upper(additionalriskmodelsetting4[0]) + + additionalriskmodelsetting4[1:] + + " riskmodels" + ) plot_1_4 = "data/" + additionalriskmodelsetting4 + "_" + series1 + ".dat" if series2 is not None: plot_2_4 = "data/" + additionalriskmodelsetting4 + "_" + series2 + ".dat" - + # Backup existing figures (so as not to overwrite them) outputfilename = "data/" + output_label + ".pdf" - backupfilename = "data/" + output_label + "_old_" + time.strftime('%Y_%b_%d_%H_%M') + ".pdf" + backupfilename = ( + "data/" + output_label + "_old_" + time.strftime("%Y_%b_%d_%H_%M") + ".pdf" + ) if os.path.exists(outputfilename): os.rename(outputfilename, backupfilename) - + # Plot and save fig = plt.figure() if series2 is not None: @@ -104,56 +138,190 @@ def plotting(output_label, timeseries_dict, riskmodelsetting1, riskmodelsetting2 else: ax0 = fig.add_subplot(111) if additionalriskmodelsetting3 is not None: - ax0.plot(range(len(timeseries_dict[plottype1][plot_1_3])), timeseries_dict[plottype1][plot_1_3], color=color3, label=label3) + ax0.plot( + range(len(timeseries_dict[plottype1][plot_1_3])), + timeseries_dict[plottype1][plot_1_3], + color=color3, + label=label3, + ) if additionalriskmodelsetting4 is not None: - ax0.plot(range(len(timeseries_dict[plottype1][plot_1_4])), timeseries_dict[plottype1][plot_1_4], color=color4, label=label4) - ax0.plot(range(len(timeseries_dict[plottype1][plot_1_1])), timeseries_dict[plottype1][plot_1_1], color=color1, label=label1) - ax0.plot(range(len(timeseries_dict[plottype1][plot_1_2])), timeseries_dict[plottype1][plot_1_2], color=color2, label=label2) - ax0.fill_between(range(len(timeseries_dict["quantile25"][plot_1_1])), timeseries_dict["quantile25"][plot_1_1], timeseries_dict["quantile75"][plot_1_1], facecolor=color1, alpha=0.25) - ax0.fill_between(range(len(timeseries_dict["quantile25"][plot_1_1])), timeseries_dict["quantile25"][plot_1_2], timeseries_dict["quantile75"][plot_1_2], facecolor=color2, alpha=0.25) - ax0.set_ylabel(labels[series1])#"Contracts") - ax0.legend(loc='best') + ax0.plot( + range(len(timeseries_dict[plottype1][plot_1_4])), + timeseries_dict[plottype1][plot_1_4], + color=color4, + label=label4, + ) + ax0.plot( + range(len(timeseries_dict[plottype1][plot_1_1])), + timeseries_dict[plottype1][plot_1_1], + color=color1, + label=label1, + ) + ax0.plot( + range(len(timeseries_dict[plottype1][plot_1_2])), + timeseries_dict[plottype1][plot_1_2], + color=color2, + label=label2, + ) + ax0.fill_between( + range(len(timeseries_dict["quantile25"][plot_1_1])), + timeseries_dict["quantile25"][plot_1_1], + timeseries_dict["quantile75"][plot_1_1], + facecolor=color1, + alpha=0.25, + ) + ax0.fill_between( + range(len(timeseries_dict["quantile25"][plot_1_1])), + timeseries_dict["quantile25"][plot_1_2], + timeseries_dict["quantile75"][plot_1_2], + facecolor=color2, + alpha=0.25, + ) + ax0.set_ylabel(labels[series1]) # "Contracts") + ax0.legend(loc="best") if series2 is not None: ax1 = fig.add_subplot(212) if additionalriskmodelsetting3 is not None: - ax1.plot(range(len(timeseries_dict[plottype2][plot_2_3])), timeseries_dict[plottype2][plot_2_3], color=color3, label=label3) + ax1.plot( + range(len(timeseries_dict[plottype2][plot_2_3])), + timeseries_dict[plottype2][plot_2_3], + color=color3, + label=label3, + ) if additionalriskmodelsetting4 is not None: - ax1.plot(range(len(timeseries_dict[plottype2][plot_2_4])), timeseries_dict[plottype2][plot_2_4], color=color4, label=label4) - ax1.plot(range(len(timeseries_dict[plottype2][plot_2_1])), timeseries_dict[plottype2][plot_2_1], color=color1, label=label1) - ax1.plot(range(len(timeseries_dict[plottype2][plot_2_2])), timeseries_dict[plottype2][plot_2_2], color=color2, label=label2) - ax1.fill_between(range(len(timeseries_dict["quantile25"][plot_2_1])), timeseries_dict["quantile25"][plot_2_1], timeseries_dict["quantile75"][plot_2_1], facecolor=color1, alpha=0.25) - ax1.fill_between(range(len(timeseries_dict["quantile25"][plot_2_1])), timeseries_dict["quantile25"][plot_2_2], timeseries_dict["quantile75"][plot_2_2], facecolor=color2, alpha=0.25) + ax1.plot( + range(len(timeseries_dict[plottype2][plot_2_4])), + timeseries_dict[plottype2][plot_2_4], + color=color4, + label=label4, + ) + ax1.plot( + range(len(timeseries_dict[plottype2][plot_2_1])), + timeseries_dict[plottype2][plot_2_1], + color=color1, + label=label1, + ) + ax1.plot( + range(len(timeseries_dict[plottype2][plot_2_2])), + timeseries_dict[plottype2][plot_2_2], + color=color2, + label=label2, + ) + ax1.fill_between( + range(len(timeseries_dict["quantile25"][plot_2_1])), + timeseries_dict["quantile25"][plot_2_1], + timeseries_dict["quantile75"][plot_2_1], + facecolor=color1, + alpha=0.25, + ) + ax1.fill_between( + range(len(timeseries_dict["quantile25"][plot_2_1])), + timeseries_dict["quantile25"][plot_2_2], + timeseries_dict["quantile75"][plot_2_2], + facecolor=color2, + alpha=0.25, + ) ax1.set_ylabel(labels[series2]) ax1.set_xlabel("Time") plt.savefig(outputfilename) plt.show() + timeseries = read_data() # for just two different riskmodel settings -plotting(output_label="fig_contracts_survival_1_2", timeseries_dict=timeseries, riskmodelsetting1="one", \ - riskmodelsetting2="two", series1="contracts", series2="operational", plottype1="mean", plottype2="median") -plotting(output_label="fig_reinsurers_contracts_survival_1_2", timeseries_dict=timeseries, riskmodelsetting1="one", \ - riskmodelsetting2="two", series1="reincontracts", series2="reinoperational", plottype1="mean", plottype2="median") -plotting(output_label="fig_premium_1_2", timeseries_dict=timeseries, riskmodelsetting1="one", riskmodelsetting2="two", \ - series1="premium", series2=None, plottype1="mean", plottype2=None) +plotting( + output_label="fig_contracts_survival_1_2", + timeseries_dict=timeseries, + riskmodelsetting1="one", + riskmodelsetting2="two", + series1="contracts", + series2="operational", + plottype1="mean", + plottype2="median", +) +plotting( + output_label="fig_reinsurers_contracts_survival_1_2", + timeseries_dict=timeseries, + riskmodelsetting1="one", + riskmodelsetting2="two", + series1="reincontracts", + series2="reinoperational", + plottype1="mean", + plottype2="median", +) +plotting( + output_label="fig_premium_1_2", + timeseries_dict=timeseries, + riskmodelsetting1="one", + riskmodelsetting2="two", + series1="premium", + series2=None, + plottype1="mean", + plottype2=None, +) raise SystemExit # for four different riskmodel settings -plotting(output_label="fig_contracts_survival_1_2", timeseries_dict=timeseries, riskmodelsetting1="one", \ - riskmodelsetting2="two", series1="contracts", series2="operational", additionalriskmodelsetting3="three", \ - additionalriskmodelsetting4="four", plottype1="mean", plottype2="median") -plotting(output_label="fig_contracts_survival_3_4", timeseries_dict=timeseries, riskmodelsetting1="three", \ - riskmodelsetting2="four", series1="contracts", series2="operational", additionalriskmodelsetting3="one", \ - additionalriskmodelsetting4="two", plottype1="mean", plottype2="median") -plotting(output_label="fig_reinsurers_contracts_survival_1_2", timeseries_dict=timeseries, riskmodelsetting1="one", \ - riskmodelsetting2="two", series1="reincontracts", series2="reinoperational", \ - additionalriskmodelsetting3="three", additionalriskmodelsetting4="four", plottype1="mean", plottype2="median") -plotting(output_label="fig_reinsurers_contracts_survival_3_4", timeseries_dict=timeseries, riskmodelsetting1="three", \ - riskmodelsetting2="four", series1="reincontracts", series2="reinoperational", \ - additionalriskmodelsetting3="one", additionalriskmodelsetting4="two", plottype1="mean", plottype2="median") -plotting(output_label="fig_premium_1_2", timeseries_dict=timeseries, riskmodelsetting1="one", riskmodelsetting2="two", \ - series1="premium", series2=None, additionalriskmodelsetting3="three", additionalriskmodelsetting4="four", \ - plottype1="mean", plottype2=None) - -#pdb.set_trace() +plotting( + output_label="fig_contracts_survival_1_2", + timeseries_dict=timeseries, + riskmodelsetting1="one", + riskmodelsetting2="two", + series1="contracts", + series2="operational", + additionalriskmodelsetting3="three", + additionalriskmodelsetting4="four", + plottype1="mean", + plottype2="median", +) +plotting( + output_label="fig_contracts_survival_3_4", + timeseries_dict=timeseries, + riskmodelsetting1="three", + riskmodelsetting2="four", + series1="contracts", + series2="operational", + additionalriskmodelsetting3="one", + additionalriskmodelsetting4="two", + plottype1="mean", + plottype2="median", +) +plotting( + output_label="fig_reinsurers_contracts_survival_1_2", + timeseries_dict=timeseries, + riskmodelsetting1="one", + riskmodelsetting2="two", + series1="reincontracts", + series2="reinoperational", + additionalriskmodelsetting3="three", + additionalriskmodelsetting4="four", + plottype1="mean", + plottype2="median", +) +plotting( + output_label="fig_reinsurers_contracts_survival_3_4", + timeseries_dict=timeseries, + riskmodelsetting1="three", + riskmodelsetting2="four", + series1="reincontracts", + series2="reinoperational", + additionalriskmodelsetting3="one", + additionalriskmodelsetting4="two", + plottype1="mean", + plottype2="median", +) +plotting( + output_label="fig_premium_1_2", + timeseries_dict=timeseries, + riskmodelsetting1="one", + riskmodelsetting2="two", + series1="premium", + series2=None, + additionalriskmodelsetting3="three", + additionalriskmodelsetting4="four", + plottype1="mean", + plottype2=None, +) + +# pdb.set_trace() diff --git a/metaplotter_pl_timescale.py b/metaplotter_pl_timescale.py index d261d11..f172949 100644 --- a/metaplotter_pl_timescale.py +++ b/metaplotter_pl_timescale.py @@ -5,11 +5,12 @@ import time import glob + def read_data(): # do not overwrite old pdfs - #if os.path.exists("data/fig_one_and_two_rm_comp.pdf"): + # if os.path.exists("data/fig_one_and_two_rm_comp.pdf"): # os.rename("data/fig_one_and_two_rm_comp.pdf", "data/fig_one_and_two_rm_comp_old_" + time.strftime('%Y_%b_%d_%H_%M') + ".pdf") - #if os.path.exists("data/fig_three_and_four_rm_comp.pdf"): + # if os.path.exists("data/fig_three_and_four_rm_comp.pdf"): # os.rename("data/fig_three_and_four_rm_comp.pdf", "data/fig_three_and_four_rm_comp_old_" + time.strftime('%Y_%b_%d_%H_%M') + ".pdf") upper_bound = 75 @@ -30,7 +31,7 @@ def read_data(): filenames_threes.sort() filenames_fours.sort() - #assert len(filenames_ones) == len(filenames_twos) == len(filenames_threes) == len(filenames_fours) + # assert len(filenames_ones) == len(filenames_twos) == len(filenames_threes) == len(filenames_fours) all_filenames = filenames_ones + filenames_twos + filenames_threes + filenames_fours for filename in all_filenames: @@ -38,7 +39,7 @@ def read_data(): rfile = open(filename, "r") data = [eval(k) for k in rfile] rfile.close() - + # compute data series data_means = [] data_medians = [] @@ -53,21 +54,41 @@ def read_data(): data_medians = np.array(data_medians) data_q25 = np.array(data_q25) data_q75 = np.array(data_q75) - + # record data series timeseries_dict["mean"][filename] = data_means timeseries_dict["median"][filename] = data_medians timeseries_dict["quantile25"][filename] = data_q25 timeseries_dict["quantile75"][filename] = data_q75 return timeseries_dict - - -def plotting(output_label, timeseries_dict, riskmodelsetting1, riskmodelsetting2, series1, series2=None, additionalriskmodelsetting3=None, additionalriskmodelsetting4=None, plottype1="mean", plottype2="mean"): + +def plotting( + output_label, + timeseries_dict, + riskmodelsetting1, + riskmodelsetting2, + series1, + series2=None, + additionalriskmodelsetting3=None, + additionalriskmodelsetting4=None, + plottype1="mean", + plottype2="mean", +): # dictionaries colors = {"one": "red", "two": "blue", "three": "green", "four": "yellow"} - labels = {"profitslosses": "Profits and Losses (Insurer)", "contracts": "Contracts (Insurers)", "cash": "Liquidity (Insurers)", "operational": "Active Insurers", "premium": "Premium", "reinprofitslosses": "Profits and Losses (Reinsurer)", "reincash": "Liquidity (Reinsurers)", "reincontracts": "Contracts (Reinsurers)", "reinoperational": "Active Reinsurers"} - + labels = { + "profitslosses": "Profits and Losses (Insurer)", + "contracts": "Contracts (Insurers)", + "cash": "Liquidity (Insurers)", + "operational": "Active Insurers", + "premium": "Premium", + "reinprofitslosses": "Profits and Losses (Reinsurer)", + "reincash": "Liquidity (Reinsurers)", + "reincontracts": "Contracts (Reinsurers)", + "reinoperational": "Active Reinsurers", + } + # prepare labels, timeseries, etc. color1 = colors[riskmodelsetting1] color2 = colors[riskmodelsetting2] @@ -80,23 +101,33 @@ def plotting(output_label, timeseries_dict, riskmodelsetting1, riskmodelsetting2 plot_2_2 = "data/" + riskmodelsetting2 + "_" + series2 + ".dat" if additionalriskmodelsetting3 is not None: color3 = colors[additionalriskmodelsetting3] - label3 = str.upper(additionalriskmodelsetting3[0]) + additionalriskmodelsetting3[1:] + " riskmodels" + label3 = ( + str.upper(additionalriskmodelsetting3[0]) + + additionalriskmodelsetting3[1:] + + " riskmodels" + ) plot_1_3 = "data/" + additionalriskmodelsetting3 + "_" + series1 + ".dat" if series2 is not None: plot_2_3 = "data/" + additionalriskmodelsetting3 + "_" + series2 + ".dat" if additionalriskmodelsetting4 is not None: color4 = colors[additionalriskmodelsetting4] - label4 = str.upper(additionalriskmodelsetting4[0]) + additionalriskmodelsetting4[1:] + " riskmodels" + label4 = ( + str.upper(additionalriskmodelsetting4[0]) + + additionalriskmodelsetting4[1:] + + " riskmodels" + ) plot_1_4 = "data/" + additionalriskmodelsetting4 + "_" + series1 + ".dat" if series2 is not None: plot_2_4 = "data/" + additionalriskmodelsetting4 + "_" + series2 + ".dat" - + # Backup existing figures (so as not to overwrite them) outputfilename = "data/" + output_label + ".pdf" - backupfilename = "data/" + output_label + "_old_" + time.strftime('%Y_%b_%d_%H_%M') + ".pdf" + backupfilename = ( + "data/" + output_label + "_old_" + time.strftime("%Y_%b_%d_%H_%M") + ".pdf" + ) if os.path.exists(outputfilename): os.rename(outputfilename, backupfilename) - + # Plot and save fig = plt.figure() if series2 is not None: @@ -105,39 +136,111 @@ def plotting(output_label, timeseries_dict, riskmodelsetting1, riskmodelsetting2 ax0 = fig.add_subplot(111) maxlen_plots = 0 if additionalriskmodelsetting3 is not None: - ax0.plot(range(len(timeseries_dict[plottype1][plot_1_3]))[200:], timeseries_dict[plottype1][plot_1_3][200:], color=color3, label=label3) + ax0.plot( + range(len(timeseries_dict[plottype1][plot_1_3]))[200:], + timeseries_dict[plottype1][plot_1_3][200:], + color=color3, + label=label3, + ) maxlen_plots = max(maxlen_plots, len(timeseries_dict[plottype1][plot_1_3])) if additionalriskmodelsetting4 is not None: - ax0.plot(range(len(timeseries_dict[plottype1][plot_1_4]))[200:], timeseries_dict[plottype1][plot_1_4][200:], color=color4, label=label4) + ax0.plot( + range(len(timeseries_dict[plottype1][plot_1_4]))[200:], + timeseries_dict[plottype1][plot_1_4][200:], + color=color4, + label=label4, + ) maxlen_plots = max(maxlen_plots, len(timeseries_dict[plottype1][plot_1_4])) - ax0.plot(range(len(timeseries_dict[plottype1][plot_1_1]))[200:], timeseries_dict[plottype1][plot_1_1][200:], color=color1, label=label1) - ax0.plot(range(len(timeseries_dict[plottype1][plot_1_2]))[200:], timeseries_dict[plottype1][plot_1_2][200:], color=color2, label=label2) - ax0.fill_between(range(len(timeseries_dict["quantile25"][plot_1_1]))[200:], timeseries_dict["quantile25"][plot_1_1][200:], timeseries_dict["quantile75"][plot_1_1][200:], facecolor=color1, alpha=0.25) - ax0.fill_between(range(len(timeseries_dict["quantile25"][plot_1_1]))[200:], timeseries_dict["quantile25"][plot_1_2][200:], timeseries_dict["quantile75"][plot_1_2][200:], facecolor=color2, alpha=0.25) - ax0.set_ylabel(labels[series1])#"Contracts") - maxlen_plots = max(maxlen_plots, len(timeseries_dict[plottype1][plot_1_1]), len(timeseries_dict[plottype1][plot_1_2])) - xticks = np.arange(200, maxlen_plots, step=120) + ax0.plot( + range(len(timeseries_dict[plottype1][plot_1_1]))[200:], + timeseries_dict[plottype1][plot_1_1][200:], + color=color1, + label=label1, + ) + ax0.plot( + range(len(timeseries_dict[plottype1][plot_1_2]))[200:], + timeseries_dict[plottype1][plot_1_2][200:], + color=color2, + label=label2, + ) + ax0.fill_between( + range(len(timeseries_dict["quantile25"][plot_1_1]))[200:], + timeseries_dict["quantile25"][plot_1_1][200:], + timeseries_dict["quantile75"][plot_1_1][200:], + facecolor=color1, + alpha=0.25, + ) + ax0.fill_between( + range(len(timeseries_dict["quantile25"][plot_1_1]))[200:], + timeseries_dict["quantile25"][plot_1_2][200:], + timeseries_dict["quantile75"][plot_1_2][200:], + facecolor=color2, + alpha=0.25, + ) + ax0.set_ylabel(labels[series1]) # "Contracts") + maxlen_plots = max( + maxlen_plots, + len(timeseries_dict[plottype1][plot_1_1]), + len(timeseries_dict[plottype1][plot_1_2]), + ) + xticks = np.arange(200, maxlen_plots, step=120) ax0.set_xticks(xticks) - ax0.set_xticklabels(["${0:d}$".format(int((xtc-200)/12)) for xtc in xticks]); + ax0.set_xticklabels(["${0:d}$".format(int((xtc - 200) / 12)) for xtc in xticks]) - ax0.legend(loc='best') + ax0.legend(loc="best") if series2 is not None: ax1 = fig.add_subplot(212) maxlen_plots = 0 if additionalriskmodelsetting3 is not None: - ax1.plot(range(len(timeseries_dict[plottype2][plot_2_3]))[200:], timeseries_dict[plottype2][plot_2_3][200:], color=color3, label=label3) + ax1.plot( + range(len(timeseries_dict[plottype2][plot_2_3]))[200:], + timeseries_dict[plottype2][plot_2_3][200:], + color=color3, + label=label3, + ) maxlen_plots = max(maxlen_plots, len(timeseries_dict[plottype1][plot_2_3])) if additionalriskmodelsetting4 is not None: - ax1.plot(range(len(timeseries_dict[plottype2][plot_2_4]))[200:], timeseries_dict[plottype2][plot_2_4][200:], color=color4, label=label4) + ax1.plot( + range(len(timeseries_dict[plottype2][plot_2_4]))[200:], + timeseries_dict[plottype2][plot_2_4][200:], + color=color4, + label=label4, + ) maxlen_plots = max(maxlen_plots, len(timeseries_dict[plottype1][plot_2_4])) - ax1.plot(range(len(timeseries_dict[plottype2][plot_2_1]))[200:], timeseries_dict[plottype2][plot_2_1][200:], color=color1, label=label1) - ax1.plot(range(len(timeseries_dict[plottype2][plot_2_2]))[200:], timeseries_dict[plottype2][plot_2_2][200:], color=color2, label=label2) - ax1.fill_between(range(len(timeseries_dict["quantile25"][plot_2_1]))[200:], timeseries_dict["quantile25"][plot_2_1][200:], timeseries_dict["quantile75"][plot_2_1][200:], facecolor=color1, alpha=0.25) - ax1.fill_between(range(len(timeseries_dict["quantile25"][plot_2_1]))[200:], timeseries_dict["quantile25"][plot_2_2][200:], timeseries_dict["quantile75"][plot_2_2][200:], facecolor=color2, alpha=0.25) - maxlen_plots = max(maxlen_plots, len(timeseries_dict[plottype1][plot_2_1]), len(timeseries_dict[plottype1][plot_2_2])) - xticks = np.arange(200, maxlen_plots, step=120) + ax1.plot( + range(len(timeseries_dict[plottype2][plot_2_1]))[200:], + timeseries_dict[plottype2][plot_2_1][200:], + color=color1, + label=label1, + ) + ax1.plot( + range(len(timeseries_dict[plottype2][plot_2_2]))[200:], + timeseries_dict[plottype2][plot_2_2][200:], + color=color2, + label=label2, + ) + ax1.fill_between( + range(len(timeseries_dict["quantile25"][plot_2_1]))[200:], + timeseries_dict["quantile25"][plot_2_1][200:], + timeseries_dict["quantile75"][plot_2_1][200:], + facecolor=color1, + alpha=0.25, + ) + ax1.fill_between( + range(len(timeseries_dict["quantile25"][plot_2_1]))[200:], + timeseries_dict["quantile25"][plot_2_2][200:], + timeseries_dict["quantile75"][plot_2_2][200:], + facecolor=color2, + alpha=0.25, + ) + maxlen_plots = max( + maxlen_plots, + len(timeseries_dict[plottype1][plot_2_1]), + len(timeseries_dict[plottype1][plot_2_2]), + ) + xticks = np.arange(200, maxlen_plots, step=120) ax1.set_xticks(xticks) - ax1.set_xticklabels(["${0:d}$".format(int((xtc-200)/12)) for xtc in xticks]); + ax1.set_xticklabels(["${0:d}$".format(int((xtc - 200) / 12)) for xtc in xticks]) ax1.set_ylabel(labels[series2]) ax1.set_xlabel("Years") else: @@ -145,32 +248,78 @@ def plotting(output_label, timeseries_dict, riskmodelsetting1, riskmodelsetting2 plt.savefig(outputfilename) plt.show() + timeseries = read_data() ## for just two different riskmodel settings -#plotting(output_label="fig_pl_survival_1_2", timeseries_dict=timeseries, riskmodelsetting1="one", \ +# plotting(output_label="fig_pl_survival_1_2", timeseries_dict=timeseries, riskmodelsetting1="one", \ # riskmodelsetting2="two", series1="profitslosses", series2="operational", plottype1="mean", plottype2="median") -#plotting(output_label="fig_reinsurers_pl_survival_1_2", timeseries_dict=timeseries, riskmodelsetting1="one", \ +# plotting(output_label="fig_reinsurers_pl_survival_1_2", timeseries_dict=timeseries, riskmodelsetting1="one", \ # riskmodelsetting2="two", series1="reinprofitslosses", series2="reinoperational", plottype1="mean", plottype2="median") -#plotting(output_label="fig_premium_1_2", timeseries_dict=timeseries, riskmodelsetting1="one", riskmodelsetting2="two", \ +# plotting(output_label="fig_premium_1_2", timeseries_dict=timeseries, riskmodelsetting1="one", riskmodelsetting2="two", \ # series1="premium", series2=None, plottype1="mean", plottype2=None) # -#raise SystemExit +# raise SystemExit # for four different riskmodel settings -plotting(output_label="fig_pl_survival_1_2", timeseries_dict=timeseries, riskmodelsetting1="one", \ - riskmodelsetting2="two", series1="profitslosses", series2="operational", additionalriskmodelsetting3="three", \ - additionalriskmodelsetting4="four", plottype1="mean", plottype2="median") -plotting(output_label="fig_pl_survival_3_4", timeseries_dict=timeseries, riskmodelsetting1="three", \ - riskmodelsetting2="four", series1="profitslosses", series2="operational", additionalriskmodelsetting3="one", \ - additionalriskmodelsetting4="two", plottype1="mean", plottype2="median") -plotting(output_label="fig_reinsurers_pl_survival_1_2", timeseries_dict=timeseries, riskmodelsetting1="one", \ - riskmodelsetting2="two", series1="reinprofitslosses", series2="reinoperational", \ - additionalriskmodelsetting3="three", additionalriskmodelsetting4="four", plottype1="mean", plottype2="median") -plotting(output_label="fig_reinsurers_pl_survival_3_4", timeseries_dict=timeseries, riskmodelsetting1="three", \ - riskmodelsetting2="four", series1="reinprofitslosses", series2="reinoperational", \ - additionalriskmodelsetting3="one", additionalriskmodelsetting4="two", plottype1="mean", plottype2="median") -plotting(output_label="fig_premium_1_2", timeseries_dict=timeseries, riskmodelsetting1="one", riskmodelsetting2="two", \ - series1="premium", series2=None, additionalriskmodelsetting3="three", additionalriskmodelsetting4="four", \ - plottype1="mean", plottype2=None) - -#pdb.set_trace() +plotting( + output_label="fig_pl_survival_1_2", + timeseries_dict=timeseries, + riskmodelsetting1="one", + riskmodelsetting2="two", + series1="profitslosses", + series2="operational", + additionalriskmodelsetting3="three", + additionalriskmodelsetting4="four", + plottype1="mean", + plottype2="median", +) +plotting( + output_label="fig_pl_survival_3_4", + timeseries_dict=timeseries, + riskmodelsetting1="three", + riskmodelsetting2="four", + series1="profitslosses", + series2="operational", + additionalriskmodelsetting3="one", + additionalriskmodelsetting4="two", + plottype1="mean", + plottype2="median", +) +plotting( + output_label="fig_reinsurers_pl_survival_1_2", + timeseries_dict=timeseries, + riskmodelsetting1="one", + riskmodelsetting2="two", + series1="reinprofitslosses", + series2="reinoperational", + additionalriskmodelsetting3="three", + additionalriskmodelsetting4="four", + plottype1="mean", + plottype2="median", +) +plotting( + output_label="fig_reinsurers_pl_survival_3_4", + timeseries_dict=timeseries, + riskmodelsetting1="three", + riskmodelsetting2="four", + series1="reinprofitslosses", + series2="reinoperational", + additionalriskmodelsetting3="one", + additionalriskmodelsetting4="two", + plottype1="mean", + plottype2="median", +) +plotting( + output_label="fig_premium_1_2", + timeseries_dict=timeseries, + riskmodelsetting1="one", + riskmodelsetting2="two", + series1="premium", + series2=None, + additionalriskmodelsetting3="three", + additionalriskmodelsetting4="four", + plottype1="mean", + plottype2=None, +) + +# pdb.set_trace() diff --git a/metaplotter_pl_timescale_additional_measures.py b/metaplotter_pl_timescale_additional_measures.py index 5b0c449..b22b452 100644 --- a/metaplotter_pl_timescale_additional_measures.py +++ b/metaplotter_pl_timescale_additional_measures.py @@ -5,11 +5,12 @@ import time import glob + def read_data(): # do not overwrite old pdfs - #if os.path.exists("data/fig_one_and_two_rm_comp.pdf"): + # if os.path.exists("data/fig_one_and_two_rm_comp.pdf"): # os.rename("data/fig_one_and_two_rm_comp.pdf", "data/fig_one_and_two_rm_comp_old_" + time.strftime('%Y_%b_%d_%H_%M') + ".pdf") - #if os.path.exists("data/fig_three_and_four_rm_comp.pdf"): + # if os.path.exists("data/fig_three_and_four_rm_comp.pdf"): # os.rename("data/fig_three_and_four_rm_comp.pdf", "data/fig_three_and_four_rm_comp_old_" + time.strftime('%Y_%b_%d_%H_%M') + ".pdf") upper_bound = 75 @@ -30,7 +31,7 @@ def read_data(): filenames_threes.sort() filenames_fours.sort() - #assert len(filenames_ones) == len(filenames_twos) == len(filenames_threes) == len(filenames_fours) + # assert len(filenames_ones) == len(filenames_twos) == len(filenames_threes) == len(filenames_fours) all_filenames = filenames_ones + filenames_twos + filenames_threes + filenames_fours for filename in all_filenames: @@ -38,7 +39,7 @@ def read_data(): rfile = open(filename, "r") data = [eval(k) for k in rfile] rfile.close() - + # compute data series data_means = [] data_medians = [] @@ -53,21 +54,45 @@ def read_data(): data_medians = np.array(data_medians) data_q25 = np.array(data_q25) data_q75 = np.array(data_q75) - + # record data series timeseries_dict["mean"][filename] = data_means timeseries_dict["median"][filename] = data_medians timeseries_dict["quantile25"][filename] = data_q25 timeseries_dict["quantile75"][filename] = data_q75 return timeseries_dict - - -def plotting(output_label, timeseries_dict, riskmodelsetting1, riskmodelsetting2, series1, series2=None, additionalriskmodelsetting3=None, additionalriskmodelsetting4=None, plottype1="mean", plottype2="mean"): + +def plotting( + output_label, + timeseries_dict, + riskmodelsetting1, + riskmodelsetting2, + series1, + series2=None, + additionalriskmodelsetting3=None, + additionalriskmodelsetting4=None, + plottype1="mean", + plottype2="mean", +): # dictionaries colors = {"one": "red", "two": "blue", "three": "green", "four": "yellow"} - labels = {"reinexcess_capital": "Excess Capital (Reinsurers)", "excess_capital": "Excess Capital (Insurers)", "cumulative_unrecovered_claims": "Uncovered Claims (cumulative)", "cumulative_bankruptcies": "Bankruptcies (cumulative)", "profitslosses": "Profits and Losses (Insurer)", "contracts": "Contracts (Insurers)", "cash": "Liquidity (Insurers)", "operational": "Active Insurers", "premium": "Premium", "reinprofitslosses": "Profits and Losses (Reinsurer)", "reincash": "Liquidity (Reinsurers)", "reincontracts": "Contracts (Reinsurers)", "reinoperational": "Active Reinsurers"} - + labels = { + "reinexcess_capital": "Excess Capital (Reinsurers)", + "excess_capital": "Excess Capital (Insurers)", + "cumulative_unrecovered_claims": "Uncovered Claims (cumulative)", + "cumulative_bankruptcies": "Bankruptcies (cumulative)", + "profitslosses": "Profits and Losses (Insurer)", + "contracts": "Contracts (Insurers)", + "cash": "Liquidity (Insurers)", + "operational": "Active Insurers", + "premium": "Premium", + "reinprofitslosses": "Profits and Losses (Reinsurer)", + "reincash": "Liquidity (Reinsurers)", + "reincontracts": "Contracts (Reinsurers)", + "reinoperational": "Active Reinsurers", + } + # prepare labels, timeseries, etc. color1 = colors[riskmodelsetting1] color2 = colors[riskmodelsetting2] @@ -80,23 +105,33 @@ def plotting(output_label, timeseries_dict, riskmodelsetting1, riskmodelsetting2 plot_2_2 = "data/" + riskmodelsetting2 + "_" + series2 + ".dat" if additionalriskmodelsetting3 is not None: color3 = colors[additionalriskmodelsetting3] - label3 = str.upper(additionalriskmodelsetting3[0]) + additionalriskmodelsetting3[1:] + " riskmodels" + label3 = ( + str.upper(additionalriskmodelsetting3[0]) + + additionalriskmodelsetting3[1:] + + " riskmodels" + ) plot_1_3 = "data/" + additionalriskmodelsetting3 + "_" + series1 + ".dat" if series2 is not None: plot_2_3 = "data/" + additionalriskmodelsetting3 + "_" + series2 + ".dat" if additionalriskmodelsetting4 is not None: color4 = colors[additionalriskmodelsetting4] - label4 = str.upper(additionalriskmodelsetting4[0]) + additionalriskmodelsetting4[1:] + " riskmodels" + label4 = ( + str.upper(additionalriskmodelsetting4[0]) + + additionalriskmodelsetting4[1:] + + " riskmodels" + ) plot_1_4 = "data/" + additionalriskmodelsetting4 + "_" + series1 + ".dat" if series2 is not None: plot_2_4 = "data/" + additionalriskmodelsetting4 + "_" + series2 + ".dat" - + # Backup existing figures (so as not to overwrite them) outputfilename = "data/" + output_label + ".pdf" - backupfilename = "data/" + output_label + "_old_" + time.strftime('%Y_%b_%d_%H_%M') + ".pdf" + backupfilename = ( + "data/" + output_label + "_old_" + time.strftime("%Y_%b_%d_%H_%M") + ".pdf" + ) if os.path.exists(outputfilename): os.rename(outputfilename, backupfilename) - + # Plot and save fig = plt.figure() if series2 is not None: @@ -105,39 +140,111 @@ def plotting(output_label, timeseries_dict, riskmodelsetting1, riskmodelsetting2 ax0 = fig.add_subplot(111) maxlen_plots = 0 if additionalriskmodelsetting3 is not None: - ax0.plot(range(len(timeseries_dict[plottype1][plot_1_3]))[200:], timeseries_dict[plottype1][plot_1_3][200:], color=color3, label=label3) + ax0.plot( + range(len(timeseries_dict[plottype1][plot_1_3]))[200:], + timeseries_dict[plottype1][plot_1_3][200:], + color=color3, + label=label3, + ) maxlen_plots = max(maxlen_plots, len(timeseries_dict[plottype1][plot_1_3])) if additionalriskmodelsetting4 is not None: - ax0.plot(range(len(timeseries_dict[plottype1][plot_1_4]))[200:], timeseries_dict[plottype1][plot_1_4][200:], color=color4, label=label4) + ax0.plot( + range(len(timeseries_dict[plottype1][plot_1_4]))[200:], + timeseries_dict[plottype1][plot_1_4][200:], + color=color4, + label=label4, + ) maxlen_plots = max(maxlen_plots, len(timeseries_dict[plottype1][plot_1_4])) - ax0.plot(range(len(timeseries_dict[plottype1][plot_1_1]))[200:], timeseries_dict[plottype1][plot_1_1][200:], color=color1, label=label1) - ax0.plot(range(len(timeseries_dict[plottype1][plot_1_2]))[200:], timeseries_dict[plottype1][plot_1_2][200:], color=color2, label=label2) - ax0.fill_between(range(len(timeseries_dict["quantile25"][plot_1_1]))[200:], timeseries_dict["quantile25"][plot_1_1][200:], timeseries_dict["quantile75"][plot_1_1][200:], facecolor=color1, alpha=0.25) - ax0.fill_between(range(len(timeseries_dict["quantile25"][plot_1_1]))[200:], timeseries_dict["quantile25"][plot_1_2][200:], timeseries_dict["quantile75"][plot_1_2][200:], facecolor=color2, alpha=0.25) - ax0.set_ylabel(labels[series1])#"Contracts") - maxlen_plots = max(maxlen_plots, len(timeseries_dict[plottype1][plot_1_1]), len(timeseries_dict[plottype1][plot_1_2])) - xticks = np.arange(200, maxlen_plots, step=120) + ax0.plot( + range(len(timeseries_dict[plottype1][plot_1_1]))[200:], + timeseries_dict[plottype1][plot_1_1][200:], + color=color1, + label=label1, + ) + ax0.plot( + range(len(timeseries_dict[plottype1][plot_1_2]))[200:], + timeseries_dict[plottype1][plot_1_2][200:], + color=color2, + label=label2, + ) + ax0.fill_between( + range(len(timeseries_dict["quantile25"][plot_1_1]))[200:], + timeseries_dict["quantile25"][plot_1_1][200:], + timeseries_dict["quantile75"][plot_1_1][200:], + facecolor=color1, + alpha=0.25, + ) + ax0.fill_between( + range(len(timeseries_dict["quantile25"][plot_1_1]))[200:], + timeseries_dict["quantile25"][plot_1_2][200:], + timeseries_dict["quantile75"][plot_1_2][200:], + facecolor=color2, + alpha=0.25, + ) + ax0.set_ylabel(labels[series1]) # "Contracts") + maxlen_plots = max( + maxlen_plots, + len(timeseries_dict[plottype1][plot_1_1]), + len(timeseries_dict[plottype1][plot_1_2]), + ) + xticks = np.arange(200, maxlen_plots, step=120) ax0.set_xticks(xticks) - ax0.set_xticklabels(["${0:d}$".format(int((xtc-200)/12)) for xtc in xticks]); + ax0.set_xticklabels(["${0:d}$".format(int((xtc - 200) / 12)) for xtc in xticks]) - ax0.legend(loc='best') + ax0.legend(loc="best") if series2 is not None: ax1 = fig.add_subplot(212) maxlen_plots = 0 if additionalriskmodelsetting3 is not None: - ax1.plot(range(len(timeseries_dict[plottype2][plot_2_3]))[200:], timeseries_dict[plottype2][plot_2_3][200:], color=color3, label=label3) + ax1.plot( + range(len(timeseries_dict[plottype2][plot_2_3]))[200:], + timeseries_dict[plottype2][plot_2_3][200:], + color=color3, + label=label3, + ) maxlen_plots = max(maxlen_plots, len(timeseries_dict[plottype1][plot_2_3])) if additionalriskmodelsetting4 is not None: - ax1.plot(range(len(timeseries_dict[plottype2][plot_2_4]))[200:], timeseries_dict[plottype2][plot_2_4][200:], color=color4, label=label4) + ax1.plot( + range(len(timeseries_dict[plottype2][plot_2_4]))[200:], + timeseries_dict[plottype2][plot_2_4][200:], + color=color4, + label=label4, + ) maxlen_plots = max(maxlen_plots, len(timeseries_dict[plottype1][plot_2_4])) - ax1.plot(range(len(timeseries_dict[plottype2][plot_2_1]))[200:], timeseries_dict[plottype2][plot_2_1][200:], color=color1, label=label1) - ax1.plot(range(len(timeseries_dict[plottype2][plot_2_2]))[200:], timeseries_dict[plottype2][plot_2_2][200:], color=color2, label=label2) - ax1.fill_between(range(len(timeseries_dict["quantile25"][plot_2_1]))[200:], timeseries_dict["quantile25"][plot_2_1][200:], timeseries_dict["quantile75"][plot_2_1][200:], facecolor=color1, alpha=0.25) - ax1.fill_between(range(len(timeseries_dict["quantile25"][plot_2_1]))[200:], timeseries_dict["quantile25"][plot_2_2][200:], timeseries_dict["quantile75"][plot_2_2][200:], facecolor=color2, alpha=0.25) - maxlen_plots = max(maxlen_plots, len(timeseries_dict[plottype1][plot_2_1]), len(timeseries_dict[plottype1][plot_2_2])) - xticks = np.arange(200, maxlen_plots, step=120) + ax1.plot( + range(len(timeseries_dict[plottype2][plot_2_1]))[200:], + timeseries_dict[plottype2][plot_2_1][200:], + color=color1, + label=label1, + ) + ax1.plot( + range(len(timeseries_dict[plottype2][plot_2_2]))[200:], + timeseries_dict[plottype2][plot_2_2][200:], + color=color2, + label=label2, + ) + ax1.fill_between( + range(len(timeseries_dict["quantile25"][plot_2_1]))[200:], + timeseries_dict["quantile25"][plot_2_1][200:], + timeseries_dict["quantile75"][plot_2_1][200:], + facecolor=color1, + alpha=0.25, + ) + ax1.fill_between( + range(len(timeseries_dict["quantile25"][plot_2_1]))[200:], + timeseries_dict["quantile25"][plot_2_2][200:], + timeseries_dict["quantile75"][plot_2_2][200:], + facecolor=color2, + alpha=0.25, + ) + maxlen_plots = max( + maxlen_plots, + len(timeseries_dict[plottype1][plot_2_1]), + len(timeseries_dict[plottype1][plot_2_2]), + ) + xticks = np.arange(200, maxlen_plots, step=120) ax1.set_xticks(xticks) - ax1.set_xticklabels(["${0:d}$".format(int((xtc-200)/12)) for xtc in xticks]); + ax1.set_xticklabels(["${0:d}$".format(int((xtc - 200) / 12)) for xtc in xticks]) ax1.set_ylabel(labels[series2]) ax1.set_xlabel("Years") else: @@ -145,43 +252,118 @@ def plotting(output_label, timeseries_dict, riskmodelsetting1, riskmodelsetting2 plt.savefig(outputfilename) plt.show() + timeseries = read_data() # for just two different riskmodel settings -#plotting(output_label="fig_pl_excap_1_2", timeseries_dict=timeseries, riskmodelsetting1="one", \ +# plotting(output_label="fig_pl_excap_1_2", timeseries_dict=timeseries, riskmodelsetting1="one", \ # riskmodelsetting2="two", series1="profitslosses", series2="excess_capital", plottype1="mean", plottype2="mean") -#plotting(output_label="fig_reinsurers_pl_excap_1_2", timeseries_dict=timeseries, riskmodelsetting1="one", \ +# plotting(output_label="fig_reinsurers_pl_excap_1_2", timeseries_dict=timeseries, riskmodelsetting1="one", \ # riskmodelsetting2="two", series1="reinprofitslosses", series2="reinexcess_capital", plottype1="mean", plottype2="mean") -#plotting(output_label="fig_bankruptcies_unrecovered_claims_1_2", timeseries_dict=timeseries, riskmodelsetting1="one", \ +# plotting(output_label="fig_bankruptcies_unrecovered_claims_1_2", timeseries_dict=timeseries, riskmodelsetting1="one", \ # riskmodelsetting2="two", series1="cumulative_bankruptcies", series2="cumulative_unrecovered_claims", plottype1="mean", plottype2="median") -#plotting(output_label="fig_premium_1_2", timeseries_dict=timeseries, riskmodelsetting1="one", riskmodelsetting2="two", \ +# plotting(output_label="fig_premium_1_2", timeseries_dict=timeseries, riskmodelsetting1="one", riskmodelsetting2="two", \ # series1="premium", series2=None, plottype1="mean", plottype2=None) # -#raise SystemExit +# raise SystemExit # for four different riskmodel settings -plotting(output_label="fig_pl_excap_1_2", timeseries_dict=timeseries, riskmodelsetting1="one", \ - riskmodelsetting2="two", series1="profitslosses", series2="excess_capital", additionalriskmodelsetting3="three", \ - additionalriskmodelsetting4="four", plottype1="mean", plottype2="mean") -plotting(output_label="fig_reinsurers_pl_excap_1_2", timeseries_dict=timeseries, riskmodelsetting1="one", \ - riskmodelsetting2="two", series1="reinprofitslosses", series2="reinexcess_capital", additionalriskmodelsetting3="three", \ - additionalriskmodelsetting4="four", plottype1="mean", plottype2="mean") -plotting(output_label="fig_bankruptcies_unrecovered_claims_1_2", timeseries_dict=timeseries, riskmodelsetting1="one", \ - riskmodelsetting2="two", series1="cumulative_bankruptcies", series2="cumulative_unrecovered_claims", additionalriskmodelsetting3="three", \ - additionalriskmodelsetting4="four", plottype1="mean", plottype2="median") -plotting(output_label="fig_premium_1_2", timeseries_dict=timeseries, riskmodelsetting1="one", riskmodelsetting2="two", \ - series1="premium", series2=None, additionalriskmodelsetting3="three", additionalriskmodelsetting4="four", plottype1="mean", plottype2=None) - -plotting(output_label="fig_pl_excap_3_4", timeseries_dict=timeseries, riskmodelsetting1="three", \ - riskmodelsetting2="four", series1="profitslosses", series2="excess_capital", additionalriskmodelsetting3="one", \ - additionalriskmodelsetting4="two", plottype1="mean", plottype2="mean") -plotting(output_label="fig_reinsurers_pl_excap_3_4", timeseries_dict=timeseries, riskmodelsetting1="three", \ - riskmodelsetting2="four", series1="reinprofitslosses", series2="reinexcess_capital", additionalriskmodelsetting3="one", \ - additionalriskmodelsetting4="two", plottype1="mean", plottype2="mean") -plotting(output_label="fig_bankruptcies_unrecovered_claims_3_4", timeseries_dict=timeseries, riskmodelsetting1="three", \ - riskmodelsetting2="four", series1="cumulative_bankruptcies", series2="cumulative_unrecovered_claims", additionalriskmodelsetting3="one", \ - additionalriskmodelsetting4="two", plottype1="mean", plottype2="median") -plotting(output_label="fig_premium_3_4", timeseries_dict=timeseries, riskmodelsetting1="three", riskmodelsetting2="four", \ - series1="premium", series2=None, additionalriskmodelsetting3="one", additionalriskmodelsetting4="two", plottype1="mean", plottype2=None) - - -#pdb.set_trace() +plotting( + output_label="fig_pl_excap_1_2", + timeseries_dict=timeseries, + riskmodelsetting1="one", + riskmodelsetting2="two", + series1="profitslosses", + series2="excess_capital", + additionalriskmodelsetting3="three", + additionalriskmodelsetting4="four", + plottype1="mean", + plottype2="mean", +) +plotting( + output_label="fig_reinsurers_pl_excap_1_2", + timeseries_dict=timeseries, + riskmodelsetting1="one", + riskmodelsetting2="two", + series1="reinprofitslosses", + series2="reinexcess_capital", + additionalriskmodelsetting3="three", + additionalriskmodelsetting4="four", + plottype1="mean", + plottype2="mean", +) +plotting( + output_label="fig_bankruptcies_unrecovered_claims_1_2", + timeseries_dict=timeseries, + riskmodelsetting1="one", + riskmodelsetting2="two", + series1="cumulative_bankruptcies", + series2="cumulative_unrecovered_claims", + additionalriskmodelsetting3="three", + additionalriskmodelsetting4="four", + plottype1="mean", + plottype2="median", +) +plotting( + output_label="fig_premium_1_2", + timeseries_dict=timeseries, + riskmodelsetting1="one", + riskmodelsetting2="two", + series1="premium", + series2=None, + additionalriskmodelsetting3="three", + additionalriskmodelsetting4="four", + plottype1="mean", + plottype2=None, +) + +plotting( + output_label="fig_pl_excap_3_4", + timeseries_dict=timeseries, + riskmodelsetting1="three", + riskmodelsetting2="four", + series1="profitslosses", + series2="excess_capital", + additionalriskmodelsetting3="one", + additionalriskmodelsetting4="two", + plottype1="mean", + plottype2="mean", +) +plotting( + output_label="fig_reinsurers_pl_excap_3_4", + timeseries_dict=timeseries, + riskmodelsetting1="three", + riskmodelsetting2="four", + series1="reinprofitslosses", + series2="reinexcess_capital", + additionalriskmodelsetting3="one", + additionalriskmodelsetting4="two", + plottype1="mean", + plottype2="mean", +) +plotting( + output_label="fig_bankruptcies_unrecovered_claims_3_4", + timeseries_dict=timeseries, + riskmodelsetting1="three", + riskmodelsetting2="four", + series1="cumulative_bankruptcies", + series2="cumulative_unrecovered_claims", + additionalriskmodelsetting3="one", + additionalriskmodelsetting4="two", + plottype1="mean", + plottype2="median", +) +plotting( + output_label="fig_premium_3_4", + timeseries_dict=timeseries, + riskmodelsetting1="three", + riskmodelsetting2="four", + series1="premium", + series2=None, + additionalriskmodelsetting3="one", + additionalriskmodelsetting4="two", + plottype1="mean", + plottype2=None, +) + + +# pdb.set_trace() diff --git a/plotter.py b/plotter.py index 593b95f..fd348d2 100755 --- a/plotter.py +++ b/plotter.py @@ -1,20 +1,20 @@ import matplotlib.pyplot as plt import numpy as np -rfile = open("data/history_logs.dat","r") +rfile = open("data/history_logs.dat", "r") data = [eval(k) for k in rfile] -contracts = data[0]['total_contracts'] -op = data[0]['total_operational'] -cash = data[0]['total_cash'] -pl = data[0]['total_profitslosses'] -reincontracts = data[0]['total_reincontracts'] -reinop = data[0]['total_reinoperational'] -reincash = data[0]['total_reincash'] -reinpl = data[0]['total_reinprofitslosses'] -premium = data[0]['market_premium'] -catbop = data[0]['total_catbondsoperational'] +contracts = data[0]["total_contracts"] +op = data[0]["total_operational"] +cash = data[0]["total_cash"] +pl = data[0]["total_profitslosses"] +reincontracts = data[0]["total_reincontracts"] +reinop = data[0]["total_reinoperational"] +reincash = data[0]["total_reincash"] +reinpl = data[0]["total_reinprofitslosses"] +premium = data[0]["market_premium"] +catbop = data[0]["total_catbondsoperational"] rfile.close() @@ -34,22 +34,22 @@ fig1 = plt.figure() ax0 = fig1.add_subplot(511) ax0.get_xaxis().set_visible(False) -ax0.plot(range(len(cs)), cs,"b") +ax0.plot(range(len(cs)), cs, "b") ax0.set_ylabel("Contracts") ax1 = fig1.add_subplot(512) ax1.get_xaxis().set_visible(False) -ax1.plot(range(len(os)), os,"b") +ax1.plot(range(len(os)), os, "b") ax1.set_ylabel("Active firms") ax2 = fig1.add_subplot(513) ax2.get_xaxis().set_visible(False) -ax2.plot(range(len(hs)), hs,"b") +ax2.plot(range(len(hs)), hs, "b") ax2.set_ylabel("Cash") ax3 = fig1.add_subplot(514) ax3.get_xaxis().set_visible(False) -ax3.plot(range(len(pls)), pls,"b") +ax3.plot(range(len(pls)), pls, "b") ax3.set_ylabel("Profits, Losses") ax9 = fig1.add_subplot(515) -ax9.plot(range(len(ps)), ps,"k") +ax9.plot(range(len(ps)), ps, "k") ax9.set_ylabel("Premium") ax9.set_xlabel("Time") plt.savefig("data/single_replication_pt1.pdf") @@ -57,22 +57,22 @@ fig2 = plt.figure() ax4 = fig2.add_subplot(511) ax4.get_xaxis().set_visible(False) -ax4.plot(range(len(cre)), cre,"r") +ax4.plot(range(len(cre)), cre, "r") ax4.set_ylabel("Contracts") ax5 = fig2.add_subplot(512) ax5.get_xaxis().set_visible(False) -ax5.plot(range(len(ore)), ore,"r") +ax5.plot(range(len(ore)), ore, "r") ax5.set_ylabel("Active reinfirms") ax6 = fig2.add_subplot(513) ax6.get_xaxis().set_visible(False) -ax6.plot(range(len(hre)), hre,"r") +ax6.plot(range(len(hre)), hre, "r") ax6.set_ylabel("Cash") ax7 = fig2.add_subplot(514) ax7.get_xaxis().set_visible(False) -ax7.plot(range(len(plre)), plre,"r") +ax7.plot(range(len(plre)), plre, "r") ax7.set_ylabel("Profits, Losses") ax8 = fig2.add_subplot(515) -ax8.plot(range(len(ocb)), ocb,"m") +ax8.plot(range(len(ocb)), ocb, "m") ax8.set_ylabel("Active cat bonds") ax8.set_xlabel("Time") diff --git a/plotter_pl_timescale.py b/plotter_pl_timescale.py index 4f517ff..26b001b 100644 --- a/plotter_pl_timescale.py +++ b/plotter_pl_timescale.py @@ -1,12 +1,14 @@ import matplotlib.pyplot as plt import numpy as np + def get_data(name): rfile = open(name, "r") out = [eval(k) for k in rfile] rfile.close() return out + contracts = get_data("data/contracts.dat") op = get_data("data/operational.dat") cash = get_data("data/cash.dat") @@ -30,7 +32,7 @@ def get_data(name): c_re = [] -o_re= [] +o_re = [] h_re = [] @@ -40,18 +42,18 @@ def get_data(name): p_e = [] -for i in range(len(contracts[0])): #for every time period i +for i in range(len(contracts[0])): # for every time period i cs = np.mean([item[i] for item in contracts]) - #pls = np.mean([item[i] for item in pl]) + # pls = np.mean([item[i] for item in pl]) os = np.median([item[i] for item in op]) hs = np.median([item[i] for item in cash]) c_s.append(cs) o_s.append(os) h_s.append(hs) - if i>0: - pls = np.mean([item[i]-item[i-1] for item in cash]) - plre = np.mean([item[i]-item[i-1] for item in reincash]) + if i > 0: + pls = np.mean([item[i] - item[i - 1] for item in cash]) + plre = np.mean([item[i] - item[i - 1] for item in reincash]) pl_s.append(pls) pl_re.append(plre) @@ -61,43 +63,43 @@ def get_data(name): c_re.append(cre) o_re.append(ore) h_re.append(hre) - + ocb = np.median([item[i] for item in catbop]) o_cb.append(ocb) - + p_s = np.median([item[i] for item in premium]) p_e.append(p_s) maxlen_plots = max(len(pl_s), len(pl_re), len(o_s), len(o_re), len(p_e)) -xticks = np.arange(200, maxlen_plots, step=120) +xticks = np.arange(200, maxlen_plots, step=120) fig0 = plt.figure() ax3 = fig0.add_subplot(511) -ax3.plot(range(len(pl_s))[200:], pl_s[200:],"b") +ax3.plot(range(len(pl_s))[200:], pl_s[200:], "b") ax3.set_ylabel("Profits, Losses") ax3.set_xticks(xticks) -ax3.set_xticklabels(["${0:d}$".format(int((xtc-200)/12)) for xtc in xticks]) +ax3.set_xticklabels(["${0:d}$".format(int((xtc - 200) / 12)) for xtc in xticks]) ax7 = fig0.add_subplot(512) -ax7.plot(range(len(pl_re))[200:], pl_re[200:],"r") +ax7.plot(range(len(pl_re))[200:], pl_re[200:], "r") ax7.set_ylabel("Profits, Losses (Reins.)") ax7.set_xticks(xticks) -ax7.set_xticklabels(["${0:d}$".format(int((xtc-200)/12)) for xtc in xticks]) +ax7.set_xticklabels(["${0:d}$".format(int((xtc - 200) / 12)) for xtc in xticks]) ax1 = fig0.add_subplot(513) -ax1.plot(range(len(o_s))[200:], o_s[200:],"b") +ax1.plot(range(len(o_s))[200:], o_s[200:], "b") ax1.set_ylabel("Active firms") ax1.set_xticks(xticks) -ax1.set_xticklabels(["${0:d}$".format(int((xtc-200)/12)) for xtc in xticks]) +ax1.set_xticklabels(["${0:d}$".format(int((xtc - 200) / 12)) for xtc in xticks]) ax5 = fig0.add_subplot(514) -ax5.plot(range(len(o_re))[200:], o_re[200:],"r") +ax5.plot(range(len(o_re))[200:], o_re[200:], "r") ax5.set_ylabel("Active reins. firms") ax5.set_xticks(xticks) -ax5.set_xticklabels(["${0:d}$".format(int((xtc-200)/12)) for xtc in xticks]) +ax5.set_xticklabels(["${0:d}$".format(int((xtc - 200) / 12)) for xtc in xticks]) ax9 = fig0.add_subplot(515) -ax9.plot(range(len(p_e))[200:], p_e[200:],"k") +ax9.plot(range(len(p_e))[200:], p_e[200:], "k") ax9.set_ylabel("Premium") ax9.set_xlabel("Years") ax9.set_xticks(xticks) -ax9.set_xticklabels(["${0:d}$".format(int((xtc-200)/12)) for xtc in xticks]) +ax9.set_xticklabels(["${0:d}$".format(int((xtc - 200) / 12)) for xtc in xticks]) plt.savefig("data/single_replication_new.pdf") diff --git a/reinsurancecontract.py b/reinsurancecontract.py index c9ced5a..86d011d 100644 --- a/reinsurancecontract.py +++ b/reinsurancecontract.py @@ -1,6 +1,7 @@ import numpy as np -from metainsurancecontract import MetaInsuranceContract +from metainsurancecontract import MetaInsuranceContract + class ReinsuranceContract(MetaInsuranceContract): """ReinsuranceContract class. @@ -9,18 +10,48 @@ class ReinsuranceContract(MetaInsuranceContract): and ReinsuranceContract objects. The signature of this class' constructor is the same as that of the InsuranceContract constructor. The class has two methods (explode, mature) that overwrite methods in InsuranceContract.""" - def __init__(self, insurer, properties, time, premium, runtime, payment_period, expire_immediately, initial_VaR=0.,\ - insurancetype="proportional", deductible_fraction=None, excess_fraction=None, reinsurance=0): - super(ReinsuranceContract, self).__init__(insurer, properties, time, premium, runtime, payment_period, \ - expire_immediately, initial_VaR, insurancetype, deductible_fraction, excess_fraction, reinsurance) - #self.is_reinsurancecontract = True - + + def __init__( + self, + insurer, + properties, + time, + premium, + runtime, + payment_period, + expire_immediately, + initial_VaR=0.0, + insurancetype="proportional", + deductible_fraction=None, + excess_fraction=None, + reinsurance=0, + ): + super(ReinsuranceContract, self).__init__( + insurer, + properties, + time, + premium, + runtime, + payment_period, + expire_immediately, + initial_VaR, + insurancetype, + deductible_fraction, + excess_fraction, + reinsurance, + ) + # self.is_reinsurancecontract = True + if self.insurancetype == "excess-of-loss": - self.property_holder.add_reinsurance(category=self.category, excess_fraction=self.excess_fraction, \ - deductible_fraction=self.deductible_fraction, contract=self) + self.property_holder.add_reinsurance( + category=self.category, + excess_fraction=self.excess_fraction, + deductible_fraction=self.deductible_fraction, + contract=self, + ) else: assert self.contract is not None - + def explode(self, time, damage_extent=None): """Explode method. Accepts agruments @@ -34,19 +65,25 @@ def explode(self, time, damage_extent=None): if self.insurancetype == "excess-of-loss" and damage_extent > self.deductible: claim = min(self.excess, damage_extent) - self.deductible - self.insurer.receive_obligation(claim, self.property_holder, time, 'claim') + self.insurer.receive_obligation(claim, self.property_holder, time, "claim") else: claim = min(self.excess, damage_extent) - self.deductible - self.insurer.receive_obligation(claim, self.property_holder, time + 1, 'claim') + self.insurer.receive_obligation( + claim, self.property_holder, time + 1, "claim" + ) # Reinsurer pays as soon as possible. - self.insurer.register_claim(claim) #Every reinsurance claim made is immediately registered. + self.insurer.register_claim( + claim + ) # Every reinsurance claim made is immediately registered. if self.expire_immediately: - self.current_claim += self.contract.claim # TODO: should proportional reinsurance claims be subject to excess_of_loss retrocession? If so, reorganize more straightforwardly - + self.current_claim += ( + self.contract.claim + ) # TODO: should proportional reinsurance claims be subject to excess_of_loss retrocession? If so, reorganize more straightforwardly + self.expiration = time - #self.terminating = True - + # self.terminating = True + def mature(self, time): """Mature method. Accepts arguments @@ -54,12 +91,15 @@ def mature(self, time): No return value. Removes any reinsurance functions this contract has and terminates any reinsurance contracts for this contract.""" - #self.terminating = True + # self.terminating = True self.terminate_reinsurance(time) - + if self.insurancetype == "excess-of-loss": - self.property_holder.delete_reinsurance(category=self.category, excess_fraction=self.excess_fraction, \ - deductible_fraction=self.deductible_fraction, contract=self) - else: #TODO: ? Instead: if self.insurancetype == "proportional": + self.property_holder.delete_reinsurance( + category=self.category, + excess_fraction=self.excess_fraction, + deductible_fraction=self.deductible_fraction, + contract=self, + ) + else: # TODO: ? Instead: if self.insurancetype == "proportional": self.contract.unreinsure() - diff --git a/reinsurancefirm.py b/reinsurancefirm.py index 1c1558b..ac2564f 100644 --- a/reinsurancefirm.py +++ b/reinsurancefirm.py @@ -1,9 +1,11 @@ -#from metainsuranceorg import MetaInsuranceOrg +# from metainsuranceorg import MetaInsuranceOrg from insurancefirm import InsuranceFirm + class ReinsuranceFirm(InsuranceFirm): """ReinsuranceFirm class. Inherits from InsuranceFirm.""" + def init(self, simulation_parameters, agent_parameters): """Constructor method. Accepts arguments diff --git a/resume.py b/resume.py index 7c60700..48c8543 100644 --- a/resume.py +++ b/resume.py @@ -6,7 +6,7 @@ import argparse import pickle import hashlib -import random +import random # import config file and apply configuration import isleconfig @@ -16,14 +16,37 @@ override_no_riskmodels = False # use argparse to handle command line arguments -parser = argparse.ArgumentParser(description='Model the Insurance sector') +parser = argparse.ArgumentParser(description="Model the Insurance sector") parser.add_argument("--abce", action="store_true", help="use abce") -parser.add_argument("--oneriskmodel", action="store_true", help="allow overriding the number of riskmodels from the standard config (with 1)") -parser.add_argument("--riskmodels", type=int, choices=[1,2,3,4], help="allow overriding the number of riskmodels from standard config (with 1 or other numbers)") -parser.add_argument("--replicid", type=int, help="if replication ID is given, pass this to the simulation so that the risk profile can be restored") -parser.add_argument("--replicating", action="store_true", help="if this is a simulation run designed to replicate another, override the config file parameter") -parser.add_argument("--randomseed", type=float, help="allow setting of numpy random seed") -parser.add_argument("--foreground", action="store_true", help="force foreground runs even if replication ID is given (which defaults to background runs)") +parser.add_argument( + "--oneriskmodel", + action="store_true", + help="allow overriding the number of riskmodels from the standard config (with 1)", +) +parser.add_argument( + "--riskmodels", + type=int, + choices=[1, 2, 3, 4], + help="allow overriding the number of riskmodels from standard config (with 1 or other numbers)", +) +parser.add_argument( + "--replicid", + type=int, + help="if replication ID is given, pass this to the simulation so that the risk profile can be restored", +) +parser.add_argument( + "--replicating", + action="store_true", + help="if this is a simulation run designed to replicate another, override the config file parameter", +) +parser.add_argument( + "--randomseed", type=float, help="allow setting of numpy random seed" +) +parser.add_argument( + "--foreground", + action="store_true", + help="force foreground runs even if replication ID is given (which defaults to background runs)", +) parser.add_argument("-p", "--showprogress", action="store_true", help="show timesteps") parser.add_argument("-v", "--verbose", action="store_true", help="more detailed output") args = parser.parse_args() @@ -39,7 +62,9 @@ replic_ID = args.replicid if args.replicating: isleconfig.replicating = True - assert replic_ID is not None, "Error: Replication requires a replication ID to identify run to be replicated" + assert ( + replic_ID is not None + ), "Error: Replication requires a replication ID to identify run to be replicated" if args.randomseed: randomseed = args.randomseed seed = int(randomseed) @@ -55,7 +80,7 @@ # import isle and abce modules if isleconfig.use_abce: - #print("Importing abce") + # print("Importing abce") import abce from abce import gui @@ -70,21 +95,24 @@ def wrapper(target_function): if not condition: return target_function return decorator_function(target_function) + return wrapper + # create non-abce placeholder gui decorator # TODO: replace this with more elegant solution if possible. Currently required since script will otherwise crash at the conditional decorator below since gui is then undefined if not isleconfig.use_abce: + def gui(*args, **kwargs): pass # main function -#@gui(simulation_parameters, serve=True) +# @gui(simulation_parameters, serve=True) @conditionally(gui(simulation_parameters, serve=False), isleconfig.use_abce) def main(): - + with open("data/simulation_save.pkl", "br") as rfile: d = pickle.load(rfile) simulation = d["simulation"] @@ -93,106 +121,120 @@ def main(): random_seed = d["random_seed"] time = d["time"] simulation_parameters = d["simulation_parameters"] - + insurancefirms_group = list(simulation.insurancefirms) reinsurancefirms_group = list(simulation.reinsurancefirms) - - #np.random.seed(seed) + + # np.random.seed(seed) np.random.set_state(np_seed) random.setstate(random_seed) - + assert not isleconfig.use_abce, "Resuming will not work with abce" ## create simulation and world objects (identical in non-abce mode) - #if isleconfig.use_abce: + # if isleconfig.use_abce: # simulation = abce.Simulation(processes=1,random_seed = seed) # - - #simulation_parameters['simulation'] = world = InsuranceSimulation(override_no_riskmodels, replic_ID, simulation_parameters) + + # simulation_parameters['simulation'] = world = InsuranceSimulation(override_no_riskmodels, replic_ID, simulation_parameters) # - #if not isleconfig.use_abce: + # if not isleconfig.use_abce: # simulation = world # - # create agents: insurance firms - #insurancefirms_group = simulation.build_agents(InsuranceFirm, + # create agents: insurance firms + # insurancefirms_group = simulation.build_agents(InsuranceFirm, # 'insurancefirm', # parameters=simulation_parameters, # agent_parameters=world.agent_parameters["insurancefirm"]) # - #if isleconfig.use_abce: + # if isleconfig.use_abce: # insurancefirm_pointers = insurancefirms_group.get_pointer() - #else: + # else: # insurancefirm_pointers = insurancefirms_group - #world.accept_agents("insurancefirm", insurancefirm_pointers, insurancefirms_group) + # world.accept_agents("insurancefirm", insurancefirm_pointers, insurancefirms_group) # - # create agents: reinsurance firms - #reinsurancefirms_group = simulation.build_agents(ReinsuranceFirm, + # create agents: reinsurance firms + # reinsurancefirms_group = simulation.build_agents(ReinsuranceFirm, # 'reinsurance', # parameters=simulation_parameters, # agent_parameters=world.agent_parameters["reinsurance"]) - #if isleconfig.use_abce: + # if isleconfig.use_abce: # reinsurancefirm_pointers = reinsurancefirms_group.get_pointer() - #else: + # else: # reinsurancefirm_pointers = reinsurancefirms_group - #world.accept_agents("reinsurance", reinsurancefirm_pointers, reinsurancefirms_group) + # world.accept_agents("reinsurance", reinsurancefirm_pointers, reinsurancefirms_group) # - + # time iteration for t in range(time, simulation_parameters["max_time"]): - + # abce time step simulation.advance_round(t) - + # create new agents # TODO: write method for this; this code block is executed almost identically 4 times if world.insurance_firm_market_entry(agent_type="InsuranceFirm"): parameters = [np.random.choice(world.agent_parameters["insurancefirm"])] parameters[0]["id"] = world.get_unique_insurer_id() - new_insurance_firm = simulation.build_agents(InsuranceFirm, - 'insurancefirm', - parameters=simulation_parameters, - agent_parameters=parameters) + new_insurance_firm = simulation.build_agents( + InsuranceFirm, + "insurancefirm", + parameters=simulation_parameters, + agent_parameters=parameters, + ) insurancefirms_group += new_insurance_firm if isleconfig.use_abce: # TODO: fix abce # may fail in abce because addressing individual agents may not be allowed # may also fail because agent methods may not be callable directly - new_insurancefirm_pointer = [new_insurance_firm[0].get_pointer()] # index 0 because this is a list with just 1 object + new_insurancefirm_pointer = [ + new_insurance_firm[0].get_pointer() + ] # index 0 because this is a list with just 1 object else: new_insurancefirm_pointer = new_insurance_firm - world.accept_agents("insurancefirm", new_insurancefirm_pointer, new_insurance_firm, time=t) - + world.accept_agents( + "insurancefirm", new_insurancefirm_pointer, new_insurance_firm, time=t + ) + if world.insurance_firm_market_entry(agent_type="ReinsuranceFirm"): parameters = [np.random.choice(world.agent_parameters["reinsurance"])] parameters[0]["id"] = world.get_unique_reinsurer_id() - new_reinsurance_firm = simulation.build_agents(ReinsuranceFirm, - 'reinsurance', - parameters=simulation_parameters, - agent_parameters=parameters) + new_reinsurance_firm = simulation.build_agents( + ReinsuranceFirm, + "reinsurance", + parameters=simulation_parameters, + agent_parameters=parameters, + ) reinsurancefirms_group += new_reinsurance_firm if isleconfig.use_abce: # TODO: fix abce # may fail in abce because addressing individual agents may not be allowed # may also fail because agent methods may not be callable directly - new_reinsurancefirm_pointer = [new_reinsurance_firm[0].get_pointer()] # index 0 because this is a list with just 1 object + new_reinsurancefirm_pointer = [ + new_reinsurance_firm[0].get_pointer() + ] # index 0 because this is a list with just 1 object else: new_reinsurancefirm_pointer = new_reinsurance_firm - world.accept_agents("reinsurance", new_reinsurancefirm_pointer, new_reinsurance_firm, time=t) - + world.accept_agents( + "reinsurance", new_reinsurancefirm_pointer, new_reinsurance_firm, time=t + ) + # iterate simulation world.iterate(t) - + # log data if isleconfig.use_abce: - #insurancefirms.logme() - #reinsurancefirms.logme() - insurancefirms_group.agg_log(variables=['cash', 'operational'], len=['underwritten_contracts']) - #reinsurancefirms_group.agg_log(variables=['cash']) + # insurancefirms.logme() + # reinsurancefirms.logme() + insurancefirms_group.agg_log( + variables=["cash", "operational"], len=["underwritten_contracts"] + ) + # reinsurancefirms_group.agg_log(variables=['cash']) else: world.save_data() - - if t > 0 and t//50 == t/50: + + if t > 0 and t // 50 == t / 50: save_simulation(t, simulation, simulation_parameters, exit_now=False) - #print("here") - + # print("here") + # finish simulation, write logs simulation.finalize() @@ -209,12 +251,15 @@ def save_simulation(t, sim, sim_param, exit_now=False): pickle.dump(d, wfile, protocol=pickle.HIGHEST_PROTOCOL) with open("data/simulation_resave.pkl", "br") as rfile: file_contents = rfile.read() - #print("\nSimulation hashes: ", hashlib.sha512(str(d).encode()).hexdigest(), "; ", hashlib.sha512(str(file_contents).encode()).hexdigest()) - # note that the hash over the dict is for some reason not identical between runs. The hash over the state saved to the file is. - print("\nSimulation hash: ", hashlib.sha512(str(file_contents).encode()).hexdigest()) + # print("\nSimulation hashes: ", hashlib.sha512(str(d).encode()).hexdigest(), "; ", hashlib.sha512(str(file_contents).encode()).hexdigest()) + # note that the hash over the dict is for some reason not identical between runs. The hash over the state saved to the file is. + print( + "\nSimulation hash: ", hashlib.sha512(str(file_contents).encode()).hexdigest() + ) if exit_now: exit(0) - + + # main entry point if __name__ == "__main__": main() diff --git a/riskmodel.py b/riskmodel.py index 4769032..22d183a 100644 --- a/riskmodel.py +++ b/riskmodel.py @@ -1,4 +1,3 @@ - import math import numpy as np import sys, pdb @@ -7,10 +6,21 @@ from distributionreinsurance import ReinsuranceDistWrapper -class RiskModel(): - def __init__(self, damage_distribution, expire_immediately, cat_separation_distribution, norm_premium, \ - category_number, init_average_exposure, init_average_risk_factor, init_profit_estimate, \ - margin_of_safety, var_tail_prob, inaccuracy): +class RiskModel: + def __init__( + self, + damage_distribution, + expire_immediately, + cat_separation_distribution, + norm_premium, + category_number, + init_average_exposure, + init_average_risk_factor, + init_profit_estimate, + margin_of_safety, + var_tail_prob, + inaccuracy, + ): self.cat_separation_distribution = cat_separation_distribution self.norm_premium = norm_premium self.var_tail_prob = 0.02 @@ -22,67 +32,69 @@ def __init__(self, damage_distribution, expire_immediately, cat_separation_distr self.margin_of_safety = margin_of_safety """damage_distribution is some scipy frozen rv distribution wich is bound between 0 and 1 and indicates the share of risks suffering damage as part of any single catastrophic peril""" - self.damage_distribution = [damage_distribution for _ in range(self.category_number)] # TODO: separate that category wise? -> DONE. - self.damage_distribution_stack = [[] for _ in range(self.category_number)] - self.reinsurance_contract_stack = [[] for _ in range(self.category_number)] - #self.inaccuracy = np.random.uniform(9/10., 10/9., size=self.category_number) + self.damage_distribution = [ + damage_distribution for _ in range(self.category_number) + ] # TODO: separate that category wise? -> DONE. + self.damage_distribution_stack = [[] for _ in range(self.category_number)] + self.reinsurance_contract_stack = [[] for _ in range(self.category_number)] + # self.inaccuracy = np.random.uniform(9/10., 10/9., size=self.category_number) self.inaccuracy = inaccuracy - + def getPPF(self, categ_id, tailSize): """Method for getting quantile function of the damage distribution (value at risk) by category. Positional arguments: categ_id integer: category tailSize (float >=0, <=1): quantile Returns value-at-risk.""" - return self.damage_distribution[categ_id].ppf(1-tailSize) + return self.damage_distribution[categ_id].ppf(1 - tailSize) def get_categ_risks(self, risks, categ_id): - #categ_risks2 = [risk for risk in risks if risk["category"]==categ_id] + # categ_risks2 = [risk for risk in risks if risk["category"]==categ_id] categ_risks = [] for risk in risks: - if risk["category"]==categ_id: + if risk["category"] == categ_id: categ_risks.append(risk) - #assert categ_risks == categ_risks2 + # assert categ_risks == categ_risks2 return categ_risks - def compute_expectation(self, categ_risks, categ_id): #TODO: more intuitive name? - #average_exposure2 = np.mean([risk["excess"]-risk["deductible"] for risk in categ_risks]) + def compute_expectation(self, categ_risks, categ_id): # TODO: more intuitive name? + # average_exposure2 = np.mean([risk["excess"]-risk["deductible"] for risk in categ_risks]) # ##average_risk_factor = np.mean([risk["risk_factor"] for risk in categ_risks]) - #average_risk_factor2 = self.inaccuracy[categ_id] * np.mean([risk["risk_factor"] for risk in categ_risks]) + # average_risk_factor2 = self.inaccuracy[categ_id] * np.mean([risk["risk_factor"] for risk in categ_risks]) # ## compute expected profits from category - #mean_runtime2 = np.mean([risk["runtime"] for risk in categ_risks]) - + # mean_runtime2 = np.mean([risk["runtime"] for risk in categ_risks]) + exposures = [] risk_factors = [] runtimes = [] for risk in categ_risks: # TODO: factor in excess instead of value? - exposures.append(risk["value"]-risk["deductible"]) + exposures.append(risk["value"] - risk["deductible"]) risk_factors.append(risk["risk_factor"]) runtimes.append(risk["runtime"]) average_exposure = np.mean(exposures) average_risk_factor = self.inaccuracy[categ_id] * np.mean(risk_factors) mean_runtime = np.mean(runtimes) - #assert average_exposure == average_exposure2 - #assert average_risk_factor == average_risk_factor2 - #assert mean_runtime == mean_runtime2 - + # assert average_exposure == average_exposure2 + # assert average_risk_factor == average_risk_factor2 + # assert mean_runtime == mean_runtime2 + if self.expire_immediately: incr_expected_profits = -1 # TODO: fix the norm_premium estimation - #incr_expected_profits = (self.norm_premium - (1 - scipy.stats.poisson(1 / self.cat_separation_distribution.mean() * \ + # incr_expected_profits = (self.norm_premium - (1 - scipy.stats.poisson(1 / self.cat_separation_distribution.mean() * \ # mean_runtime).pmf(0)) * self.damage_distribution[categ_id].mean() * average_risk_factor) * average_exposure * len(categ_risks) else: incr_expected_profits = -1 # TODO: expected profits should only be returned once the expire_immediately == False case is fixed - #incr_expected_profits = (self.norm_premium - mean_runtime / self.cat_separation_distribution[categ_id].mean() * self.damage_distribution.mean() * average_risk_factor) * average_exposure * len(categ_risks) - + # incr_expected_profits = (self.norm_premium - mean_runtime / self.cat_separation_distribution[categ_id].mean() * self.damage_distribution.mean() * average_risk_factor) * average_exposure * len(categ_risks) + return average_risk_factor, average_exposure, incr_expected_profits - + def evaluate_proportional(self, risks, cash): - + assert len(cash) == self.category_number # prepare variables @@ -91,42 +103,62 @@ def evaluate_proportional(self, risks, cash): cash_left_by_category = np.copy(cash) expected_profits = 0 necessary_liquidity = 0 - + var_per_risk_per_categ = np.zeros(self.category_number) - + # compute acceptable risks by category for categ_id in range(self.category_number): - # compute number of acceptable risks of this category - + # compute number of acceptable risks of this category + categ_risks = self.get_categ_risks(risks=risks, categ_id=categ_id) - #categ_risks = [risk for risk in risks if risk["category"]==categ_id] - + # categ_risks = [risk for risk in risks if risk["category"]==categ_id] + if len(categ_risks) > 0: - average_risk_factor, average_exposure, incr_expected_profits = self.compute_expectation(categ_risks=categ_risks, categ_id=categ_id) + average_risk_factor, average_exposure, incr_expected_profits = self.compute_expectation( + categ_risks=categ_risks, categ_id=categ_id + ) else: average_risk_factor = self.init_average_risk_factor average_exposure = self.init_average_exposure incr_expected_profits = -1 # TODO: expected profits should only be returned once the expire_immediately == False case is fixed - #incr_expected_profits = 0 + # incr_expected_profits = 0 expected_profits += incr_expected_profits - + # compute value at risk - var_per_risk = self.getPPF(categ_id=categ_id, tailSize=self.var_tail_prob) * average_risk_factor * average_exposure * self.margin_of_safety - + var_per_risk = ( + self.getPPF(categ_id=categ_id, tailSize=self.var_tail_prob) + * average_risk_factor + * average_exposure + * self.margin_of_safety + ) + # record liquidity requirement and apply margin of safety for liquidity requirement - necessary_liquidity += var_per_risk * self.margin_of_safety * len(categ_risks) - #print("RISKMODEL: ", self.getPPF(categ_id=categ_id, tailSize=0.01) * average_risk_factor * average_exposure, " = PPF(0.01) * ", average_risk_factor, " * ", average_exposure, " vs. cash: ", cash[categ_id], "TOTAL_RISK_IN_CATEG: ", self.getPPF(categ_id=categ_id, tailSize=0.01) * average_risk_factor * average_exposure * len(categ_risks)) + necessary_liquidity += ( + var_per_risk * self.margin_of_safety * len(categ_risks) + ) + # print("RISKMODEL: ", self.getPPF(categ_id=categ_id, tailSize=0.01) * average_risk_factor * average_exposure, " = PPF(0.01) * ", average_risk_factor, " * ", average_exposure, " vs. cash: ", cash[categ_id], "TOTAL_RISK_IN_CATEG: ", self.getPPF(categ_id=categ_id, tailSize=0.01) * average_risk_factor * average_exposure * len(categ_risks)) if isleconfig.verbose: print(self.inaccuracy) - print("RISKMODEL: ", var_per_risk, " = PPF(0.02) * ", average_risk_factor, " * ", average_exposure, " vs. cash: ", cash[categ_id], "TOTAL_RISK_IN_CATEG: ", var_per_risk * len(categ_risks)) - #print("RISKMODEL: ", self.getPPF(categ_id=categ_id, tailSize=0.05) * average_risk_factor * average_exposure, " = PPF(0.05) * ", average_risk_factor, " * ", average_exposure, " vs. cash: ", cash[categ_id], "TOTAL_RISK_IN_CATEG: ", self.getPPF(categ_id=categ_id, tailSize=0.05) * average_risk_factor * average_exposure * len(categ_risks)) - #print("RISKMODEL: ", self.getPPF(categ_id=categ_id, tailSize=0.1) * average_risk_factor * average_exposure, " = PPF(0.1) * ", average_risk_factor, " * ", average_exposure, " vs. cash: ", cash[categ_id], "TOTAL_RISK_IN_CATEG: ", self.getPPF(categ_id=categ_id, tailSize=0.1) * average_risk_factor * average_exposure * len(categ_risks)) - #print("RISKMODEL: ", self.getPPF(categ_id=categ_id, tailSize=0.25) * average_risk_factor * average_exposure, " = PPF(0.25) * ", average_risk_factor, " * ", average_exposure, " vs. cash: ", cash[categ_id], "TOTAL_RISK_IN_CATEG: ", self.getPPF(categ_id=categ_id, tailSize=0.25) * average_risk_factor * average_exposure * len(categ_risks)) - #print("RISKMODEL: ", self.getPPF(categ_id=categ_id, tailSize=0.5) * average_risk_factor * average_exposure, " = PPF(0.5) * ", average_risk_factor, " * ", average_exposure, " vs. cash: ", cash[categ_id], "TOTAL_RISK_IN_CATEG: ", self.getPPF(categ_id=categ_id, tailSize=0.5) * average_risk_factor * average_exposure * len(categ_risks)) - #if cash[categ_id] < 0: + print( + "RISKMODEL: ", + var_per_risk, + " = PPF(0.02) * ", + average_risk_factor, + " * ", + average_exposure, + " vs. cash: ", + cash[categ_id], + "TOTAL_RISK_IN_CATEG: ", + var_per_risk * len(categ_risks), + ) + # print("RISKMODEL: ", self.getPPF(categ_id=categ_id, tailSize=0.05) * average_risk_factor * average_exposure, " = PPF(0.05) * ", average_risk_factor, " * ", average_exposure, " vs. cash: ", cash[categ_id], "TOTAL_RISK_IN_CATEG: ", self.getPPF(categ_id=categ_id, tailSize=0.05) * average_risk_factor * average_exposure * len(categ_risks)) + # print("RISKMODEL: ", self.getPPF(categ_id=categ_id, tailSize=0.1) * average_risk_factor * average_exposure, " = PPF(0.1) * ", average_risk_factor, " * ", average_exposure, " vs. cash: ", cash[categ_id], "TOTAL_RISK_IN_CATEG: ", self.getPPF(categ_id=categ_id, tailSize=0.1) * average_risk_factor * average_exposure * len(categ_risks)) + # print("RISKMODEL: ", self.getPPF(categ_id=categ_id, tailSize=0.25) * average_risk_factor * average_exposure, " = PPF(0.25) * ", average_risk_factor, " * ", average_exposure, " vs. cash: ", cash[categ_id], "TOTAL_RISK_IN_CATEG: ", self.getPPF(categ_id=categ_id, tailSize=0.25) * average_risk_factor * average_exposure * len(categ_risks)) + # print("RISKMODEL: ", self.getPPF(categ_id=categ_id, tailSize=0.5) * average_risk_factor * average_exposure, " = PPF(0.5) * ", average_risk_factor, " * ", average_exposure, " vs. cash: ", cash[categ_id], "TOTAL_RISK_IN_CATEG: ", self.getPPF(categ_id=categ_id, tailSize=0.5) * average_risk_factor * average_exposure * len(categ_risks)) + # if cash[categ_id] < 0: # pdb.set_trace() try: acceptable = int(math.floor(cash[categ_id] / var_per_risk)) @@ -149,24 +181,34 @@ def evaluate_proportional(self, risks, cash): expected_profits = self.init_profit_estimate * cash[0] else: expected_profits /= necessary_liquidity - + max_cash_by_categ = max(cash_left_by_category) floored_cash_by_categ = cash_left_by_category.copy() floored_cash_by_categ[floored_cash_by_categ < 0] = 0 remaining_acceptable_by_category_old = remaining_acceptable_by_category.copy() for categ_id in range(self.category_number): remaining_acceptable_by_category[categ_id] = math.floor( - remaining_acceptable_by_category[categ_id] * pow( - floored_cash_by_categ[categ_id] / max_cash_by_categ, 5)) + remaining_acceptable_by_category[categ_id] + * pow(floored_cash_by_categ[categ_id] / max_cash_by_categ, 5) + ) if isleconfig.verbose: - print("RISKMODEL returns: ", expected_profits, remaining_acceptable_by_category) - return expected_profits, remaining_acceptable_by_category, cash_left_by_category, var_per_risk_per_categ + print( + "RISKMODEL returns: ", + expected_profits, + remaining_acceptable_by_category, + ) + return ( + expected_profits, + remaining_acceptable_by_category, + cash_left_by_category, + var_per_risk_per_categ, + ) + + def evaluate_excess_of_loss(self, risks, cash, offered_risk=None): - def evaluate_excess_of_loss(self, risks, cash, offered_risk = None): - cash_left_by_categ = np.copy(cash) assert len(cash_left_by_categ) == self.category_number - + # prepare variables additional_required = np.zeros(self.category_number) additional_var_per_categ = np.zeros(self.category_number) @@ -174,40 +216,60 @@ def evaluate_excess_of_loss(self, risks, cash, offered_risk = None): # values at risk and liquidity requirements by category for categ_id in range(self.category_number): categ_risks = self.get_categ_risks(risks=risks, categ_id=categ_id) - + # TODO: allow for different risk distributions for different categories # TODO: factor in risk_factors - percentage_value_at_risk = self.getPPF(categ_id=categ_id, tailSize=self.var_tail_prob) - + percentage_value_at_risk = self.getPPF( + categ_id=categ_id, tailSize=self.var_tail_prob + ) + # compute liquidity requirements from existing contracts for risk in categ_risks: - expected_damage = percentage_value_at_risk * risk["value"] * risk["risk_factor"] \ - * self.inaccuracy[categ_id] - expected_claim = min(expected_damage, risk["excess"]) - risk["deductible"] - + expected_damage = ( + percentage_value_at_risk + * risk["value"] + * risk["risk_factor"] + * self.inaccuracy[categ_id] + ) + expected_claim = ( + min(expected_damage, risk["excess"]) - risk["deductible"] + ) + # record liquidity requirement and apply margin of safety for liquidity requirement cash_left_by_categ[categ_id] -= expected_claim * self.margin_of_safety - + # compute additional liquidity requirements from newly offered contract - if (offered_risk is not None) and (offered_risk.get("category") == categ_id): - expected_damage_fraction = percentage_value_at_risk * offered_risk["risk_factor"] \ - * self.inaccuracy[categ_id] - expected_claim_fraction = min(expected_damage_fraction, offered_risk["excess_fraction"]) - offered_risk["deductible_fraction"] + if (offered_risk is not None) and ( + offered_risk.get("category") == categ_id + ): + expected_damage_fraction = ( + percentage_value_at_risk + * offered_risk["risk_factor"] + * self.inaccuracy[categ_id] + ) + expected_claim_fraction = ( + min(expected_damage_fraction, offered_risk["excess_fraction"]) + - offered_risk["deductible_fraction"] + ) expected_claim_total = expected_claim_fraction * offered_risk["value"] - + # record liquidity requirement and apply margin of safety for liquidity requirement - additional_required[categ_id] += expected_claim_total * self.margin_of_safety + additional_required[categ_id] += ( + expected_claim_total * self.margin_of_safety + ) additional_var_per_categ[categ_id] += expected_claim_total - + # Additional value at risk should only occur in one category. Assert that this is the case. - assert sum(additional_var_per_categ > 0) <= 1 + assert sum(additional_var_per_categ > 0) <= 1 var_this_risk = max(additional_var_per_categ) - + return cash_left_by_categ, additional_required, var_this_risk def evaluate(self, risks, cash, offered_risk=None): # ensure that any risk to be considered supplied directly as argument is non-proportional/excess-of-loss - assert (offered_risk is None) or offered_risk.get("insurancetype") == "excess-of-loss" + assert (offered_risk is None) or offered_risk.get( + "insurancetype" + ) == "excess-of-loss" # construct cash_left_by_categ as a sequence, defining remaining liquidity by category if not isinstance(cash, (np.ndarray, list)): @@ -217,33 +279,60 @@ def evaluate(self, risks, cash, offered_risk=None): assert len(cash_left_by_categ) == self.category_number # sort current contracts - el_risks = [risk for risk in risks if risk["insurancetype"] == 'excess-of-loss'] - risks = [risk for risk in risks if risk["insurancetype"] == 'proportional'] + el_risks = [risk for risk in risks if risk["insurancetype"] == "excess-of-loss"] + risks = [risk for risk in risks if risk["insurancetype"] == "proportional"] # compute liquidity requirements and acceptable risks from existing contract if (offered_risk is not None) or (len(el_risks) > 0): - cash_left_by_categ, additional_required, var_this_risk = self.evaluate_excess_of_loss(el_risks, cash_left_by_categ, offered_risk) + cash_left_by_categ, additional_required, var_this_risk = self.evaluate_excess_of_loss( + el_risks, cash_left_by_categ, offered_risk + ) if (offered_risk is None) or (len(risks) > 0): - expected_profits_proportional, remaining_acceptable_by_categ, cash_left_by_categ, var_per_risk_per_categ = self.evaluate_proportional(risks, cash_left_by_categ) + expected_profits_proportional, remaining_acceptable_by_categ, cash_left_by_categ, var_per_risk_per_categ = self.evaluate_proportional( + risks, cash_left_by_categ + ) if offered_risk is None: # return numbers of remaining acceptable risks by category - return expected_profits_proportional, remaining_acceptable_by_categ, cash_left_by_categ, var_per_risk_per_categ, min(cash_left_by_categ) + return ( + expected_profits_proportional, + remaining_acceptable_by_categ, + cash_left_by_categ, + var_per_risk_per_categ, + min(cash_left_by_categ), + ) else: # return boolean value whether the offered excess_of_loss risk can be accepted if isleconfig.verbose: - print("REINSURANCE RISKMODEL", cash, cash_left_by_categ,(cash_left_by_categ - additional_required > 0).all()) + print( + "REINSURANCE RISKMODEL", + cash, + cash_left_by_categ, + (cash_left_by_categ - additional_required > 0).all(), + ) # if not (cash_left_by_categ - additional_required > 0).all(): # pdb.set_trace() - return (cash_left_by_categ - additional_required > 0).all(), cash_left_by_categ, var_this_risk, min(cash_left_by_categ) + return ( + (cash_left_by_categ - additional_required > 0).all(), + cash_left_by_categ, + var_this_risk, + min(cash_left_by_categ), + ) def add_reinsurance(self, categ_id, excess_fraction, deductible_fraction, contract): - self.damage_distribution_stack[categ_id].append(self.damage_distribution[categ_id]) + self.damage_distribution_stack[categ_id].append( + self.damage_distribution[categ_id] + ) self.reinsurance_contract_stack[categ_id].append(contract) - self.damage_distribution[categ_id] = ReinsuranceDistWrapper(lower_bound=deductible_fraction, \ - upper_bound=excess_fraction, \ - dist=self.damage_distribution[categ_id]) + self.damage_distribution[categ_id] = ReinsuranceDistWrapper( + lower_bound=deductible_fraction, + upper_bound=excess_fraction, + dist=self.damage_distribution[categ_id], + ) - def delete_reinsurance(self, categ_id, excess_fraction, deductible_fraction, contract): + def delete_reinsurance( + self, categ_id, excess_fraction, deductible_fraction, contract + ): assert self.reinsurance_contract_stack[categ_id][-1] == contract self.reinsurance_contract_stack[categ_id].pop() - self.damage_distribution[categ_id] = self.damage_distribution_stack[categ_id].pop() - + self.damage_distribution[categ_id] = self.damage_distribution_stack[ + categ_id + ].pop() diff --git a/setup.py b/setup.py index 65a9fdf..519817f 100644 --- a/setup.py +++ b/setup.py @@ -22,8 +22,8 @@ import isleconfig from distributiontruncated import TruncatedDistWrapper -class SetupSim(): +class SetupSim: def __init__(self): self.simulation_parameters = isleconfig.simulation_parameters @@ -32,11 +32,16 @@ def __init__(self): self.max_time = self.simulation_parameters["max_time"] self.no_categories = self.simulation_parameters["no_categories"] - """set distribution""" - self.non_truncated = scipy.stats.pareto(b=2, loc=0, scale=0.25) #It is assumed that the damages of the catastrophes are drawn from a truncated Pareto distribution. - self.damage_distribution = TruncatedDistWrapper(lower_bound=0.25, upper_bound=1., dist=self.non_truncated) - self.cat_separation_distribution = scipy.stats.expon(0, self.simulation_parameters["event_time_mean_separation"]) #It is assumed that the time between catastrophes is exponentially distributed. + self.non_truncated = scipy.stats.pareto( + b=2, loc=0, scale=0.25 + ) # It is assumed that the damages of the catastrophes are drawn from a truncated Pareto distribution. + self.damage_distribution = TruncatedDistWrapper( + lower_bound=0.25, upper_bound=1.0, dist=self.non_truncated + ) + self.cat_separation_distribution = scipy.stats.expon( + 0, self.simulation_parameters["event_time_mean_separation"] + ) # It is assumed that the time between catastrophes is exponentially distributed. """"random seeds""" self.np_seed = [] @@ -44,19 +49,33 @@ def __init__(self): self.general_rc_event_schedule = [] self.general_rc_event_damage = [] - def schedule(self, replications): #This method returns the lists of schedule times and damages for an ensemble of replications of the model. The argument (replications) is the number of replications. + def schedule( + self, replications + ): # This method returns the lists of schedule times and damages for an ensemble of replications of the model. The argument (replications) is the number of replications. - general_rc_event_schedule = [] #In this list will be stored the lists of schedule times of catastrophes for an ensemble of simulations of the model. ([[Schedule 1], [Schedule 2], [Schedule 3],...,[Schedule N]]) - general_rc_event_damage = [] #In this list will be stored the lists of schedule damages of catastrophes for an ensemble of simulations of the model. ([[Schedule 1], [Schedule 2], [Schedule 3],...,[Schedule N]]) + general_rc_event_schedule = ( + [] + ) # In this list will be stored the lists of schedule times of catastrophes for an ensemble of simulations of the model. ([[Schedule 1], [Schedule 2], [Schedule 3],...,[Schedule N]]) + general_rc_event_damage = ( + [] + ) # In this list will be stored the lists of schedule damages of catastrophes for an ensemble of simulations of the model. ([[Schedule 1], [Schedule 2], [Schedule 3],...,[Schedule N]]) for i in range(replications): - rc_event_schedule = [] #In this list will be stored the lists of times when there will be catastrophes for every category of the model during a single run. ([[times for C1],[times for C2],[times for C3],[times for C4]]) - rc_event_damage = [] #In this list will be stored the lists of catastrophe damages for every category of the model during a single run. ([[damages for C1],[damages for C2],[damages for C3],[damages for C4]]) + rc_event_schedule = ( + [] + ) # In this list will be stored the lists of times when there will be catastrophes for every category of the model during a single run. ([[times for C1],[times for C2],[times for C3],[times for C4]]) + rc_event_damage = ( + [] + ) # In this list will be stored the lists of catastrophe damages for every category of the model during a single run. ([[damages for C1],[damages for C2],[damages for C3],[damages for C4]]) for j in range(self.no_categories): - event_schedule = [] #In this list will be stored the times when there will be a catastrophe related to a particular category. - event_damage = [] #In this list will be stored the damages of a catastrophe related to a particular category. + event_schedule = ( + [] + ) # In this list will be stored the times when there will be a catastrophe related to a particular category. + event_damage = ( + [] + ) # In this list will be stored the damages of a catastrophe related to a particular category. total = 0 - while (total < self.max_time): + while total < self.max_time: separation_time = self.cat_separation_distribution.rvs() total += int(math.ceil(separation_time)) if total < self.max_time: @@ -70,18 +89,21 @@ def schedule(self, replications): #This method returns the lists of schedule ti return self.general_rc_event_schedule, self.general_rc_event_damage - def seeds(self, replications): #This method returns the seeds required for an ensemble of replications of the model. The argument (replications) is the number of replications. + def seeds( + self, replications + ): # This method returns the seeds required for an ensemble of replications of the model. The argument (replications) is the number of replications. """draw random variates for random seeds""" for i in range(replications): - np_seed, random_seed = scipy.stats.randint.rvs(0, 2**32 - 1, size=2) + np_seed, random_seed = scipy.stats.randint.rvs(0, 2 ** 32 - 1, size=2) self.np_seed.append(np_seed) self.random_seed.append(random_seed) return self.np_seed, self.random_seed - - def store(self, replications): #This method stores in a file the the schedules and random seeds required for an ensemble of replications of the model. The argument (replications) is the number of replications. - #With the information stored it is possible to replicate the entire behavior of the ensemble at a later time. + def store( + self, replications + ): # This method stores in a file the the schedules and random seeds required for an ensemble of replications of the model. The argument (replications) is the number of replications. + # With the information stored it is possible to replicate the entire behavior of the ensemble at a later time. event_schedules = [] for i in range(replications): @@ -97,7 +119,9 @@ def store(self, replications): #This method stores in a file the the schedules """ ensure that logging directory exists""" if not os.path.isdir("data"): - assert not os.path.exists("data"), "./data exists as regular file. This filename is required for the logging and event schedule directory" + assert not os.path.exists( + "data" + ), "./data exists as regular file. This filename is required for the logging and event schedule directory" os.makedirs("data") """Save as both pickle and txt""" @@ -106,21 +130,29 @@ def store(self, replications): #This method stores in a file the the schedules with open("./data/risk_event_schedules.txt", "w") as wfile: for rep_schedule in event_schedules: - wfile.write(str(rep_schedule).replace("\n","").replace("array", "np.array").replace("uint32", "np.uint32") + "\n") - - - def obtain_ensemble(self, replications): #This method returns all the information (schedules and seeds) required to run an ensemble of simulations of the model. Since it also stores the information in a file it will be possible to replicate the ensemble at a later time. The argument (replications) is the number of replications. - #This method will be called either form ensemble.py or start.py - [general_rc_event_schedule, general_rc_event_damage] = self.schedule(replications) + wfile.write( + str(rep_schedule) + .replace("\n", "") + .replace("array", "np.array") + .replace("uint32", "np.uint32") + + "\n" + ) + + def obtain_ensemble( + self, replications + ): # This method returns all the information (schedules and seeds) required to run an ensemble of simulations of the model. Since it also stores the information in a file it will be possible to replicate the ensemble at a later time. The argument (replications) is the number of replications. + # This method will be called either form ensemble.py or start.py + [general_rc_event_schedule, general_rc_event_damage] = self.schedule( + replications + ) [np_seeds, random_seeds] = self.seeds(replications) self.store(replications) - return general_rc_event_schedule, general_rc_event_damage, np_seeds, random_seeds - - - - - - + return ( + general_rc_event_schedule, + general_rc_event_damage, + np_seeds, + random_seeds, + ) diff --git a/start.py b/start.py index b75e0a9..0460d8f 100644 --- a/start.py +++ b/start.py @@ -24,10 +24,12 @@ from reinsurancefirm import ReinsuranceFirm import logger import calibrationscore - + # ensure that logging directory exists if not os.path.isdir("data"): - assert not os.path.exists("data"), "./data exists as regular file. This filename is required for the logging directory" + assert not os.path.exists( + "data" + ), "./data exists as regular file. This filename is required for the logging directory" os.makedirs("data") # create conditional decorator @@ -36,119 +38,169 @@ def wrapper(target_function): if not condition: return target_function return decorator_function(target_function) + return wrapper + # create non-abce placeholder gui decorator # TODO: replace this with more elegant solution if possible. Currently required since script will otherwise crash at the conditional decorator below since gui is then undefined if not isleconfig.use_abce: + def gui(*args, **kwargs): pass # main function -#@gui(simulation_parameters, serve=True) +# @gui(simulation_parameters, serve=True) @conditionally(gui(simulation_parameters, serve=False), isleconfig.use_abce) -def main(simulation_parameters, rc_event_schedule, rc_event_damage, np_seed, random_seed, save_iter, requested_logs=None): +def main( + simulation_parameters, + rc_event_schedule, + rc_event_damage, + np_seed, + random_seed, + save_iter, + requested_logs=None, +): np.random.seed(np_seed) random.seed(random_seed) # create simulation and world objects (identical in non-abce mode) if isleconfig.use_abce: - simulation = abce.Simulation(processes=1,random_seed = random_seed) + simulation = abce.Simulation(processes=1, random_seed=random_seed) - simulation_parameters['simulation'] = world = InsuranceSimulation(override_no_riskmodels, replic_ID, simulation_parameters, rc_event_schedule, rc_event_damage) + simulation_parameters["simulation"] = world = InsuranceSimulation( + override_no_riskmodels, + replic_ID, + simulation_parameters, + rc_event_schedule, + rc_event_damage, + ) if not isleconfig.use_abce: simulation = world - - # create agents: insurance firms - insurancefirms_group = simulation.build_agents(InsuranceFirm, - 'insurancefirm', - parameters=simulation_parameters, - agent_parameters=world.agent_parameters["insurancefirm"]) - + + # create agents: insurance firms + insurancefirms_group = simulation.build_agents( + InsuranceFirm, + "insurancefirm", + parameters=simulation_parameters, + agent_parameters=world.agent_parameters["insurancefirm"], + ) + if isleconfig.use_abce: insurancefirm_pointers = insurancefirms_group.get_pointer() else: insurancefirm_pointers = insurancefirms_group world.accept_agents("insurancefirm", insurancefirm_pointers, insurancefirms_group) - # create agents: reinsurance firms - reinsurancefirms_group = simulation.build_agents(ReinsuranceFirm, - 'reinsurance', - parameters=simulation_parameters, - agent_parameters=world.agent_parameters["reinsurance"]) + # create agents: reinsurance firms + reinsurancefirms_group = simulation.build_agents( + ReinsuranceFirm, + "reinsurance", + parameters=simulation_parameters, + agent_parameters=world.agent_parameters["reinsurance"], + ) if isleconfig.use_abce: reinsurancefirm_pointers = reinsurancefirms_group.get_pointer() else: reinsurancefirm_pointers = reinsurancefirms_group world.accept_agents("reinsurance", reinsurancefirm_pointers, reinsurancefirms_group) - + # time iteration for t in range(simulation_parameters["max_time"]): - + # abce time step simulation.advance_round(t) - + # create new agents # TODO: write method for this; this code block is executed almost identically 4 times if world.insurance_firm_market_entry(agent_type="InsuranceFirm"): parameters = [np.random.choice(world.agent_parameters["insurancefirm"])] - parameters = [world.agent_parameters["insurancefirm"][simulation.insurance_entry_index()]] + parameters = [ + world.agent_parameters["insurancefirm"][ + simulation.insurance_entry_index() + ] + ] parameters[0]["id"] = world.get_unique_insurer_id() - new_insurance_firm = simulation.build_agents(InsuranceFirm, - 'insurancefirm', - parameters=simulation_parameters, - agent_parameters=parameters) + new_insurance_firm = simulation.build_agents( + InsuranceFirm, + "insurancefirm", + parameters=simulation_parameters, + agent_parameters=parameters, + ) insurancefirms_group += new_insurance_firm if isleconfig.use_abce: # TODO: fix abce # may fail in abce because addressing individual agents may not be allowed # may also fail because agent methods may not be callable directly - new_insurancefirm_pointer = [new_insurance_firm[0].get_pointer()] # index 0 because this is a list with just 1 object + new_insurancefirm_pointer = [ + new_insurance_firm[0].get_pointer() + ] # index 0 because this is a list with just 1 object else: new_insurancefirm_pointer = new_insurance_firm - world.accept_agents("insurancefirm", new_insurancefirm_pointer, new_insurance_firm, time=t) - + world.accept_agents( + "insurancefirm", new_insurancefirm_pointer, new_insurance_firm, time=t + ) + if world.insurance_firm_market_entry(agent_type="ReinsuranceFirm"): parameters = [np.random.choice(world.agent_parameters["reinsurance"])] - parameters[0]["initial_cash"] = world.reinsurance_capital_entry() #Since the value of the reinrisks varies overtime it makes sense that the market entry of reinsures depends on those values. The method world.reinsurance_capital_entry() determines the capital market entry of reinsurers. - parameters = [world.agent_parameters["reinsurance"][simulation.reinsurance_entry_index()]] + parameters[0][ + "initial_cash" + ] = ( + world.reinsurance_capital_entry() + ) # Since the value of the reinrisks varies overtime it makes sense that the market entry of reinsures depends on those values. The method world.reinsurance_capital_entry() determines the capital market entry of reinsurers. + parameters = [ + world.agent_parameters["reinsurance"][ + simulation.reinsurance_entry_index() + ] + ] parameters[0]["id"] = world.get_unique_reinsurer_id() - new_reinsurance_firm = simulation.build_agents(ReinsuranceFirm, - 'reinsurance', - parameters=simulation_parameters, - agent_parameters=parameters) + new_reinsurance_firm = simulation.build_agents( + ReinsuranceFirm, + "reinsurance", + parameters=simulation_parameters, + agent_parameters=parameters, + ) reinsurancefirms_group += new_reinsurance_firm if isleconfig.use_abce: # TODO: fix abce # may fail in abce because addressing individual agents may not be allowed # may also fail because agent methods may not be callable directly - new_reinsurancefirm_pointer = [new_reinsurance_firm[0].get_pointer()] # index 0 because this is a list with just 1 object + new_reinsurancefirm_pointer = [ + new_reinsurance_firm[0].get_pointer() + ] # index 0 because this is a list with just 1 object else: new_reinsurancefirm_pointer = new_reinsurance_firm - world.accept_agents("reinsurance", new_reinsurancefirm_pointer, new_reinsurance_firm, time=t) - + world.accept_agents( + "reinsurance", new_reinsurancefirm_pointer, new_reinsurance_firm, time=t + ) + # iterate simulation world.iterate(t) - + # log data if isleconfig.use_abce: - #insurancefirms.logme() - #reinsurancefirms.logme() - insurancefirms_group.agg_log(variables=['cash', 'operational'], len=['underwritten_contracts']) - #reinsurancefirms_group.agg_log(variables=['cash']) + # insurancefirms.logme() + # reinsurancefirms.logme() + insurancefirms_group.agg_log( + variables=["cash", "operational"], len=["underwritten_contracts"] + ) + # reinsurancefirms_group.agg_log(variables=['cash']) else: world.save_data() - - if t%50 == save_iter: + + if t % 50 == save_iter: save_simulation(t, simulation, simulation_parameters, exit_now=False) - + # finish simulation, write logs simulation.finalize() - return simulation.obtain_log(requested_logs) #It is required to return this list to download all the data generated by a single run of the model from the cloud. + return simulation.obtain_log( + requested_logs + ) # It is required to return this list to download all the data generated by a single run of the model from the cloud. + # save function def save_simulation(t, sim, sim_param, exit_now=False): @@ -162,33 +214,66 @@ def save_simulation(t, sim, sim_param, exit_now=False): pickle.dump(d, wfile, protocol=pickle.HIGHEST_PROTOCOL) with open("data/simulation_save.pkl", "br") as rfile: file_contents = rfile.read() - #print("\nSimulation hashes: ", hashlib.sha512(str(d).encode()).hexdigest(), "; ", hashlib.sha512(str(file_contents).encode()).hexdigest()) + # print("\nSimulation hashes: ", hashlib.sha512(str(d).encode()).hexdigest(), "; ", hashlib.sha512(str(file_contents).encode()).hexdigest()) # note that the hash over the dict is for some reason not identical between runs. The hash over the state saved to the file is. - print("\nSimulation hash: ", hashlib.sha512(str(file_contents).encode()).hexdigest()) + print( + "\nSimulation hash: ", hashlib.sha512(str(file_contents).encode()).hexdigest() + ) if exit_now: exit(0) + # main entry point if __name__ == "__main__": """ use argparse to handle command line arguments""" - parser = argparse.ArgumentParser(description='Model the Insurance sector') + parser = argparse.ArgumentParser(description="Model the Insurance sector") parser.add_argument("--abce", action="store_true", help="use abce") - parser.add_argument("--oneriskmodel", action="store_true", - help="allow overriding the number of riskmodels from the standard config (with 1)") - parser.add_argument("--riskmodels", type=int, choices=[1, 2, 3, 4], - help="allow overriding the number of riskmodels from standard config (with 1 or other numbers)") - parser.add_argument("--replicid", type=int, - help="if replication ID is given, pass this to the simulation so that the risk profile can be restored") - parser.add_argument("--replicating", action="store_true", - help="if this is a simulation run designed to replicate another, override the config file parameter") - parser.add_argument("--randomseed", type=float, help="allow setting of numpy random seed") - parser.add_argument("--foreground", action="store_true", - help="force foreground runs even if replication ID is given (which defaults to background runs)") - parser.add_argument("--shownetwork", action="store_true", help="show reinsurance relations as network") - parser.add_argument("-p", "--showprogress", action="store_true", help="show timesteps") - parser.add_argument("-v", "--verbose", action="store_true", help="more detailed output") - parser.add_argument("--save_iterations", type=int, help="number of iterations to iterate before saving world state") + parser.add_argument( + "--oneriskmodel", + action="store_true", + help="allow overriding the number of riskmodels from the standard config (with 1)", + ) + parser.add_argument( + "--riskmodels", + type=int, + choices=[1, 2, 3, 4], + help="allow overriding the number of riskmodels from standard config (with 1 or other numbers)", + ) + parser.add_argument( + "--replicid", + type=int, + help="if replication ID is given, pass this to the simulation so that the risk profile can be restored", + ) + parser.add_argument( + "--replicating", + action="store_true", + help="if this is a simulation run designed to replicate another, override the config file parameter", + ) + parser.add_argument( + "--randomseed", type=float, help="allow setting of numpy random seed" + ) + parser.add_argument( + "--foreground", + action="store_true", + help="force foreground runs even if replication ID is given (which defaults to background runs)", + ) + parser.add_argument( + "--shownetwork", + action="store_true", + help="show reinsurance relations as network", + ) + parser.add_argument( + "-p", "--showprogress", action="store_true", help="show timesteps" + ) + parser.add_argument( + "-v", "--verbose", action="store_true", help="more detailed output" + ) + parser.add_argument( + "--save_iterations", + type=int, + help="number of iterations to iterate before saving world state", + ) args = parser.parse_args() if args.abce: @@ -198,11 +283,13 @@ def save_simulation(t, sim, sim_param, exit_now=False): override_no_riskmodels = 1 if args.riskmodels: override_no_riskmodels = args.riskmodels - if args.replicid is not None: # TODO: this is broken, must be fixed or removed + if args.replicid is not None: # TODO: this is broken, must be fixed or removed replic_ID = args.replicid if args.replicating: isleconfig.replicating = True - assert replic_ID is not None, "Error: Replication requires a replication ID to identify run to be replicated" + assert ( + replic_ID is not None + ), "Error: Replication requires a replication ID to identify run to be replicated" if args.randomseed: randomseed = args.randomseed seed = int(randomseed) @@ -214,8 +301,9 @@ def save_simulation(t, sim, sim_param, exit_now=False): if args.shownetwork: isleconfig.show_network = True """Option requires reloading of InsuranceSimulation so that modules to show network can be loaded. - # TODO: change all module imports of the form "from module import class" to "import module". """ + # TODO: change all module imports of the form "from module import class" to "import module". """ import insurancesimulation + importlib.reload(insurancesimulation) from insurancesimulation import InsuranceSimulation if args.showprogress: @@ -232,20 +320,36 @@ def save_simulation(t, sim, sim_param, exit_now=False): # print("Importing abce") import abce from abce import gui - + from setup import SetupSim - setup = SetupSim() #Here the setup for the simulation is done. - [general_rc_event_schedule, general_rc_event_damage, np_seeds, random_seeds] = setup.obtain_ensemble(1) #Only one ensemble. This part will only be run locally (laptop). - log = main(simulation_parameters, general_rc_event_schedule[0], general_rc_event_damage[0], np_seeds[0], random_seeds[0], save_iter) - + setup = SetupSim() # Here the setup for the simulation is done. + [ + general_rc_event_schedule, + general_rc_event_damage, + np_seeds, + random_seeds, + ] = setup.obtain_ensemble( + 1 + ) # Only one ensemble. This part will only be run locally (laptop). + + log = main( + simulation_parameters, + general_rc_event_schedule[0], + general_rc_event_damage[0], + np_seeds[0], + random_seeds[0], + save_iter, + ) + """ Restore the log at the end of the single simulation run for saving and for potential further study """ - is_background = (not isleconfig.force_foreground) and (isleconfig.replicating or (replic_ID in locals())) + is_background = (not isleconfig.force_foreground) and ( + isleconfig.replicating or (replic_ID in locals()) + ) L = logger.Logger() L.restore_logger_object(list(log)) L.save_log(is_background) - + """ Obtain calibration score """ CS = calibrationscore.CalibrationScore(L) score = CS.test_all() - diff --git a/visualisation.py b/visualisation.py index 2ce6814..8790d02 100644 --- a/visualisation.py +++ b/visualisation.py @@ -5,9 +5,18 @@ import argparse - class TimeSeries(object): - def __init__(self, series_list, title="",xlabel="Time", colour='k', axlst=None, fig=None, percentiles=None, alpha=0.7): + def __init__( + self, + series_list, + title="", + xlabel="Time", + colour="k", + axlst=None, + fig=None, + percentiles=None, + alpha=0.7, + ): self.series_list = series_list self.size = len(series_list) self.xlabel = xlabel @@ -15,22 +24,32 @@ def __init__(self, series_list, title="",xlabel="Time", colour='k', axlst=None, self.alpha = alpha self.percentiles = percentiles self.title = title - self.timesteps = [t for t in range(len(series_list[0][0]))] # assume all data series are the same size + self.timesteps = [ + t for t in range(len(series_list[0][0])) + ] # assume all data series are the same size if axlst is not None and fig is not None: self.axlst = axlst self.fig = fig else: - self.fig, self.axlst = plt.subplots(self.size,sharex=True) + self.fig, self.axlst = plt.subplots(self.size, sharex=True) - #self.plot() # we create the object when we want the plot so call plot() in the constructor + # self.plot() # we create the object when we want the plot so call plot() in the constructor def plot(self): - for i, (series, series_label, fill_lower, fill_upper) in enumerate(self.series_list): - self.axlst[i].plot(self.timesteps, series,color=self.colour) + for i, (series, series_label, fill_lower, fill_upper) in enumerate( + self.series_list + ): + self.axlst[i].plot(self.timesteps, series, color=self.colour) self.axlst[i].set_ylabel(series_label) if fill_lower is not None and fill_upper is not None: - self.axlst[i].fill_between(self.timesteps, fill_lower, fill_upper, color=self.colour, alpha=self.alpha) - self.axlst[self.size-1].set_xlabel(self.xlabel) + self.axlst[i].fill_between( + self.timesteps, + fill_lower, + fill_upper, + color=self.colour, + alpha=self.alpha, + ) + self.axlst[self.size - 1].set_xlabel(self.xlabel) self.fig.suptitle(self.title) return self.fig, self.axlst @@ -38,20 +57,24 @@ def save(self, filename): self.fig.savefig("{filename}".format(filename=filename)) return + class InsuranceFirmAnimation(object): - '''class takes in a run of insurance data and produces animations ''' + """class takes in a run of insurance data and produces animations """ + def __init__(self, data): self.data = data self.fig, self.ax = plt.subplots() self.stream = self.data_stream() - self.ani = animation.FuncAnimation(self.fig, self.update, repeat=False, interval=100,) - #init_func=self.setup_plot) + self.ani = animation.FuncAnimation( + self.fig, self.update, repeat=False, interval=100 + ) + # init_func=self.setup_plot) def setup_plot(self): # initial drawing of the plot - casharr,idarr = next(self.stream) - self.pie = self.ax.pie(casharr, labels=idarr,autopct='%1.0f%%') - return self.pie, + casharr, idarr = next(self.stream) + self.pie = self.ax.pie(casharr, labels=idarr, autopct="%1.0f%%") + return (self.pie,) def data_stream(self): # unpack data in a format ready for update() @@ -62,59 +85,81 @@ def data_stream(self): if operational: casharr.append(cash) idarr.append(id) - yield casharr,idarr + yield casharr, idarr def update(self, i): # clear plot and redraw self.ax.clear() - self.ax.axis('equal') - casharr,idarr = next(self.stream) - self.pie = self.ax.pie(casharr, labels=idarr,autopct='%1.0f%%') - self.ax.set_title("Timestep : {:,.0f} | Total cash : {:,.0f}".format(i,sum(casharr))) - return self.pie, + self.ax.axis("equal") + casharr, idarr = next(self.stream) + self.pie = self.ax.pie(casharr, labels=idarr, autopct="%1.0f%%") + self.ax.set_title( + "Timestep : {:,.0f} | Total cash : {:,.0f}".format(i, sum(casharr)) + ) + return (self.pie,) - def save(self,filename): - self.ani.save(filename, writer='ffmpeg', dpi=80) + def save(self, filename): + self.ani.save(filename, writer="ffmpeg", dpi=80) def show(self): plt.show() + class visualisation(object): def __init__(self, history_logs_list): self.history_logs_list = history_logs_list # unused data in history_logs - #self.excess_capital = history_logs['total_excess_capital'] - #self.reinexcess_capital = history_logs['total_reinexcess_capital'] - #self.diffvar = history_logs['market_diffvar'] - #self.cumulative_bankruptcies = history_logs['cumulative_bankruptcies'] - #self.cumulative_unrecovered_claims = history_logs['cumulative_unrecovered_claims'] + # self.excess_capital = history_logs['total_excess_capital'] + # self.reinexcess_capital = history_logs['total_reinexcess_capital'] + # self.diffvar = history_logs['market_diffvar'] + # self.cumulative_bankruptcies = history_logs['cumulative_bankruptcies'] + # self.cumulative_unrecovered_claims = history_logs['cumulative_unrecovered_claims'] return def insurer_pie_animation(self, run=0): data = self.history_logs_list[run] - insurance_cash = np.array(data['insurance_firms_cash']) + insurance_cash = np.array(data["insurance_firms_cash"]) self.ins_pie_anim = InsuranceFirmAnimation(insurance_cash) return self.ins_pie_anim def reinsurer_pie_animation(self, run=0): data = self.history_logs_list[run] - reinsurance_cash = np.array(data['reinsurance_firms_cash']) + reinsurance_cash = np.array(data["reinsurance_firms_cash"]) self.reins_pie_anim = InsuranceFirmAnimation(reinsurance_cash) return self.reins_pie_anim - def insurer_time_series(self, runs=None, axlst=None, fig=None, title="Insurer", colour='black', percentiles=[25,75]): + def insurer_time_series( + self, + runs=None, + axlst=None, + fig=None, + title="Insurer", + colour="black", + percentiles=[25, 75], + ): # runs should be a list of the indexes you want included in the ensemble for consideration if runs: data = [self.history_logs_list[x] for x in runs] else: data = self.history_logs_list - + # Take the element-wise means/medians of the ensemble set (axis=0) - contracts_agg = [history_logs['total_contracts'] for history_logs in self.history_logs_list] - profitslosses_agg = [history_logs['total_profitslosses'] for history_logs in self.history_logs_list] - operational_agg = [history_logs['total_operational'] for history_logs in self.history_logs_list] - cash_agg = [history_logs['total_cash'] for history_logs in self.history_logs_list] - premium_agg = [history_logs['market_premium'] for history_logs in self.history_logs_list] + contracts_agg = [ + history_logs["total_contracts"] for history_logs in self.history_logs_list + ] + profitslosses_agg = [ + history_logs["total_profitslosses"] + for history_logs in self.history_logs_list + ] + operational_agg = [ + history_logs["total_operational"] for history_logs in self.history_logs_list + ] + cash_agg = [ + history_logs["total_cash"] for history_logs in self.history_logs_list + ] + premium_agg = [ + history_logs["market_premium"] for history_logs in self.history_logs_list + ] contracts = np.mean(contracts_agg, axis=0) profitslosses = np.mean(profitslosses_agg, axis=0) @@ -122,16 +167,56 @@ def insurer_time_series(self, runs=None, axlst=None, fig=None, title="Insurer", cash = np.median(cash_agg, axis=0) premium = np.median(premium_agg, axis=0) - self.ins_time_series = TimeSeries([ - (contracts, 'Contracts', np.percentile(contracts_agg,percentiles[0], axis=0), np.percentile(contracts_agg, percentiles[1], axis=0)), - (profitslosses, 'Profitslosses', np.percentile(profitslosses_agg,percentiles[0], axis=0), np.percentile(profitslosses_agg, percentiles[1], axis=0)), - (operational, 'Operational', np.percentile(operational_agg,percentiles[0], axis=0), np.percentile(operational_agg, percentiles[1], axis=0)), - (cash, 'Cash', np.percentile(cash_agg,percentiles[0], axis=0), np.percentile(cash_agg, percentiles[1], axis=0)), - (premium, "Premium", np.percentile(premium_agg,percentiles[0], axis=0), np.percentile(premium_agg, percentiles[1], axis=0)), - ],title=title, xlabel = "Time", axlst=axlst, fig=fig, colour=colour).plot() + self.ins_time_series = TimeSeries( + [ + ( + contracts, + "Contracts", + np.percentile(contracts_agg, percentiles[0], axis=0), + np.percentile(contracts_agg, percentiles[1], axis=0), + ), + ( + profitslosses, + "Profitslosses", + np.percentile(profitslosses_agg, percentiles[0], axis=0), + np.percentile(profitslosses_agg, percentiles[1], axis=0), + ), + ( + operational, + "Operational", + np.percentile(operational_agg, percentiles[0], axis=0), + np.percentile(operational_agg, percentiles[1], axis=0), + ), + ( + cash, + "Cash", + np.percentile(cash_agg, percentiles[0], axis=0), + np.percentile(cash_agg, percentiles[1], axis=0), + ), + ( + premium, + "Premium", + np.percentile(premium_agg, percentiles[0], axis=0), + np.percentile(premium_agg, percentiles[1], axis=0), + ), + ], + title=title, + xlabel="Time", + axlst=axlst, + fig=fig, + colour=colour, + ).plot() return self.ins_time_series - def reinsurer_time_series(self, runs=None, axlst=None, fig=None, title="Reinsurer", colour='black', percentiles=[25,75]): + def reinsurer_time_series( + self, + runs=None, + axlst=None, + fig=None, + title="Reinsurer", + colour="black", + percentiles=[25, 75], + ): # runs should be a list of the indexes you want included in the ensemble for consideration if runs: data = [self.history_logs_list[x] for x in runs] @@ -139,11 +224,25 @@ def reinsurer_time_series(self, runs=None, axlst=None, fig=None, title="Reinsure data = self.history_logs_list # Take the element-wise means/medians of the ensemble set (axis=0) - reincontracts_agg = [history_logs['total_reincontracts'] for history_logs in self.history_logs_list] - reinprofitslosses_agg = [history_logs['total_reinprofitslosses'] for history_logs in self.history_logs_list] - reinoperational_agg = [history_logs['total_reinoperational'] for history_logs in self.history_logs_list] - reincash_agg = [history_logs['total_reincash'] for history_logs in self.history_logs_list] - catbonds_number_agg = [history_logs['total_catbondsoperational'] for history_logs in self.history_logs_list] + reincontracts_agg = [ + history_logs["total_reincontracts"] + for history_logs in self.history_logs_list + ] + reinprofitslosses_agg = [ + history_logs["total_reinprofitslosses"] + for history_logs in self.history_logs_list + ] + reinoperational_agg = [ + history_logs["total_reinoperational"] + for history_logs in self.history_logs_list + ] + reincash_agg = [ + history_logs["total_reincash"] for history_logs in self.history_logs_list + ] + catbonds_number_agg = [ + history_logs["total_catbondsoperational"] + for history_logs in self.history_logs_list + ] reincontracts = np.mean(reincontracts_agg, axis=0) reinprofitslosses = np.mean(reinprofitslosses_agg, axis=0) @@ -151,73 +250,169 @@ def reinsurer_time_series(self, runs=None, axlst=None, fig=None, title="Reinsure reincash = np.median(reincash_agg, axis=0) catbonds_number = np.median(catbonds_number_agg, axis=0) - self.reins_time_series = TimeSeries([ - (reincontracts, 'Contracts', np.percentile(reincontracts_agg,percentiles[0], axis=0), np.percentile(reincontracts_agg, percentiles[1], axis=0)), - (reinprofitslosses, 'Profitslosses', np.percentile(reinprofitslosses_agg,percentiles[0], axis=0), np.percentile(reinprofitslosses_agg, percentiles[1], axis=0)), - (reinoperational, 'Operational', np.percentile(reinoperational_agg,percentiles[0], axis=0), np.percentile(reinoperational_agg, percentiles[1], axis=0)), - (reincash, 'Cash', np.percentile(reincash_agg,percentiles[0], axis=0), np.percentile(reincash_agg, percentiles[1], axis=0)), - (catbonds_number, "Activate Cat Bonds", np.percentile(catbonds_number_agg,percentiles[0], axis=0), np.percentile(catbonds_number_agg, percentiles[1], axis=0)), - ],title= title, xlabel = "Time", axlst=axlst, fig=fig, colour=colour).plot() + self.reins_time_series = TimeSeries( + [ + ( + reincontracts, + "Contracts", + np.percentile(reincontracts_agg, percentiles[0], axis=0), + np.percentile(reincontracts_agg, percentiles[1], axis=0), + ), + ( + reinprofitslosses, + "Profitslosses", + np.percentile(reinprofitslosses_agg, percentiles[0], axis=0), + np.percentile(reinprofitslosses_agg, percentiles[1], axis=0), + ), + ( + reinoperational, + "Operational", + np.percentile(reinoperational_agg, percentiles[0], axis=0), + np.percentile(reinoperational_agg, percentiles[1], axis=0), + ), + ( + reincash, + "Cash", + np.percentile(reincash_agg, percentiles[0], axis=0), + np.percentile(reincash_agg, percentiles[1], axis=0), + ), + ( + catbonds_number, + "Activate Cat Bonds", + np.percentile(catbonds_number_agg, percentiles[0], axis=0), + np.percentile(catbonds_number_agg, percentiles[1], axis=0), + ), + ], + title=title, + xlabel="Time", + axlst=axlst, + fig=fig, + colour=colour, + ).plot() return self.reins_time_series def metaplotter_timescale(self): # Take the element-wise means/medians of the ensemble set (axis=0) - contracts = np.mean([history_logs['total_contracts'] for history_logs in self.history_logs_list],axis=0) - profitslosses = np.mean([history_logs['total_profitslosses'] for history_logs in self.history_logs_list],axis=0) - operational = np.median([history_logs['total_operational'] for history_logs in self.history_logs_list],axis=0) - cash = np.median([history_logs['total_cash'] for history_logs in self.history_logs_list],axis=0) - premium = np.median([history_logs['market_premium'] for history_logs in self.history_logs_list],axis=0) - reincontracts = np.mean([history_logs['total_reincontracts'] for history_logs in self.history_logs_list],axis=0) - reinprofitslosses = np.mean([history_logs['total_reinprofitslosses'] for history_logs in self.history_logs_list],axis=0) - reinoperational = np.median([history_logs['total_reinoperational'] for history_logs in self.history_logs_list],axis=0) - reincash = np.median([history_logs['total_reincash'] for history_logs in self.history_logs_list],axis=0) - catbonds_number = np.median([history_logs['total_catbondsoperational'] for history_logs in self.history_logs_list],axis=0) + contracts = np.mean( + [ + history_logs["total_contracts"] + for history_logs in self.history_logs_list + ], + axis=0, + ) + profitslosses = np.mean( + [ + history_logs["total_profitslosses"] + for history_logs in self.history_logs_list + ], + axis=0, + ) + operational = np.median( + [ + history_logs["total_operational"] + for history_logs in self.history_logs_list + ], + axis=0, + ) + cash = np.median( + [history_logs["total_cash"] for history_logs in self.history_logs_list], + axis=0, + ) + premium = np.median( + [history_logs["market_premium"] for history_logs in self.history_logs_list], + axis=0, + ) + reincontracts = np.mean( + [ + history_logs["total_reincontracts"] + for history_logs in self.history_logs_list + ], + axis=0, + ) + reinprofitslosses = np.mean( + [ + history_logs["total_reinprofitslosses"] + for history_logs in self.history_logs_list + ], + axis=0, + ) + reinoperational = np.median( + [ + history_logs["total_reinoperational"] + for history_logs in self.history_logs_list + ], + axis=0, + ) + reincash = np.median( + [history_logs["total_reincash"] for history_logs in self.history_logs_list], + axis=0, + ) + catbonds_number = np.median( + [ + history_logs["total_catbondsoperational"] + for history_logs in self.history_logs_list + ], + axis=0, + ) return def show(self): plt.show() return + class compare_riskmodels(object): - def __init__(self,vis_list, colour_list): + def __init__(self, vis_list, colour_list): # take in list of visualisation objects and call their plot methods self.vis_list = vis_list self.colour_list = colour_list - - def create_insurer_timeseries(self, fig=None, axlst=None, percentiles=[25,75]): + + def create_insurer_timeseries(self, fig=None, axlst=None, percentiles=[25, 75]): # create the time series for each object in turn and superpose them? fig = axlst = None - for vis,colour in zip(self.vis_list, self.colour_list): - (fig, axlst) = vis.insurer_time_series(fig=fig, axlst=axlst, colour=colour, percentiles=percentiles) + for vis, colour in zip(self.vis_list, self.colour_list): + (fig, axlst) = vis.insurer_time_series( + fig=fig, axlst=axlst, colour=colour, percentiles=percentiles + ) - def create_reinsurer_timeseries(self, fig=None, axlst=None, percentiles=[25,75]): + def create_reinsurer_timeseries(self, fig=None, axlst=None, percentiles=[25, 75]): # create the time series for each object in turn and superpose them? fig = axlst = None - for vis,colour in zip(self.vis_list, self.colour_list): - (fig, axlst) = vis.reinsurer_time_series(fig=fig, axlst=axlst, colour=colour, percentiles=percentiles) + for vis, colour in zip(self.vis_list, self.colour_list): + (fig, axlst) = vis.reinsurer_time_series( + fig=fig, axlst=axlst, colour=colour, percentiles=percentiles + ) def show(self): plt.show() + def save(self): # logic to save plots pass - -if __name__ == "__main__": +if __name__ == "__main__": + # use argparse to handle command line arguments - parser = argparse.ArgumentParser(description='Model the Insurance sector') - parser.add_argument("--single", action="store_true", help="plot time series of a single run of the insurance model") - parser.add_argument("--comparison", action="store_true", help="plot the result of an ensemble of replicatons of the insurance model") + parser = argparse.ArgumentParser(description="Model the Insurance sector") + parser.add_argument( + "--single", + action="store_true", + help="plot time series of a single run of the insurance model", + ) + parser.add_argument( + "--comparison", + action="store_true", + help="plot the result of an ensemble of replicatons of the insurance model", + ) args = parser.parse_args() - if args.single: # load in data from the history_logs dictionarywith open("data/history_logs.dat","r") as rfile: - with open("data/history_logs.dat","r") as rfile: - history_logs_list = [eval(k) for k in rfile] # one dict on each line + with open("data/history_logs.dat", "r") as rfile: + history_logs_list = [eval(k) for k in rfile] # one dict on each line # first create visualisation object, then create graph/animation objects as necessary vis = visualisation(history_logs_list) vis.insurer_pie_animation() @@ -227,26 +422,27 @@ def save(self): vis.show() N = len(history_logs_list) - if args.comparison: # for each run, generate an animation and time series for insurer and reinsurer # TODO: provide some way for these to be lined up nicely rather than having to manually arrange screen - #for i in range(N): + # for i in range(N): # vis.insurer_pie_animation(run=i) # vis.insurer_time_series(runs=[i]) # vis.reinsurer_pie_animation(run=i) # vis.reinsurer_time_series(runs=[i]) # vis.show() vis_list = [] - filenames = ["./data/"+x+"_history_logs.dat" for x in ["one","two","three","four"]] + filenames = [ + "./data/" + x + "_history_logs.dat" for x in ["one", "two", "three", "four"] + ] for filename in filenames: - with open(filename,'r') as rfile: - history_logs_list = [eval(k) for k in rfile] # one dict on each line + with open(filename, "r") as rfile: + history_logs_list = [eval(k) for k in rfile] # one dict on each line vis_list.append(visualisation(history_logs_list)) - colour_list = ['blue', 'yellow', 'red', 'green'] + colour_list = ["blue", "yellow", "red", "green"] cmp_rsk = compare_riskmodels(vis_list, colour_list) - cmp_rsk.create_insurer_timeseries(percentiles=[10,90]) - cmp_rsk.create_reinsurer_timeseries(percentiles=[10,90]) + cmp_rsk.create_insurer_timeseries(percentiles=[10, 90]) + cmp_rsk.create_reinsurer_timeseries(percentiles=[10, 90]) cmp_rsk.show() diff --git a/visualization_network.py b/visualization_network.py index 82d9129..3e76ee9 100644 --- a/visualization_network.py +++ b/visualization_network.py @@ -2,64 +2,102 @@ import matplotlib.pyplot as plt import numpy as np -class ReinsuranceNetwork(): + +class ReinsuranceNetwork: def __init__(self, insurancefirms, reinsurancefirms, catbonds): """save entities""" self.insurancefirms = insurancefirms self.reinsurancefirms = reinsurancefirms self.catbonds = catbonds - + """obtain lists of operational entities""" op_entities = {} self.num_entities = {} - for firmtype, firmlist in [("insurers", self.insurancefirms), ("reinsurers", self.reinsurancefirms), ("catbonds", self.catbonds)]: + for firmtype, firmlist in [ + ("insurers", self.insurancefirms), + ("reinsurers", self.reinsurancefirms), + ("catbonds", self.catbonds), + ]: op_firmtype = [firm for firm in firmlist if firm.operational] op_entities[firmtype] = op_firmtype self.num_entities[firmtype] = len(op_firmtype) - - #op_entities_flat = [firm for firm in entities_list for entities_list in op_entities] + + # op_entities_flat = [firm for firm in entities_list for entities_list in op_entities] self.network_size = sum(self.num_entities.values()) - + """create weigthed adjacency matrix""" - weights_matrix = np.zeros(self.network_size**2).reshape(self.network_size, self.network_size) - for idx_to, firm in enumerate(op_entities["insurers"] + op_entities["reinsurers"]): + weights_matrix = np.zeros(self.network_size ** 2).reshape( + self.network_size, self.network_size + ) + for idx_to, firm in enumerate( + op_entities["insurers"] + op_entities["reinsurers"] + ): eolrs = firm.get_excess_of_loss_reinsurance() for eolr in eolrs: - #pdb.set_trace() - idx_from = self.num_entities["insurers"] + (op_entities["reinsurers"] + op_entities["catbonds"]).index(eolr["reinsurer"]) + # pdb.set_trace() + idx_from = self.num_entities["insurers"] + ( + op_entities["reinsurers"] + op_entities["catbonds"] + ).index(eolr["reinsurer"]) weights_matrix[idx_from][idx_to] = eolr["value"] - + """unweighted adjacency matrix""" adj_matrix = np.sign(weights_matrix) - + """define network""" - self.network = nx.from_numpy_array(weights_matrix, create_using=nx.DiGraph()) # weighted - self.network_unweighted = nx.from_numpy_array(adj_matrix, create_using=nx.DiGraph()) # unweighted - + self.network = nx.from_numpy_array( + weights_matrix, create_using=nx.DiGraph() + ) # weighted + self.network_unweighted = nx.from_numpy_array( + adj_matrix, create_using=nx.DiGraph() + ) # unweighted + def compute_measures(self): """obtain measures""" - #degrees = self.network.degree() + # degrees = self.network.degree() degree_distr = dict(self.network.degree()).values() in_degree_distr = dict(self.network.in_degree()).values() out_degree_distr = dict(self.network.out_degree()).values() is_connected = nx.is_weakly_connected(self.network) - #is_connected = nx.is_strongly_connected(self.network) # must always be False + # is_connected = nx.is_strongly_connected(self.network) # must always be False try: node_centralities = nx.eigenvector_centrality(self.network) except: node_centralities = nx.betweenness_centrality(self.network) # TODO: and more, choose more meaningful ones... - - print("Graph is connected: ", is_connected, "\nIn degrees ", in_degree_distr, "\nOut degrees", out_degree_distr, \ - "\nCentralities", node_centralities) - def visualize(self): + print( + "Graph is connected: ", + is_connected, + "\nIn degrees ", + in_degree_distr, + "\nOut degrees", + out_degree_distr, + "\nCentralities", + node_centralities, + ) + + def visualize(self): """visualize""" plt.figure() firmtypes = np.ones(self.network_size) - firmtypes[self.num_entities["insurers"]:self.num_entities["insurers"]+self.num_entities["reinsurers"]] = 0.5 - firmtypes[self.num_entities["insurers"]+self.num_entities["reinsurers"]:] = 1.3 - print(firmtypes, self.num_entities["insurers"], self.num_entities["insurers"] + self.num_entities["reinsurers"]) + firmtypes[ + self.num_entities["insurers"] : self.num_entities["insurers"] + + self.num_entities["reinsurers"] + ] = 0.5 + firmtypes[ + self.num_entities["insurers"] + self.num_entities["reinsurers"] : + ] = 1.3 + print( + firmtypes, + self.num_entities["insurers"], + self.num_entities["insurers"] + self.num_entities["reinsurers"], + ) pos = nx.spring_layout(self.network_unweighted) - nx.draw(self.network_unweighted, pos, node_color=firmtypes, with_labels=True, cmap=plt.cm.winter) + nx.draw( + self.network_unweighted, + pos, + node_color=firmtypes, + with_labels=True, + cmap=plt.cm.winter, + ) plt.show() From 9713c537931273c0ae7b2a9fe02434fc8ffa1917 Mon Sep 17 00:00:00 2001 From: Chris Hughes Date: Tue, 9 Jul 2019 14:22:01 +0100 Subject: [PATCH 020/125] Small change --- catbond.py | 15 ++++++-------- distributionreinsurance.py | 7 ++----- ensemble.py | 16 +++++++-------- insurancecontract.py | 2 -- insurancesimulation.py | 4 ---- logger.py | 2 +- metainsurancecontract.py | 14 ++----------- metainsuranceorg.py | 20 +++++++++---------- metaplotter.py | 6 +++--- metaplotter_pl_timescale.py | 6 +++--- ...lotter_pl_timescale_additional_measures.py | 6 +++--- plotter.py | 1 - reinsurancecontract.py | 4 ++-- resume.py | 9 ++------- riskmodel.py | 5 +++-- setup_simulation.py | 7 ++++--- start.py | 16 ++++++--------- visualisation.py | 1 + 18 files changed, 54 insertions(+), 87 deletions(-) diff --git a/catbond.py b/catbond.py index 1f65c85..d655df8 100644 --- a/catbond.py +++ b/catbond.py @@ -1,12 +1,5 @@ import isleconfig -import numpy as np -import scipy.stats -from insurancecontract import InsuranceContract -from reinsurancecontract import ReinsuranceContract from metainsuranceorg import MetaInsuranceOrg -from riskmodel import RiskModel -import sys, pdb -import uuid class CatBond(MetaInsuranceOrg): @@ -59,9 +52,11 @@ def iterate(self, time): contracts_dissolved = len(maturing) """effect payments from contracts""" - [contract.check_payment_due(time) for contract in self.underwritten_contracts] + for contract in self.underwritten_contracts: + contract.check_payment_due(time) - if self.underwritten_contracts == []: + if not self.underwritten_contracts: + # If there are no contracts left, the bond is matured self.mature_bond() # TODO: mature_bond method should check if operational # TODO: dividend should only be payed according to pre-arranged schedule, @@ -82,6 +77,8 @@ def set_contract(self, contract): self.underwritten_contracts.append(contract) def mature_bond(self): + # QUERY: does this ever run when self.operational == False? + # assert self.operational obligation = { "amount": self.cash, "recipient": self.simulation, diff --git a/distributionreinsurance.py b/distributionreinsurance.py index beeff7f..61b425c 100644 --- a/distributionreinsurance.py +++ b/distributionreinsurance.py @@ -1,9 +1,6 @@ -import scipy.stats -import numpy as np -from math import ceil -import scipy -import pdb import functools +import numpy as np +import scipy.stats class ReinsuranceDistWrapper: diff --git a/ensemble.py b/ensemble.py index bc5fe90..c0998ff 100644 --- a/ensemble.py +++ b/ensemble.py @@ -2,18 +2,16 @@ # It can be run locally if no argument is passed when called from the terminal. # It can be run in the cloud if it is passed as argument the server that will be used. import sys -import random -import os -import math + import copy -import scipy.stats -import start -import logger -import listify +import os +from sandman2.api import operation, Session + import isleconfig -from distributiontruncated import TruncatedDistWrapper +import listify +import logger +import start from setup_simulation import SetupSim -from sandman2.api import operation, Session @operation diff --git a/insurancecontract.py b/insurancecontract.py index cbb6b75..bc996e6 100644 --- a/insurancecontract.py +++ b/insurancecontract.py @@ -1,5 +1,3 @@ -import numpy as np - from metainsurancecontract import MetaInsuranceContract diff --git a/insurancesimulation.py b/insurancesimulation.py index 1e7579b..bb49768 100644 --- a/insurancesimulation.py +++ b/insurancesimulation.py @@ -1,7 +1,3 @@ -from insurancefirm import InsuranceFirm - -# from riskmodel import RiskModel -from reinsurancefirm import ReinsuranceFirm from distributiontruncated import TruncatedDistWrapper import numpy as np import scipy.stats diff --git a/logger.py b/logger.py index ddb7472..b07d3ad 100644 --- a/logger.py +++ b/logger.py @@ -1,7 +1,7 @@ """Logging class. Handles records of a single simulation run. Can save and reload. """ import numpy as np -import pdb + import listify LOG_DEFAULT = ( diff --git a/metainsurancecontract.py b/metainsurancecontract.py index 68cb984..3870a08 100644 --- a/metainsurancecontract.py +++ b/metainsurancecontract.py @@ -1,7 +1,3 @@ -import numpy as np -import sys, pdb - - class MetaInsuranceContract: def __init__( self, @@ -60,10 +56,7 @@ def __init__( self.initial_VaR = initial_var # set deductible from argument, risk property or default value, whichever first is not None default_deductible_fraction = 0.0 - if deductible_fraction is not None: - self.deductible_fraction = deductible_fraction - else: - self.deductible_fraction = properties.get( + self.deductible_fraction = deductible_fraction if deductible_fraction is not None else properties.get( "deductible_fraction", default_deductible_fraction ) @@ -71,10 +64,7 @@ def __init__( # set excess from argument, risk property or default value, whichever first is not None default_excess_fraction = 1.0 - if excess_fraction is not None: - self.excess_fraction = excess_fraction - else: - self.excess_fraction = properties.get( + self.excess_fraction = excess_fraction if excess_fraction is not None else properties.get( "excess_fraction", default_excess_fraction ) diff --git a/metainsuranceorg.py b/metainsuranceorg.py index abdfcee..62808f4 100644 --- a/metainsuranceorg.py +++ b/metainsuranceorg.py @@ -6,8 +6,6 @@ from insurancecontract import InsuranceContract from reinsurancecontract import ReinsuranceContract from riskmodel import RiskModel -import sys, pdb -import uuid import functools @@ -425,7 +423,8 @@ def effect_payments(self, time): self.obligations = [ item for item in self.obligations if item["due_time"] > time ] - # TODO: could this cause a firm to enter illiquidity if it has obligations to non-operational firms? + # TODO: could this cause a firm to enter illiquidity if it has obligations to non-operational firms? Such + # firms can't recieve payment, so this possibly shouldn't happen. sum_due = sum([item["amount"] for item in due]) if sum_due > self.cash: self.obligations += due @@ -609,20 +608,19 @@ def balanced_portfolio(self, risk, cash_left_by_categ, var_per_risk): total_cash_reserved_by_categ_post = sum(cash_reserved_by_categ_store) - if (std_post * total_cash_reserved_by_categ_post / self.cash) <= ( - self.balance_ratio * mean + # Doing a < b*c is about 10% faster than a/c < b + if (std_post * total_cash_reserved_by_categ_post) <= ( + self.balance_ratio * mean * self.cash ) or std_post < std_pre: # The new risk is accepted if the standard deviation is reduced or the cash reserved by category is very # well balanced. (std_post) <= (self.balance_ratio * mean) - for i in range(len(cash_left_by_categ)): - # The balance condition is not taken into account if the cash reserve is far away from the limit. - # (total_cash_employed_by_categ_post/self.cash <<< 1) - cash_left_by_categ[i] = self.cash - cash_reserved_by_categ_store[i] + # The balance condition is not taken into account if the cash reserve is far away from the limit. + # (total_cash_employed_by_categ_post/self.cash <<< 1) + cash_left_by_categ = self.cash - cash_reserved_by_categ_store return True, cash_left_by_categ else: - for i in range(len(cash_left_by_categ)): - cash_left_by_categ[i] = self.cash - cash_reserved_by_categ[i] + cash_left_by_categ = self.cash - cash_reserved_by_categ return False, cash_left_by_categ diff --git a/metaplotter.py b/metaplotter.py index 858987e..40a6050 100644 --- a/metaplotter.py +++ b/metaplotter.py @@ -1,9 +1,9 @@ +import time + +import glob import matplotlib.pyplot as plt import numpy as np -import pdb import os -import time -import glob def read_data(): diff --git a/metaplotter_pl_timescale.py b/metaplotter_pl_timescale.py index f172949..48eb650 100644 --- a/metaplotter_pl_timescale.py +++ b/metaplotter_pl_timescale.py @@ -1,9 +1,9 @@ +import time + +import glob import matplotlib.pyplot as plt import numpy as np -import pdb import os -import time -import glob def read_data(): diff --git a/metaplotter_pl_timescale_additional_measures.py b/metaplotter_pl_timescale_additional_measures.py index 5b35274..01098ca 100644 --- a/metaplotter_pl_timescale_additional_measures.py +++ b/metaplotter_pl_timescale_additional_measures.py @@ -1,9 +1,9 @@ +import time + +import glob import matplotlib.pyplot as plt import numpy as np -import pdb import os -import time -import glob def read_data(): diff --git a/plotter.py b/plotter.py index fd348d2..7ccba97 100755 --- a/plotter.py +++ b/plotter.py @@ -1,5 +1,4 @@ import matplotlib.pyplot as plt -import numpy as np rfile = open("data/history_logs.dat", "r") diff --git a/reinsurancecontract.py b/reinsurancecontract.py index 8060477..959fc21 100644 --- a/reinsurancecontract.py +++ b/reinsurancecontract.py @@ -1,5 +1,3 @@ -import numpy as np - from metainsurancecontract import MetaInsuranceContract @@ -78,6 +76,8 @@ def explode(self, time, damage_extent=None): self.insurer.receive_obligation( claim, self.property_holder, time + 1, "claim" ) + else: + raise ValueError(f"Unexpected insurance type {self.insurancetype}") # Reinsurer pays as soon as possible. self.insurer.register_claim( claim diff --git a/resume.py b/resume.py index 2d066e3..c63acc2 100644 --- a/resume.py +++ b/resume.py @@ -1,11 +1,8 @@ # import common packages -import numpy as np -import scipy.stats -import math -import sys, pdb import argparse -import pickle import hashlib +import numpy as np +import pickle import random # import config file and apply configuration @@ -80,9 +77,7 @@ # import isle modules -from insurancesimulation import InsuranceSimulation from insurancefirm import InsuranceFirm -from riskmodel import RiskModel from reinsurancefirm import ReinsuranceFirm diff --git a/riskmodel.py b/riskmodel.py index 33a4451..851092d 100644 --- a/riskmodel.py +++ b/riskmodel.py @@ -1,7 +1,7 @@ import math + import numpy as np -import sys, pdb -import scipy.stats + import isleconfig from distributionreinsurance import ReinsuranceDistWrapper @@ -271,6 +271,7 @@ def evaluate(self, risks, cash, offered_risk=None): el_risks = [risk for risk in risks if risk["insurancetype"] == "excess-of-loss"] risks = [risk for risk in risks if risk["insurancetype"] == "proportional"] # compute liquidity requirements and acceptable risks from existing contract + # TODO: Consider edge cases here if (offered_risk is not None) or (len(el_risks) > 0): cash_left_by_categ, additional_required, var_this_risk = self.evaluate_excess_of_loss( el_risks, cash_left_by_categ, offered_risk diff --git a/setup_simulation.py b/setup_simulation.py index e10f237..69ed1f3 100644 --- a/setup_simulation.py +++ b/setup_simulation.py @@ -14,11 +14,12 @@ random.random.seed(d["np_seed"]) """ -import argparse -import scipy.stats -import pickle import math + import os +import pickle +import scipy.stats + import isleconfig from distributiontruncated import TruncatedDistWrapper diff --git a/start.py b/start.py index 8474a9f..7416c71 100644 --- a/start.py +++ b/start.py @@ -1,22 +1,18 @@ # import common packages -import numpy as np -import scipy.stats -import math -import sys, pdb import argparse +import hashlib +import numpy as np import os import pickle -import hashlib import random +import calibrationscore +import insurancefirm +import insurancesimulation # import config file and apply configuration import isleconfig - -import insurancesimulation -import insurancefirm -import reinsurancefirm import logger -import calibrationscore +import reinsurancefirm simulation_parameters = isleconfig.simulation_parameters filepath = None diff --git a/visualisation.py b/visualisation.py index 5235f5f..da6a75f 100644 --- a/visualisation.py +++ b/visualisation.py @@ -31,6 +31,7 @@ def __init__( self.axlst = axlst self.fig = fig else: + # noinspection PyTypeChecker self.fig, self.axlst = plt.subplots(self.size, sharex=True) # self.plot() # we create the object when we want the plot so call plot() in the constructor From 5c0f021002ffc3429b2087af023f4412161a51c5 Mon Sep 17 00:00:00 2001 From: KloskaT Date: Tue, 9 Jul 2019 16:36:43 +0100 Subject: [PATCH 021/125] Added docstring to all methods. General clean up. --- riskmodel.py | 139 ++++++++++++++++++++++++++++++++++----------------- 1 file changed, 94 insertions(+), 45 deletions(-) diff --git a/riskmodel.py b/riskmodel.py index 8ad0c69..ed259c0 100644 --- a/riskmodel.py +++ b/riskmodel.py @@ -1,4 +1,3 @@ - import math import numpy as np import sys, pdb @@ -7,10 +6,12 @@ from distributionreinsurance import ReinsuranceDistWrapper -class RiskModel(): +class RiskModel: def __init__(self, damage_distribution, expire_immediately, cat_separation_distribution, norm_premium, \ category_number, init_average_exposure, init_average_risk_factor, init_profit_estimate, \ margin_of_safety, var_tail_prob, inaccuracy): + """Initialising method for RiskModel class. All accepted arguments are initialised in the __init__ method of + insurancesimulation, and are mostly from isleconfig.py. """ self.cat_separation_distribution = cat_separation_distribution self.norm_premium = norm_premium self.var_tail_prob = 0.02 @@ -22,7 +23,7 @@ def __init__(self, damage_distribution, expire_immediately, cat_separation_distr self.margin_of_safety = margin_of_safety """damage_distribution is some scipy frozen rv distribution which is bound between 0 and 1 and indicates the share of risks suffering damage as part of any single catastrophic peril""" - self.damage_distribution = [damage_distribution for _ in range(self.category_number)] # TODO: separate that category wise? -> DONE. + self.damage_distribution = [damage_distribution for _ in range(self.category_number)] self.damage_distribution_stack = [[] for _ in range(self.category_number)] self.reinsurance_contract_stack = [[] for _ in range(self.category_number)] self.inaccuracy = inaccuracy @@ -31,28 +32,30 @@ def getPPF(self, categ_id, tailSize): """Method for getting quantile function of the damage distribution (value at risk) by category. Positional arguments: categ_id integer: category - tailSize (float >=0, <=1): quantile + tailSize (float 1=>x=>0): quantile Returns value-at-risk.""" return self.damage_distribution[categ_id].ppf(1-tailSize) def get_categ_risks(self, risks, categ_id): - #categ_risks2 = [risk for risk in risks if risk["category"]==categ_id] - categ_risks = [] - for risk in risks: - if risk["category"]==categ_id: - categ_risks.append(risk) - #assert categ_risks == categ_risks2 + """Method takes list of all risks and only returns a list of all the risks belonging to the given category. + Accepts: + risks: Type List of DataDicts + categ_id: Type Integer. + Returns: + categ_risks: Type List of DataDicts.""" + categ_risks = [risk for risk in risks if risk["category"] == categ_id] return categ_risks def compute_expectation(self, categ_risks, categ_id): #TODO: more intuitive name? - #average_exposure2 = np.mean([risk["excess"]-risk["deductible"] for risk in categ_risks]) - # - ##average_risk_factor = np.mean([risk["risk_factor"] for risk in categ_risks]) - #average_risk_factor2 = self.inaccuracy[categ_id] * np.mean([risk["risk_factor"] for risk in categ_risks]) - # - ## compute expected profits from category - #mean_runtime2 = np.mean([risk["runtime"] for risk in categ_risks]) - + """Method to compute the average exposure and risk factor as well as the increase in expected profits for the + risks in a given category. + Accepts: + categ_risks: Type List of DataDicts. + categ_id: Type Integer. + Returns: + average_risk_factor: Type Decimal. + average_exposure: Type Decimal. Mean risk factor in given category multiplied by inaccuracy. + incr_expected_profits: Type Decimal (currently only returns -1)""" exposures = [] risk_factors = [] runtimes = [] @@ -64,9 +67,6 @@ def compute_expectation(self, categ_risks, categ_id): #TODO: more intuitive average_exposure = np.mean(exposures) average_risk_factor = self.inaccuracy[categ_id] * np.mean(risk_factors) mean_runtime = np.mean(runtimes) - #assert average_exposure == average_exposure2 - #assert average_risk_factor == average_risk_factor2 - #assert mean_runtime == mean_runtime2 if self.expire_immediately: incr_expected_profits = -1 @@ -81,7 +81,18 @@ def compute_expectation(self, categ_risks, categ_id): #TODO: more intuitive return average_risk_factor, average_exposure, incr_expected_profits def evaluate_proportional(self, risks, cash): - + """Method to evaluate proportional type risks. + Accepts: + risks: Type List of DataDicts. + cash: Type List. Gives cash available for each category. + Returns: + expected_profits: Type Decimal (Currently returns None) + remaining_acceptable_by_category: Type List of Integers. Number of risks that would not be covered by firms cash. + cash_left_by_category: Type List of Integers. Firms expected cash left if underwriting the risks from that category. + var_per_risk_per_categ: List of Integers. Average VaR per category. + This method iterates through the risks in each category and calculates the average VaR, how many could be + underwritten according to their average VaR, how much cash would be left per category if all risks were + underwritten at average VaR, and the total expected profit (currently always None).""" assert len(cash) == self.category_number # prepare variables @@ -95,20 +106,16 @@ def evaluate_proportional(self, risks, cash): # compute acceptable risks by category for categ_id in range(self.category_number): - # compute number of acceptable risks of this category - + # compute number of acceptable risks of this category categ_risks = self.get_categ_risks(risks=risks, categ_id=categ_id) - #categ_risks = [risk for risk in risks if risk["category"]==categ_id] if len(categ_risks) > 0: average_risk_factor, average_exposure, incr_expected_profits = self.compute_expectation(categ_risks=categ_risks, categ_id=categ_id) else: average_risk_factor = self.init_average_risk_factor average_exposure = self.init_average_exposure - incr_expected_profits = -1 # TODO: expected profits should only be returned once the expire_immediately == False case is fixed - #incr_expected_profits = 0 expected_profits += incr_expected_profits @@ -117,16 +124,9 @@ def evaluate_proportional(self, risks, cash): # record liquidity requirement and apply margin of safety for liquidity requirement necessary_liquidity += var_per_risk * self.margin_of_safety * len(categ_risks) - #print("RISKMODEL: ", self.getPPF(categ_id=categ_id, tailSize=0.01) * average_risk_factor * average_exposure, " = PPF(0.01) * ", average_risk_factor, " * ", average_exposure, " vs. cash: ", cash[categ_id], "TOTAL_RISK_IN_CATEG: ", self.getPPF(categ_id=categ_id, tailSize=0.01) * average_risk_factor * average_exposure * len(categ_risks)) if isleconfig.verbose: print(self.inaccuracy) print("RISKMODEL: ", var_per_risk, " = PPF(0.02) * ", average_risk_factor, " * ", average_exposure, " vs. cash: ", cash[categ_id], "TOTAL_RISK_IN_CATEG: ", var_per_risk * len(categ_risks)) - #print("RISKMODEL: ", self.getPPF(categ_id=categ_id, tailSize=0.05) * average_risk_factor * average_exposure, " = PPF(0.05) * ", average_risk_factor, " * ", average_exposure, " vs. cash: ", cash[categ_id], "TOTAL_RISK_IN_CATEG: ", self.getPPF(categ_id=categ_id, tailSize=0.05) * average_risk_factor * average_exposure * len(categ_risks)) - #print("RISKMODEL: ", self.getPPF(categ_id=categ_id, tailSize=0.1) * average_risk_factor * average_exposure, " = PPF(0.1) * ", average_risk_factor, " * ", average_exposure, " vs. cash: ", cash[categ_id], "TOTAL_RISK_IN_CATEG: ", self.getPPF(categ_id=categ_id, tailSize=0.1) * average_risk_factor * average_exposure * len(categ_risks)) - #print("RISKMODEL: ", self.getPPF(categ_id=categ_id, tailSize=0.25) * average_risk_factor * average_exposure, " = PPF(0.25) * ", average_risk_factor, " * ", average_exposure, " vs. cash: ", cash[categ_id], "TOTAL_RISK_IN_CATEG: ", self.getPPF(categ_id=categ_id, tailSize=0.25) * average_risk_factor * average_exposure * len(categ_risks)) - #print("RISKMODEL: ", self.getPPF(categ_id=categ_id, tailSize=0.5) * average_risk_factor * average_exposure, " = PPF(0.5) * ", average_risk_factor, " * ", average_exposure, " vs. cash: ", cash[categ_id], "TOTAL_RISK_IN_CATEG: ", self.getPPF(categ_id=categ_id, tailSize=0.5) * average_risk_factor * average_exposure * len(categ_risks)) - #if cash[categ_id] < 0: - # pdb.set_trace() try: acceptable = int(math.floor(cash[categ_id] / var_per_risk)) remaining = acceptable - len(categ_risks) @@ -152,17 +152,28 @@ def evaluate_proportional(self, risks, cash): max_cash_by_categ = max(cash_left_by_category) floored_cash_by_categ = cash_left_by_category.copy() floored_cash_by_categ[floored_cash_by_categ < 0] = 0 - remaining_acceptable_by_category_old = remaining_acceptable_by_category.copy() for categ_id in range(self.category_number): - remaining_acceptable_by_category[categ_id] = math.floor( - remaining_acceptable_by_category[categ_id] * pow( - floored_cash_by_categ[categ_id] / max_cash_by_categ, 5)) + remaining_acceptable_by_category[categ_id] = math.floor(remaining_acceptable_by_category[categ_id] * pow( + floored_cash_by_categ[categ_id] / max_cash_by_categ, 5)) if isleconfig.verbose: print("RISKMODEL returns: ", expected_profits, remaining_acceptable_by_category) + return expected_profits, remaining_acceptable_by_category, cash_left_by_category, var_per_risk_per_categ - def evaluate_excess_of_loss(self, risks, cash, offered_risk = None): - + def evaluate_excess_of_loss(self, risks, cash, offered_risk=None): + """Method to evaluate excess-of-loss type risks. + Accepts: + risks: Type List of DataDicts. + cash: Type List. Gives cash available for each category. + offered risk: Type DataDict + Returns: + additional_required: Type List of Decimals. Capital required to cover offered risks potential claim + (including margin of safety) per category. Only one will be non-zero. + cash_left_by_category: Type List of Decimals. Cash left per category if all risks claimed. + var_this_risk: Type Decimal. Expected claim of offered risk. + This method iterates through the risks in each category and calculates the cash left in each category if + each underwritten contract were to be claimed at expected values. The additional cash required to cover the + offered risk (if applicable) is then calculated (should only be one).""" cash_left_by_categ = np.copy(cash) assert len(cash_left_by_categ) == self.category_number @@ -175,13 +186,11 @@ def evaluate_excess_of_loss(self, risks, cash, offered_risk = None): categ_risks = self.get_categ_risks(risks=risks, categ_id=categ_id) # TODO: allow for different risk distributions for different categories - # TODO: factor in risk_factors percentage_value_at_risk = self.getPPF(categ_id=categ_id, tailSize=self.var_tail_prob) # compute liquidity requirements from existing contracts for risk in categ_risks: - expected_damage = percentage_value_at_risk * risk["value"] * risk["risk_factor"] \ - * self.inaccuracy[categ_id] + expected_damage = percentage_value_at_risk * risk["value"] * risk["risk_factor"] * self.inaccuracy[categ_id] expected_claim = min(expected_damage, risk["excess"]) - risk["deductible"] # record liquidity requirement and apply margin of safety for liquidity requirement @@ -189,8 +198,7 @@ def evaluate_excess_of_loss(self, risks, cash, offered_risk = None): # compute additional liquidity requirements from newly offered contract if (offered_risk is not None) and (offered_risk.get("category") == categ_id): - expected_damage_fraction = percentage_value_at_risk * offered_risk["risk_factor"] \ - * self.inaccuracy[categ_id] + expected_damage_fraction = percentage_value_at_risk * offered_risk["risk_factor"] * self.inaccuracy[categ_id] expected_claim_fraction = min(expected_damage_fraction, offered_risk["excess_fraction"]) - offered_risk["deductible_fraction"] expected_claim_total = expected_claim_fraction * offered_risk["value"] @@ -205,6 +213,29 @@ def evaluate_excess_of_loss(self, risks, cash, offered_risk = None): return cash_left_by_categ, additional_required, var_this_risk def evaluate(self, risks, cash, offered_risk=None): + """Method to evaluate given risks and the offered risk. + Accepts: + risks: List of DataDicts. + cash: Type Decimal. + Optional: + offered_risk: Type DataDict or defaults to None. + (offered_risk = None) Returns: + expected_profits_proportional: Type Decimal (Currently returns None) + remaining_acceptable_by_categ: Type List of Integers. Number of risks that would not be covered by firms cash. + cash_left_by_categ: Type List of Integers. Firms expected cash left if underwriting the risks from that category. + var_per_risk_per_categ: List of Integers. Average VaR per category + min(cash_left_by_categ): Type Decimal. Minimum + (offered_risk != None) Returns: + (cash_left_by_categ - additional_required > 0).all(): Type Boolean. Returns True only if all categories have + enough to cover the additional capital to insure risk. + cash_left_by_categ: Type List of Decimals. Cash left per category if all risks claimed. + var_this_risk: Type Decimal. Expected claim of offered risk. + min(cash_left_by_categ): Type Decimal. Minimum value of cash left in a category after covering all expected claims. + This method organises all risks by insurance type then delegates then to respective methods + (evaluate_prop/evaluate_excess_of_loss). Excess of loss risks are processed one at a time and are admitted using + the offered_risk argument, whereas proportional risks are processed all at once leaving offered_risk = 0. This + results in two sets of return values being used. These return values are what is used to determine if risks are + underwritten or not.""" # ensure that any risk to be considered supplied directly as argument is non-proportional/excess-of-loss assert (offered_risk is None) or offered_risk.get("insurancetype") == "excess-of-loss" @@ -236,6 +267,15 @@ def evaluate(self, risks, cash, offered_risk=None): return (cash_left_by_categ - additional_required > 0).all(), cash_left_by_categ, var_this_risk, min(cash_left_by_categ) def add_reinsurance(self, categ_id, excess_fraction, deductible_fraction, contract): + """Method to add any instance of reinsurance to risk models list of reinsurance contracts, and add damage + distribution to stack of damage distributions per category, then replace with a new distribution. Only used in + thad add_reinsurance method of insurancefirm. + Accepts: + categ_id: Type Integer. + excess_fraction: Type Decimal. + deductible_fraction: Type Decimal. + contract: Type DataDict. + No return values.""" self.damage_distribution_stack[categ_id].append(self.damage_distribution[categ_id]) self.reinsurance_contract_stack[categ_id].append(contract) self.damage_distribution[categ_id] = ReinsuranceDistWrapper(lower_bound=deductible_fraction, \ @@ -243,6 +283,15 @@ def add_reinsurance(self, categ_id, excess_fraction, deductible_fraction, contra dist=self.damage_distribution[categ_id]) def delete_reinsurance(self, categ_id, excess_fraction, deductible_fraction, contract): + """Method to remove any instance of reinsurance to risk models list of reinsurance contracts, and remove its + damage distribution from the stack of damage distributions per category. Only used in the delete_reinsurance + method of insurancefirm. + Accepts: + categ_id: Type Integer. + excess_fraction: Type Decimal. + deductible_fraction: Type Decimal. + contract: Type DataDict. + No return values.""" assert self.reinsurance_contract_stack[categ_id][-1] == contract self.reinsurance_contract_stack[categ_id].pop() self.damage_distribution[categ_id] = self.damage_distribution_stack[categ_id].pop() From 697e7f633a51dea880d43800a513f6b04cfe9f86 Mon Sep 17 00:00:00 2001 From: Chris Hughes Date: Tue, 9 Jul 2019 16:45:32 +0100 Subject: [PATCH 022/125] Small change --- metainsuranceorg.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/metainsuranceorg.py b/metainsuranceorg.py index 62808f4..3eeb594 100644 --- a/metainsuranceorg.py +++ b/metainsuranceorg.py @@ -573,7 +573,9 @@ def balanced_portfolio(self, risk, cash_left_by_categ, var_per_risk): _, std_pre = get_mean_std(tuple(cash_reserved_by_categ)) - cash_reserved_by_categ_store = np.copy(cash_reserved_by_categ) + # For some reason just recreating the array is faster than copying it + # cash_reserved_by_categ_store = np.copy(cash_reserved_by_categ) + cash_reserved_by_categ_store = np.array(cash_reserved_by_categ) if risk.get("insurancetype") == "excess-of-loss": percentage_value_at_risk = self.riskmodel.get_ppf( From a346ab8e2b73fd8bbc43160303179b5bd027802f Mon Sep 17 00:00:00 2001 From: Chris Hughes Date: Tue, 9 Jul 2019 17:05:21 +0100 Subject: [PATCH 023/125] Bugfix (I think) and small improvement (bigger for more risk categories) --- metainsuranceorg.py | 4 ++-- riskmodel.py | 20 +++++++++----------- 2 files changed, 11 insertions(+), 13 deletions(-) diff --git a/metainsuranceorg.py b/metainsuranceorg.py index 3eeb594..5d580cc 100644 --- a/metainsuranceorg.py +++ b/metainsuranceorg.py @@ -550,9 +550,9 @@ def risks_reinrisks_organizer(self, new_risks): # This method organizes the new risks received by the insurer (or reinsurer) # This method organizes the new risks received by category in the nested list "risks_per_categ". - risks_per_categ = [[]] * self.simulation_parameters["no_categories"] + risks_per_categ = [[] for _ in range(self.simulation_parameters["no_categories"])] # This method also counts the new risks received by category in the list "number_risks_categ". - number_risks_categ = [0] * self.simulation_parameters["no_categories"] + number_risks_categ = [0 for _ in range(self.simulation_parameters["no_categories"])] for categ_id in range(self.simulation_parameters["no_categories"]): risks_per_categ[categ_id] = [ diff --git a/riskmodel.py b/riskmodel.py index 851092d..0f95608 100644 --- a/riskmodel.py +++ b/riskmodel.py @@ -49,11 +49,11 @@ def get_ppf(self, categ_id, tail_size): Returns value-at-risk.""" return self.damage_distribution[categ_id].ppf(1 - tail_size) - @staticmethod - def get_categ_risks(risks, categ_id): - # List comprehension is slightly faster than repeated appending - categ_risks = [risk for risk in risks if risk["category"] == categ_id] - return categ_risks + def get_risks_by_categ(self, risks): + risks_by_categ = [[] for _ in range(self.category_number)] + for risk in risks: + risks_by_categ[risk["category"]].append(risk) + return risks_by_categ def compute_expectation(self, categ_risks, categ_id): # TODO: more intuitive name? @@ -94,14 +94,11 @@ def evaluate_proportional(self, risks, cash): necessary_liquidity = 0 var_per_risk_per_categ = np.zeros(self.category_number) - + risks_by_categ = self.get_risks_by_categ(risks) # compute acceptable risks by category for categ_id in range(self.category_number): # compute number of acceptable risks of this category - - categ_risks = self.get_categ_risks(risks=risks, categ_id=categ_id) - # categ_risks = [risk for risk in risks if risk["category"]==categ_id] - + categ_risks = risks_by_categ[categ_id] if len(categ_risks) > 0: average_risk_factor, average_exposure, incr_expected_profits = self.compute_expectation( categ_risks=categ_risks, categ_id=categ_id @@ -202,9 +199,10 @@ def evaluate_excess_of_loss(self, risks, cash, offered_risk=None): additional_required = np.zeros(self.category_number) additional_var_per_categ = np.zeros(self.category_number) + risks_by_categ = self.get_risks_by_categ(risks) # values at risk and liquidity requirements by category for categ_id in range(self.category_number): - categ_risks = self.get_categ_risks(risks=risks, categ_id=categ_id) + categ_risks = risks_by_categ[categ_id] # TODO: allow for different risk distributions for different categories # TODO: factor in risk_factors From 7e52a5cc3b7b4ab2cc8fa7fe5732714acd4d65fe Mon Sep 17 00:00:00 2001 From: KloskaT Date: Wed, 10 Jul 2019 14:41:07 +0100 Subject: [PATCH 024/125] Added method to allow continually updating network in same figure with the category each reinsurance firm is insuring on the edge labels. --- insurancesimulation.py | 19 +++---- visualization_network.py | 119 +++++++++++++++++++++++++++------------ 2 files changed, 92 insertions(+), 46 deletions(-) diff --git a/insurancesimulation.py b/insurancesimulation.py index e7cb4bc..51a5d67 100644 --- a/insurancesimulation.py +++ b/insurancesimulation.py @@ -1,6 +1,6 @@ from insurancefirm import InsuranceFirm -# from riskmodel import RiskModel from reinsurancefirm import ReinsuranceFirm +from centralbank import CentralBank from distributiontruncated import TruncatedDistWrapper import numpy as np import scipy.stats @@ -71,6 +71,7 @@ def __init__(self, override_no_riskmodels, replic_ID, simulation_parameters, rc_ "Set up monetary system (should instead be with the customers, if customers are modeled explicitly)" self.money_supply = self.simulation_parameters["money_supply"] self.obligations = [] + self.bank = CentralBank() "Set up risk categories" self.riskcategories = list(range(self.simulation_parameters["no_categories"])) @@ -214,7 +215,6 @@ def accept_agents(self, agent_class_string, agents, agent_group=None, time=0): time: Integer type, not used Returns: None""" - # TODO: fix agent id's for late entrants (both firms and catbonds) if agent_class_string == "insurancefirm": try: self.insurancefirms += agents @@ -222,7 +222,6 @@ def accept_agents(self, agent_class_string, agents, agent_group=None, time=0): except: print(sys.exc_info()) pdb.set_trace() - # fix self.history_logs['individual_contracts'] list for agent in agents: self.logger.add_insurance_agent() # remove new agent cash from simulation cash to ensure stock flow consistency @@ -336,10 +335,12 @@ def iterate(self, t): if reinsurer.riskmodel.inaccuracy == self.inaccuracy[i]: self.reinsurance_models_counter[i] += 1 - if isleconfig.show_network and t % 40 == 0 and t > 0: - RN = visualization_network.ReinsuranceNetwork(self.insurancefirms, self.reinsurancefirms, self.catbonds) - RN.compute_measures() - RN.visualize() + network_division = 2 # How often network is updated. + if isleconfig.show_network and t % network_division == 0 and t > 0: + if t == network_division: + self.RN = visualization_network.ReinsuranceNetwork() # Only creates once instance so only one figure. + self.RN.update(self.insurancefirms, self.reinsurancefirms, self.catbonds) + self.RN.visualize() def save_data(self): """Method to collect statistics about the current state of the simulation. Will pass these to the @@ -435,7 +436,6 @@ def receive_obligation(self, amount, recipient, due_time, purpose): Due Time Purpose: Reason for obligation (Interest due) Returns None""" - obligation = {"amount": amount, "recipient": recipient, "due_time": due_time, "purpose": purpose} self.obligations.append(obligation) @@ -589,7 +589,6 @@ def get_reinsurance_premium(self, np_reinsurance_deductible_fraction): np_reinsurance_deductible_fraction: Type Integer Returns reinsurance premium (Type: Integer)""" # TODO: cut this out of the insurance market premium -> OBSOLETE?? - # TODO: make premiums dependend on the deductible per value (np_reinsurance_deductible_fraction) -> DONE. # TODO: make max_reduction into simulation_parameter ? if self.reinsurance_off: return float('inf') @@ -608,7 +607,7 @@ def get_cat_bond_price(self, np_reinsurance_deductible_fraction): if self.catbonds_off: return float('inf') max_reduction = 0.9 - max_CB_surcharge = 0.5 + max_CB_surcharge = 0.5 return self.reinsurance_market_premium * (1. + max_CB_surcharge - max_reduction * np_reinsurance_deductible_fraction) def append_reinrisks(self, item): diff --git a/visualization_network.py b/visualization_network.py index 82d9129..b0497d1 100644 --- a/visualization_network.py +++ b/visualization_network.py @@ -3,12 +3,45 @@ import numpy as np class ReinsuranceNetwork(): - def __init__(self, insurancefirms, reinsurancefirms, catbonds): - """save entities""" + def __init__(self): + """Initialising method for ReinsuranceNetwork. + No accepted values. + This created the figure that the network will be displayed on so only called once, and only if show_network is + True.""" + self.figure = plt.figure(num=None, figsize=(10, 8), dpi=100, facecolor='w', edgecolor='k') + + def compute_measures(self): + """Method to obtain the network distribution and print it. + No accepted values. + No return values.""" + #degrees = self.network.degree() + degree_distr = dict(self.network.degree()).values() + in_degree_distr = dict(self.network.in_degree()).values() + out_degree_distr = dict(self.network.out_degree()).values() + is_connected = nx.is_weakly_connected(self.network) + #is_connected = nx.is_strongly_connected(self.network) # must always be False + try: + node_centralities = nx.eigenvector_centrality(self.network) + except: + node_centralities = nx.betweenness_centrality(self.network) + # TODO: and more, choose more meaningful ones... + + print("Graph is connected: ", is_connected, "\nIn degrees ", in_degree_distr, "\nOut degrees", out_degree_distr, \ + "\nCentralities", node_centralities) + + def update(self, insurancefirms, reinsurancefirms, catbonds): + """Method to update the network. + Accepts: + insurancefirms: Type List of DataDicts. + resinurancefirn.Type List of DataDicts. + catbonds: Type List of DataDicts. + No return values. + This method is called from insurancesimulation for every iteration a network is to be shown. It takes the list + of agents and creates both a weighted and unweighted networkx network with it.""" self.insurancefirms = insurancefirms self.reinsurancefirms = reinsurancefirms self.catbonds = catbonds - + """obtain lists of operational entities""" op_entities = {} self.num_entities = {} @@ -16,50 +49,64 @@ def __init__(self, insurancefirms, reinsurancefirms, catbonds): op_firmtype = [firm for firm in firmlist if firm.operational] op_entities[firmtype] = op_firmtype self.num_entities[firmtype] = len(op_firmtype) - - #op_entities_flat = [firm for firm in entities_list for entities_list in op_entities] + self.network_size = sum(self.num_entities.values()) - - """create weigthed adjacency matrix""" - weights_matrix = np.zeros(self.network_size**2).reshape(self.network_size, self.network_size) + + """Create weighted adjacency matrix and category edge labels""" + weights_matrix = np.zeros(self.network_size ** 2).reshape(self.network_size, self.network_size) + self.edge_labels = {} for idx_to, firm in enumerate(op_entities["insurers"] + op_entities["reinsurers"]): eolrs = firm.get_excess_of_loss_reinsurance() for eolr in eolrs: - #pdb.set_trace() - idx_from = self.num_entities["insurers"] + (op_entities["reinsurers"] + op_entities["catbonds"]).index(eolr["reinsurer"]) - weights_matrix[idx_from][idx_to] = eolr["value"] - + # pdb.set_trace() + try: + idx_from = self.num_entities["insurers"] + (op_entities["reinsurers"] + op_entities["catbonds"]).index(eolr["reinsurer"]) + weights_matrix[idx_from][idx_to] = eolr["value"] + self.edge_labels[idx_to, idx_from] = eolr["category"] + except ValueError: + print("Reinsurer is not in list of reinsurance companies") + """unweighted adjacency matrix""" adj_matrix = np.sign(weights_matrix) - + """define network""" self.network = nx.from_numpy_array(weights_matrix, create_using=nx.DiGraph()) # weighted - self.network_unweighted = nx.from_numpy_array(adj_matrix, create_using=nx.DiGraph()) # unweighted - - def compute_measures(self): - """obtain measures""" - #degrees = self.network.degree() - degree_distr = dict(self.network.degree()).values() - in_degree_distr = dict(self.network.in_degree()).values() - out_degree_distr = dict(self.network.out_degree()).values() - is_connected = nx.is_weakly_connected(self.network) - #is_connected = nx.is_strongly_connected(self.network) # must always be False - try: - node_centralities = nx.eigenvector_centrality(self.network) - except: - node_centralities = nx.betweenness_centrality(self.network) - # TODO: and more, choose more meaningful ones... - - print("Graph is connected: ", is_connected, "\nIn degrees ", in_degree_distr, "\nOut degrees", out_degree_distr, \ - "\nCentralities", node_centralities) + self.network_unweighted = nx.from_numpy_array(adj_matrix, create_using=nx.DiGraph()) # unweighted - def visualize(self): - """visualize""" - plt.figure() + def visualize(self): + """Method to add the network to the figure initialised in __init__. + No accepted values. + No return values. + This method takes the network created in update method and then draws it onto the figure with edge labels + corresponding to the category being reinsured, and adds a legend to indicate which node is insurer, reinsurer, + or CatBond. This method allows the figure to be updated without a new figure being created or stopping the + program.""" + plt.ion() # Turns on interactive graph mode. firmtypes = np.ones(self.network_size) firmtypes[self.num_entities["insurers"]:self.num_entities["insurers"]+self.num_entities["reinsurers"]] = 0.5 firmtypes[self.num_entities["insurers"]+self.num_entities["reinsurers"]:] = 1.3 - print(firmtypes, self.num_entities["insurers"], self.num_entities["insurers"] + self.num_entities["reinsurers"]) + print("Number of insurers: %i, Number of Reinsurers: %i, CatBonds: %i" + % (self.num_entities["insurers"], self.num_entities["reinsurers"], self.num_entities['catbonds'])) + + # Either this or below create a network, this one has id's but no key. + # pos = nx.spring_layout(self.network_unweighted) + # nx.draw(self.network_unweighted, pos, node_color=firmtypes, with_labels=True, cmap=plt.cm.plasma) + # nx.draw_networkx_edge_labels(self.network_unweighted, pos, self.edge_labels, font_size=5) + + "Draw Network" pos = nx.spring_layout(self.network_unweighted) - nx.draw(self.network_unweighted, pos, node_color=firmtypes, with_labels=True, cmap=plt.cm.winter) + nx.draw_networkx_nodes(self.network_unweighted, pos, list(range(self.num_entities["insurers"])), + node_color='b', node_size=50, alpha=0.9, label='Insurer') + nx.draw_networkx_nodes(self.network_unweighted, pos, list(range(self.num_entities["insurers"], self.num_entities["insurers"]+self.num_entities["reinsurers"])), + node_color='r', node_size=50, alpha=0.9, label='Reinsurer') + nx.draw_networkx_nodes(self.network_unweighted, pos, list(range(self.num_entities["insurers"] + self.num_entities["reinsurers"], self.num_entities["insurers"] + self.num_entities["reinsurers"] + self.num_entities['catbonds'])), + node_color='g', node_size=50, alpha=0.9, label='CatBond') + nx.draw_networkx_edges(self.network_unweighted, pos, width=1.0, alpha=0.5, node_size=50) + nx.draw_networkx_edge_labels(self.network_unweighted, pos, self.edge_labels, font_size=5) + plt.legend(scatterpoints=1, loc='upper right') + plt.axis('off') plt.show() + + """Update figure""" + self.figure.canvas.flush_events() + self.figure.clear() From 1ea7ae38eea0a3a3643ae4bfa277bf05bb3cc888 Mon Sep 17 00:00:00 2001 From: KloskaT Date: Wed, 10 Jul 2019 16:57:47 +0100 Subject: [PATCH 025/125] Removed last of abce references. --- isleconfig.py | 3 +-- start.py | 21 --------------------- 2 files changed, 1 insertion(+), 23 deletions(-) diff --git a/isleconfig.py b/isleconfig.py index c32cc40..8882c5d 100644 --- a/isleconfig.py +++ b/isleconfig.py @@ -1,10 +1,9 @@ -use_abce = False oneriskmodel = False replicating = False force_foreground = False verbose = False showprogress = True -show_network = True # Should network be visualized? This should be False by default, to be overridden by commandline arguments +show_network = True # Should network be visualized? This should be False by default, to be overridden by commandline arguments slim_log = False # Should logs be small in ensemble runs (only aggregated level data)? simulation_parameters = {"no_categories": 4, diff --git a/start.py b/start.py index c5b2925..cf59512 100644 --- a/start.py +++ b/start.py @@ -23,26 +23,7 @@ os.makedirs("data") -"""create conditional decorator""" -def conditionally(decorator_function, condition): - def wrapper(target_function): - if not condition: - return target_function - return decorator_function(target_function) - return wrapper - - -"""Create placeholder gui decorator""" -# TODO: replace this with more elegant solution if possible. Currently required since script will otherwise crash\ -# at the conditional decorator below since gui is then undefined -def gui(*args, **kwargs): - pass - - -@conditionally(gui(simulation_parameters, serve=False), isleconfig.use_abce) -# TODO: Fix/remove gui and remove last of abce def main(simulation_parameters, rc_event_schedule, rc_event_damage, np_seed, random_seed, save_iter, requested_logs=None): - np.random.seed(np_seed) random.seed(random_seed) @@ -100,7 +81,6 @@ def main(simulation_parameters, rc_event_schedule, rc_event_damage, np_seed, ran # It is required to return this list to download all the data generated by a single run of the model from the cloud. -"""save function""" def save_simulation(t, sim, sim_param, exit_now=False): d = {} d["np_seed"] = np.random.get_state() @@ -121,7 +101,6 @@ def save_simulation(t, sim, sim_param, exit_now=False): """main entry point""" if __name__ == "__main__": - """ use argparse to handle command line arguments""" parser = argparse.ArgumentParser(description='Model the Insurance sector') parser.add_argument("--oneriskmodel", action="store_true", From b70f5046d3dbdcc1ceea3421d1ef4a4e49bbb403 Mon Sep 17 00:00:00 2001 From: KloskaT Date: Wed, 10 Jul 2019 16:59:25 +0100 Subject: [PATCH 026/125] Updated to include new arguments, removed abce, altered plotting section --- README.md | 26 ++++++++++++-------------- 1 file changed, 12 insertions(+), 14 deletions(-) diff --git a/README.md b/README.md index e402c5a..73086df 100644 --- a/README.md +++ b/README.md @@ -14,7 +14,7 @@ $ pip install -r requirements.txt ## Simulation -Execute the simulation with this command +Execute a single simulation run with the command: ``` $ python3 start.py @@ -25,9 +25,9 @@ $ python3 start.py The ```start.py``` script accepts a number of options. ``` -usage: start.py [-h] [--abce] [--oneriskmodel] [--riskmodels {1,2,3,4}] - [--replicid REPLICID] [--replicating] - [--randomseed RANDOMSEED] [--foreground] [-p] [-v] +usage: start.py [--oneriskmodel] [--riskmodels {1,2,3,4}] [--replicid REPLICID] + [--replicating] [--randomseed RANDOMSEED] [--foreground] + [--shownetwork] [-p] [-v] [--save_iterations] ``` See the help for more details @@ -36,17 +36,9 @@ See the help for more details python3 start.py --help ``` -## Graphical simulation runs - -abce can be used to run simulations with a graphical interface: - -``` -python3 start.py --abce -``` - ## Ensemble simulations -The bash scripts ```starter_*.sh``` can be used to run ensembles of a large number of simulations for settings with 1-4 different riskmodels. ```starter_two.sh``` is set up to generate random seeds and risk event schedules that are - for consistency and comparability - also used by the other scripts (i.e. ```starter_two.sh``` needs to be run first). +The bash scripts ```starter_*.sh``` can be used to run ensembles of a large number of simulations for settings with 1-4 different risk models. ```starter_two.sh``` is set up to generate random seeds and risk event schedules that are - for consistency and comparability - also used by the other scripts (i.e. ```starter_two.sh``` needs to be run first). ``` bash starter_two.sh @@ -57,5 +49,11 @@ bash starter_three.sh ## Plotting -Use the scripts ```plotter_pl_timescale.py``` and ```visualize.py``` for plotting/visualizing single simulation runs. Use ```.py```, ```metaplotter_pl_timescale.py```, or ```metaplotter_pl_timescale_additional_measures.py``` to visualize ensemble runs. +####Single runs +Use the script ```plotter.py``` to plot insurer and reinsurer data, or run +```visualisation.py [--single]``` from the command line to plot this and visualise the run. + +####Ensemble runs +Use ```metaplotter_pl_timescale.py```, ```metaplotter_pl_timescale_additional_measures.py```, +or ```visualisation.py [--comparison]``` to visualize ensemble runs. From 9493a180453aaeb7f5cea35a54061a60c63a99e5 Mon Sep 17 00:00:00 2001 From: Thomas Kloska <43939847+KloskaT@users.noreply.github.com> Date: Wed, 10 Jul 2019 17:10:59 +0100 Subject: [PATCH 027/125] Update README.md --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 73086df..385044d 100644 --- a/README.md +++ b/README.md @@ -49,11 +49,11 @@ bash starter_three.sh ## Plotting -####Single runs +#### Single runs Use the script ```plotter.py``` to plot insurer and reinsurer data, or run ```visualisation.py [--single]``` from the command line to plot this and visualise the run. -####Ensemble runs +#### Ensemble runs Use ```metaplotter_pl_timescale.py```, ```metaplotter_pl_timescale_additional_measures.py```, or ```visualisation.py [--comparison]``` to visualize ensemble runs. From c742f3ae38420ccd5b2d18e2a078e446cd7d0239 Mon Sep 17 00:00:00 2001 From: KloskaT Date: Thu, 11 Jul 2019 11:20:47 +0100 Subject: [PATCH 028/125] Save state now includes events. Removed abce from resume.py and now works, including network visualisation. --- resume.py | 131 ++++++++++++++---------------------------------------- start.py | 8 ++-- 2 files changed, 39 insertions(+), 100 deletions(-) diff --git a/resume.py b/resume.py index 7c60700..5ce4223 100644 --- a/resume.py +++ b/resume.py @@ -17,19 +17,18 @@ # use argparse to handle command line arguments parser = argparse.ArgumentParser(description='Model the Insurance sector') -parser.add_argument("--abce", action="store_true", help="use abce") parser.add_argument("--oneriskmodel", action="store_true", help="allow overriding the number of riskmodels from the standard config (with 1)") parser.add_argument("--riskmodels", type=int, choices=[1,2,3,4], help="allow overriding the number of riskmodels from standard config (with 1 or other numbers)") parser.add_argument("--replicid", type=int, help="if replication ID is given, pass this to the simulation so that the risk profile can be restored") parser.add_argument("--replicating", action="store_true", help="if this is a simulation run designed to replicate another, override the config file parameter") parser.add_argument("--randomseed", type=float, help="allow setting of numpy random seed") parser.add_argument("--foreground", action="store_true", help="force foreground runs even if replication ID is given (which defaults to background runs)") +parser.add_argument("--shownetwork", action="store_true", help="show reinsurance relations as network") parser.add_argument("-p", "--showprogress", action="store_true", help="show timesteps") parser.add_argument("-v", "--verbose", action="store_true", help="more detailed output") +parser.add_argument("--save_iterations", type=int, help="number of iterations to iterate before saving world state") args = parser.parse_args() -if args.abce: - isleconfig.use_abce = True if args.oneriskmodel: isleconfig.oneriskmodel = True override_no_riskmodels = 1 @@ -48,41 +47,25 @@ seed = np.random.randint(0, 2 ** 31 - 1) if args.foreground: isleconfig.force_foreground = True +if args.shownetwork: + isleconfig.show_network = True if args.showprogress: isleconfig.showprogress = True if args.verbose: isleconfig.verbose = True +if args.save_iterations: + save_iter = args.save_iterations +else: + save_iter = 200 -# import isle and abce modules -if isleconfig.use_abce: - #print("Importing abce") - import abce - from abce import gui - +# import isle modules from insurancesimulation import InsuranceSimulation from insurancefirm import InsuranceFirm from riskmodel import RiskModel from reinsurancefirm import ReinsuranceFirm -# create conditional decorator -def conditionally(decorator_function, condition): - def wrapper(target_function): - if not condition: - return target_function - return decorator_function(target_function) - return wrapper - -# create non-abce placeholder gui decorator -# TODO: replace this with more elegant solution if possible. Currently required since script will otherwise crash at the conditional decorator below since gui is then undefined -if not isleconfig.use_abce: - def gui(*args, **kwargs): - pass - # main function - -#@gui(simulation_parameters, serve=True) -@conditionally(gui(simulation_parameters, serve=False), isleconfig.use_abce) def main(): with open("data/simulation_save.pkl", "br") as rfile: @@ -93,56 +76,25 @@ def main(): random_seed = d["random_seed"] time = d["time"] simulation_parameters = d["simulation_parameters"] - + event_schedule = d["event_schedule"] + event_damage = d["event_damage"] + + event_schedule_trunc = [] + event_damage_trunc = [] + for categ in range(simulation_parameters["no_categories"]): + event_schedule_trunc.append([event for event in event_schedule[categ] if event >= time]) + event_damage_trunc.append(event_damage[categ][-len(event_schedule_trunc[categ]):]) + insurancefirms_group = list(simulation.insurancefirms) reinsurancefirms_group = list(simulation.reinsurancefirms) - - #np.random.seed(seed) + + # np.random.seed(seed) np.random.set_state(np_seed) random.setstate(random_seed) - - assert not isleconfig.use_abce, "Resuming will not work with abce" - ## create simulation and world objects (identical in non-abce mode) - #if isleconfig.use_abce: - # simulation = abce.Simulation(processes=1,random_seed = seed) - # - - #simulation_parameters['simulation'] = world = InsuranceSimulation(override_no_riskmodels, replic_ID, simulation_parameters) - # - #if not isleconfig.use_abce: - # simulation = world - # - # create agents: insurance firms - #insurancefirms_group = simulation.build_agents(InsuranceFirm, - # 'insurancefirm', - # parameters=simulation_parameters, - # agent_parameters=world.agent_parameters["insurancefirm"]) - # - #if isleconfig.use_abce: - # insurancefirm_pointers = insurancefirms_group.get_pointer() - #else: - # insurancefirm_pointers = insurancefirms_group - #world.accept_agents("insurancefirm", insurancefirm_pointers, insurancefirms_group) - # - # create agents: reinsurance firms - #reinsurancefirms_group = simulation.build_agents(ReinsuranceFirm, - # 'reinsurance', - # parameters=simulation_parameters, - # agent_parameters=world.agent_parameters["reinsurance"]) - #if isleconfig.use_abce: - # reinsurancefirm_pointers = reinsurancefirms_group.get_pointer() - #else: - # reinsurancefirm_pointers = reinsurancefirms_group - #world.accept_agents("reinsurance", reinsurancefirm_pointers, reinsurancefirms_group) - # - - # time iteration + for t in range(time, simulation_parameters["max_time"]): - # abce time step - simulation.advance_round(t) - - # create new agents # TODO: write method for this; this code block is executed almost identically 4 times + # create new agents # TODO: write method for this; this code block is executed almost identically 4 times if world.insurance_firm_market_entry(agent_type="InsuranceFirm"): parameters = [np.random.choice(world.agent_parameters["insurancefirm"])] parameters[0]["id"] = world.get_unique_insurer_id() @@ -151,46 +103,28 @@ def main(): parameters=simulation_parameters, agent_parameters=parameters) insurancefirms_group += new_insurance_firm - if isleconfig.use_abce: - # TODO: fix abce - # may fail in abce because addressing individual agents may not be allowed - # may also fail because agent methods may not be callable directly - new_insurancefirm_pointer = [new_insurance_firm[0].get_pointer()] # index 0 because this is a list with just 1 object - else: - new_insurancefirm_pointer = new_insurance_firm + new_insurancefirm_pointer = new_insurance_firm world.accept_agents("insurancefirm", new_insurancefirm_pointer, new_insurance_firm, time=t) if world.insurance_firm_market_entry(agent_type="ReinsuranceFirm"): - parameters = [np.random.choice(world.agent_parameters["reinsurance"])] + parameters = [np.random.choice(world.agent_parameters["reinsurancefirm"])] parameters[0]["id"] = world.get_unique_reinsurer_id() new_reinsurance_firm = simulation.build_agents(ReinsuranceFirm, - 'reinsurance', + 'reinsurancefirm', parameters=simulation_parameters, agent_parameters=parameters) reinsurancefirms_group += new_reinsurance_firm - if isleconfig.use_abce: - # TODO: fix abce - # may fail in abce because addressing individual agents may not be allowed - # may also fail because agent methods may not be callable directly - new_reinsurancefirm_pointer = [new_reinsurance_firm[0].get_pointer()] # index 0 because this is a list with just 1 object - else: - new_reinsurancefirm_pointer = new_reinsurance_firm - world.accept_agents("reinsurance", new_reinsurancefirm_pointer, new_reinsurance_firm, time=t) + new_reinsurancefirm_pointer = new_reinsurance_firm + world.accept_agents("reinsurancefirm", new_reinsurancefirm_pointer, new_reinsurance_firm, time=t) # iterate simulation world.iterate(t) # log data - if isleconfig.use_abce: - #insurancefirms.logme() - #reinsurancefirms.logme() - insurancefirms_group.agg_log(variables=['cash', 'operational'], len=['underwritten_contracts']) - #reinsurancefirms_group.agg_log(variables=['cash']) - else: - world.save_data() + world.save_data() if t > 0 and t//50 == t/50: - save_simulation(t, simulation, simulation_parameters, exit_now=False) + save_simulation(t, simulation, simulation_parameters, event_schedule, event_damage, exit_now=False) #print("here") # finish simulation, write logs @@ -198,13 +132,15 @@ def main(): # save function -def save_simulation(t, sim, sim_param, exit_now=False): +def save_simulation(t, sim, sim_param, event_schedule, event_damage, exit_now=False): d = {} d["np_seed"] = np.random.get_state() d["random_seed"] = random.getstate() d["time"] = t d["simulation"] = sim d["simulation_parameters"] = sim_param + d["event_schedule"] = event_schedule + d["event_damage"] = event_damage with open("data/simulation_resave.pkl", "bw") as wfile: pickle.dump(d, wfile, protocol=pickle.HIGHEST_PROTOCOL) with open("data/simulation_resave.pkl", "br") as rfile: @@ -214,7 +150,8 @@ def save_simulation(t, sim, sim_param, exit_now=False): print("\nSimulation hash: ", hashlib.sha512(str(file_contents).encode()).hexdigest()) if exit_now: exit(0) - + + # main entry point if __name__ == "__main__": main() diff --git a/start.py b/start.py index cf59512..4f05902 100644 --- a/start.py +++ b/start.py @@ -72,7 +72,7 @@ def main(simulation_parameters, rc_event_schedule, rc_event_damage, np_seed, ran world.save_data() if t%50 == save_iter: - save_simulation(t, simulation, simulation_parameters, exit_now=False) + save_simulation(t, simulation, simulation_parameters, rc_event_schedule, rc_event_damage, exit_now=False) """finish simulation, write logs""" simulation.finalize() @@ -81,13 +81,15 @@ def main(simulation_parameters, rc_event_schedule, rc_event_damage, np_seed, ran # It is required to return this list to download all the data generated by a single run of the model from the cloud. -def save_simulation(t, sim, sim_param, exit_now=False): +def save_simulation(t, sim, sim_param, event_schedule, event_damage, exit_now=False): d = {} d["np_seed"] = np.random.get_state() d["random_seed"] = random.getstate() d["time"] = t d["simulation"] = sim d["simulation_parameters"] = sim_param + d["event_schedule"] = event_schedule + d["event_damage"] = event_damage with open("data/simulation_save.pkl", "bw") as wfile: pickle.dump(d, wfile, protocol=pickle.HIGHEST_PROTOCOL) with open("data/simulation_save.pkl", "br") as rfile: @@ -152,7 +154,7 @@ def save_simulation(t, sim, sim_param, exit_now=False): if args.save_iterations: save_iter = args.save_iterations else: - save_iter = 200 + save_iter = 20 from setup import SetupSim setup = SetupSim() #Here the setup for the simulation is done. From d3080e7433ccc67a84bbbbbc5bedd9d6ce59a459 Mon Sep 17 00:00:00 2001 From: KloskaT Date: Thu, 11 Jul 2019 11:56:50 +0100 Subject: [PATCH 029/125] Fixed resume.py --- resume.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/resume.py b/resume.py index 5ce4223..49f374f 100644 --- a/resume.py +++ b/resume.py @@ -59,12 +59,9 @@ save_iter = 200 # import isle modules -from insurancesimulation import InsuranceSimulation from insurancefirm import InsuranceFirm -from riskmodel import RiskModel from reinsurancefirm import ReinsuranceFirm - # main function def main(): From fbc6944f808e0d7d88ad0846a3b2653c73359ded Mon Sep 17 00:00:00 2001 From: KloskaT Date: Thu, 11 Jul 2019 11:59:12 +0100 Subject: [PATCH 030/125] Cleaned up, added central bank to award interest --- metainsuranceorg.py | 73 +++++++++++++-------------------------------- 1 file changed, 20 insertions(+), 53 deletions(-) diff --git a/metainsuranceorg.py b/metainsuranceorg.py index 211a329..15490d2 100644 --- a/metainsuranceorg.py +++ b/metainsuranceorg.py @@ -1,4 +1,3 @@ - import isleconfig import numpy as np import scipy.stats @@ -14,6 +13,7 @@ def get_mean(x): return sum(x) / len(x) + def get_mean_std(x): m = get_mean(x) variance = sum((val - m) ** 2 for val in x) @@ -105,7 +105,7 @@ def init(self, simulation_parameters, agent_parameters): self.cash_left_by_categ = [self.cash for i in range(self.simulation_parameters["no_categories"])] self.market_permanency_counter = 0 - def iterate(self, time): # TODO: split function so that only the sequence of events remains here and everything else is in separate methods + def iterate(self, time): """Method that iterates each firm by one time step. Accepts: Time: Type Integer @@ -115,7 +115,7 @@ def iterate(self, time): # TODO: split function so that only the sequence firms receive new risks to evaluate, pay dividends, adjust capacity.""" """Obtain interest generated by cash""" - self.obtain_yield(time) + self.simulation.bank.award_interest(self, self.cash) """realize due payments""" self.effect_payments(time) @@ -138,33 +138,20 @@ def iterate(self, time): # TODO: split function so that only the sequence if self.operational: - """request risks to be considered for underwriting in the next period and collect those for this period""" - new_nonproportional_risks, new_risks = self.get_newrisks_by_type(contracts_dissolved) - - """ - new_risks = [] - if self.is_insurer: - new_risks += self.simulation.solicit_insurance_requests(self.id, self.cash, self) - if self.is_reinsurer: - new_risks += self.simulation.solicit_reinsurance_requests(self.id, self.cash, self) + """request risks to be considered for underwriting in the next period, organised by insurance type""" + new_nonproportional_risks, new_risks = self.get_newrisks_by_type() contracts_offered = len(new_risks) if isleconfig.verbose and contracts_offered < 2 * contracts_dissolved: print("Something wrong; agent {0:d} receives too few new contracts {1:d} <= {2:d}".format( - self.id, contracts_offered, 2*contracts_dissolved)) - - new_nonproportional_risks = [risk for risk in new_risks if risk.get("insurancetype")=='excess-of-loss' and risk["owner"] is not self] - new_risks = [risk for risk in new_risks if risk.get("insurancetype") in ['proportional', None] and risk["owner"] is not self] - """ + self.id, contracts_offered, 2 * contracts_dissolved)) - """deal with non-proportional risks first as they must evaluate each request separatly, then with proportional ones""" - [reinrisks_per_categ, number_reinrisks_categ] = self.risks_reinrisks_organizer(new_nonproportional_risks) #Here the new reinrisks are organized by category. - - for repetition in range(self.recursion_limit): # TODO: find an efficient way to stop the recursion if there are no more risks to accept or if it is not accepting any more over several iterations. + """deal with non-proportional risks first as they must evaluate each request separately""" + [reinrisks_per_categ, number_reinrisks_categ] = self.risks_reinrisks_organizer(new_nonproportional_risks) + for repetition in range(self.recursion_limit): former_reinrisks_per_categ = copy.copy(reinrisks_per_categ) [reinrisks_per_categ, not_accepted_reinrisks] = self.process_newrisks_reinsurer(reinrisks_per_categ, number_reinrisks_categ, time) #Here we process all the new reinrisks in order to keep the portfolio as balanced as possible. - if former_reinrisks_per_categ == reinrisks_per_categ: #Stop condition implemented. Might solve the previous TODO. + if former_reinrisks_per_categ == reinrisks_per_categ: break - self.simulation.return_reinrisks(not_accepted_reinrisks) underwritten_risks = [{"value": contract.value, "category": contract.category, \ @@ -184,11 +171,7 @@ def iterate(self, time): # TODO: split function so that only the sequence max_var_by_categ = self.cash - self.excess_capital self.adjust_capacity_target(max_var_by_categ) actual_capacity = self.increase_capacity(time, max_var_by_categ) - # seek reinsurance - #if self.is_insurer: - # # TODO: Why should only insurers be able to get reinsurance (not reinsurers)? (Technically, it should work) --> OBSOLETE - # self.ask_reinsurance(time) - # # TODO: make independent of insurer/reinsurer, but change this to different deductible values + # TODO: make independent of insurer/reinsurer, but change this to different deductible values """handle capital market interactions: capital history, dividends""" self.cash_last_periods = [self.cash] + self.cash_last_periods[:3] @@ -202,15 +185,13 @@ def iterate(self, time): # TODO: split function so that only the sequence acceptable_by_category = acceptable_by_category * growth_limit / sum(acceptable_by_category) acceptable_by_category = np.int64(np.round(acceptable_by_category)) - [risks_per_categ, number_risks_categ] = self.risks_reinrisks_organizer(new_risks) #Here the new risks are organized by category. - - for repetition in range(self.recursion_limit): # TODO: find an efficient way to stop the recursion if there are no more risks to accept or if it is not accepting any more over several iterations. + [risks_per_categ, number_risks_categ] = self.risks_reinrisks_organizer(new_risks) + for repetition in range(self.recursion_limit): former_risks_per_categ = copy.copy(risks_per_categ) [risks_per_categ, not_accepted_risks] = self.process_newrisks_insurer(risks_per_categ, number_risks_categ, acceptable_by_category, var_per_risk_per_categ, cash_left_by_categ, time) #Here we process all the new risks in order to keep the portfolio as balanced as possible. - if former_risks_per_categ == risks_per_categ: #Stop condition implemented. Might solve the previous TODO. + if former_risks_per_categ == risks_per_categ: break - self.simulation.return_risks(not_accepted_risks) """not implemented @@ -350,14 +331,6 @@ def pay_dividends(self, time): No return value If firm has positive profits will pay percentage of them as dividends. Currently pays to simulation.""" self.receive_obligation(self.per_period_dividend, self.owner, time, 'dividend') - - def obtain_yield(self, time): - """Method to calculate interest generated by agents total cash - Accepts: - time: Type integer - No return value""" - amount = self.cash * self.interest_rate # TODO: agent should not award her own interest. This interest rate should be taken from self.simulation with a getter method - self.simulation.receive_obligation(amount, self, time, 'yields') def get_cash(self): """Method to return agents cash. Only used to calculate total sum of capital to recalculate market premium @@ -416,11 +389,10 @@ def estimated_var(self): else: self.var_counter_per_risk = 0 - def get_newrisks_by_type(self, contracts_dissolved): + def get_newrisks_by_type(self): """Method for soliciting new risks from insurance simulation then organising them based if non-proportional or not. - Accepts: - contracts_dissolved: list of contracts dissolved, needed for verbose condition. + No accepted Values. Returns: new_non_proportional_risks: Type list of DataDicts. new_risks: Type list of DataDicts.""" @@ -429,10 +401,6 @@ def get_newrisks_by_type(self, contracts_dissolved): new_risks += self.simulation.solicit_insurance_requests(self.id, self.cash, self) if self.is_reinsurer: new_risks += self.simulation.solicit_reinsurance_requests(self.id, self.cash, self) - contracts_offered = len(new_risks) - if isleconfig.verbose and contracts_offered < 2 * contracts_dissolved: - print("Something wrong; agent {0:d} receives too few new contracts {1:d} <= {2:d}".format( - self.id, contracts_offered, 2 * contracts_dissolved)) new_nonproportional_risks = [risk for risk in new_risks if risk.get("insurancetype") == 'excess-of-loss' and risk["owner"] is not self] @@ -517,7 +485,7 @@ def process_newrisks_reinsurer(self, reinrisks_per_categ, number_reinrisks_categ risks are accepted then a contract is written.""" for iterion in range(max(number_reinrisks_categ)): - for categ_id in range(self.simulation_parameters["no_categories"]): #Here we take only one risk per category at a time to achieve risk[C1], risk[C2], risk[C3], risk[C4], risk[C1], risk[C2], ... if possible. + for categ_id in range(self.simulation_parameters["no_categories"]): if iterion < number_reinrisks_categ[categ_id] and reinrisks_per_categ[categ_id][iterion] is not None: risk_to_insure = reinrisks_per_categ[categ_id][iterion] underwritten_risks = [{"value": contract.value, "category": contract.category, \ @@ -528,7 +496,7 @@ def process_newrisks_reinsurer(self, reinrisks_per_categ, number_reinrisks_categ self.underwritten_contracts if contract.insurancetype == "excess-of-loss"] accept, cash_left_by_categ, var_this_risk, self.excess_capital = self.riskmodel.evaluate( underwritten_risks, self.cash, - risk_to_insure) # TODO: change riskmodel.evaluate() to accept new risk to be evaluated and to account for existing non-proportional risks correctly -> DONE. + risk_to_insure) if accept: per_value_reinsurance_premium = self.np_reinsurance_premium_share * risk_to_insure[ "periodized_total_premium"] * risk_to_insure["runtime"] * (self.simulation.get_market_reinpremium()/self.simulation.get_market_premium()) / risk_to_insure[ @@ -593,8 +561,6 @@ def process_newrisks_insurer(self, risks_per_categ, number_risks_categ, acceptab self.underwritten_contracts.append(contract) self.cash_left_by_categ = cash_left_by_categ risks_per_categ[categ_id][iter] = None - # TODO: move this to insurancecontract (ca. line 14) -> DONE - # TODO: do not write into other object's properties, use setter -> DONE else: [condition, cash_left_by_categ] = self.balanced_portfolio(risk_to_insure, cash_left_by_categ, var_per_risk_per_categ) #Here it is check whether the portfolio is balanced or not if the risk (risk_to_insure) is underwritten. Return True if it is balanced. False otherwise. @@ -703,7 +669,8 @@ def roll_over(self, time): for reincontract in maturing_next: if reincontract.property_holder.operational: reincontract.roll_over_flag = 1 - reinrisk = reincontract.property_holder.create_reinrisk(time, reincontract.category) + #reinrisk = reincontract.property_holder.create_reinrisk(time, reincontract.category) + reinrisk = reincontract.property_holder.ask_reinsurance_non_proportional_by_category(time, reincontract.category, purpose='rollover') if np.random.uniform(0,1,1) < self.simulation_parameters["reinsurance_retention"]: if reinrisk is not None: self.reinrisks_kept.append(reinrisk) From 3408f8b3e91b2a2ef3f5e35a2b098b164cafd158 Mon Sep 17 00:00:00 2001 From: Chris Hughes Date: Wed, 10 Jul 2019 14:42:14 +0100 Subject: [PATCH 031/125] Starting to add docstrings and set relevant attributes and methods to private --- ensemble.py | 6 +- insurancesimulation.py | 185 +++++++++++++++-------------------------- isleconfig.py | 13 +-- metainsuranceorg.py | 9 +- resume.py | 4 +- start.py | 10 +-- 6 files changed, 90 insertions(+), 137 deletions(-) diff --git a/ensemble.py b/ensemble.py index c0998ff..65423b3 100644 --- a/ensemble.py +++ b/ensemble.py @@ -25,9 +25,9 @@ def rake(hostname): """Configuration of the ensemble""" - replications = ( - 70 - ) # Number of replications to be carried out for each configuration. Usually one risk model, two risk models, three risk models, four risk models. + # Number of replications to be carried out for each configuration. Usually one risk model, two risk models, + # three risk models, four risk models. + replications = 70 model = start.main diff --git a/insurancesimulation.py b/insurancesimulation.py index bb49768..e3a66e4 100644 --- a/insurancesimulation.py +++ b/insurancesimulation.py @@ -12,24 +12,45 @@ class InsuranceSimulation: - # Note: if we ever want to have a larger number of categories we should use numpy arrays for all of the - # "by_categ"-type variables and then where possible do numpy operations instead of iterating over them. + """ Simulation object that is responsible for handling all aspects of the world. + + Tracks all agents (firms, catbonds) as well as acting as the insurance market. Iterates other objects, + distributes risks, pays premiums, recieves claims, tracks and inflicts perils, etc. Also contains functionality + to log the state of the simulation. + + Each insurer is given a set of inaccuracy values, on for each category. This is a factor that is inserted when the + insurer calculates the expected damage from a catastophe. In the current configuration, this uses the + riskmodel_inaccuracy_parameter in the configuration - a randomly chosen category has its inaccuracy set to the + inverse of that parameter, and the others are set to that parameter.""" + def __init__( self, - override_no_riskmodels, - replic_ID, - simulation_parameters, - rc_event_schedule, - rc_event_damage, - damage_distribution=TruncatedDistWrapper( - lower_bound=0.25, - upper_bound=1.0, - dist=scipy.stats.pareto(b=2, loc=0, scale=0.25), - ), - ): + override_no_riskmodels: bool, + replic_ID, + simulation_parameters: dict, + rc_event_schedule: list, + rc_event_damage: list, + damage_distribution=TruncatedDistWrapper( + lower_bound=0.25, + upper_bound=1.0, + dist=scipy.stats.pareto(b=2, loc=0, scale=0.25), + ), + ) -> None: + """ + Builds the simulation + Args: + override_no_riskmodels: Positive int overrides the number of riskmodels + replic_ID: TODO: define and fix + simulation_parameters: Dict of the simulation parameters + rc_event_schedule: List of lists of the times that catastrophes will occur, with one sublist for each category + rc_event_damage: As rc_event_schedule but with damage inflicted instead of time + damage_distribution: The distribution that the damages are pulled from. Only the mean is used. + """ + # QUERY: Do we actually care about the premiums # override one-riskmodel case (this is to ensure all other parameters are truly identical for comparison runs) if override_no_riskmodels: simulation_parameters["no_riskmodels"] = override_no_riskmodels + # QUERY: why do we keep duplicates of so many simulation parameters (and then not use many of them)? self.number_riskmodels = simulation_parameters["no_riskmodels"] # save parameters @@ -46,10 +67,11 @@ def __init__( # remaining parameters self.catbonds_off = simulation_parameters["catbonds_off"] self.reinsurance_off = simulation_parameters["reinsurance_off"] + # TODO: research whether this is accurate, is it different for different types of catastrophy? self.cat_separation_distribution = scipy.stats.expon( 0, simulation_parameters["event_time_mean_separation"] ) - # TODO: research whether this is accurate, is it different for different types of catastrophy? + self.risk_factor_lower_bound = simulation_parameters["risk_factor_lower_bound"] self.risk_factor_spread = ( simulation_parameters["risk_factor_upper_bound"] @@ -114,7 +136,7 @@ def __init__( self.rc_event_damage = copy.copy(rc_event_damage) self.rc_event_damage_initial = copy.copy(rc_event_damage) else: # Otherwise the schedules and damages are generated. - self.setup_risk_categories_caller() + raise Exception("No event schedules and damages supplied") # set up risks risk_value_mean = self.risk_value_distribution.mean() @@ -154,7 +176,7 @@ def __init__( # for i in range(self.simulation_parameters["no_categories"])] \ # for j in range(self.simulation_parameters["no_riskmodels"])] - self.inaccuracy = self.get_all_riskmodel_combinations( + self.inaccuracy = self._get_all_riskmodel_combinations( self.simulation_parameters["no_categories"], self.simulation_parameters["riskmodel_inaccuracy_parameter"], ) @@ -162,7 +184,6 @@ def __init__( self.inaccuracy = random.sample( self.inaccuracy, self.simulation_parameters["no_riskmodels"] ) - risk_model_configurations = [ { "damage_distribution": self.damage_distribution, @@ -194,7 +215,7 @@ def __init__( # TODO: collapse the following two loops into one generic one? for i in range(simulation_parameters["no_insurancefirms"]): # Set up the parameters for each insurance firm - # Determine the level of reinsurance the firm will aim for? #QUERY: is that what this is? + # Determine the level of reinsurance the firm will aim for? # QUERY: is that what this is? if simulation_parameters["static_non-proportional_reinsurance_levels"]: insurance_reinsurance_level = simulation_parameters[ "default_non-proportional_reinsurance_deductible" @@ -322,6 +343,7 @@ def __init__( ) def add_agents(self, agent_class, agent_class_string): + # TODO: implement this to merge build_agents and accept_agents pass def build_agents( @@ -349,7 +371,7 @@ def accept_agents(self, agent_class_string, agents, time=0): self.logger.add_insurance_agent() # remove new agent cash from simulation cash to ensure stock flow consistency total_new_agent_cash = sum([agent.cash for agent in agents]) - self.reduce_money_supply(total_new_agent_cash) + self._reduce_money_supply(total_new_agent_cash) elif agent_class_string == "reinsurancefirm": try: self.reinsurancefirms += agents @@ -359,7 +381,7 @@ def accept_agents(self, agent_class_string, agents, time=0): pdb.set_trace() # remove new agent cash from simulation cash to ensure stock flow consistency total_new_agent_cash = sum([agent.cash for agent in agents]) - self.reduce_money_supply(total_new_agent_cash) + self._reduce_money_supply(total_new_agent_cash) elif agent_class_string == "catbond": try: self.catbonds += agents @@ -390,12 +412,12 @@ def iterate(self, t): # adjust market premiums sum_capital = sum([agent.get_cash() for agent in self.insurancefirms]) - self.adjust_market_premium(capital=sum_capital) + self._adjust_market_premium(capital=sum_capital) sum_capital = sum([agent.get_cash() for agent in self.reinsurancefirms]) - self.adjust_reinsurance_market_premium(capital=sum_capital) + self._adjust_reinsurance_market_premium(capital=sum_capital) # pay obligations - self.effect_payments(t) + self._effect_payments(t) # identify perils and effect claims for categ_id in range(len(self.rc_event_schedule)): @@ -414,7 +436,7 @@ def iterate(self, t): damage_extent = copy.copy( self.rc_event_damage[categ_id][0] ) # Schedules of catastrophes and damages must me generated at the same time. - self.inflict_peril(categ_id=categ_id, damage=damage_extent, t=t) + self._inflict_peril(categ_id=categ_id, damage=damage_extent, t=t) self.rc_event_damage[categ_id] = self.rc_event_damage[categ_id][1:] # TODO: Ideally don't want to be taking from the beginning of lists, consider having soonest events at # the end of the list. Probably fine though, only happens once per iteration @@ -423,10 +445,10 @@ def iterate(self, t): print("Next peril ", self.rc_event_schedule[categ_id]) # shuffle risks (insurance and reinsurance risks) - self.shuffle_risks() + self._shuffle_risks() # reset reinweights - self.reset_reinsurance_weights() + self._reset_reinsurance_weights() # iterate reinsurnace firm agents for reinagent in self.reinsurancefirms: @@ -436,7 +458,7 @@ def iterate(self, t): self.reinrisks = [] # reset weights - self.reset_insurance_weights() + self._reset_insurance_weights() # iterate insurance firm agents for agent in self.insurancefirms: @@ -593,7 +615,7 @@ def finalize(self, *args): """ pass - def inflict_peril(self, categ_id, damage, t): + def _inflict_peril(self, categ_id, damage, t): affected_contracts = [ contract for insurer in self.insurancefirms @@ -620,7 +642,7 @@ def receive_obligation(self, amount, recipient, due_time, purpose): } self.obligations.append(obligation) - def effect_payments(self, time): + def _effect_payments(self, time): if self.get_operational(): due = [item for item in self.obligations if item["due_time"] <= time] # print("SIMULATION obligations: ", len(self.obligations), " of which are due: ", len(due)) @@ -629,9 +651,9 @@ def effect_payments(self, time): ] # sum_due = sum([item["amount"] for item in due]) for obligation in due: - self.pay(obligation) + self._pay(obligation) - def pay(self, obligation): + def _pay(self, obligation): amount = obligation["amount"] recipient = obligation["recipient"] purpose = obligation["purpose"] @@ -645,13 +667,13 @@ def receive(self, amount): """Method to accept cash payments.""" self.money_supply += amount - def reduce_money_supply(self, amount): + def _reduce_money_supply(self, amount): """Method to reduce money supply immediately and without payment recipient (used to adjust money supply to compensate for agent endowment).""" self.money_supply -= amount assert self.money_supply >= 0 - def reset_reinsurance_weights(self): + def _reset_reinsurance_weights(self): self.not_accepted_reinrisks = [] @@ -685,8 +707,8 @@ def reset_reinsurance_weights(self): else: self.not_accepted_reinrisks = self.reinrisks - def reset_insurance_weights(self): - + def _reset_insurance_weights(self): + # QUERY: What do the weights represent? operational_no = sum( [insurancefirm.operational for insurancefirm in self.insurancefirms] ) @@ -715,11 +737,11 @@ def reset_insurance_weights(self): s = math.floor(np.random.uniform(0, len(operational_firms), 1)) self.insurers_weights[operational_firms[s].id] += 1 - def shuffle_risks(self): + def _shuffle_risks(self): np.random.shuffle(self.reinrisks) np.random.shuffle(self.risks) - def adjust_market_premium(self, capital): + def _adjust_market_premium(self, capital): """Adjust_market_premium Method. Accepts arguments capital: Type float. The total capital (cash) available in the insurance market (insurance only). @@ -728,6 +750,8 @@ def adjust_market_premium(self, capital): with the capital available in the insurance market and viceversa. The premium reduces until it reaches a minimum below which no insurer is willing to reduce further the price. This method is only called in the self.iterate() method of this class.""" + # QUERY: is there some market god giving out the true value of self.damage_distribution.mean()? Maybe the + # firms should have to infer this empirically, possibly with a valid starting value. self.market_premium = self.norm_premium * ( self.simulation_parameters["upper_price_limit"] - self.simulation_parameters["premium_sensitivity"] @@ -743,7 +767,7 @@ def adjust_market_premium(self, capital): self.norm_premium * self.simulation_parameters["lower_price_limit"], ) - def adjust_reinsurance_market_premium(self, capital): + def _adjust_reinsurance_market_premium(self, capital): """Adjust_market_premium Method. Accepts arguments capital: Type float. The total capital (cash) available in the reinsurance market (reinsurance only). @@ -776,6 +800,7 @@ def get_market_premium(self): return self.market_premium def get_market_reinpremium(self): + # QUERY: What's the difference between this an get_reinsurance_premium? """Get_market_reinpremium Method. Accepts no arguments. Returns: @@ -790,7 +815,7 @@ def get_reinsurance_premium(self, np_reinsurance_deductible_fraction): if self.reinsurance_off: return float("inf") max_reduction = 0.1 - # QUERY: why is this this way? Why no, say, 1.0 - min(max_reduction * np_reinsurance_deductible_fraction)? + # QUERY: why is this this way? Why not, say, 1.0 - min(max_reduction * np_reinsurance_deductible_fraction)? return self.reinsurance_market_premium * ( 1.0 - max_reduction * np_reinsurance_deductible_fraction ) @@ -828,7 +853,7 @@ def solicit_insurance_requests(self, insurer_id, cash, insurer): for risk in insurer.risks_kept: risks_to_be_sent.append(risk) - # QUERY: what actuall is insurancefirm.risks_kept? + # QUERY: what actually is InsuranceFirm.risks_kept? insurer.risks_kept = [] np.random.shuffle(risks_to_be_sent) @@ -857,7 +882,7 @@ def return_reinrisks(self, not_accepted_risks): self.not_accepted_reinrisks += not_accepted_risks # QUERY: What does this represent? - def get_all_riskmodel_combinations(self, n, rm_factor): + def _get_all_riskmodel_combinations(self, n, rm_factor): riskmodels = [] for i in range(self.simulation_parameters["no_categories"]): riskmodel_combination = rm_factor * np.ones( @@ -867,81 +892,8 @@ def get_all_riskmodel_combinations(self, n, rm_factor): riskmodels.append(riskmodel_combination.tolist()) return riskmodels - # TODO: could make an Event() class with time, damage, etc. - def setup_risk_categories(self): - for i in self.riskcategories: - event_schedule = [] - event_damage = [] - total = 0 - while total < self.simulation_parameters["max_time"]: - separation_time = self.cat_separation_distribution.rvs() - total += int(math.ceil(separation_time)) - if total < self.simulation_parameters["max_time"]: - event_schedule.append(total) - event_damage.append(self.damage_distribution.rvs()) - # Schedules of catastrophes and damages must me generated at the same time. Reason: replication - # across different risk models. - self.rc_event_schedule.append(event_schedule) - self.rc_event_damage.append(event_damage) - - # For debugging (cloud debugging) purposes is good to store the initial schedule of catastrophes - # and damages that will be use in a single run of the model. - self.rc_event_schedule_initial = copy.copy(self.rc_event_damage) - self.rc_event_damage_initial = copy.copy(self.rc_event_damage) - - def setup_risk_categories_caller(self): - if self.replic_ID is not None: - if isleconfig.replicating: - self.restore_state_and_risk_categories() - else: - self.setup_risk_categories() - self.save_state_and_risk_categories() - else: - self.setup_risk_categories() - - def save_state_and_risk_categories(self): - # save numpy Mersenne Twister state - mersennetwoster_randomseed = str(np.random.get_state()) - mersennetwoster_randomseed = ( - mersennetwoster_randomseed.replace("\n", "") - .replace("array", "np.array") - .replace("uint32", "np.uint32") - ) - with open("data/replication_randomseed.dat", "a") as wfile: - wfile.write(mersennetwoster_randomseed + "\n") - # save event schedule - with open("data/replication_rc_event_schedule.dat", "a") as wfile: - wfile.write(str(self.rc_event_schedule) + "\n") - - def restore_state_and_risk_categories(self): - with open("data/replication_rc_event_schedule.dat", "r") as rfile: - found = False - for i, line in enumerate(rfile): - # print(i, self.replic_ID) - if i == self.replic_ID: - self.rc_event_schedule = eval( - line - ) # TODO: eval could be considered dangerous - found = True - if not found: - raise Exception( - f"rc event schedule for current replication ID number {self.replic_ID} not found in data file." - ) - - with open("data/replication_randomseed.dat", "r") as rfile: - found = False - for i, line in enumerate(rfile): - # print(i, self.replic_ID) - if i == self.replic_ID: - mersennetwister_randomseed = eval(line) - found = True - if not found: - raise Exception( - f"mersennetwister randomseed for current replication ID number {self.replic_ID} not found in data file. Exiting." - ) - np.random.set_state(mersennetwister_randomseed) - - def insurance_firm_enters_market(self, prob=-1, agent_type="InsuranceFirm"): + def firm_enters_market(self, prob=-1, agent_type="InsuranceFirm"): + # TODO: Do firms really enter the market randomly, with at most one in each timestep? if prob == -1: if agent_type == "InsuranceFirm": prob = self.simulation_parameters[ @@ -1056,7 +1008,6 @@ def reinsurance_entry_index(self): ].argmin() def get_operational(self): - # QUERY: because the simulation can recieve money so is always operational? return True def reinsurance_capital_entry(self): diff --git a/isleconfig.py b/isleconfig.py index 785bbfb..d000f7b 100644 --- a/isleconfig.py +++ b/isleconfig.py @@ -10,12 +10,13 @@ simulation_parameters = { "no_categories": 4, - "no_insurancefirms": 20, - "no_reinsurancefirms": 4, - "no_riskmodels": 2, - "riskmodel_inaccuracy_parameter": 2, # values >=1; inaccuracy higher with higher values - "riskmodel_margin_of_safety": 2, + "no_insurancefirms": 200, + "no_reinsurancefirms": 5, + "no_riskmodels": 3, + # values >=1; inaccuracy higher with higher values + "riskmodel_inaccuracy_parameter": 2, # values >=1; factor of additional liquidity beyond value at risk + "riskmodel_margin_of_safety": 2, "margin_increase": 0, # "margin_increase" modifies the margin of safety depending on the number of risks models available in the market. # When it is 0 all risk models have the same margin of safety. @@ -98,5 +99,5 @@ "reinsurance_limit": 0.1, "upper_price_limit": 1.2, "lower_price_limit": 0.85, - "no_risks": 20000, + "no_risks": 200000, } diff --git a/metainsuranceorg.py b/metainsuranceorg.py index 5d580cc..0200db0 100644 --- a/metainsuranceorg.py +++ b/metainsuranceorg.py @@ -248,8 +248,7 @@ def collect_process_evaluate_risks(self, time, contracts_dissolved): # Stop condition implemented. Might solve the previous TODO. break - # QUERY: it's typically dangerous to compare floats with !=, is it okay in this case? Probably, since - # no arithmetic is done + # TODO: This takes up about 8% of processing time. Can we update the list instead of rebuilding it? underwritten_risks = [ { "value": contract.value, @@ -333,7 +332,7 @@ def enter_illiquidity(self, time): time: Type integer. The current time. No return value. This method is called when a firm does not have enough cash to pay all its obligations. It is only called from - the method self.effect_payments() which is called at the beginning of the self.iterate() method of this class. + the method self._effect_payments() which is called at the beginning of the self.iterate() method of this class. This method formalizes the bankruptcy through the method self.enter_bankruptcy().""" self.enter_bankruptcy(time) @@ -343,7 +342,7 @@ def enter_bankruptcy(self, time): time: Type integer. The current time. No return value. This method is used when a firm does not have enough cash to pay all its obligations. It is only called from - the method self.enter_illiquidity() which is only called from the method self.effect_payments(). This method + the method self.enter_illiquidity() which is only called from the method self._effect_payments(). This method dissolves the firm through the method self.dissolve().""" self.dissolve(time, "record_bankruptcy") @@ -765,6 +764,8 @@ def process_newrisks_insurer( # to insure it # Here it is check whether the portfolio is balanced or not if the risk (risk_to_insure) is # underwritten. Return True if it is balanced. False otherwise. + # QUERY: Why is the premium always the market premium? Isn't the setting of premiums an + # important part of the risk model? No if condition: contract = InsuranceContract( self, diff --git a/resume.py b/resume.py index c63acc2..4c59fc6 100644 --- a/resume.py +++ b/resume.py @@ -106,7 +106,7 @@ def main(): # TODO: this script should probably be an argument for start.py simulation.advance_round(t) # create new agents # TODO: write method for this; this code block is executed almost identically 4 times - if world.insurance_firm_enters_market(agent_type="InsuranceFirm"): + if world.firm_enters_market(agent_type="InsuranceFirm"): parameters = [np.random.choice(world.agent_parameters["insurancefirm"])] parameters[0]["id"] = world.get_unique_insurer_id() new_insurance_firm = simulation.build_agents( @@ -119,7 +119,7 @@ def main(): # TODO: this script should probably be an argument for start.py new_insurancefirm_pointer = new_insurance_firm world.accept_agents("insurancefirm", new_insurancefirm_pointer, time=t) - if world.insurance_firm_enters_market(agent_type="ReinsuranceFirm"): + if world.firm_enters_market(agent_type="ReinsuranceFirm"): parameters = [np.random.choice(world.agent_parameters["reinsurancefirm"])] parameters[0]["id"] = world.get_unique_reinsurer_id() new_reinsurance_firm = simulation.build_agents( diff --git a/start.py b/start.py index 7416c71..c20339c 100644 --- a/start.py +++ b/start.py @@ -35,7 +35,7 @@ def main( rc_event_damage, np_seed, random_seed, - save_iter, + save_iter: int, replic_ID, requested_logs=None, ): @@ -75,7 +75,7 @@ def main( for t in range(simulation_parameters["max_time"]): # create new agents # TODO: write method for this; this code block is executed almost identically 4 times # In fact this should probably all go in insurancesimulation.py, as part of simulation.iterate(t) - if simulation.insurance_firm_enters_market(agent_type="InsuranceFirm"): + if simulation.firm_enters_market(agent_type="InsuranceFirm"): parameters = [ np.random.choice(simulation.agent_parameters["insurancefirm"]) ] # QUERY Which of these should be used? @@ -97,7 +97,7 @@ def main( insurancefirms_group += new_insurance_firm simulation.accept_agents("insurancefirm", new_insurance_firm, time=t) - if simulation.insurance_firm_enters_market(agent_type="ReinsuranceFirm"): + if simulation.firm_enters_market(agent_type="ReinsuranceFirm"): parameters = [ np.random.choice(simulation.agent_parameters["reinsurancefirm"]) ] @@ -128,7 +128,7 @@ def main( # log data simulation.save_data() - if t % 50 == save_iter: + if t % save_iter == 0 and t > 0: save_simulation(t, simulation, simulation_parameters, exit_now=False) # finish simulation, write logs @@ -282,8 +282,8 @@ def save_simulation(t, sim, sim_param, exit_now=False): general_rc_event_damage[0], np_seeds[0], random_seeds[0], - filepath, save_iter, + filepath, ) replic_ID = filepath From 64810afccc97dc7ebda53344c88fd3cb72859b39 Mon Sep 17 00:00:00 2001 From: Chris Hughes Date: Fri, 12 Jul 2019 09:44:40 +0100 Subject: [PATCH 032/125] Fix simulation saving --- reinsurancecontract.py | 2 +- resume.py | 6 ++++-- start.py | 2 +- 3 files changed, 6 insertions(+), 4 deletions(-) diff --git a/reinsurancecontract.py b/reinsurancecontract.py index 959fc21..7fea4cd 100644 --- a/reinsurancecontract.py +++ b/reinsurancecontract.py @@ -24,7 +24,7 @@ def __init__( excess_fraction=None, reinsurance=0, ): - super(ReinsuranceContract, self).__init__( + super().__init__( insurer, properties, time, diff --git a/resume.py b/resume.py index c63acc2..76dba03 100644 --- a/resume.py +++ b/resume.py @@ -7,6 +7,8 @@ # import config file and apply configuration import isleconfig +# TODO: this whole module could be an argument for start.py + simulation_parameters = isleconfig.simulation_parameters replic_ID = None @@ -82,7 +84,7 @@ # main function -def main(): # TODO: this script should probably be an argument for start.py +def main(): with open("data/simulation_save.pkl", "br") as rfile: d = pickle.load(rfile) simulation = d["simulation"] @@ -105,7 +107,7 @@ def main(): # TODO: this script should probably be an argument for start.py # abce time step simulation.advance_round(t) - # create new agents # TODO: write method for this; this code block is executed almost identically 4 times + # create new agents # TODO: write method for this; this code block is executed almost identically 4 times if world.insurance_firm_enters_market(agent_type="InsuranceFirm"): parameters = [np.random.choice(world.agent_parameters["insurancefirm"])] parameters[0]["id"] = world.get_unique_insurer_id() diff --git a/start.py b/start.py index 7416c71..f368a01 100644 --- a/start.py +++ b/start.py @@ -128,7 +128,7 @@ def main( # log data simulation.save_data() - if t % 50 == save_iter: + if t % save_iter == 0 and t > 0: save_simulation(t, simulation, simulation_parameters, exit_now=False) # finish simulation, write logs From 1ccb7f118afa797e46c7bbabb4a3a2905b2fffd8 Mon Sep 17 00:00:00 2001 From: Chris Hughes Date: Fri, 12 Jul 2019 10:02:06 +0100 Subject: [PATCH 033/125] black.py --- calibration_conditions.py | 134 +++- calibrationscore.py | 33 +- catbond.py | 40 +- centralbank.py | 10 +- compute_profits_losses_from_cash.py | 8 +- condition_aux.py | 131 +++- distribution_wrapper_test.py | 16 +- distributionreinsurance.py | 64 +- distributiontruncated.py | 50 +- ensemble.py | 196 +++-- genericagent.py | 9 +- genericagentabce.py | 1 + insurancecontract.py | 45 +- insurancefirm.py | 278 +++++--- insurancesimulation.py | 668 ++++++++++++------ isleconfig.py | 146 ++-- listify.py | 16 +- logger.py | 125 ++-- metainsurancecontract.py | 95 ++- metainsuranceorg.py | 606 +++++++++++----- metaplotter.py | 272 +++++-- metaplotter_pl_timescale.py | 261 +++++-- ...lotter_pl_timescale_additional_measures.py | 314 ++++++-- plotter.py | 42 +- plotter_pl_timescale.py | 40 +- reinsurancecontract.py | 86 ++- reinsurancefirm.py | 4 +- resume.py | 127 +++- riskmodel.py | 211 ++++-- setup.py | 90 ++- start.py | 218 ++++-- visualisation.py | 378 +++++++--- visualization_network.py | 129 +++- 33 files changed, 3466 insertions(+), 1377 deletions(-) diff --git a/calibration_conditions.py b/calibration_conditions.py index 33bc6f7..94a8980 100644 --- a/calibration_conditions.py +++ b/calibration_conditions.py @@ -31,100 +31,158 @@ import condition_aux import isleconfig + def condition_stationary_state_cash(logobj): """Stationarity test for total cash""" - return condition_aux.condition_stationary_state(logobj.history_logs['total_cash']) - + return condition_aux.condition_stationary_state(logobj.history_logs["total_cash"]) + + def condition_stationary_state_excess_capital(logobj): """Stationarity test for total excess capital""" - return condition_aux.condition_stationary_state(logobj.history_logs['total_excess_capital']) + return condition_aux.condition_stationary_state( + logobj.history_logs["total_excess_capital"] + ) + def condition_stationary_state_profits_losses(logobj): """Stationarity test for total profits and losses""" - return condition_aux.condition_stationary_state(logobj.history_logs['total_profitslosses']) + return condition_aux.condition_stationary_state( + logobj.history_logs["total_profitslosses"] + ) + def condition_stationary_state_contracts(logobj): """Stationarity test for total number of contracts""" - return condition_aux.condition_stationary_state(logobj.history_logs['total_contracts']) + return condition_aux.condition_stationary_state( + logobj.history_logs["total_contracts"] + ) + def condition_stationary_state_rein_cash(logobj): """Stationarity test for total cash (reinsurers)""" - return condition_aux.condition_stationary_state(logobj.history_logs['total_reincash']) + return condition_aux.condition_stationary_state( + logobj.history_logs["total_reincash"] + ) + def condition_stationary_state_rein_excess_capital(logobj): """Stationarity test for total excess capital (reinsurers)""" - return condition_aux.condition_stationary_state(logobj.history_logs['total_reinexcess_capital']) + return condition_aux.condition_stationary_state( + logobj.history_logs["total_reinexcess_capital"] + ) + def condition_stationary_state_rein_profits_losses(logobj): """Stationarity test for total profits and losses (reinsurers)""" - return condition_aux.condition_stationary_state(logobj.history_logs['total_reinprofitslosses']) + return condition_aux.condition_stationary_state( + logobj.history_logs["total_reinprofitslosses"] + ) + def condition_stationary_state_rein_contracts(logobj): """Stationarity test for total number of reinsured contracts""" - return condition_aux.condition_stationary_state(logobj.history_logs['total_reincontracts']) + return condition_aux.condition_stationary_state( + logobj.history_logs["total_reincontracts"] + ) + def condition_stationary_state_market_premium(logobj): """Stationarity test for insurance market premium""" - return condition_aux.condition_stationary_state(logobj.history_logs['market_premium']) + return condition_aux.condition_stationary_state( + logobj.history_logs["market_premium"] + ) + def condition_stationary_state_rein_market_premium(logobj): """Stationarity test for reinsurance market premium""" - return condition_aux.condition_stationary_state(logobj.history_logs['market_reinpremium']) + return condition_aux.condition_stationary_state( + logobj.history_logs["market_reinpremium"] + ) -def condition_defaults_insurance(logobj): # TODO: develop this into a non-binary measure + +def condition_defaults_insurance( + logobj +): # TODO: develop this into a non-binary measure """Test for number of insurance bankruptcies (non zero, not all insurers)""" - #series = logobj.history_logs['total_operational'] - #if series[-1] != 0 and any(series[i]-series[i-1] < 0 for i in range(1,len(series))): - opseries = [logobj.history_logs["insurance_firms_cash"][-1][i][2] for i in \ - range(len(logobj.history_logs["insurance_firms_cash"][-1]))] + # series = logobj.history_logs['total_operational'] + # if series[-1] != 0 and any(series[i]-series[i-1] < 0 for i in range(1,len(series))): + opseries = [ + logobj.history_logs["insurance_firms_cash"][-1][i][2] + for i in range(len(logobj.history_logs["insurance_firms_cash"][-1])) + ] if any(opseries) and not all(opseries): return 1 else: return 0 -def condition_defaults_reinsurance(logobj): # TODO: develop this into a non-binary measure + +def condition_defaults_reinsurance( + logobj +): # TODO: develop this into a non-binary measure """Test for number of reinsurance bankruptcies (non zero, not all reinsurers)""" - #series = logobj.history_logs['total_reinoperational'] - #if series[-1] != 0 and any(series[i]-series[i-1] < 0 for i in range(1,len(series))): - opseries = [logobj.history_logs["reinsurance_firms_cash"][-1][i][2] for i in \ - range(len(logobj.history_logs["reinsurance_firms_cash"][-1]))] + # series = logobj.history_logs['total_reinoperational'] + # if series[-1] != 0 and any(series[i]-series[i-1] < 0 for i in range(1,len(series))): + opseries = [ + logobj.history_logs["reinsurance_firms_cash"][-1][i][2] + for i in range(len(logobj.history_logs["reinsurance_firms_cash"][-1])) + ] if any(opseries) and not all(opseries): return 1 else: return 0 + def condition_insurance_coverage(logobj): """Test for insurance coverage close to 100%""" - return logobj.history_logs['total_contracts'][-1] * 1. / isleconfig.simulation_parameters["no_risks"] + return ( + logobj.history_logs["total_contracts"][-1] + * 1.0 + / isleconfig.simulation_parameters["no_risks"] + ) + def condition_reinsurance_coverage(logobj, minimum=0.6): """Test for reinsurance coverage close to some minimum that may be less than 100% (default 60%)""" - score = logobj.history_logs['total_reincontracts'][-1] * 1. / (minimum * logobj.history_logs['total_contracts'][-1]) - score = 1 if score>1 else score + score = ( + logobj.history_logs["total_reincontracts"][-1] + * 1.0 + / (minimum * logobj.history_logs["total_contracts"][-1]) + ) + score = 1 if score > 1 else score return score -def condition_insurance_firm_dist(logobj): + +def condition_insurance_firm_dist(logobj): """Empirical calibration test for insurance firm size (total assets; cash)""" """filter operational firms""" - #dist = [logobj.history_logs["insurance_firms_cash"][-1][i][0] for i in range(len(logobj.history_logs["insurance_firms_cash"])) if \ + # dist = [logobj.history_logs["insurance_firms_cash"][-1][i][0] for i in range(len(logobj.history_logs["insurance_firms_cash"])) if \ # logobj.history_logs["insurance_firms_cash"][-1][i][0] > isleconfig.simulation_parameters["cash_permanency_limit"]] - dist = [logobj.history_logs["insurance_firms_cash"][-1][i][0] for i in \ - range(len(logobj.history_logs["insurance_firms_cash"][-1])) if \ - logobj.history_logs["insurance_firms_cash"][-1][i][2]] + dist = [ + logobj.history_logs["insurance_firms_cash"][-1][i][0] + for i in range(len(logobj.history_logs["insurance_firms_cash"][-1])) + if logobj.history_logs["insurance_firms_cash"][-1][i][2] + ] """run two-sided KS test""" - KS_statistic, p_value = stats.ks_2samp(condition_aux.scaler(condition_aux.insurance_firm_sizes_empirical_2017), - condition_aux.scaler(dist)) + KS_statistic, p_value = stats.ks_2samp( + condition_aux.scaler(condition_aux.insurance_firm_sizes_empirical_2017), + condition_aux.scaler(dist), + ) return p_value -def condition_reinsurance_firm_dist(logobj): + +def condition_reinsurance_firm_dist(logobj): """Empirical calibration test for reinsurance firm size (total assets; cash)""" """filter operational firms""" - #dist = [logobj.history_logs["reinsurance_firms_cash"][-1][i][0] for i in range(len(logobj.history_logs["reinsurance_firms_cash"])) if \ + # dist = [logobj.history_logs["reinsurance_firms_cash"][-1][i][0] for i in range(len(logobj.history_logs["reinsurance_firms_cash"])) if \ # logobj.history_logs["reinsurance_firms_cash"][-1][i][0] > isleconfig.simulation_parameters["cash_permanency_limit"]] - dist = [logobj.history_logs["reinsurance_firms_cash"][-1][i][0] for i in \ - range(len(logobj.history_logs["reinsurance_firms_cash"][-1])) if - logobj.history_logs["reinsurance_firms_cash"][-1][i][2]] + dist = [ + logobj.history_logs["reinsurance_firms_cash"][-1][i][0] + for i in range(len(logobj.history_logs["reinsurance_firms_cash"][-1])) + if logobj.history_logs["reinsurance_firms_cash"][-1][i][2] + ] """run two-sided KS test""" - KS_statistic, p_value = stats.ks_2samp(condition_aux.scaler(condition_aux.reinsurance_firm_sizes_empirical_2017), - condition_aux.scaler(dist)) + KS_statistic, p_value = stats.ks_2samp( + condition_aux.scaler(condition_aux.reinsurance_firm_sizes_empirical_2017), + condition_aux.scaler(dist), + ) return p_value diff --git a/calibrationscore.py b/calibrationscore.py index 32d870c..b0a86a6 100644 --- a/calibrationscore.py +++ b/calibrationscore.py @@ -5,9 +5,10 @@ from inspect import getmembers, isfunction import numpy as np -import calibration_conditions # Test functions +import calibration_conditions # Test functions -class CalibrationScore(): + +class CalibrationScore: def __init__(self, L): """Constructor method. Arguments: @@ -17,31 +18,41 @@ def __init__(self, L): """Assert sanity of log and save log.""" assert isinstance(L, logger.Logger) self.logger = L - + """Prepare list of calibration tests from calibration_conditions.py""" - self.conditions = [f for f in getmembers(calibration_conditions) if isfunction(f[1])] - + self.conditions = [ + f for f in getmembers(calibration_conditions) if isfunction(f[1]) + ] + """Prepare calibration score variable.""" self.calibration_score = None - + def test_all(self): """Method to test all calibration tests. No arguments. Returns combined calibration score as float \in [0,1].""" - + """Compute score components""" - scores = {condition[0]: condition[1](self.logger) for condition in self.conditions} + scores = { + condition[0]: condition[1](self.logger) for condition in self.conditions + } """Print components""" print("\n") for cond_name, score in scores.items(): print("{0:47s}: {1:8f}".format(cond_name, score)) """Compute combined score""" - self.calibration_score = self.combine_scores(np.array([*scores.values()], dtype=object)) + self.calibration_score = self.combine_scores( + np.array([*scores.values()], dtype=object) + ) """Print combined score""" - print("\n Total calibration score: {0:8f}".format(self.calibration_score)) + print( + "\n Total calibration score: {0:8f}".format( + self.calibration_score + ) + ) """Return""" return self.calibration_score - + def combine_scores(self, slist): """Method to combine calibration score components. Combination is additive (mean). Change the function for other combination methods (multiplicative or minimum). diff --git a/catbond.py b/catbond.py index 62e02f5..65281dd 100644 --- a/catbond.py +++ b/catbond.py @@ -1,4 +1,3 @@ - import isleconfig import numpy as np import scipy.stats @@ -38,23 +37,34 @@ def iterate(self, time): self.simulation.bank.award_interest(self, self.cash) self.effect_payments(time) if isleconfig.verbose: - print(time, ":", self.id, len(self.underwritten_contracts), self.cash, self.operational) - + print( + time, + ":", + self.id, + len(self.underwritten_contracts), + self.cash, + self.operational, + ) + """mature contracts""" print("Number of underwritten contracts ", len(self.underwritten_contracts)) - maturing = [contract for contract in self.underwritten_contracts if contract.expiration <= time] + maturing = [ + contract + for contract in self.underwritten_contracts + if contract.expiration <= time + ] for contract in maturing: self.underwritten_contracts.remove(contract) contract.mature(time) [contract.check_payment_due(time) for contract in self.underwritten_contracts] - + if self.underwritten_contracts == []: self.mature_bond() - else: #TODO: dividend should only be payed according to pre-arranged schedule, and only if no risk events have materialized so far + else: # TODO: dividend should only be payed according to pre-arranged schedule, and only if no risk events have materialized so far if self.operational: self.pay_dividends(time) - + def set_owner(self, owner): """Method to set owner of the Cat Bond. Accepts: @@ -63,7 +73,7 @@ def set_owner(self, owner): self.owner = owner if isleconfig.verbose: print("SOLD") - + def set_contract(self, contract): """Method to record new instances of CatBonds. Accepts: @@ -71,7 +81,7 @@ def set_contract(self, contract): No return values Only one contract is ever added to the list of underwritten contracts as each CatBond is a contract itself.""" self.underwritten_contracts.append(contract) - + def mature_bond(self): """Method to mature CatBond. No accepted values @@ -79,10 +89,14 @@ def mature_bond(self): When the catbond contract matures this is called which pays the value of the catbond to the simulation, and is then deleted from the list of agents.""" if self.operational: - obligation = {"amount": self.cash, "recipient": self.simulation, "due_time": 1, "purpose": 'mature'} + obligation = { + "amount": self.cash, + "recipient": self.simulation, + "due_time": 1, + "purpose": "mature", + } self.pay(obligation) self.simulation.delete_agents("catbond", [self]) self.operational = False - else: print('CatBond is not operational so cannot mature') - - + else: + print("CatBond is not operational so cannot mature") diff --git a/centralbank.py b/centralbank.py index bf903ae..969d319 100644 --- a/centralbank.py +++ b/centralbank.py @@ -6,7 +6,7 @@ def __init__(self): """Constructor Method. No accepted arguments. Constructs the CentralBank class. This class is currently only used to award interest payments.""" - self.interest_rate = simulation_parameters['interest_rate'] + self.interest_rate = simulation_parameters["interest_rate"] self.inflation_target = 0.02 self.actual_inflation = 0 self.onemonth_CPI = 0 @@ -57,6 +57,10 @@ def calculate_inflation(self, current_price, time): if time < 13: self.actual_inflation = self.inflation_target else: - self.onemonth_CPI = (current_price - self.prices_list[-2])/self.prices_list[-2] - self.twelvemonth_CPI = (current_price - self.prices_list[-13])/self.prices_list[-13] + self.onemonth_CPI = ( + current_price - self.prices_list[-2] + ) / self.prices_list[-2] + self.twelvemonth_CPI = ( + current_price - self.prices_list[-13] + ) / self.prices_list[-13] self.actual_inflation = self.twelvemonth_CPI diff --git a/compute_profits_losses_from_cash.py b/compute_profits_losses_from_cash.py index ed6691a..865f749 100644 --- a/compute_profits_losses_from_cash.py +++ b/compute_profits_losses_from_cash.py @@ -9,11 +9,9 @@ infile.close() filename = "data/" + r + "_" + ft + "profitslosses.dat" outfile = open(filename, "w") - + for series in data: - outputdata = [series[i]-series[i-1] for i in range(1, len(series))] + outputdata = [series[i] - series[i - 1] for i in range(1, len(series))] outfile.write(str(outputdata) + "\n") - - outfile.close() - + outfile.close() diff --git a/condition_aux.py b/condition_aux.py index 9ffab5b..eefc0f5 100644 --- a/condition_aux.py +++ b/condition_aux.py @@ -6,20 +6,98 @@ """Data""" """Bloomberg size data for US firms""" -insurance_firm_sizes_empirical_2017 = [42.4701, 108.0418, 110.2641, 114.437, 130.2988, 133.674, 146.438, 152.3354, - 239.032, 337.689, 375.914, 376.988, 395.859, 436.191, 482.503, 585.824, 667.849, - 842.264, 894.848, 896.227, 904.873, 1231.126, 1357.016, 1454.999, 1518.236, - 1665.859, 1681.94, 1737.9198, 1771.21, 1807.279, 1989.742, 2059.921, 2385.485, - 2756.695, 2947.244, 3014.3, 3659.2, 3840.1, 4183.431, 4929.197, 5101.323, - 5224.622, 5900.881, 7686.431, 8376.2, 8439.743, 8764.0, 9095.0, 11198.34, - 14433.0, 15469.6, 19403.5, 21843.0, 23192.374, 24299.917, 25218.63, 31843.0, - 32051.658, 32805.016, 38701.2, 56567.0, 60658.0, 79586.0, 103483.0, 112422.0, - 167022.0, 225260.0, 498301.0, 702095.0] -reinsurance_firm_sizes_empirical_2017 = [396.898, 627.808, 6644.189, 15226.131, 25384.317, 23591.792, 3357.393, - 13606.422, 4671.794, 614.121, 60514.818, 24760.177, 2001.669, 182.2, 12906.4] +insurance_firm_sizes_empirical_2017 = [ + 42.4701, + 108.0418, + 110.2641, + 114.437, + 130.2988, + 133.674, + 146.438, + 152.3354, + 239.032, + 337.689, + 375.914, + 376.988, + 395.859, + 436.191, + 482.503, + 585.824, + 667.849, + 842.264, + 894.848, + 896.227, + 904.873, + 1231.126, + 1357.016, + 1454.999, + 1518.236, + 1665.859, + 1681.94, + 1737.9198, + 1771.21, + 1807.279, + 1989.742, + 2059.921, + 2385.485, + 2756.695, + 2947.244, + 3014.3, + 3659.2, + 3840.1, + 4183.431, + 4929.197, + 5101.323, + 5224.622, + 5900.881, + 7686.431, + 8376.2, + 8439.743, + 8764.0, + 9095.0, + 11198.34, + 14433.0, + 15469.6, + 19403.5, + 21843.0, + 23192.374, + 24299.917, + 25218.63, + 31843.0, + 32051.658, + 32805.016, + 38701.2, + 56567.0, + 60658.0, + 79586.0, + 103483.0, + 112422.0, + 167022.0, + 225260.0, + 498301.0, + 702095.0, +] +reinsurance_firm_sizes_empirical_2017 = [ + 396.898, + 627.808, + 6644.189, + 15226.131, + 25384.317, + 23591.792, + 3357.393, + 13606.422, + 4671.794, + 614.121, + 60514.818, + 24760.177, + 2001.669, + 182.2, + 12906.4, +] """Functions""" + def condition_stationary_state(series): """Stationarity test function for time series. Tests if the mean of the last 25% of the time series is within 1-2 standard deviation of the mean of the middle section (between 25% and 75% of the time series). The first @@ -29,24 +107,29 @@ def condition_stationary_state(series): Returns: Calibration score between 0 and 1. Is 1 if last 25% are within one standard deviation, between 0 and 1 if they are between 1 and 2 standard deviations, 0 otherwise.""" - + """Compute means and standard deviation""" - mean_reference = np.mean(series[int(len(series)*.25):int(len(series)*.75)]) - std_reference = np.std(series[int(len(series)*.25):int(len(series)*.75)]) - mean_test = np.mean(series[int(len(series)*.75):int(len(series)*1.)]) - + mean_reference = np.mean(series[int(len(series) * 0.25) : int(len(series) * 0.75)]) + std_reference = np.std(series[int(len(series) * 0.25) : int(len(series) * 0.75)]) + mean_test = np.mean(series[int(len(series) * 0.75) : int(len(series) * 1.0)]) + """Compute score""" score = 1 + (np.abs(mean_test - mean_reference) - std_reference) / std_reference - score = 1 if score>1 else score - score = 0 if score<0 else score - + score = 1 if score > 1 else score + score = 0 if score < 0 else score + """Set score to one if standard deviation is zero""" - if score == np.nan and np.std(series[int(len(series)*.25):int(len(series)*.75)]) == 0: + if ( + score == np.nan + and np.std(series[int(len(series) * 0.25) : int(len(series) * 0.75)]) == 0 + ): score = 1 return score - -def scaler(series): # TODO: find a better way to scale heavy-tailed distributions than to use standard score scaling on logs + +def scaler( + series +): # TODO: find a better way to scale heavy-tailed distributions than to use standard score scaling on logs """Function to do a standard score scaling of the log of a heavy-tailed distribution. This is used to calibrate distributions where the unit is not important (distributions of sizes of firms e.g.). This would be perfectly appropriate for lognormal distributions, but should work reasonably well for calibration of other heavy-tailed @@ -56,10 +139,10 @@ def scaler(series): # TODO: find a better way to scale heavy-tailed distribution Returns: Calibratied series.""" series = np.asarray(series) - assert (series>1).all() + assert (series > 1).all() logseries = np.log(series) mean = np.mean(logseries) std = np.std(logseries) - z = (logseries - mean)/std + z = (logseries - mean) / std newseries = np.exp(z) return newseries diff --git a/distribution_wrapper_test.py b/distribution_wrapper_test.py index b4bfa97..81734f9 100644 --- a/distribution_wrapper_test.py +++ b/distribution_wrapper_test.py @@ -5,14 +5,20 @@ import pdb non_truncated_dist = scipy.stats.pareto(b=2, loc=0, scale=0.5) -truncated_dist = TruncatedDistWrapper(lower_bound=0.6, upper_bound=1., dist=non_truncated_dist) -reinsurance_dist = ReinsuranceDistWrapper(lower_bound=0.85, upper_bound=0.95, dist=truncated_dist) +truncated_dist = TruncatedDistWrapper( + lower_bound=0.6, upper_bound=1.0, dist=non_truncated_dist +) +reinsurance_dist = ReinsuranceDistWrapper( + lower_bound=0.85, upper_bound=0.95, dist=truncated_dist +) x1 = np.linspace(non_truncated_dist.ppf(0.01), non_truncated_dist.ppf(0.99), 100) -x2 = np.linspace(truncated_dist.ppf(0.01), truncated_dist.ppf(1.), 100) -x3 = np.linspace(reinsurance_dist.ppf(0.01), reinsurance_dist.ppf(1.), 100) +x2 = np.linspace(truncated_dist.ppf(0.01), truncated_dist.ppf(1.0), 100) +x3 = np.linspace(reinsurance_dist.ppf(0.01), reinsurance_dist.ppf(1.0), 100) x_val_1 = reinsurance_dist.lower_bound -x_val_2 = truncated_dist.upper_bound - (reinsurance_dist.upper_bound - reinsurance_dist.lower_bound) +x_val_2 = truncated_dist.upper_bound - ( + reinsurance_dist.upper_bound - reinsurance_dist.lower_bound +) x_val_3 = reinsurance_dist.upper_bound x_val_4 = truncated_dist.upper_bound diff --git a/distributionreinsurance.py b/distributionreinsurance.py index fd85eb3..8d9dcb2 100644 --- a/distributionreinsurance.py +++ b/distributionreinsurance.py @@ -4,7 +4,8 @@ import scipy import pdb -class ReinsuranceDistWrapper(): + +class ReinsuranceDistWrapper: def __init__(self, dist, lower_bound=None, upper_bound=None): assert lower_bound is not None or upper_bound is not None self.dist = dist @@ -17,57 +18,72 @@ def __init__(self, dist, lower_bound=None, upper_bound=None): assert self.upper_bound > self.lower_bound self.redistributed_share = dist.cdf(upper_bound) - dist.cdf(lower_bound) - def pdf(self, x): x = np.array(x, ndmin=1) - r = map(lambda Y: self.dist.pdf(Y) if Y < self.lower_bound \ - else np.inf if Y==self.lower_bound \ - else self.dist.pdf(Y + self.upper_bound - self.lower_bound), x) + r = map( + lambda Y: self.dist.pdf(Y) + if Y < self.lower_bound + else np.inf + if Y == self.lower_bound + else self.dist.pdf(Y + self.upper_bound - self.lower_bound), + x, + ) r = np.array(list(r)) if len(r.flatten()) == 1: r = float(r) return r - + def cdf(self, x): x = np.array(x, ndmin=1) - r = map(lambda Y: self.dist.cdf(Y) if Y < self.lower_bound \ - else self.dist.cdf(Y + self.upper_bound - self.lower_bound), x) + r = map( + lambda Y: self.dist.cdf(Y) + if Y < self.lower_bound + else self.dist.cdf(Y + self.upper_bound - self.lower_bound), + x, + ) r = np.array(list(r)) if len(r.flatten()) == 1: r = float(r) return r - + def ppf(self, x): x = np.array(x, ndmin=1) assert (x >= 0).all() and (x <= 1).all() - r = map(lambda Y: self.dist.ppf(Y) if Y <= self.dist.cdf(self.lower_bound) \ - else self.dist.ppf(self.dist.cdf(self.lower_bound)) if Y <= self.dist.cdf(self.upper_bound) \ - else self.dist.ppf(Y) - self.upper_bound + self.lower_bound, x) + r = map( + lambda Y: self.dist.ppf(Y) + if Y <= self.dist.cdf(self.lower_bound) + else self.dist.ppf(self.dist.cdf(self.lower_bound)) + if Y <= self.dist.cdf(self.upper_bound) + else self.dist.ppf(Y) - self.upper_bound + self.lower_bound, + x, + ) r = np.array(list(r)) if len(r.flatten()) == 1: r = float(r) return r - + def rvs(self, size=1): sample = self.dist.rvs(size=size) - sample1 = sample[sample<=self.lower_bound] - sample2 = sample[sample>self.lower_bound] - sample3 = sample2[sample2>=self.upper_bound] - sample2 = sample2[sample2 self.lower_bound] + sample3 = sample2[sample2 >= self.upper_bound] + sample2 = sample2[sample2 < self.upper_bound] + sample2 = np.ones(len(sample2)) * self.lower_bound - sample3 = sample3 -self.upper_bound + self.lower_bound - - sample = np.append(np.append(sample1,sample2),sample3) + sample3 = sample3 - self.upper_bound + self.lower_bound + + sample = np.append(np.append(sample1, sample2), sample3) return sample[:size] if __name__ == "__main__": non_truncated = scipy.stats.pareto(b=2, loc=0, scale=0.5) - #truncated = ReinsuranceDistWrapper(lower_bound=0, upper_bound=1, dist=non_truncated) - truncated = ReinsuranceDistWrapper(lower_bound=0.9, upper_bound=1.1, dist=non_truncated) + # truncated = ReinsuranceDistWrapper(lower_bound=0, upper_bound=1, dist=non_truncated) + truncated = ReinsuranceDistWrapper( + lower_bound=0.9, upper_bound=1.1, dist=non_truncated + ) x = np.linspace(non_truncated.ppf(0.01), non_truncated.ppf(0.99), 100) x2 = np.linspace(truncated.ppf(0.01), truncated.ppf(0.99), 100) - #pdb.set_trace() + # pdb.set_trace() diff --git a/distributiontruncated.py b/distributiontruncated.py index f0c36f3..18b8552 100644 --- a/distributiontruncated.py +++ b/distributiontruncated.py @@ -4,55 +4,73 @@ import scipy.integrate -class TruncatedDistWrapper(): +class TruncatedDistWrapper: def __init__(self, dist, lower_bound=0, upper_bound=1): self.dist = dist self.normalizing_factor = dist.cdf(upper_bound) - dist.cdf(lower_bound) self.lower_bound = lower_bound self.upper_bound = upper_bound assert self.upper_bound > self.lower_bound - + def pdf(self, x): x = np.array(x, ndmin=1) - r = map(lambda Y: self.dist.pdf(Y) / self.normalizing_factor \ - if (Y >= self.lower_bound and Y <= self.upper_bound) else 0, x) + r = map( + lambda Y: self.dist.pdf(Y) / self.normalizing_factor + if (Y >= self.lower_bound and Y <= self.upper_bound) + else 0, + x, + ) r = np.array(list(r)) if len(r.flatten()) == 1: r = float(r) return r - + def cdf(self, x): x = np.array(x, ndmin=1) - r = map(lambda Y: 0 if Y < self.lower_bound else 1 if Y > self.upper_bound \ - else (self.dist.cdf(Y) - self.dist.cdf(self.lower_bound))/ self.normalizing_factor, x) + r = map( + lambda Y: 0 + if Y < self.lower_bound + else 1 + if Y > self.upper_bound + else (self.dist.cdf(Y) - self.dist.cdf(self.lower_bound)) + / self.normalizing_factor, + x, + ) r = np.array(list(r)) if len(r.flatten()) == 1: r = float(r) return r - + def ppf(self, x): x = np.array(x, ndmin=1) assert (x >= 0).all() and (x <= 1).all() - return self.dist.ppf(x * self.normalizing_factor + self.dist.cdf(self.lower_bound)) + return self.dist.ppf( + x * self.normalizing_factor + self.dist.cdf(self.lower_bound) + ) def rvs(self, size=1): init_sample_size = int(ceil(size / self.normalizing_factor * 1.1)) sample = self.dist.rvs(size=init_sample_size) - sample = sample[sample>=self.lower_bound] - sample = sample[sample<=self.upper_bound] + sample = sample[sample >= self.lower_bound] + sample = sample[sample <= self.upper_bound] while len(sample) < size: sample = np.append(sample, self.rvs(size - len(sample))) - return sample[:size] - + return sample[:size] + def mean(self): - mean_estimate, mean_error = scipy.integrate.quad(lambda Y: Y*self.pdf(Y), self.lower_bound, self.upper_bound) + mean_estimate, mean_error = scipy.integrate.quad( + lambda Y: Y * self.pdf(Y), self.lower_bound, self.upper_bound + ) return mean_estimate + if __name__ == "__main__": non_truncated = scipy.stats.pareto(b=2, loc=0, scale=0.5) - truncated = TruncatedDistWrapper(lower_bound=0.55, upper_bound=1., dist=non_truncated) + truncated = TruncatedDistWrapper( + lower_bound=0.55, upper_bound=1.0, dist=non_truncated + ) x = np.linspace(non_truncated.ppf(0.01), non_truncated.ppf(0.99), 100) x2 = np.linspace(truncated.ppf(0.01), truncated.ppf(0.99), 100) - + print(truncated.mean()) diff --git a/ensemble.py b/ensemble.py index 442d340..4af37b7 100644 --- a/ensemble.py +++ b/ensemble.py @@ -1,6 +1,6 @@ -#This script allows to launch an ensemble of simulations for different number of risks models. -#It can be run locally if no argument is passed when called from the terminal. -#It can be run in the cloud if it is passed as argument the server that will be used. +# This script allows to launch an ensemble of simulations for different number of risks models. +# It can be run locally if no argument is passed when called from the terminal. +# It can be run in the cloud if it is passed as argument the server that will be used. import sys import random import os @@ -16,11 +16,10 @@ from sandman2.api import operation, Session - @operation def agg(*outputs): # do nothing - return outputs + return outputs def rake(hostname): @@ -29,58 +28,62 @@ def rake(hostname): """Configuration of the ensemble""" - replications = 70 # Number of replications to be carried out for each configuration. Usually one risk model, two risk models, three risk models, four risk models. + replications = ( + 70 + ) # Number of replications to be carried out for each configuration. Usually one risk model, two risk models, three risk models, four risk models. model = start.main - m = operation(model, include_modules = True) + m = operation(model, include_modules=True) - riskmodels = [1,2,3,4] # The number of risk models that will be used. + riskmodels = [1, 2, 3, 4] # The number of risk models that will be used. parameters = isleconfig.simulation_parameters - nums = {'1': 'one', - '2': 'two', - '3': 'three', - '4': 'four', - '5': 'five', - '6': 'six', - '7': 'seven', - '8': 'eight', - '9': 'nine'} + nums = { + "1": "one", + "2": "two", + "3": "three", + "4": "four", + "5": "five", + "6": "six", + "7": "seven", + "8": "eight", + "9": "nine", + } """Configure the return values and corresponding file suffixes where they should be saved""" - requested_logs = {'total_cash': '_cash.dat', - 'total_excess_capital': '_excess_capital.dat', - 'total_profitslosses': '_profitslosses.dat', - 'total_contracts': '_contracts.dat', - 'total_operational': '_operational.dat', - 'total_reincash': '_reincash.dat', - 'total_reinexcess_capital': '_reinexcess_capital.dat', - 'total_reinprofitslosses': '_reinprofitslosses.dat', - 'total_reincontracts': '_reincontracts.dat', - 'total_reinoperational': '_reinoperational.dat', - 'total_catbondsoperational': '_total_catbondsoperational.dat', - 'market_premium': '_premium.dat', - 'market_reinpremium': '_reinpremium.dat', - 'cumulative_bankruptcies': '_cumulative_bankruptcies.dat', - 'cumulative_market_exits': '_cumulative_market_exits', # TODO: correct filename - 'cumulative_unrecovered_claims': '_cumulative_unrecovered_claims.dat', - 'cumulative_claims': '_cumulative_claims.dat', - 'insurance_firms_cash': '_insurance_firms_cash.dat', - 'reinsurance_firms_cash': '_reinsurance_firms_cash.dat', - 'market_diffvar': '_market_diffvar.dat', - 'rc_event_schedule_initial': '_rc_event_schedule.dat', - 'rc_event_damage_initial': '_rc_event_damage.dat', - 'number_riskmodels': '_number_riskmodels.dat' - } - + requested_logs = { + "total_cash": "_cash.dat", + "total_excess_capital": "_excess_capital.dat", + "total_profitslosses": "_profitslosses.dat", + "total_contracts": "_contracts.dat", + "total_operational": "_operational.dat", + "total_reincash": "_reincash.dat", + "total_reinexcess_capital": "_reinexcess_capital.dat", + "total_reinprofitslosses": "_reinprofitslosses.dat", + "total_reincontracts": "_reincontracts.dat", + "total_reinoperational": "_reinoperational.dat", + "total_catbondsoperational": "_total_catbondsoperational.dat", + "market_premium": "_premium.dat", + "market_reinpremium": "_reinpremium.dat", + "cumulative_bankruptcies": "_cumulative_bankruptcies.dat", + "cumulative_market_exits": "_cumulative_market_exits", # TODO: correct filename + "cumulative_unrecovered_claims": "_cumulative_unrecovered_claims.dat", + "cumulative_claims": "_cumulative_claims.dat", + "insurance_firms_cash": "_insurance_firms_cash.dat", + "reinsurance_firms_cash": "_reinsurance_firms_cash.dat", + "market_diffvar": "_market_diffvar.dat", + "rc_event_schedule_initial": "_rc_event_schedule.dat", + "rc_event_damage_initial": "_rc_event_damage.dat", + "number_riskmodels": "_number_riskmodels.dat", + } + if isleconfig.slim_log: - for name in ['insurance_firms_cash', 'reinsurance_firms_cash']: + for name in ["insurance_firms_cash", "reinsurance_firms_cash"]: del requested_logs[name] - - assert "number_riskmodels" in requested_logs + assert "number_riskmodels" in requested_logs """Configure log directory and ensure that the directory exists""" dir_prefix = "/data/" @@ -96,78 +99,123 @@ def rake(hostname): if os.path.exists(filename): os.remove(filename) - """Setup of the simulations""" - setup = SetupSim() # Here the setup for the simulation is done. - [general_rc_event_schedule, general_rc_event_damage, np_seeds, random_seeds] = setup.obtain_ensemble(replications) #Since this script is used to carry out simulations in the cloud will usually have more than 1 replication.. - save_iter = isleconfig.simulation_parameters["max_time"] + 2 # never save simulation state in ensemble runs (resuming is impossible anyway) - - for i in riskmodels: #In this loop the parameters, schedules and random seeds for every run are prepared. Different risk models will be run with the same schedule, damage size and random seed for a fair comparison. - - simulation_parameters = copy.copy(parameters) #Here the parameters used for the simulation are loaded. Clone is needed otherwise all the runs will be carried out with the last number of thee loop. - simulation_parameters["no_riskmodels"] = i #Since we want to obtain ensembles for different number of risk models, we vary here the number of risks models. - job = [m(simulation_parameters, general_rc_event_schedule[x], general_rc_event_damage[x], np_seeds[x], random_seeds[x], save_iter, list(requested_logs.keys())) for x in range(replications)] #Here is assembled each job with the corresponding: simulation parameters, time events, damage events, seeds, simulation state save interval (never, i.e. longer than max_time), and list of requested logs. - jobs.append(job) # All jobs are collected in the jobs list. - + setup = SetupSim() # Here the setup for the simulation is done. + [ + general_rc_event_schedule, + general_rc_event_damage, + np_seeds, + random_seeds, + ] = setup.obtain_ensemble( + replications + ) # Since this script is used to carry out simulations in the cloud will usually have more than 1 replication.. + save_iter = ( + isleconfig.simulation_parameters["max_time"] + 2 + ) # never save simulation state in ensemble runs (resuming is impossible anyway) + + for ( + i + ) in ( + riskmodels + ): # In this loop the parameters, schedules and random seeds for every run are prepared. Different risk models will be run with the same schedule, damage size and random seed for a fair comparison. + + simulation_parameters = copy.copy( + parameters + ) # Here the parameters used for the simulation are loaded. Clone is needed otherwise all the runs will be carried out with the last number of thee loop. + simulation_parameters[ + "no_riskmodels" + ] = ( + i + ) # Since we want to obtain ensembles for different number of risk models, we vary here the number of risks models. + job = [ + m( + simulation_parameters, + general_rc_event_schedule[x], + general_rc_event_damage[x], + np_seeds[x], + random_seeds[x], + save_iter, + list(requested_logs.keys()), + ) + for x in range(replications) + ] # Here is assembled each job with the corresponding: simulation parameters, time events, damage events, seeds, simulation state save interval (never, i.e. longer than max_time), and list of requested logs. + jobs.append(job) # All jobs are collected in the jobs list. """Here the jobs are submitted""" with Session(host=hostname, default_cb_to_stdout=True) as sess: - for job in jobs: #If there are 4 risk models jobs will be a list with 4 elements. - + for ( + job + ) in jobs: # If there are 4 risk models jobs will be a list with 4 elements. + """Run simulation and obtain result""" result = sess.submit(job) - - + """find number of riskmodels from log""" delistified_result = [listify.delistify(list(res)) for res in result] - #nrmidx = result[0][-1].index("number_riskmodels") - #nrm = result[0][nrmidx] + # nrmidx = result[0][-1].index("number_riskmodels") + # nrm = result[0][nrmidx] nrm = delistified_result[0]["number_riskmodels"] """These are the files created to collect the results""" wfiles_dict = {} logfile_dict = {} - + for name in requested_logs.keys(): if "rc_event" in name or "number_riskmodels" in name: - logfile_dict[name] = os.getcwd() + dir_prefix + "check_" + str(nums[str(nrm)]) + requested_logs[name] + logfile_dict[name] = ( + os.getcwd() + + dir_prefix + + "check_" + + str(nums[str(nrm)]) + + requested_logs[name] + ) elif "firms_cash" in name: - logfile_dict[name] = os.getcwd() + dir_prefix + "record_" + str(nums[str(nrm)]) + requested_logs[name] + logfile_dict[name] = ( + os.getcwd() + + dir_prefix + + "record_" + + str(nums[str(nrm)]) + + requested_logs[name] + ) else: - logfile_dict[name] = os.getcwd() + dir_prefix + str(nums[str(nrm)]) + requested_logs[name] + logfile_dict[name] = ( + os.getcwd() + + dir_prefix + + str(nums[str(nrm)]) + + requested_logs[name] + ) for name in logfile_dict: wfiles_dict[name] = open(logfile_dict[name], "w") - """Recreate logger object locally and save logs""" - + """Create local object""" L = logger.Logger() for i in range(len(job)): """Populate logger object with logs obtained from remote simulation run""" L.restore_logger_object(list(result[i])) - + """Save logs as dict (to _history_logs.dat)""" L.save_log(True) - + """Save logs as indivitual files""" for name in logfile_dict: wfiles_dict[name].write(str(delistified_result[i][name]) + "\n") - + """Once the data is stored in disk the files are closed""" for name in logfile_dict: wfiles_dict[name].close() del wfiles_dict[name] -if __name__ == '__main__': +if __name__ == "__main__": host = None if len(sys.argv) > 1: - host = sys.argv[1] #The server is passed as an argument. + host = sys.argv[1] # The server is passed as an argument. rake(host) diff --git a/genericagent.py b/genericagent.py index 8e58efe..4985372 100644 --- a/genericagent.py +++ b/genericagent.py @@ -1,7 +1,8 @@ - -class GenericAgent(): +class GenericAgent: def __init__(self, *args, **kwargs): self.init(*args, **kwargs) - + def init(*args, **kwargs): - assert False, "Error: GenericAgent init method should have been overridden but was not." + assert ( + False + ), "Error: GenericAgent init method should have been overridden but was not." diff --git a/genericagentabce.py b/genericagentabce.py index c0eb884..28d9531 100644 --- a/genericagentabce.py +++ b/genericagentabce.py @@ -1,4 +1,5 @@ import abce + class GenericAgent(abce.Agent): pass diff --git a/insurancecontract.py b/insurancecontract.py index d331a8d..55a8fc3 100644 --- a/insurancecontract.py +++ b/insurancecontract.py @@ -11,11 +11,35 @@ class InsuranceContract(MetaInsuranceContract): The signature of this class' constructor is the same as that of the InsuranceContract constructor. The class has two methods (explode, mature) that overwrite methods in InsuranceContract.""" - def __init__(self, insurer, properties, time, premium, runtime, payment_period, expire_immediately, initial_VaR=0.,\ - insurancetype="proportional", deductible_fraction=None, excess_fraction=None, reinsurance=0): - super(InsuranceContract, self).__init__(insurer, properties, time, premium, runtime, payment_period, \ - expire_immediately, initial_VaR, insurancetype, deductible_fraction, - excess_fraction, reinsurance) + def __init__( + self, + insurer, + properties, + time, + premium, + runtime, + payment_period, + expire_immediately, + initial_VaR=0.0, + insurancetype="proportional", + deductible_fraction=None, + excess_fraction=None, + reinsurance=0, + ): + super(InsuranceContract, self).__init__( + insurer, + properties, + time, + premium, + runtime, + payment_period, + expire_immediately, + initial_VaR, + insurancetype, + deductible_fraction, + excess_fraction, + reinsurance, + ) self.risk_data = properties @@ -34,10 +58,14 @@ def explode(self, time, uniform_value, damage_extent): if uniform_value < self.risk_factor: # if True: claim = min(self.excess, damage_extent * self.value) - self.deductible - self.insurer.register_claim(claim) #Every insurance claim made is immediately registered. + self.insurer.register_claim( + claim + ) # Every insurance claim made is immediately registered. self.current_claim += claim - self.insurer.receive_obligation(claim, self.property_holder, time + 2, 'claim') + self.insurer.receive_obligation( + claim, self.property_holder, time + 2, "claim" + ) # Insurer pays one time step after reinsurer to avoid bankruptcy. # TODO: Is this realistic? Change this? if self.expire_immediately: @@ -51,10 +79,9 @@ def mature(self, time): No return value. Returns risk to simulation as contract terminates. Calls terminate_reinsurance to dissolve any reinsurance contracts.""" - #self.terminating = True + # self.terminating = True self.terminate_reinsurance(time) if not self.roll_over_flag: self.property_holder.return_risks([self.risk_data]) - diff --git a/insurancefirm.py b/insurancefirm.py index da75298..723b9a7 100644 --- a/insurancefirm.py +++ b/insurancefirm.py @@ -8,6 +8,7 @@ class InsuranceFirm(MetaInsuranceOrg): """ReinsuranceFirm class. Inherits from InsuranceFirm.""" + def init(self, simulation_parameters, agent_parameters): """Constructor method. Accepts arguments @@ -26,10 +27,14 @@ def adjust_dividends(self, time, actual_capacity): No return values. Method is called from MetaInsuranceOrg iterate method between evaluating reinsurance and insurance risks to calculate dividend to be payed if the firm has made profit and has achieved capital targets.""" - #TODO: Implement algorithm from flowchart + # TODO: Implement algorithm from flowchart profits = self.profits_losses - self.per_period_dividend = max(0, self.dividend_share_of_profits * profits) # max function ensures that no negative dividends are paid - if actual_capacity < self.capacity_target: # no dividends if firm misses capital target + self.per_period_dividend = max( + 0, self.dividend_share_of_profits * profits + ) # max function ensures that no negative dividends are paid + if ( + actual_capacity < self.capacity_target + ): # no dividends if firm misses capital target self.per_period_dividend = 0 def get_reinsurance_VaR_estimate(self, max_var): @@ -40,13 +45,20 @@ def get_reinsurance_VaR_estimate(self, max_var): reinsurance_VaR_estimate: Type Decimal. This method takes the max VaR and mulitiplies it by a factor that estimates the VaR if another reinsurance contract was to be taken. Called by the adjust_target_capacity and get_capacity methods.""" - reinsurance_factor_estimate = (sum([ 1 for categ_id in range(self.simulation_no_risk_categories) \ - if (self.category_reinsurance[categ_id] is None)]) \ - * 1. / self.simulation_no_risk_categories) \ - * (1. - self.np_reinsurance_deductible_fraction) - reinsurance_VaR_estimate = max_var * (1. + reinsurance_factor_estimate) + reinsurance_factor_estimate = ( + sum( + [ + 1 + for categ_id in range(self.simulation_no_risk_categories) + if (self.category_reinsurance[categ_id] is None) + ] + ) + * 1.0 + / self.simulation_no_risk_categories + ) * (1.0 - self.np_reinsurance_deductible_fraction) + reinsurance_VaR_estimate = max_var * (1.0 + reinsurance_factor_estimate) return reinsurance_VaR_estimate - + def adjust_capacity_target(self, max_var): """Method to adjust capacity target. Accepts: @@ -55,12 +67,22 @@ def adjust_capacity_target(self, max_var): This method decides to increase/decrease the capacity target dependant on if the ratio of capacity target to max VaR is above/below a predetermined limit.""" reinsurance_VaR_estimate = self.get_reinsurance_VaR_estimate(max_var) - capacity_target_var_ratio_estimate = (self.capacity_target + reinsurance_VaR_estimate) * 1. / (max_var + reinsurance_VaR_estimate) - if capacity_target_var_ratio_estimate > self.capacity_target_increment_threshold: + capacity_target_var_ratio_estimate = ( + (self.capacity_target + reinsurance_VaR_estimate) + * 1.0 + / (max_var + reinsurance_VaR_estimate) + ) + if ( + capacity_target_var_ratio_estimate + > self.capacity_target_increment_threshold + ): self.capacity_target *= self.capacity_target_increment_factor - elif capacity_target_var_ratio_estimate < self.capacity_target_decrement_threshold: + elif ( + capacity_target_var_ratio_estimate + < self.capacity_target_decrement_threshold + ): self.capacity_target *= self.capacity_target_decrement_factor - return + return def get_capacity(self, max_var): """Method to get capacity of firm. @@ -71,10 +93,12 @@ def get_capacity(self, max_var): This method is called by increase_capacity to get the real capacity of the firm. If the firm has enough money to cover its max value at risk then its capacity is its cash + the reinsurance VaR estimate, otherwise the firm is recovering from some losses and so capacity is just cash.""" - if max_var < self.cash: # ensure presence of sufficiently much cash to cover VaR + if ( + max_var < self.cash + ): # ensure presence of sufficiently much cash to cover VaR reinsurance_VaR_estimate = self.get_reinsurance_VaR_estimate(max_var) return self.cash + reinsurance_VaR_estimate - return self.cash # Ensure insurer recovers complete coverage. + return self.cash # Ensure insurer recovers complete coverage. def increase_capacity(self, time, max_var): """Method to increase the capacity of the firm. @@ -89,28 +113,50 @@ def increase_capacity(self, time, max_var): market premium is above its average premium, otherwise firm is 'forced' to get a catbond or reinsurance. Only implemented for non-proportional(excess of loss) reinsurance. Only issues one reinsurance or catbond per iteration unless not enough capacity to meet target.""" - assert self.simulation_reinsurance_type == 'non-proportional' - reinsurance_price = self.simulation.get_reinsurance_premium(self.np_reinsurance_deductible_fraction) - cat_bond_price = self.simulation.get_cat_bond_price(self.np_reinsurance_deductible_fraction) + assert self.simulation_reinsurance_type == "non-proportional" + reinsurance_price = self.simulation.get_reinsurance_premium( + self.np_reinsurance_deductible_fraction + ) + cat_bond_price = self.simulation.get_cat_bond_price( + self.np_reinsurance_deductible_fraction + ) capacity = None - if not reinsurance_price == cat_bond_price == float('inf'): - categ_ids = [categ_id for categ_id in range(self.simulation_no_risk_categories) if (self.category_reinsurance[categ_id] is None)] + if not reinsurance_price == cat_bond_price == float("inf"): + categ_ids = [ + categ_id + for categ_id in range(self.simulation_no_risk_categories) + if (self.category_reinsurance[categ_id] is None) + ] if len(categ_ids) > 1: np.random.shuffle(categ_ids) - while len(categ_ids) >= 1: + while len(categ_ids) >= 1: categ_id = categ_ids.pop() capacity = self.get_capacity(max_var) if self.capacity_target < capacity: - if self.increase_capacity_by_category(time, categ_id, reinsurance_price=reinsurance_price, cat_bond_price=cat_bond_price, force=False): + if self.increase_capacity_by_category( + time, + categ_id, + reinsurance_price=reinsurance_price, + cat_bond_price=cat_bond_price, + force=False, + ): categ_ids = [] else: - self.increase_capacity_by_category(time, categ_id, reinsurance_price=reinsurance_price, cat_bond_price=cat_bond_price, force=True) + self.increase_capacity_by_category( + time, + categ_id, + reinsurance_price=reinsurance_price, + cat_bond_price=cat_bond_price, + force=True, + ) # capacity is returned in order not to recompute more often than necessary - if capacity is None: + if capacity is None: capacity = self.get_capacity(max_var) - return capacity + return capacity - def increase_capacity_by_category(self, time, categ_id, reinsurance_price, cat_bond_price, force=False): + def increase_capacity_by_category( + self, time, categ_id, reinsurance_price, cat_bond_price, force=False + ): """Method to increase capacity. Only called by increase_capacity. Accepts: time: Type Integer> @@ -123,20 +169,26 @@ def increase_capacity_by_category(self, time, categ_id, reinsurance_price, cat_b firm for the given category. This is forced if firm does not have enough capacity to meet target otherwise will only issue if market premium is greater than firms average premium.""" if isleconfig.verbose: - print("IF {0:d} increasing capacity in period {1:d}, cat bond price: {2:f}, reinsurance premium {3:f}".format(self.id, time, cat_bond_price, reinsurance_price)) + print( + "IF {0:d} increasing capacity in period {1:d}, cat bond price: {2:f}, reinsurance premium {3:f}".format( + self.id, time, cat_bond_price, reinsurance_price + ) + ) if not force: actual_premium = self.get_average_premium(categ_id) possible_premium = self.simulation.get_market_premium() if actual_premium >= possible_premium: return False - '''on the basis of prices decide for obtaining reinsurance or for issuing cat bond''' + """on the basis of prices decide for obtaining reinsurance or for issuing cat bond""" if reinsurance_price > cat_bond_price: if isleconfig.verbose: print("IF {0:d} issuing Cat bond in period {1:d}".format(self.id, time)) self.issue_cat_bond(time, categ_id) else: if isleconfig.verbose: - print("IF {0:d} getting reinsurance in period {1:d}".format(self.id, time)) + print( + "IF {0:d} getting reinsurance in period {1:d}".format(self.id, time) + ) self.ask_reinsurance_non_proportional_by_category(time, categ_id) return True @@ -154,18 +206,18 @@ def get_average_premium(self, categ_id): contract_premium = contract.periodized_premium * contract.runtime weighted_premium_sum += contract_premium if total_weight == 0: - return 0 # will prevent any attempt to reinsure empty categories + return 0 # will prevent any attempt to reinsure empty categories return weighted_premium_sum * 1.0 / total_weight - + def ask_reinsurance(self, time): """Method called specifically to call relevant reinsurance function for simulations reinsurance type. Only non-proportional type is used as this is the one mainly used in reality. Accepts: time: Type Integer. No return values.""" - if self.simulation_reinsurance_type == 'proportional': + if self.simulation_reinsurance_type == "proportional": self.ask_reinsurance_proportional() - elif self.simulation_reinsurance_type == 'non-proportional': + elif self.simulation_reinsurance_type == "non-proportional": self.ask_reinsurance_non_proportional(time) else: assert False, "Undefined reinsurance type" @@ -180,7 +232,7 @@ def ask_reinsurance_non_proportional(self, time): """Evaluate by risk category""" for categ_id in range(self.simulation_no_risk_categories): """Seek reinsurance only with probability 10% if not already reinsured""" # TODO: find a more generic way to decide whether to request reinsurance for category in this period - if (self.category_reinsurance[categ_id] is None): + if self.category_reinsurance[categ_id] is None: self.ask_reinsurance_non_proportional_by_category(time, categ_id) def characterize_underwritten_risks_by_category(self, time, categ_id): @@ -204,11 +256,13 @@ def characterize_underwritten_risks_by_category(self, time, categ_id): avg_risk_factor += contract.risk_factor number_risks += 1 periodized_total_premium += contract.periodized_premium - if number_risks > 0: + if number_risks > 0: avg_risk_factor /= number_risks return total_value, avg_risk_factor, number_risks, periodized_total_premium - def ask_reinsurance_non_proportional_by_category(self, time, categ_id, purpose='newrisk'): + def ask_reinsurance_non_proportional_by_category( + self, time, categ_id, purpose="newrisk" + ): """Method to create a reinsurance risk for a given category for firm that calls it. Called from increase_ capacity_by_category, ask_reinsurance_non_proportional, and roll_over in metainsuranceorg. Accepts: @@ -221,20 +275,29 @@ def ask_reinsurance_non_proportional_by_category(self, time, categ_id, purpose=' and, assuming firms has underwritten risks in category, creates new reinsurance risk with values based on firms existing underwritten risks. If the method was called to create a new risks then it is appended to list of 'reinrisks', otherwise used for creating the risk when a reinsurance contract rolls over.""" - total_value, avg_risk_factor, number_risks, periodized_total_premium = self.characterize_underwritten_risks_by_category(time, categ_id) - if number_risks > 0: - risk = {"value": total_value, "category": categ_id, "owner": self, - #"identifier": uuid.uuid1(), - "insurancetype": 'excess-of-loss', "number_risks": number_risks, - "deductible_fraction": self.np_reinsurance_deductible_fraction, - "excess_fraction": self.np_reinsurance_excess_fraction, - "periodized_total_premium": periodized_total_premium, "runtime": 12, - "expiration": time + 12, "risk_factor": avg_risk_factor} # TODO: make runtime into a parameter - if purpose == 'newrisk': + total_value, avg_risk_factor, number_risks, periodized_total_premium = self.characterize_underwritten_risks_by_category( + time, categ_id + ) + if number_risks > 0: + risk = { + "value": total_value, + "category": categ_id, + "owner": self, + # "identifier": uuid.uuid1(), + "insurancetype": "excess-of-loss", + "number_risks": number_risks, + "deductible_fraction": self.np_reinsurance_deductible_fraction, + "excess_fraction": self.np_reinsurance_excess_fraction, + "periodized_total_premium": periodized_total_premium, + "runtime": 12, + "expiration": time + 12, + "risk_factor": avg_risk_factor, + } # TODO: make runtime into a parameter + if purpose == "newrisk": self.simulation.append_reinrisks(risk) - elif purpose == 'rollover': + elif purpose == "rollover": return risk - elif number_risks == 0 and purpose == 'rollover': + elif number_risks == 0 and purpose == "rollover": return None def ask_reinsurance_proportional(self): @@ -248,16 +311,25 @@ def ask_reinsurance_proportional(self): nonreinsured.reverse() - if len(nonreinsured) >= (1 - self.reinsurance_limit) * len(self.underwritten_contracts): + if len(nonreinsured) >= (1 - self.reinsurance_limit) * len( + self.underwritten_contracts + ): counter = 0 - limitrein = len(nonreinsured) - (1 - self.reinsurance_limit) * len(self.underwritten_contracts) + limitrein = len(nonreinsured) - (1 - self.reinsurance_limit) * len( + self.underwritten_contracts + ) for contract in nonreinsured: if counter < limitrein: - risk = {"value": contract.value, "category": contract.category, "owner": self, - #"identifier": uuid.uuid1(), - "reinsurance_share": 1., - "expiration": contract.expiration, "contract": contract, - "risk_factor": contract.risk_factor} + risk = { + "value": contract.value, + "category": contract.category, + "owner": self, + # "identifier": uuid.uuid1(), + "reinsurance_share": 1.0, + "expiration": contract.expiration, + "contract": contract, + "risk_factor": contract.risk_factor, + } self.simulation.append_reinrisks(risk) counter += 1 @@ -273,10 +345,14 @@ def add_reinsurance(self, category, excess_fraction, deductible_fraction, contra deductible_fraction: Type Decimal. Value of deductible. contract: Type Class. Reinsurance contract issued to firm. No return values.""" - self.riskmodel.add_reinsurance(category, excess_fraction, deductible_fraction, contract) + self.riskmodel.add_reinsurance( + category, excess_fraction, deductible_fraction, contract + ) self.category_reinsurance[category] = contract - def delete_reinsurance(self, category, excess_fraction, deductible_fraction, contract): + def delete_reinsurance( + self, category, excess_fraction, deductible_fraction, contract + ): """Method called by reinsurancecontract to delete the reinsurance contract from the firms counter for the given category, used so that another reinsurance contract can be issued for that category if needed. Accepts: @@ -285,9 +361,11 @@ def delete_reinsurance(self, category, excess_fraction, deductible_fraction, con deductible_fraction: Type Decimal. Value of deductible. contract: Type Class. Reinsurance contract issued to firm. No return values.""" - self.riskmodel.delete_reinsurance(category, excess_fraction, deductible_fraction, contract) + self.riskmodel.delete_reinsurance( + category, excess_fraction, deductible_fraction, contract + ) self.category_reinsurance[category] = None - + def issue_cat_bond(self, time, categ_id, per_value_per_period_premium=0): """Method to issue cat bond to given firm for given category. Accepts: @@ -298,29 +376,55 @@ def issue_cat_bond(self, time, categ_id, per_value_per_period_premium=0): Method is only called by increase_capacity_by_category method when CatBond prices are lower than reinsurance. It then creates the CatBond as a quasi-reinsurance contract that is paid for immediately (by simulation) with no premium payments.""" - total_value, avg_risk_factor, number_risks, periodized_total_premium = self.characterize_underwritten_risks_by_category(time, categ_id) + total_value, avg_risk_factor, number_risks, periodized_total_premium = self.characterize_underwritten_risks_by_category( + time, categ_id + ) if number_risks > 0: - risk = {"value": total_value, "category": categ_id, "owner": self, - #"identifier": uuid.uuid1(), - "insurancetype": 'excess-of-loss', "number_risks": number_risks, - "deductible_fraction": self.np_reinsurance_deductible_fraction, - "excess_fraction": self.np_reinsurance_excess_fraction, - "periodized_total_premium": 0, "runtime": 12, - "expiration": time + 12, "risk_factor": avg_risk_factor} # TODO: make runtime into a parameter + risk = { + "value": total_value, + "category": categ_id, + "owner": self, + # "identifier": uuid.uuid1(), + "insurancetype": "excess-of-loss", + "number_risks": number_risks, + "deductible_fraction": self.np_reinsurance_deductible_fraction, + "excess_fraction": self.np_reinsurance_excess_fraction, + "periodized_total_premium": 0, + "runtime": 12, + "expiration": time + 12, + "risk_factor": avg_risk_factor, + } # TODO: make runtime into a parameter _, _, var_this_risk, _ = self.riskmodel.evaluate([], self.cash, risk) per_period_premium = per_value_per_period_premium * risk["value"] - total_premium = sum([per_period_premium * ((1/(1+self.interest_rate))**i) for i in range(risk["runtime"])]) + total_premium = sum( + [ + per_period_premium * ((1 / (1 + self.interest_rate)) ** i) + for i in range(risk["runtime"]) + ] + ) catbond = CatBond(self.simulation, per_period_premium, self.simulation) - contract = ReinsuranceContract(catbond, risk, time, 0, risk["runtime"], - self.default_contract_payment_period, \ - expire_immediately=self.simulation_parameters["expire_immediately"], \ - initial_VaR=var_this_risk, insurancetype=risk["insurancetype"]) + contract = ReinsuranceContract( + catbond, + risk, + time, + 0, + risk["runtime"], + self.default_contract_payment_period, + expire_immediately=self.simulation_parameters["expire_immediately"], + initial_VaR=var_this_risk, + insurancetype=risk["insurancetype"], + ) catbond.set_contract(contract) - self.simulation.receive_obligation(var_this_risk, self, time, 'bond') + self.simulation.receive_obligation(var_this_risk, self, time, "bond") """hand cash over to cat bond such that var_this_risk is covered""" - obligation = {"amount": var_this_risk + total_premium, "recipient": catbond, "due_time": time, "purpose": 'bond'} + obligation = { + "amount": var_this_risk + total_premium, + "recipient": catbond, + "due_time": time, + "purpose": "bond", + } self.pay(obligation) self.simulation.accept_agents("catbond", [catbond], time=time) @@ -339,12 +443,17 @@ def make_reinsurance_claims(self, time): categ_id, claims, is_proportional = contract.get_and_reset_current_claim() if is_proportional: claims_this_turn[categ_id] += claims - if (contract.reincontract != None): + if contract.reincontract != None: contract.reincontract.explode(time, claims) for categ_id in range(self.simulation_no_risk_categories): - if claims_this_turn[categ_id] > 0 and self.category_reinsurance[categ_id] is not None: - self.category_reinsurance[categ_id].explode(time, claims_this_turn[categ_id]) + if ( + claims_this_turn[categ_id] > 0 + and self.category_reinsurance[categ_id] is not None + ): + self.category_reinsurance[categ_id].explode( + time, claims_this_turn[categ_id] + ) def get_excess_of_loss_reinsurance(self): """Method to return list containing the reinsurance for each category interms of the reinsurer, value of @@ -355,10 +464,13 @@ def get_excess_of_loss_reinsurance(self): reinsurance = [] for categ_id in range(self.simulation_no_risk_categories): if self.category_reinsurance[categ_id] is not None: - reinsurance_contract = {} - reinsurance_contract["reinsurer"] = self.category_reinsurance[categ_id].insurer - reinsurance_contract["value"] = self.category_reinsurance[categ_id].value - reinsurance_contract["category"] = categ_id - reinsurance.append(reinsurance_contract) + reinsurance_contract = {} + reinsurance_contract["reinsurer"] = self.category_reinsurance[ + categ_id + ].insurer + reinsurance_contract["value"] = self.category_reinsurance[ + categ_id + ].value + reinsurance_contract["category"] = categ_id + reinsurance.append(reinsurance_contract) return reinsurance - diff --git a/insurancesimulation.py b/insurancesimulation.py index 51a5d67..7bd4715 100644 --- a/insurancesimulation.py +++ b/insurancesimulation.py @@ -15,8 +15,15 @@ import visualization_network -class InsuranceSimulation(): - def __init__(self, override_no_riskmodels, replic_ID, simulation_parameters, rc_event_schedule, rc_event_damage): +class InsuranceSimulation: + def __init__( + self, + override_no_riskmodels, + replic_ID, + simulation_parameters, + rc_event_schedule, + rc_event_damage, + ): """Initialises the simulation (Called from start.py) Accepts: override_no_riskmodels: Boolean determining if number of risk models should be overwritten @@ -29,43 +36,65 @@ def __init__(self, override_no_riskmodels, replic_ID, simulation_parameters, rc_ if override_no_riskmodels: simulation_parameters["no_riskmodels"] = override_no_riskmodels self.number_riskmodels = simulation_parameters["no_riskmodels"] - + "Save parameters, sets parameters of sim according to isleconfig.py" if (replic_ID is None) or isleconfig.force_foreground: - self.background_run = False + self.background_run = False else: self.background_run = True self.replic_ID = replic_ID self.simulation_parameters = simulation_parameters - + "Unpacks parameters and sets distributions" self.catbonds_off = simulation_parameters["catbonds_off"] self.reinsurance_off = simulation_parameters["reinsurance_off"] self.total_no_risks = simulation_parameters["no_risks"] self.risk_factor_lower_bound = simulation_parameters["risk_factor_lower_bound"] - self.cat_separation_distribution = scipy.stats.expon(0, simulation_parameters["event_time_mean_separation"]) - - self.risk_factor_spread = simulation_parameters["risk_factor_upper_bound"] - simulation_parameters["risk_factor_lower_bound"] - self.risk_factor_distribution = scipy.stats.uniform(loc=self.risk_factor_lower_bound, scale=self.risk_factor_spread) + self.cat_separation_distribution = scipy.stats.expon( + 0, simulation_parameters["event_time_mean_separation"] + ) + + self.risk_factor_spread = ( + simulation_parameters["risk_factor_upper_bound"] + - simulation_parameters["risk_factor_lower_bound"] + ) + self.risk_factor_distribution = scipy.stats.uniform( + loc=self.risk_factor_lower_bound, scale=self.risk_factor_spread + ) if not simulation_parameters["risk_factors_present"]: self.risk_factor_distribution = scipy.stats.uniform(loc=1.0, scale=0) - self.risk_value_distribution = scipy.stats.uniform(loc=1000, scale=0) #TODO is this correct? + self.risk_value_distribution = scipy.stats.uniform( + loc=1000, scale=0 + ) # TODO is this correct? risk_factor_mean = self.risk_factor_distribution.mean() - if np.isnan(risk_factor_mean): # unfortunately scipy.stats.mean is not well-defined if scale = 0 + if np.isnan( + risk_factor_mean + ): # unfortunately scipy.stats.mean is not well-defined if scale = 0 risk_factor_mean = self.risk_factor_distribution.rvs() non_truncated = scipy.stats.pareto(b=2, loc=0, scale=0.25) - self.damage_distribution = TruncatedDistWrapper(lower_bound=0.25, upper_bound=1., dist=non_truncated) + self.damage_distribution = TruncatedDistWrapper( + lower_bound=0.25, upper_bound=1.0, dist=non_truncated + ) "set initial market price (normalized, i.e. must be multiplied by value or excess-deductible)" if self.simulation_parameters["expire_immediately"]: assert self.cat_separation_distribution.dist.name == "expon" - expected_damage_frequency = 1 - scipy.stats.poisson(1 / self.simulation_parameters["event_time_mean_separation"] * \ - self.simulation_parameters["mean_contract_runtime"]).pmf(0) + expected_damage_frequency = 1 - scipy.stats.poisson( + 1 + / self.simulation_parameters["event_time_mean_separation"] + * self.simulation_parameters["mean_contract_runtime"] + ).pmf(0) else: - expected_damage_frequency = self.simulation_parameters["mean_contract_runtime"] / \ - self.cat_separation_distribution.mean() - self.norm_premium = expected_damage_frequency * self.damage_distribution.mean() * \ - risk_factor_mean * (1 + self.simulation_parameters["norm_profit_markup"]) + expected_damage_frequency = ( + self.simulation_parameters["mean_contract_runtime"] + / self.cat_separation_distribution.mean() + ) + self.norm_premium = ( + expected_damage_frequency + * self.damage_distribution.mean() + * risk_factor_mean + * (1 + self.simulation_parameters["norm_profit_markup"]) + ) self.reinsurance_market_premium = self.market_premium = self.norm_premium "Set up monetary system (should instead be with the customers, if customers are modeled explicitly)" @@ -77,58 +106,100 @@ def __init__(self, override_no_riskmodels, replic_ID, simulation_parameters, rc_ self.riskcategories = list(range(self.simulation_parameters["no_categories"])) self.rc_event_schedule = [] self.rc_event_damage = [] - self.rc_event_schedule_initial = [] # For debugging (cloud debugging) purposes is good to store the initial schedule of catastrophes - self.rc_event_damage_initial = [] # and damages that will be use in a single run of the model. - - if rc_event_schedule is not None and rc_event_damage is not None: # If we have schedules pass as arguments we used them. + self.rc_event_schedule_initial = ( + [] + ) # For debugging (cloud debugging) purposes is good to store the initial schedule of catastrophes + self.rc_event_damage_initial = ( + [] + ) # and damages that will be use in a single run of the model. + + if ( + rc_event_schedule is not None and rc_event_damage is not None + ): # If we have schedules pass as arguments we used them. self.rc_event_schedule = copy.copy(rc_event_schedule) self.rc_event_schedule_initial = copy.copy(rc_event_schedule) self.rc_event_damage = copy.copy(rc_event_damage) self.rc_event_damage_initial = copy.copy(rc_event_damage) - else: # Otherwise the schedules and damages are generated. + else: # Otherwise the schedules and damages are generated. self.setup_risk_categories_caller() "Set up risks" risk_value_mean = self.risk_value_distribution.mean() - if np.isnan(risk_value_mean): # unfortunately scipy.stats.mean is not well-defined if scale = 0 + if np.isnan( + risk_value_mean + ): # unfortunately scipy.stats.mean is not well-defined if scale = 0 risk_value_mean = self.risk_value_distribution.rvs() - rrisk_factors = self.risk_factor_distribution.rvs(size=self.simulation_parameters["no_risks"]) - rvalues = self.risk_value_distribution.rvs(size=self.simulation_parameters["no_risks"]) - rcategories = np.random.randint(0, self.simulation_parameters["no_categories"], size=self.simulation_parameters["no_risks"]) - self.risks = [{"risk_factor": rrisk_factors[i], "value": rvalues[i], "category": rcategories[i], "owner": self} for i in range(self.simulation_parameters["no_risks"])] - self.risks_counter = [0,0,0,0] + rrisk_factors = self.risk_factor_distribution.rvs( + size=self.simulation_parameters["no_risks"] + ) + rvalues = self.risk_value_distribution.rvs( + size=self.simulation_parameters["no_risks"] + ) + rcategories = np.random.randint( + 0, + self.simulation_parameters["no_categories"], + size=self.simulation_parameters["no_risks"], + ) + self.risks = [ + { + "risk_factor": rrisk_factors[i], + "value": rvalues[i], + "category": rcategories[i], + "owner": self, + } + for i in range(self.simulation_parameters["no_risks"]) + ] + self.risks_counter = [0, 0, 0, 0] for item in self.risks: - self.risks_counter[item["category"]] = self.risks_counter[item["category"]] + 1 - - self.inaccuracy = self.get_all_riskmodel_combinations(self.simulation_parameters["riskmodel_inaccuracy_parameter"]) - - self.inaccuracy = random.sample(self.inaccuracy, self.simulation_parameters["no_riskmodels"]) - - risk_model_configurations = [{"damage_distribution": self.damage_distribution, - "expire_immediately": self.simulation_parameters["expire_immediately"], - "cat_separation_distribution": self.cat_separation_distribution, - "norm_premium": self.norm_premium, - "no_categories": self.simulation_parameters["no_categories"], - "risk_value_mean": risk_value_mean, - "risk_factor_mean": risk_factor_mean, - "norm_profit_markup": self.simulation_parameters["norm_profit_markup"], - "margin_of_safety": self.simulation_parameters["riskmodel_margin_of_safety"], - "var_tail_prob": self.simulation_parameters["value_at_risk_tail_probability"], - "inaccuracy_by_categ": self.inaccuracy[i]} \ - for i in range(self.simulation_parameters["no_riskmodels"])] - + self.risks_counter[item["category"]] = ( + self.risks_counter[item["category"]] + 1 + ) + + self.inaccuracy = self.get_all_riskmodel_combinations( + self.simulation_parameters["riskmodel_inaccuracy_parameter"] + ) + + self.inaccuracy = random.sample( + self.inaccuracy, self.simulation_parameters["no_riskmodels"] + ) + + risk_model_configurations = [ + { + "damage_distribution": self.damage_distribution, + "expire_immediately": self.simulation_parameters["expire_immediately"], + "cat_separation_distribution": self.cat_separation_distribution, + "norm_premium": self.norm_premium, + "no_categories": self.simulation_parameters["no_categories"], + "risk_value_mean": risk_value_mean, + "risk_factor_mean": risk_factor_mean, + "norm_profit_markup": self.simulation_parameters["norm_profit_markup"], + "margin_of_safety": self.simulation_parameters[ + "riskmodel_margin_of_safety" + ], + "var_tail_prob": self.simulation_parameters[ + "value_at_risk_tail_probability" + ], + "inaccuracy_by_categ": self.inaccuracy[i], + } + for i in range(self.simulation_parameters["no_riskmodels"]) + ] + "Setting up agents (to be done from start.py)" self.agent_parameters = {"insurancefirm": [], "reinsurancefirm": []} - self.initialize_agent_parameters("insurancefirm", simulation_parameters, risk_model_configurations) - self.initialize_agent_parameters("reinsurancefirm", simulation_parameters, risk_model_configurations) + self.initialize_agent_parameters( + "insurancefirm", simulation_parameters, risk_model_configurations + ) + self.initialize_agent_parameters( + "reinsurancefirm", simulation_parameters, risk_model_configurations + ) "Agent lists" self.reinsurancefirms = [] self.insurancefirms = [] self.catbonds = [] - + "Lists of agent weights" self.insurers_weights = {} self.reinsurers_weights = {} @@ -136,22 +207,30 @@ def __init__(self, override_no_riskmodels, replic_ID, simulation_parameters, rc_ "List of reinsurance risks offered for underwriting" self.reinrisks = [] self.not_accepted_reinrisks = [] - + "Cumulative variables for history and logging" self.cumulative_bankruptcies = 0 self.cumulative_market_exits = 0 self.cumulative_unrecovered_claims = 0.0 self.cumulative_claims = 0.0 - + "Lists for logging history" - self.logger = logger.Logger(no_riskmodels=simulation_parameters["no_riskmodels"], - rc_event_schedule_initial=self.rc_event_schedule_initial, - rc_event_damage_initial=self.rc_event_damage_initial) - - self.insurance_models_counter = np.zeros(self.simulation_parameters["no_categories"]) - self.reinsurance_models_counter = np.zeros(self.simulation_parameters["no_categories"]) - - def initialize_agent_parameters(self, firmtype, simulation_parameters, risk_model_configurations): + self.logger = logger.Logger( + no_riskmodels=simulation_parameters["no_riskmodels"], + rc_event_schedule_initial=self.rc_event_schedule_initial, + rc_event_damage_initial=self.rc_event_damage_initial, + ) + + self.insurance_models_counter = np.zeros( + self.simulation_parameters["no_categories"] + ) + self.reinsurance_models_counter = np.zeros( + self.simulation_parameters["no_categories"] + ) + + def initialize_agent_parameters( + self, firmtype, simulation_parameters, risk_model_configurations + ): """General function for initialising the agent parameters Takes the firm type as argument, also needing sim params and risk configs Creates the agent parameters of both firm types for the initial number specified in isleconfig.py @@ -160,37 +239,71 @@ def initialize_agent_parameters(self, firmtype, simulation_parameters, risk_mode self.insurer_id_counter = 0 no_firms = simulation_parameters["no_insurancefirms"] initial_cash = "initial_agent_cash" - reinsurance_level_lowerbound = simulation_parameters["insurance_reinsurance_levels_lower_bound"] - reinsurance_level_upperbound = simulation_parameters["insurance_reinsurance_levels_upper_bound"] + reinsurance_level_lowerbound = simulation_parameters[ + "insurance_reinsurance_levels_lower_bound" + ] + reinsurance_level_upperbound = simulation_parameters[ + "insurance_reinsurance_levels_upper_bound" + ] elif firmtype == "reinsurancefirm": self.reinsurer_id_counter = 0 no_firms = simulation_parameters["no_reinsurancefirms"] initial_cash = "initial_reinagent_cash" - reinsurance_level_lowerbound = simulation_parameters["reinsurance_reinsurance_levels_lower_bound"] - reinsurance_level_upperbound = simulation_parameters["reinsurance_reinsurance_levels_upper_bound"] + reinsurance_level_lowerbound = simulation_parameters[ + "reinsurance_reinsurance_levels_lower_bound" + ] + reinsurance_level_upperbound = simulation_parameters[ + "reinsurance_reinsurance_levels_upper_bound" + ] for i in range(no_firms): - if simulation_parameters['static_non-proportional_reinsurance_levels']: - reinsurance_level = simulation_parameters["default_non-proportional_reinsurance_deductible"] + if simulation_parameters["static_non-proportional_reinsurance_levels"]: + reinsurance_level = simulation_parameters[ + "default_non-proportional_reinsurance_deductible" + ] else: - reinsurance_level = np.random.uniform(reinsurance_level_lowerbound, reinsurance_level_upperbound) - - riskmodel_config = risk_model_configurations[i % len(risk_model_configurations)] - self.agent_parameters[firmtype].append({'id': self.get_unique_insurer_id(), 'initial_cash': simulation_parameters[initial_cash], - 'riskmodel_config': riskmodel_config, 'norm_premium': self.norm_premium, - 'profit_target': simulation_parameters["norm_profit_markup"], - 'initial_acceptance_threshold': simulation_parameters["initial_acceptance_threshold"], - 'acceptance_threshold_friction': simulation_parameters["acceptance_threshold_friction"], - 'reinsurance_limit': simulation_parameters["reinsurance_limit"], - 'non-proportional_reinsurance_level': reinsurance_level, - 'capacity_target_decrement_threshold': simulation_parameters['capacity_target_decrement_threshold'], - 'capacity_target_increment_threshold': simulation_parameters['capacity_target_increment_threshold'], - 'capacity_target_decrement_factor': simulation_parameters['capacity_target_decrement_factor'], - 'capacity_target_increment_factor': simulation_parameters['capacity_target_increment_factor'], - 'interest_rate': simulation_parameters["interest_rate"]}) - - def build_agents(self, agent_class, agent_class_string, parameters, agent_parameters): + reinsurance_level = np.random.uniform( + reinsurance_level_lowerbound, reinsurance_level_upperbound + ) + + riskmodel_config = risk_model_configurations[ + i % len(risk_model_configurations) + ] + self.agent_parameters[firmtype].append( + { + "id": self.get_unique_insurer_id(), + "initial_cash": simulation_parameters[initial_cash], + "riskmodel_config": riskmodel_config, + "norm_premium": self.norm_premium, + "profit_target": simulation_parameters["norm_profit_markup"], + "initial_acceptance_threshold": simulation_parameters[ + "initial_acceptance_threshold" + ], + "acceptance_threshold_friction": simulation_parameters[ + "acceptance_threshold_friction" + ], + "reinsurance_limit": simulation_parameters["reinsurance_limit"], + "non-proportional_reinsurance_level": reinsurance_level, + "capacity_target_decrement_threshold": simulation_parameters[ + "capacity_target_decrement_threshold" + ], + "capacity_target_increment_threshold": simulation_parameters[ + "capacity_target_increment_threshold" + ], + "capacity_target_decrement_factor": simulation_parameters[ + "capacity_target_decrement_factor" + ], + "capacity_target_increment_factor": simulation_parameters[ + "capacity_target_increment_factor" + ], + "interest_rate": simulation_parameters["interest_rate"], + } + ) + + def build_agents( + self, agent_class, agent_class_string, parameters, agent_parameters + ): """Method for building new agents, only used for re/insurance firms. Loops through the agent parameters for each initialised agent to create an instance of them using re/insurancefirm. Accepts: @@ -204,7 +317,7 @@ def build_agents(self, agent_class, agent_class_string, parameters, agent_parame for ap in agent_parameters: agents.append(agent_class(parameters, ap)) return agents - + def accept_agents(self, agent_class_string, agents, agent_group=None, time=0): """Method to 'accept' agents in that it adds agent to relevant list of agents kept by simulation instance, also adds agent to logger. Also takes created agents initial cash out of economy. @@ -242,9 +355,11 @@ def accept_agents(self, agent_class_string, agents, agent_group=None, time=0): self.catbonds += agents except: print(sys.exc_info()) - pdb.set_trace() + pdb.set_trace() else: - assert False, "Error: Unexpected agent class used {0:s}".format(agent_class_string) + assert False, "Error: Unexpected agent class used {0:s}".format( + agent_class_string + ) def delete_agents(self, agent_class_string, agents): """Method for deleting catbonds as it is only agent that is allowed to be removed @@ -254,8 +369,10 @@ def delete_agents(self, agent_class_string, agents): for agent in agents: self.catbonds.remove(agent) else: - assert False, "Trying to remove unremovable agent, type: {0:s}".format(agent_class_string) - + assert False, "Trying to remove unremovable agent, type: {0:s}".format( + agent_class_string + ) + def iterate(self, t): """Function that is called from start.py for each iteration that settles obligations, capital then reselects risks for the insurance and reinsurance companies to evaluate. Firms are then iterated through to accept @@ -279,7 +396,7 @@ def iterate(self, t): # Pay obligations self.effect_payments(t) - + # Identify perils and effect claims for categ_id in range(len(self.rc_event_schedule)): try: @@ -287,21 +404,28 @@ def iterate(self, t): assert self.rc_event_schedule[categ_id][0] >= t except: print("Something wrong; past events not deleted", file=sys.stderr) - if len(self.rc_event_schedule[categ_id]) > 0 and self.rc_event_schedule[categ_id][0] == t: + if ( + len(self.rc_event_schedule[categ_id]) > 0 + and self.rc_event_schedule[categ_id][0] == t + ): self.rc_event_schedule[categ_id] = self.rc_event_schedule[categ_id][1:] - damage_extent = copy.copy(self.rc_event_damage[categ_id][0]) # Schedules of catastrophes and damages must be generated at the same time. - self.inflict_peril(categ_id=categ_id, damage=damage_extent, t=t) # TODO: consider splitting the following lines from this method and running it with nb.jit + damage_extent = copy.copy( + self.rc_event_damage[categ_id][0] + ) # Schedules of catastrophes and damages must be generated at the same time. + self.inflict_peril( + categ_id=categ_id, damage=damage_extent, t=t + ) # TODO: consider splitting the following lines from this method and running it with nb.jit self.rc_event_damage[categ_id] = self.rc_event_damage[categ_id][1:] else: if isleconfig.verbose: print("Next peril ", self.rc_event_schedule[categ_id]) - + # Shuffle risks (insurance and reinsurance risks) self.shuffle_risks() # Reset reinweights self.reset_reinsurance_weights() - + # Iterate reinsurnace firm agents for reinagent in self.reinsurancefirms: reinagent.iterate(t) @@ -310,16 +434,18 @@ def iterate(self, t): # Reset weights self.reset_insurance_weights() - + # Iterate insurance firm agents for agent in self.insurancefirms: agent.iterate(t) - + # Iterate catbonds for agent in self.catbonds: agent.iterate(t) - self.insurance_models_counter = np.zeros(self.simulation_parameters["no_categories"]) + self.insurance_models_counter = np.zeros( + self.simulation_parameters["no_categories"] + ) for insurer in self.insurancefirms: for i in range(len(self.inaccuracy)): @@ -327,7 +453,9 @@ def iterate(self, t): if insurer.riskmodel.inaccuracy == self.inaccuracy[i]: self.insurance_models_counter[i] += 1 - self.reinsurance_models_counter = np.zeros(self.simulation_parameters["no_categories"]) + self.reinsurance_models_counter = np.zeros( + self.simulation_parameters["no_categories"] + ) for reinsurer in self.reinsurancefirms: for i in range(len(self.inaccuracy)): @@ -335,10 +463,12 @@ def iterate(self, t): if reinsurer.riskmodel.inaccuracy == self.inaccuracy[i]: self.reinsurance_models_counter[i] += 1 - network_division = 2 # How often network is updated. + network_division = 2 # How often network is updated. if isleconfig.show_network and t % network_division == 0 and t > 0: if t == network_division: - self.RN = visualization_network.ReinsuranceNetwork() # Only creates once instance so only one figure. + self.RN = ( + visualization_network.ReinsuranceNetwork() + ) # Only creates once instance so only one figure. self.RN.update(self.insurancefirms, self.reinsurancefirms, self.catbonds) self.RN.visualize() @@ -347,62 +477,110 @@ def save_data(self): Logger object (self.logger) to be recorded. No arguments. Returns None.""" - + """ collect data """ - total_cash_no = sum([insurancefirm.cash for insurancefirm in self.insurancefirms]) - total_excess_capital = sum([insurancefirm.get_excess_capital() for insurancefirm in self.insurancefirms]) - total_profitslosses = sum([insurancefirm.get_profitslosses() for insurancefirm in self.insurancefirms]) - total_contracts_no = sum([len(insurancefirm.underwritten_contracts) for insurancefirm in self.insurancefirms]) - total_reincash_no = sum([reinsurancefirm.cash for reinsurancefirm in self.reinsurancefirms]) - total_reinexcess_capital = sum([reinsurancefirm.get_excess_capital() for reinsurancefirm in self.reinsurancefirms]) - total_reinprofitslosses = sum([reinsurancefirm.get_profitslosses() for reinsurancefirm in self.reinsurancefirms]) - total_reincontracts_no = sum([len(reinsurancefirm.underwritten_contracts) for reinsurancefirm in self.reinsurancefirms]) - operational_no = sum([insurancefirm.operational for insurancefirm in self.insurancefirms]) - reinoperational_no = sum([reinsurancefirm.operational for reinsurancefirm in self.reinsurancefirms]) + total_cash_no = sum( + [insurancefirm.cash for insurancefirm in self.insurancefirms] + ) + total_excess_capital = sum( + [ + insurancefirm.get_excess_capital() + for insurancefirm in self.insurancefirms + ] + ) + total_profitslosses = sum( + [insurancefirm.get_profitslosses() for insurancefirm in self.insurancefirms] + ) + total_contracts_no = sum( + [ + len(insurancefirm.underwritten_contracts) + for insurancefirm in self.insurancefirms + ] + ) + total_reincash_no = sum( + [reinsurancefirm.cash for reinsurancefirm in self.reinsurancefirms] + ) + total_reinexcess_capital = sum( + [ + reinsurancefirm.get_excess_capital() + for reinsurancefirm in self.reinsurancefirms + ] + ) + total_reinprofitslosses = sum( + [ + reinsurancefirm.get_profitslosses() + for reinsurancefirm in self.reinsurancefirms + ] + ) + total_reincontracts_no = sum( + [ + len(reinsurancefirm.underwritten_contracts) + for reinsurancefirm in self.reinsurancefirms + ] + ) + operational_no = sum( + [insurancefirm.operational for insurancefirm in self.insurancefirms] + ) + reinoperational_no = sum( + [reinsurancefirm.operational for reinsurancefirm in self.reinsurancefirms] + ) catbondsoperational_no = sum([catbond.operational for catbond in self.catbonds]) - + """ collect agent-level data """ - insurance_firms = [(insurancefirm.cash,insurancefirm.id,insurancefirm.operational) for insurancefirm in self.insurancefirms] - reinsurance_firms = [(reinsurancefirm.cash,reinsurancefirm.id,reinsurancefirm.operational) for reinsurancefirm in self.reinsurancefirms] - + insurance_firms = [ + (insurancefirm.cash, insurancefirm.id, insurancefirm.operational) + for insurancefirm in self.insurancefirms + ] + reinsurance_firms = [ + (reinsurancefirm.cash, reinsurancefirm.id, reinsurancefirm.operational) + for reinsurancefirm in self.reinsurancefirms + ] + """ prepare dict """ current_log = {} - current_log['total_cash'] = total_cash_no - current_log['total_excess_capital'] = total_excess_capital - current_log['total_profitslosses'] = total_profitslosses - current_log['total_contracts'] = total_contracts_no - current_log['total_operational'] = operational_no - current_log['total_reincash'] = total_reincash_no - current_log['total_reinexcess_capital'] = total_reinexcess_capital - current_log['total_reinprofitslosses'] = total_reinprofitslosses - current_log['total_reincontracts'] = total_reincontracts_no - current_log['total_reinoperational'] = reinoperational_no - current_log['total_catbondsoperational'] = catbondsoperational_no - current_log['market_premium'] = self.market_premium - current_log['market_reinpremium'] = self.reinsurance_market_premium - current_log['cumulative_bankruptcies'] = self.cumulative_bankruptcies - current_log['cumulative_market_exits'] = self.cumulative_market_exits - current_log['cumulative_unrecovered_claims'] = self.cumulative_unrecovered_claims - current_log['cumulative_claims'] = self.cumulative_claims #Log the cumulative claims received so far. - - """ add agent-level data to dict""" - current_log['insurance_firms_cash'] = insurance_firms - current_log['reinsurance_firms_cash'] = reinsurance_firms - current_log['market_diffvar'] = self.compute_market_diffvar() - - current_log['individual_contracts'] = [] - individual_contracts_no = [len(insurancefirm.underwritten_contracts) for insurancefirm in self.insurancefirms] + current_log["total_cash"] = total_cash_no + current_log["total_excess_capital"] = total_excess_capital + current_log["total_profitslosses"] = total_profitslosses + current_log["total_contracts"] = total_contracts_no + current_log["total_operational"] = operational_no + current_log["total_reincash"] = total_reincash_no + current_log["total_reinexcess_capital"] = total_reinexcess_capital + current_log["total_reinprofitslosses"] = total_reinprofitslosses + current_log["total_reincontracts"] = total_reincontracts_no + current_log["total_reinoperational"] = reinoperational_no + current_log["total_catbondsoperational"] = catbondsoperational_no + current_log["market_premium"] = self.market_premium + current_log["market_reinpremium"] = self.reinsurance_market_premium + current_log["cumulative_bankruptcies"] = self.cumulative_bankruptcies + current_log["cumulative_market_exits"] = self.cumulative_market_exits + current_log[ + "cumulative_unrecovered_claims" + ] = self.cumulative_unrecovered_claims + current_log[ + "cumulative_claims" + ] = self.cumulative_claims # Log the cumulative claims received so far. + + """ add agent-level data to dict""" + current_log["insurance_firms_cash"] = insurance_firms + current_log["reinsurance_firms_cash"] = reinsurance_firms + current_log["market_diffvar"] = self.compute_market_diffvar() + + current_log["individual_contracts"] = [] + individual_contracts_no = [ + len(insurancefirm.underwritten_contracts) + for insurancefirm in self.insurancefirms + ] for i in range(len(individual_contracts_no)): - current_log['individual_contracts'].append(individual_contracts_no[i]) + current_log["individual_contracts"].append(individual_contracts_no[i]) """ call to Logger object """ self.logger.record_data(current_log) - + def obtain_log(self, requested_logs=None): """This function allows to return in a list all the data generated by the model. There is no other way to transfer it back from the cloud.""" return self.logger.obtain_log(requested_logs) - + def finalize(self, *args): """Function to handle operations after the end of the simulation run. Currently empty. @@ -420,13 +598,23 @@ def inflict_peril(self, categ_id, damage, t): Given severity of damage from pareto distribution Time iteration No return value""" - affected_contracts = [contract for insurer in self.insurancefirms for contract in insurer.underwritten_contracts if contract.category == categ_id] + affected_contracts = [ + contract + for insurer in self.insurancefirms + for contract in insurer.underwritten_contracts + if contract.category == categ_id + ] if isleconfig.verbose: print("**** PERIL ", damage) - damagevalues = np.random.beta(1, 1./damage -1, size=self.risks_counter[categ_id]) + damagevalues = np.random.beta( + 1, 1.0 / damage - 1, size=self.risks_counter[categ_id] + ) uniformvalues = np.random.uniform(0, 1, size=self.risks_counter[categ_id]) - [contract.explode(t, uniformvalues[i], damagevalues[i]) for i, contract in enumerate(affected_contracts)] - + [ + contract.explode(t, uniformvalues[i], damagevalues[i]) + for i, contract in enumerate(affected_contracts) + ] + def receive_obligation(self, amount, recipient, due_time, purpose): """Method for adding obligation to list that is resolved at the start if each iteration of simulation. Only called by metainsuranceorg for adding interest to cash. @@ -436,7 +624,12 @@ def receive_obligation(self, amount, recipient, due_time, purpose): Due Time Purpose: Reason for obligation (Interest due) Returns None""" - obligation = {"amount": amount, "recipient": recipient, "due_time": due_time, "purpose": purpose} + obligation = { + "amount": amount, + "recipient": recipient, + "due_time": due_time, + "purpose": purpose, + } self.obligations.append(obligation) def effect_payments(self, time): @@ -444,8 +637,10 @@ def effect_payments(self, time): Arguments Current time to allow check if due Returns None""" - due = [item for item in self.obligations if item["due_time"]<=time] - self.obligations = [item for item in self.obligations if item["due_time"]>time] + due = [item for item in self.obligations if item["due_time"] <= time] + self.obligations = [ + item for item in self.obligations if item["due_time"] > time + ] sum_due = sum([item["amount"] for item in due]) for obligation in due: self.pay(obligation) @@ -486,7 +681,11 @@ def reset_reinsurance_weights(self): how many offered reinsurance risks there are.""" self.not_accepted_reinrisks = [] - operational_reinfirms = [reinsurancefirm for reinsurancefirm in self.reinsurancefirms if reinsurancefirm.operational] + operational_reinfirms = [ + reinsurancefirm + for reinsurancefirm in self.reinsurancefirms + if reinsurancefirm.operational + ] operational_no = len(operational_reinfirms) @@ -499,8 +698,8 @@ def reset_reinsurance_weights(self): if operational_no > 0: - if reinrisks_no/operational_no > 1: - weights = reinrisks_no/operational_no + if reinrisks_no / operational_no > 1: + weights = reinrisks_no / operational_no for reinsurer in self.reinsurancefirms: self.reinsurers_weights[reinsurer.id] = math.floor(weights) else: @@ -514,9 +713,15 @@ def reset_insurance_weights(self): """Method for clearing and setting insurance weights dependant on how many insurance companies exist and how many insurance risks are offered. This determined which risks are sent to metainsuranceorg iteration.""" - operational_no = sum([insurancefirm.operational for insurancefirm in self.insurancefirms]) + operational_no = sum( + [insurancefirm.operational for insurancefirm in self.insurancefirms] + ) - operational_firms = [insurancefirm for insurancefirm in self.insurancefirms if insurancefirm.operational] + operational_firms = [ + insurancefirm + for insurancefirm in self.insurancefirms + if insurancefirm.operational + ] risks_no = len(self.risks) @@ -527,8 +732,8 @@ def reset_insurance_weights(self): if operational_no > 0: - if risks_no/operational_no > 1: - weights = risks_no/operational_no + if risks_no / operational_no > 1: + weights = risks_no / operational_no for insurer in self.insurancefirms: self.insurers_weights[insurer.id] = math.floor(weights) else: @@ -550,10 +755,24 @@ def adjust_market_premium(self, capital): with the capital available in the insurance market and viceversa. The premium reduces until it reaches a minimum below which no insurer is willing to reduce further the price. This method is only called in the self.iterate() method of this class.""" - self.market_premium = self.norm_premium * (self.simulation_parameters["upper_price_limit"] - self.simulation_parameters["premium_sensitivity"] * capital / (self.simulation_parameters["initial_agent_cash"] * self.damage_distribution.mean() * self.simulation_parameters["no_risks"])) - if self.market_premium < self.norm_premium * self.simulation_parameters["lower_price_limit"]: - self.market_premium = self.norm_premium * self.simulation_parameters["lower_price_limit"] - + self.market_premium = self.norm_premium * ( + self.simulation_parameters["upper_price_limit"] + - self.simulation_parameters["premium_sensitivity"] + * capital + / ( + self.simulation_parameters["initial_agent_cash"] + * self.damage_distribution.mean() + * self.simulation_parameters["no_risks"] + ) + ) + if ( + self.market_premium + < self.norm_premium * self.simulation_parameters["lower_price_limit"] + ): + self.market_premium = ( + self.norm_premium * self.simulation_parameters["lower_price_limit"] + ) + def adjust_reinsurance_market_premium(self, capital): """Adjust_market_premium Method. Accepts arguments @@ -563,9 +782,23 @@ def adjust_reinsurance_market_premium(self, capital): with the capital available in the reinsurance market and viceversa. The premium reduces until it reaches a minimum below which no reinsurer is willing to reduce further the price. This method is only called in the self.iterate() method of this class.""" - self.reinsurance_market_premium = self.norm_premium * (self.simulation_parameters["upper_price_limit"] - self.simulation_parameters["reinpremium_sensitivity"] * capital / (self.simulation_parameters["initial_agent_cash"] * self.damage_distribution.mean() * self.simulation_parameters["no_risks"])) - if self.reinsurance_market_premium < self.norm_premium * self.simulation_parameters["lower_price_limit"]: - self.reinsurance_market_premium = self.norm_premium * self.simulation_parameters["lower_price_limit"] + self.reinsurance_market_premium = self.norm_premium * ( + self.simulation_parameters["upper_price_limit"] + - self.simulation_parameters["reinpremium_sensitivity"] + * capital + / ( + self.simulation_parameters["initial_agent_cash"] + * self.damage_distribution.mean() + * self.simulation_parameters["no_risks"] + ) + ) + if ( + self.reinsurance_market_premium + < self.norm_premium * self.simulation_parameters["lower_price_limit"] + ): + self.reinsurance_market_premium = ( + self.norm_premium * self.simulation_parameters["lower_price_limit"] + ) def get_market_premium(self): """Get_market_premium Method. @@ -591,10 +824,12 @@ def get_reinsurance_premium(self, np_reinsurance_deductible_fraction): # TODO: cut this out of the insurance market premium -> OBSOLETE?? # TODO: make max_reduction into simulation_parameter ? if self.reinsurance_off: - return float('inf') + return float("inf") max_reduction = 0.1 - return self.reinsurance_market_premium * (1. - max_reduction * np_reinsurance_deductible_fraction) - + return self.reinsurance_market_premium * ( + 1.0 - max_reduction * np_reinsurance_deductible_fraction + ) + def get_cat_bond_price(self, np_reinsurance_deductible_fraction): """Method to calculate and return catbond price. If catbonds are not desired will return infinity so no catbonds will be issued. Otherwise calculates based on reinsurance market premium, catbond premium, deductible fraction. @@ -605,11 +840,13 @@ def get_cat_bond_price(self, np_reinsurance_deductible_fraction): # TODO: implement function dependent on total capital in cat bonds and on deductible () # TODO: make max_reduction and max_CB_surcharge into simulation_parameters ? if self.catbonds_off: - return float('inf') + return float("inf") max_reduction = 0.9 max_CB_surcharge = 0.5 - return self.reinsurance_market_premium * (1. + max_CB_surcharge - max_reduction * np_reinsurance_deductible_fraction) - + return self.reinsurance_market_premium * ( + 1.0 + max_CB_surcharge - max_reduction * np_reinsurance_deductible_fraction + ) + def append_reinrisks(self, item): """Method for appending reinrisks to simulation instance. Called from insurancefirm Accepts: item (Type: List)""" @@ -630,8 +867,8 @@ def solicit_insurance_requests(self, id, cash, insurer): insurer: Type firm metainsuranceorg instance Returns: risks_to_be_sent: Type List""" - risks_to_be_sent = self.risks[:int(self.insurers_weights[insurer.id])] - self.risks = self.risks[int(self.insurers_weights[insurer.id]):] + risks_to_be_sent = self.risks[: int(self.insurers_weights[insurer.id])] + self.risks = self.risks[int(self.insurers_weights[insurer.id]) :] for risk in insurer.risks_kept: risks_to_be_sent.append(risk) @@ -649,8 +886,10 @@ def solicit_reinsurance_requests(self, id, cash, reinsurer): reinsurer: Type firm metainsuranceorg instance Returns: reinrisks_to_be_sent: Type List""" - reinrisks_to_be_sent = self.reinrisks[:int(self.reinsurers_weights[reinsurer.id])] - self.reinrisks = self.reinrisks[int(self.reinsurers_weights[reinsurer.id]):] + reinrisks_to_be_sent = self.reinrisks[ + : int(self.reinsurers_weights[reinsurer.id]) + ] + self.reinrisks = self.reinrisks[int(self.reinsurers_weights[reinsurer.id]) :] for reinrisk in reinsurer.reinrisks_kept: reinrisks_to_be_sent.append(reinrisk) @@ -686,8 +925,10 @@ def get_all_riskmodel_combinations(self, rm_factor): riskmodels: Type list""" riskmodels = [] for i in range(self.simulation_parameters["no_categories"]): - riskmodel_combination = rm_factor * np.ones(self.simulation_parameters["no_categories"]) - riskmodel_combination[i] = 1/rm_factor + riskmodel_combination = rm_factor * np.ones( + self.simulation_parameters["no_categories"] + ) + riskmodel_combination[i] = 1 / rm_factor riskmodels.append(riskmodel_combination.tolist()) return riskmodels @@ -705,12 +946,18 @@ def setup_risk_categories(self): total += int(math.ceil(separation_time)) if total < self.simulation_parameters["max_time"]: event_schedule.append(total) - event_damage.append(self.damage_distribution.rvs()) #Schedules of catastrophes and damages must me generated at the same time. Reason: replication across different risk models. + event_damage.append( + self.damage_distribution.rvs() + ) # Schedules of catastrophes and damages must me generated at the same time. Reason: replication across different risk models. self.rc_event_schedule.append(event_schedule) self.rc_event_damage.append(event_damage) - self.rc_event_schedule_initial = copy.copy(self.rc_event_damage) #For debugging (cloud debugging) purposes is good to store the initial schedule of catastrophes - self.rc_event_damage_initial = copy.copy(self.rc_event_damage) #and damages that will be use in a single run of the model. + self.rc_event_schedule_initial = copy.copy( + self.rc_event_damage + ) # For debugging (cloud debugging) purposes is good to store the initial schedule of catastrophes + self.rc_event_damage_initial = copy.copy( + self.rc_event_damage + ) # and damages that will be use in a single run of the model. def setup_risk_categories_caller(self): """Method for calling setup_risk_categories. If conditions are set such that the system is replicating it is @@ -727,35 +974,47 @@ def setup_risk_categories_caller(self): def save_state_and_risk_categories(self): """Method to save numpy Mersenne Twister state and event schedule to allow for replication and continuation.""" mersennetwoster_randomseed = str(np.random.get_state()) - mersennetwoster_randomseed = mersennetwoster_randomseed.replace("\n","").replace("array", "np.array").replace("uint32", "np.uint32") - wfile = open("data/replication_randomseed.dat","a") - wfile.write(mersennetwoster_randomseed+"\n") + mersennetwoster_randomseed = ( + mersennetwoster_randomseed.replace("\n", "") + .replace("array", "np.array") + .replace("uint32", "np.uint32") + ) + wfile = open("data/replication_randomseed.dat", "a") + wfile.write(mersennetwoster_randomseed + "\n") wfile.close() - wfile = open("data/replication_rc_event_schedule.dat","a") - wfile.write(str(self.rc_event_schedule)+"\n") + wfile = open("data/replication_rc_event_schedule.dat", "a") + wfile.write(str(self.rc_event_schedule) + "\n") wfile.close() - + def restore_state_and_risk_categories(self): """Method to access saved event schedule, seed, and Mersenne twister state to allow for continuation.""" - rfile = open("data/replication_rc_event_schedule.dat","r") + rfile = open("data/replication_rc_event_schedule.dat", "r") found = False for i, line in enumerate(rfile): if i == self.replic_ID: self.rc_event_schedule = eval(line) found = True rfile.close() - assert found, "rc event schedule for current replication ID number {0:d} not found in data file. Exiting.".format(self.replic_ID) - rfile = open("data/replication_randomseed.dat","r") + assert ( + found + ), "rc event schedule for current replication ID number {0:d} not found in data file. Exiting.".format( + self.replic_ID + ) + rfile = open("data/replication_randomseed.dat", "r") found = False for i, line in enumerate(rfile): - #print(i, self.replic_ID) + # print(i, self.replic_ID) if i == self.replic_ID: mersennetwister_randomseed = eval(line) found = True rfile.close() np.random.set_state(mersennetwister_randomseed) - assert found, "mersennetwister randomseed for current replication ID number {0:d} not found in data file. Exiting.".format(self.replic_ID) + assert ( + found + ), "mersennetwister randomseed for current replication ID number {0:d} not found in data file. Exiting.".format( + self.replic_ID + ) def insurance_firm_market_entry(self, agent_type="InsuranceFirm"): """Method to determine if re/insurance firm enters the market based on set entry probabilities and a random @@ -769,9 +1028,15 @@ def insurance_firm_market_entry(self, agent_type="InsuranceFirm"): if agent_type == "InsuranceFirm": prob = self.simulation_parameters["insurance_firm_market_entry_probability"] elif agent_type == "ReinsuranceFirm": - prob = self.simulation_parameters["reinsurance_firm_market_entry_probability"] + prob = self.simulation_parameters[ + "reinsurance_firm_market_entry_probability" + ] else: - assert False, "Unknown agent type. Simulation requested to create agent of type {0:s}".format(agent_type) + assert ( + False + ), "Unknown agent type. Simulation requested to create agent of type {0:s}".format( + agent_type + ) if np.random.random() < prob: return True else: @@ -805,7 +1070,7 @@ def record_claims(self, claims): """This method records every claim made to insurers and reinsurers. It is called from both insurers and reinsurers (metainsuranceorg.py).""" self.cumulative_claims += claims - + def log(self): """Method to save the data of the simulation. No accepted values @@ -814,7 +1079,7 @@ def log(self): not for replicating instances. This depends on parameters force_foreground and if the run is replicating or not.""" self.logger.save_log(self.background_run) - + def compute_market_diffvar(self): """Method for calculating difference between number of all firms and the total value at risk. Used only in save data when adding to the logger data dict.""" @@ -843,9 +1108,9 @@ def compute_market_diffvar(self): totalreal = totalreal + sum(varsreinfirms) totaldiff = totalina - totalreal - + return totaldiff - #self.history_logs['market_diffvar'].append(totaldiff) + # self.history_logs['market_diffvar'].append(totaldiff) def get_unique_insurer_id(self): """Method for getting unique id for insurer. Used in initialising agents in start.py and insurancesimulation. @@ -870,14 +1135,18 @@ def insurance_entry_index(self): that is taken from the list of already created parameters. Returns: Indices of the type of riskmodel that the least firms are using.""" - return self.insurance_models_counter[0:self.simulation_parameters["no_riskmodels"]].argmin() + return self.insurance_models_counter[ + 0 : self.simulation_parameters["no_riskmodels"] + ].argmin() def reinsurance_entry_index(self): """Method that returns the entry index for reinsurance firms, i.e. the index for the initial agent parameters that is taken from the list of already created parameters. Returns: Indices of the type of riskmodel that the least reinsurance firms are using.""" - return self.reinsurance_models_counter[0:self.simulation_parameters["no_riskmodels"]].argmin() + return self.reinsurance_models_counter[ + 0 : self.simulation_parameters["no_riskmodels"] + ].argmin() def get_operational(self): """Method to return if simulation is operational. Always true. Used only in pay methods above and @@ -899,4 +1168,3 @@ def reset_pls(self): for catbond in self.catbonds: catbond.reset_pl() - diff --git a/isleconfig.py b/isleconfig.py index 8882c5d..978cdd9 100644 --- a/isleconfig.py +++ b/isleconfig.py @@ -3,76 +3,78 @@ force_foreground = False verbose = False showprogress = True -show_network = True # Should network be visualized? This should be False by default, to be overridden by commandline arguments -slim_log = False # Should logs be small in ensemble runs (only aggregated level data)? - -simulation_parameters = {"no_categories": 4, - "no_insurancefirms": 20, - "no_reinsurancefirms": 4, - "no_riskmodels": 2, - "riskmodel_inaccuracy_parameter": 2, # values >=1; inaccuracy higher with higher values - "riskmodel_margin_of_safety": 2, # values >=1; factor of additional liquidity beyond value at risk - "margin_increase": 0, # This parameter modifies the margin of safety depending on the number of risks models available in the market. When is 0 all risk models have the same margin of safety. - "value_at_risk_tail_probability": 0.005, # values <1, >0, usually close to 0; tail probability at which the value at risk is taken by the risk models - "norm_profit_markup": 0.15, - "rein_norm_profit_markup": 0.15, - "dividend_share_of_profits": 0.4, - "mean_contract_runtime": 12, - "contract_runtime_halfspread": 2, - "default_contract_payment_period": 3, - "max_time": 1000, - "money_supply": 2000000000, - "event_time_mean_separation": 100 / 3., - "expire_immediately": False, - "risk_factors_present": False, - "risk_factor_lower_bound": 0.4, - "risk_factor_upper_bound": 0.6, - "initial_acceptance_threshold": 0.5, - "acceptance_threshold_friction": 0.9, - "insurance_firm_market_entry_probability": 0.3, #0.02, - "reinsurance_firm_market_entry_probability": 0.05, #0.004, - "simulation_reinsurance_type": 'non-proportional', - "default_non-proportional_reinsurance_deductible": 0.3, - "default_non-proportional_reinsurance_excess": 1.0, - "default_non-proportional_reinsurance_premium_share": 0.3, - "static_non-proportional_reinsurance_levels": False, - "catbonds_off": False, - "reinsurance_off": False, - "capacity_target_decrement_threshold": 1.8, - "capacity_target_increment_threshold": 1.2, - "capacity_target_decrement_factor": 24/25., - "capacity_target_increment_factor": 25/24., - # Retention parameters - "insurance_retention": 0.85, # Ratio of insurance contracts retained every iteration. - "reinsurance_retention": 1, # Ratio of reinsurance contracts retained every iteration. - # Premium sensitivity parameters - "premium_sensitivity": 5, # This parameter represents how sensitive is the variation of the insurance premium with respect of the capital of the market. Higher means more sensitive. - "reinpremium_sensitivity": 6, # This parameter represents how sensitive is the variation of the reinsurance premium with respect of the capital of the market. Higher means more sensitive. - # Balanced portfolio parameters - "insurers_balance_ratio": 0.1, # This ratio represents how low we want to keep the standard deviation of the cash reserved below the mean for insurers. Lower means more balanced. - "reinsurers_balance_ratio": 20, # This ratio represents how low we want to keep the standard deviation of the cash reserved below the mean for reinsurers. Lower means more balanced. (Deactivated for the moment) - "insurers_recursion_limit": 50, # Intensity of the recursion algorithm to balance the portfolio of risks for insurers. - "reinsurers_recursion_limit": 10, # Intensity of the recursion algorithm to balance the portfolio of risks for reinsurers. - # Market permanency parameters - "market_permanency_off": False, # This parameter activates (deactivates) the following market permanency constraints. - "cash_permanency_limit": 100, # This parameter enforces the limit under which the firms leave the market because they cannot underwrite anything. - "insurance_permanency_contracts_limit": 4, # If insurers stay for too long under this limit of contracts they decide to leave the market. - "insurance_permanency_ratio_limit": 0.6, # If insurers stay for too long under this limit they decide to leave the market because they have too much capital. - "insurance_permanency_time_constraint": 24, # This parameter defines the period that the insurers wait if they have few capital or few contract before leaving the market. - "reinsurance_permanency_contracts_limit": 2, # If reinsurers stay for too long under this limit of contracts they decide to leave the market. - "reinsurance_permanency_ratio_limit": 0.8, # If reinsurers stay for too long under this limit they decide to leave the market because they have too much capital. - "reinsurance_permanency_time_constraint": 48, # This parameter defines the period that the reinsurers wait if they have few capital or few contract before leaving the market. - # Insurance and Reinsurance deductibles - "insurance_reinsurance_levels_lower_bound": 0.25, - "insurance_reinsurance_levels_upper_bound": 0.30, - "reinsurance_reinsurance_levels_lower_bound": 0.5, - "reinsurance_reinsurance_levels_upper_bound": 0.95, - "initial_agent_cash": 80000, - "initial_reinagent_cash": 2000000, - "interest_rate": 0.001, - "reinsurance_limit": 0.1, - "upper_price_limit": 1.2, - "lower_price_limit": 0.85, - "no_risks": 20000} - +show_network = ( + True +) # Should network be visualized? This should be False by default, to be overridden by commandline arguments +slim_log = False # Should logs be small in ensemble runs (only aggregated level data)? +simulation_parameters = { + "no_categories": 4, + "no_insurancefirms": 20, + "no_reinsurancefirms": 4, + "no_riskmodels": 2, + "riskmodel_inaccuracy_parameter": 2, # values >=1; inaccuracy higher with higher values + "riskmodel_margin_of_safety": 2, # values >=1; factor of additional liquidity beyond value at risk + "margin_increase": 0, # This parameter modifies the margin of safety depending on the number of risks models available in the market. When is 0 all risk models have the same margin of safety. + "value_at_risk_tail_probability": 0.005, # values <1, >0, usually close to 0; tail probability at which the value at risk is taken by the risk models + "norm_profit_markup": 0.15, + "rein_norm_profit_markup": 0.15, + "dividend_share_of_profits": 0.4, + "mean_contract_runtime": 12, + "contract_runtime_halfspread": 2, + "default_contract_payment_period": 3, + "max_time": 1000, + "money_supply": 2000000000, + "event_time_mean_separation": 100 / 3.0, + "expire_immediately": False, + "risk_factors_present": False, + "risk_factor_lower_bound": 0.4, + "risk_factor_upper_bound": 0.6, + "initial_acceptance_threshold": 0.5, + "acceptance_threshold_friction": 0.9, + "insurance_firm_market_entry_probability": 0.3, # 0.02, + "reinsurance_firm_market_entry_probability": 0.05, # 0.004, + "simulation_reinsurance_type": "non-proportional", + "default_non-proportional_reinsurance_deductible": 0.3, + "default_non-proportional_reinsurance_excess": 1.0, + "default_non-proportional_reinsurance_premium_share": 0.3, + "static_non-proportional_reinsurance_levels": False, + "catbonds_off": False, + "reinsurance_off": False, + "capacity_target_decrement_threshold": 1.8, + "capacity_target_increment_threshold": 1.2, + "capacity_target_decrement_factor": 24 / 25.0, + "capacity_target_increment_factor": 25 / 24.0, + # Retention parameters + "insurance_retention": 0.85, # Ratio of insurance contracts retained every iteration. + "reinsurance_retention": 1, # Ratio of reinsurance contracts retained every iteration. + # Premium sensitivity parameters + "premium_sensitivity": 5, # This parameter represents how sensitive is the variation of the insurance premium with respect of the capital of the market. Higher means more sensitive. + "reinpremium_sensitivity": 6, # This parameter represents how sensitive is the variation of the reinsurance premium with respect of the capital of the market. Higher means more sensitive. + # Balanced portfolio parameters + "insurers_balance_ratio": 0.1, # This ratio represents how low we want to keep the standard deviation of the cash reserved below the mean for insurers. Lower means more balanced. + "reinsurers_balance_ratio": 20, # This ratio represents how low we want to keep the standard deviation of the cash reserved below the mean for reinsurers. Lower means more balanced. (Deactivated for the moment) + "insurers_recursion_limit": 50, # Intensity of the recursion algorithm to balance the portfolio of risks for insurers. + "reinsurers_recursion_limit": 10, # Intensity of the recursion algorithm to balance the portfolio of risks for reinsurers. + # Market permanency parameters + "market_permanency_off": False, # This parameter activates (deactivates) the following market permanency constraints. + "cash_permanency_limit": 100, # This parameter enforces the limit under which the firms leave the market because they cannot underwrite anything. + "insurance_permanency_contracts_limit": 4, # If insurers stay for too long under this limit of contracts they decide to leave the market. + "insurance_permanency_ratio_limit": 0.6, # If insurers stay for too long under this limit they decide to leave the market because they have too much capital. + "insurance_permanency_time_constraint": 24, # This parameter defines the period that the insurers wait if they have few capital or few contract before leaving the market. + "reinsurance_permanency_contracts_limit": 2, # If reinsurers stay for too long under this limit of contracts they decide to leave the market. + "reinsurance_permanency_ratio_limit": 0.8, # If reinsurers stay for too long under this limit they decide to leave the market because they have too much capital. + "reinsurance_permanency_time_constraint": 48, # This parameter defines the period that the reinsurers wait if they have few capital or few contract before leaving the market. + # Insurance and Reinsurance deductibles + "insurance_reinsurance_levels_lower_bound": 0.25, + "insurance_reinsurance_levels_upper_bound": 0.30, + "reinsurance_reinsurance_levels_lower_bound": 0.5, + "reinsurance_reinsurance_levels_upper_bound": 0.95, + "initial_agent_cash": 80000, + "initial_reinagent_cash": 2000000, + "interest_rate": 0.001, + "reinsurance_limit": 0.1, + "upper_price_limit": 1.2, + "lower_price_limit": 0.85, + "no_risks": 20000, +} diff --git a/listify.py b/listify.py index 591e626..c410c21 100644 --- a/listify.py +++ b/listify.py @@ -1,6 +1,7 @@ """Auxiliary function to transform dicts into lists and back for transfer from cloud (sandman2) to local.""" + def listify(d): """Function to convert dict to list with keys in last list element. Arguments: @@ -8,16 +9,17 @@ def listify(d): Returns: list with dict values as elements [:-1] and dict keys as last element.""" - + """extract keys""" keys = list(d.keys()) - + """create list""" l = [d[key] for key in keys] l.append(keys) - + return l + def delistify(l): """Function to convert listified dict back to dict. Arguments: @@ -26,12 +28,12 @@ def delistify(l): dict keys as list in the last element. Returns: dict - The restored dict.""" - + """extract keys""" keys = l.pop() assert len(keys) == len(l) - + """create dict""" - d = {key: l[i] for i,key in enumerate(keys)} - + d = {key: l[i] for i, key in enumerate(keys)} + return d diff --git a/logger.py b/logger.py index 8fe928f..60f14d3 100644 --- a/logger.py +++ b/logger.py @@ -5,16 +5,22 @@ import listify LOG_DEFAULT = ( - 'total_cash total_excess_capital total_profitslosses total_contracts ' - 'total_operational total_reincash total_reinexcess_capital total_reinprofitslosses ' - 'total_reincontracts total_reinoperational total_catbondsoperational market_premium ' - 'market_reinpremium cumulative_bankruptcies cumulative_market_exits cumulative_unrecovered_claims ' - 'cumulative_claims insurance_firms_cash reinsurance_firms_cash market_diffvar ' - 'rc_event_schedule_initial rc_event_damage_initial number_riskmodels' -).split(' ') - -class Logger(): - def __init__(self, no_riskmodels=None, rc_event_schedule_initial=None, rc_event_damage_initial=None): + "total_cash total_excess_capital total_profitslosses total_contracts " + "total_operational total_reincash total_reinexcess_capital total_reinprofitslosses " + "total_reincontracts total_reinoperational total_catbondsoperational market_premium " + "market_reinpremium cumulative_bankruptcies cumulative_market_exits cumulative_unrecovered_claims " + "cumulative_claims insurance_firms_cash reinsurance_firms_cash market_diffvar " + "rc_event_schedule_initial rc_event_damage_initial number_riskmodels" +).split(" ") + + +class Logger: + def __init__( + self, + no_riskmodels=None, + rc_event_schedule_initial=None, + rc_event_damage_initial=None, + ): """Constructor. Prepares history_logs atribute as dict for the logs. Records initial event schedule of simulation run. Arguments @@ -22,82 +28,90 @@ def __init__(self, no_riskmodels=None, rc_event_schedule_initial=None, rc_event_ rc_event_schedule_initial: list of lists of int. Times of risk events by category rc_event_damage_initial: list of arrays (or lists) of float. Damage by peril for each category as share of total possible damage (maximum insured or excess). - Returns class instance.""" - + Returns class instance.""" + """Record number of riskmodels""" self.number_riskmodels = no_riskmodels - + """Record initial event schedule""" self.rc_event_schedule_initial = rc_event_schedule_initial self.rc_event_damage_initial = rc_event_damage_initial """Prepare history log dict""" self.history_logs = {} - + """Variables pertaining to insurance sector""" # TODO: should we not have `cumulative_bankruptcies` and # `cumulative_market_exits` for both insurance firms and reinsurance firms? # `cumulative_claims`: Here are stored the total cumulative claims received # by the whole insurance sector until a certain time. - insurance_sector = ('total_cash total_excess_capital total_profitslosses ' - 'total_contracts total_operational cumulative_bankruptcies ' - 'cumulative_market_exits cumulative_claims cumulative_unrecovered_claims').split(' ') + insurance_sector = ( + "total_cash total_excess_capital total_profitslosses " + "total_contracts total_operational cumulative_bankruptcies " + "cumulative_market_exits cumulative_claims cumulative_unrecovered_claims" + ).split(" ") for _v in insurance_sector: self.history_logs[_v] = [] - + """Variables pertaining to individual insurance firms""" - self.history_logs['individual_contracts'] = [] # TODO: Should there not be a similar record for reinsurance - self.history_logs['insurance_firms_cash'] = [] - + self.history_logs[ + "individual_contracts" + ] = [] # TODO: Should there not be a similar record for reinsurance + self.history_logs["insurance_firms_cash"] = [] + """Variables pertaining to reinsurance sector""" - self.history_logs['total_reincash'] = [] - self.history_logs['total_reinexcess_capital'] = [] - self.history_logs['total_reinprofitslosses'] = [] - self.history_logs['total_reincontracts'] = [] - self.history_logs['total_reinoperational'] = [] + self.history_logs["total_reincash"] = [] + self.history_logs["total_reinexcess_capital"] = [] + self.history_logs["total_reinprofitslosses"] = [] + self.history_logs["total_reincontracts"] = [] + self.history_logs["total_reinoperational"] = [] """Variables pertaining to individual reinsurance firms""" - self.history_logs['reinsurance_firms_cash'] = [] + self.history_logs["reinsurance_firms_cash"] = [] """Variables pertaining to cat bonds""" - self.history_logs['total_catbondsoperational'] = [] + self.history_logs["total_catbondsoperational"] = [] """Variables pertaining to premiums""" - self.history_logs['market_premium'] = [] - self.history_logs['market_reinpremium'] = [] - self.history_logs['market_diffvar'] = [] - + self.history_logs["market_premium"] = [] + self.history_logs["market_reinpremium"] = [] + self.history_logs["market_diffvar"] = [] + def record_data(self, data_dict): """Method to record data for one period Arguments data_dict: Type dict. Data with the same keys as are used in self.history_log(). - Returns None.""" + Returns None.""" for key in data_dict.keys(): if key != "individual_contracts": self.history_logs[key].append(data_dict[key]) else: for i in range(len(data_dict["individual_contracts"])): - self.history_logs['individual_contracts'][i].append(data_dict["individual_contracts"][i]) + self.history_logs["individual_contracts"][i].append( + data_dict["individual_contracts"][i] + ) - def obtain_log(self, requested_logs=LOG_DEFAULT): #This function allows to return in a list all the data generated by the model. There is no other way to transfer it back from the cloud. + def obtain_log( + self, requested_logs=LOG_DEFAULT + ): # This function allows to return in a list all the data generated by the model. There is no other way to transfer it back from the cloud. """Method to transfer entire log (self.history_log as well as risk event schedule). This is used to transfer the log to master core from work cores in ensemble runs in the cloud. No arguments. Returns list (listified dict).""" - + """Include environment variables (number of risk models and risk event schedule)""" self.history_logs["number_riskmodels"] = self.number_riskmodels self.history_logs["rc_event_damage_initial"] = self.rc_event_damage_initial self.history_logs["rc_event_schedule_initial"] = self.rc_event_schedule_initial - + """Parse logs to be returned""" if requested_logs == None: requested_logs = LOG_DEFAULT log = {name: self.history_logs[name] for name in requested_logs} - + """Convert to list and return""" return listify.listify(log) - + def restore_logger_object(self, log): """Method to restore logger object. A log can be restored later. It can also be restored on a different machine. This is useful in the case of ensemble runs to move the log to @@ -110,13 +124,17 @@ def restore_logger_object(self, log): """Restore dict""" log = listify.delistify(log) - + """Extract environment variables (number of risk models and risk event schedule)""" self.rc_event_schedule_initial = log["rc_event_schedule_initial"] self.rc_event_damage_initial = log["rc_event_damage_initial"] self.number_riskmodels = log["number_riskmodels"] - del log["rc_event_schedule_initial"], log["rc_event_damage_initial"], log["number_riskmodels"] - + del ( + log["rc_event_schedule_initial"], + log["rc_event_damage_initial"], + log["number_riskmodels"], + ) + """Restore history log""" self.history_logs = log @@ -126,18 +144,18 @@ def save_log(self, background_run): Arguments: background_run: Type bool. Is this an ensemble run (true) or not (false). Returns None.""" - + """Prepare writing tasks""" if background_run: to_log = self.replication_log_prepare() else: to_log = self.single_log_prepare() - + """Write to disk""" for filename, data, operation_character in to_log: with open(filename, operation_character) as wfile: wfile.write(str(data) + "\n") - + def replication_log_prepare(self): """Method to prepare writing tasks for ensemble run saving. No arguments @@ -150,7 +168,7 @@ def replication_log_prepare(self): to_log = [] to_log.append(("data/" + fpf + "_history_logs.dat", self.history_logs, "a")) return to_log - + def single_log_prepare(self): """Method to prepare writing tasks for single run saving. No arguments @@ -161,17 +179,18 @@ def single_log_prepare(self): to_log = [] to_log.append(("data/history_logs.dat", self.history_logs, "w")) return to_log - - def add_insurance_agent(self): + + def add_insurance_agent(self): """Method for adding an additional insurer agent to the history log. This is necessary to keep the number of individual insurance firm logs constant in time. No arguments. Returns None.""" - # TODO: should this not also be done for self.history_logs['insurance_firms_cash'] and + # TODO: should this not also be done for self.history_logs['insurance_firms_cash'] and # self.history_logs['reinsurance_firms_cash'] - if len(self.history_logs['individual_contracts']) > 0: - zeroes_to_append = list(np.zeros(len(self.history_logs['individual_contracts'][0]), dtype=int)) + if len(self.history_logs["individual_contracts"]) > 0: + zeroes_to_append = list( + np.zeros(len(self.history_logs["individual_contracts"][0]), dtype=int) + ) else: zeroes_to_append = [] - self.history_logs['individual_contracts'].append(zeroes_to_append) - + self.history_logs["individual_contracts"].append(zeroes_to_append) diff --git a/metainsurancecontract.py b/metainsurancecontract.py index c464e33..551a677 100644 --- a/metainsurancecontract.py +++ b/metainsurancecontract.py @@ -1,9 +1,23 @@ import numpy as np import sys, pdb -class MetaInsuranceContract(): - def __init__(self, insurer, properties, time, premium, runtime, payment_period, expire_immediately, initial_VaR=0., \ - insurancetype="proportional", deductible_fraction=None, excess_fraction=None, reinsurance=0): + +class MetaInsuranceContract: + def __init__( + self, + insurer, + properties, + time, + premium, + runtime, + payment_period, + expire_immediately, + initial_VaR=0.0, + insurancetype="proportional", + deductible_fraction=None, + excess_fraction=None, + reinsurance=0, + ): """Constructor method. Accepts arguments insurer: Type InsuranceFirm. @@ -24,15 +38,19 @@ def __init__(self, insurer, properties, time, premium, runtime, payment_period, Creates InsuranceContract, saves parameters. Creates obligation for premium payment. Includes contract in reinsurance network if applicable (e.g. if this is a ReinsuranceContract).""" # TODO: argument reinsurance seems senseless; remove? - + # Save parameters self.insurer = insurer self.risk_factor = properties["risk_factor"] self.category = properties["category"] self.property_holder = properties["owner"] self.value = properties["value"] - self.contract = properties.get("contract") # will assign None if key does not exist - self.insurancetype = properties.get("insurancetype") if insurancetype is None else insurancetype + self.contract = properties.get( + "contract" + ) # will assign None if key does not exist + self.insurancetype = ( + properties.get("insurancetype") if insurancetype is None else insurancetype + ) self.runtime = runtime self.starttime = time self.expiration = runtime + time @@ -40,35 +58,56 @@ def __init__(self, insurer, properties, time, premium, runtime, payment_period, self.terminating = False self.current_claim = 0 self.initial_VaR = initial_VaR - - # set deductible from argument, risk property or default value, whichever first is not None + + # set deductible from argument, risk property or default value, whichever first is not None default_deductible_fraction = 0.0 - deductible_fraction_generator = (item for item in [deductible_fraction, properties.get("deductible_fraction"), \ - default_deductible_fraction] if item is not None) + deductible_fraction_generator = ( + item + for item in [ + deductible_fraction, + properties.get("deductible_fraction"), + default_deductible_fraction, + ] + if item is not None + ) self.deductible_fraction = next(deductible_fraction_generator) self.deductible = self.deductible_fraction * self.value - - # set excess from argument, risk property or default value, whichever first is not None + + # set excess from argument, risk property or default value, whichever first is not None default_excess_fraction = 1.0 - excess_fraction_generator = (item for item in [excess_fraction, properties.get("excess_fraction"), \ - default_excess_fraction] if item is not None) + excess_fraction_generator = ( + item + for item in [ + excess_fraction, + properties.get("excess_fraction"), + default_excess_fraction, + ] + if item is not None + ) self.excess_fraction = next(excess_fraction_generator) self.excess = self.excess_fraction * self.value - + self.reinsurance = reinsurance self.reinsurer = None self.reincontract = None self.reinsurance_share = None # setup payment schedule - total_premium = premium * self.value + total_premium = premium * self.value self.periodized_premium = total_premium / self.runtime - self.payment_times = [time + i for i in range(runtime) if i % payment_period == 0] - self.payment_values = total_premium * (np.ones(len(self.payment_times)) / len(self.payment_times)) + self.payment_times = [ + time + i for i in range(runtime) if i % payment_period == 0 + ] + self.payment_values = total_premium * ( + np.ones(len(self.payment_times)) / len(self.payment_times) + ) if self.contract is not None: - self.contract.reinsure(reinsurer=self.insurer, reinsurance_share=properties["reinsurance_share"], \ - reincontract=self) + self.contract.reinsure( + reinsurer=self.insurer, + reinsurance_share=properties["reinsurance_share"], + reincontract=self, + ) # This flag is set to 1, when the contract is about to expire and there is an attempt to roll it over. self.roll_over_flag = 0 @@ -81,12 +120,14 @@ def check_payment_due(self, time): This method checks if a scheduled premium payment is due, pays it to the insurer, and removes from schedule.""" if len(self.payment_times) > 0 and time >= self.payment_times[0]: # Create obligation for premium payment - self.property_holder.receive_obligation(self.payment_values[0], self.insurer, time, 'premium') - + self.property_holder.receive_obligation( + self.payment_values[0], self.insurer, time, "premium" + ) + # Remove current payment from payment schedule self.payment_times = self.payment_times[1:] self.payment_values = self.payment_values[1:] - + def get_and_reset_current_claim(self): """Method to return and reset claim. No accepted values @@ -107,7 +148,7 @@ def terminate_reinsurance(self, time): Causes any reinsurance contracts to be dissolved as the present contract terminates.""" if self.reincontract is not None: self.reincontract.dissolve(time) - + def dissolve(self, time): """Dissolve method. Accepts arguments @@ -128,9 +169,9 @@ def reinsure(self, reinsurer, reinsurance_share, reincontract): self.reinsurance = self.value * reinsurance_share self.reinsurance_share = reinsurance_share self.reincontract = reincontract - assert self.reinsurance_share in [None, 0.0, 1.0] - - def unreinsure(self): + assert self.reinsurance_share in [None, 0.0, 1.0] + + def unreinsure(self): """Unreinsurance Method. Accepts no arguments: No return value. diff --git a/metainsuranceorg.py b/metainsuranceorg.py index 15490d2..29513c0 100644 --- a/metainsuranceorg.py +++ b/metainsuranceorg.py @@ -28,81 +28,125 @@ def init(self, simulation_parameters, agent_parameters): agent_parameters: Type DataDict Constructor creates general instance of an insurance company which is inherited by the reinsurance and insurance firm classes. Initialises all necessary values provided by config file.""" - self.simulation = simulation_parameters['simulation'] + self.simulation = simulation_parameters["simulation"] self.simulation_parameters = simulation_parameters - self.contract_runtime_dist = scipy.stats.randint(simulation_parameters["mean_contract_runtime"] - \ - simulation_parameters["contract_runtime_halfspread"], simulation_parameters["mean_contract_runtime"] \ - + simulation_parameters["contract_runtime_halfspread"] + 1) - self.default_contract_payment_period = simulation_parameters["default_contract_payment_period"] - self.id = agent_parameters['id'] - self.cash = agent_parameters['initial_cash'] + self.contract_runtime_dist = scipy.stats.randint( + simulation_parameters["mean_contract_runtime"] + - simulation_parameters["contract_runtime_halfspread"], + simulation_parameters["mean_contract_runtime"] + + simulation_parameters["contract_runtime_halfspread"] + + 1, + ) + self.default_contract_payment_period = simulation_parameters[ + "default_contract_payment_period" + ] + self.id = agent_parameters["id"] + self.cash = agent_parameters["initial_cash"] self.capacity_target = self.cash * 0.9 - self.capacity_target_decrement_threshold = agent_parameters['capacity_target_decrement_threshold'] - self.capacity_target_increment_threshold = agent_parameters['capacity_target_increment_threshold'] - self.capacity_target_decrement_factor = agent_parameters['capacity_target_decrement_factor'] - self.capacity_target_increment_factor = agent_parameters['capacity_target_increment_factor'] + self.capacity_target_decrement_threshold = agent_parameters[ + "capacity_target_decrement_threshold" + ] + self.capacity_target_increment_threshold = agent_parameters[ + "capacity_target_increment_threshold" + ] + self.capacity_target_decrement_factor = agent_parameters[ + "capacity_target_decrement_factor" + ] + self.capacity_target_increment_factor = agent_parameters[ + "capacity_target_increment_factor" + ] self.excess_capital = self.cash self.premium = agent_parameters["norm_premium"] - self.profit_target = agent_parameters['profit_target'] - self.acceptance_threshold = agent_parameters['initial_acceptance_threshold'] # 0.5 - self.acceptance_threshold_friction = agent_parameters['acceptance_threshold_friction'] # 0.9 #1.0 to switch off + self.profit_target = agent_parameters["profit_target"] + self.acceptance_threshold = agent_parameters[ + "initial_acceptance_threshold" + ] # 0.5 + self.acceptance_threshold_friction = agent_parameters[ + "acceptance_threshold_friction" + ] # 0.9 #1.0 to switch off self.interest_rate = agent_parameters["interest_rate"] self.reinsurance_limit = agent_parameters["reinsurance_limit"] self.simulation_no_risk_categories = simulation_parameters["no_categories"] - self.simulation_reinsurance_type = simulation_parameters["simulation_reinsurance_type"] - self.dividend_share_of_profits = simulation_parameters["dividend_share_of_profits"] - + self.simulation_reinsurance_type = simulation_parameters[ + "simulation_reinsurance_type" + ] + self.dividend_share_of_profits = simulation_parameters[ + "dividend_share_of_profits" + ] + self.owner = self.simulation # TODO: Make this into agent_parameter value? self.per_period_dividend = 0 - self.cash_last_periods = list(np.zeros(4, dtype=int)*self.cash) - - rm_config = agent_parameters['riskmodel_config'] + self.cash_last_periods = list(np.zeros(4, dtype=int) * self.cash) + + rm_config = agent_parameters["riskmodel_config"] """Here we modify the margin of safety depending on the number of risks models available in the market. When is 0 all risk models have the same margin of safety. The reason for doing this is that with more risk models the firms tend to be closer to the max capacity""" - margin_of_safety_correction = (rm_config["margin_of_safety"] + (simulation_parameters["no_riskmodels"] - 1) * simulation_parameters["margin_increase"]) - - self.riskmodel = RiskModel(damage_distribution=rm_config["damage_distribution"], \ - expire_immediately=rm_config["expire_immediately"], \ - cat_separation_distribution=rm_config["cat_separation_distribution"], \ - norm_premium=rm_config["norm_premium"], \ - category_number=rm_config["no_categories"], \ - init_average_exposure=rm_config["risk_value_mean"], \ - init_average_risk_factor=rm_config["risk_factor_mean"], \ - init_profit_estimate=rm_config["norm_profit_markup"], \ - margin_of_safety=margin_of_safety_correction, \ - var_tail_prob=rm_config["var_tail_prob"], \ - inaccuracy=rm_config["inaccuracy_by_categ"]) - - self.category_reinsurance = [None for i in range(self.simulation_no_risk_categories)] - if self.simulation_reinsurance_type == 'non-proportional': - if agent_parameters['non-proportional_reinsurance_level'] is not None: - self.np_reinsurance_deductible_fraction = agent_parameters['non-proportional_reinsurance_level'] + margin_of_safety_correction = ( + rm_config["margin_of_safety"] + + (simulation_parameters["no_riskmodels"] - 1) + * simulation_parameters["margin_increase"] + ) + + self.riskmodel = RiskModel( + damage_distribution=rm_config["damage_distribution"], + expire_immediately=rm_config["expire_immediately"], + cat_separation_distribution=rm_config["cat_separation_distribution"], + norm_premium=rm_config["norm_premium"], + category_number=rm_config["no_categories"], + init_average_exposure=rm_config["risk_value_mean"], + init_average_risk_factor=rm_config["risk_factor_mean"], + init_profit_estimate=rm_config["norm_profit_markup"], + margin_of_safety=margin_of_safety_correction, + var_tail_prob=rm_config["var_tail_prob"], + inaccuracy=rm_config["inaccuracy_by_categ"], + ) + + self.category_reinsurance = [ + None for i in range(self.simulation_no_risk_categories) + ] + if self.simulation_reinsurance_type == "non-proportional": + if agent_parameters["non-proportional_reinsurance_level"] is not None: + self.np_reinsurance_deductible_fraction = agent_parameters[ + "non-proportional_reinsurance_level" + ] else: - self.np_reinsurance_deductible_fraction = simulation_parameters["default_non-proportional_reinsurance_deductible"] - self.np_reinsurance_excess_fraction = simulation_parameters["default_non-proportional_reinsurance_excess"] - self.np_reinsurance_premium_share = simulation_parameters["default_non-proportional_reinsurance_premium_share"] + self.np_reinsurance_deductible_fraction = simulation_parameters[ + "default_non-proportional_reinsurance_deductible" + ] + self.np_reinsurance_excess_fraction = simulation_parameters[ + "default_non-proportional_reinsurance_excess" + ] + self.np_reinsurance_premium_share = simulation_parameters[ + "default_non-proportional_reinsurance_premium_share" + ] self.obligations = [] self.underwritten_contracts = [] self.profits_losses = 0 - #self.reinsurance_contracts = [] + # self.reinsurance_contracts = [] self.operational = True self.is_insurer = True self.is_reinsurer = False - + """set up risk value estimate variables""" - self.var_counter = 0 # sum over risk model inaccuracies for all contracts - self.var_counter_per_risk = 0 # average risk model inaccuracy across contracts - self.var_sum = 0 # sum over initial VaR for all contracts - self.counter_category = np.zeros(self.simulation_no_risk_categories) # var_counter disaggregated by category - self.var_category = np.zeros(self.simulation_no_risk_categories) # var_sum disaggregated by category + self.var_counter = 0 # sum over risk model inaccuracies for all contracts + self.var_counter_per_risk = 0 # average risk model inaccuracy across contracts + self.var_sum = 0 # sum over initial VaR for all contracts + self.counter_category = np.zeros( + self.simulation_no_risk_categories + ) # var_counter disaggregated by category + self.var_category = np.zeros( + self.simulation_no_risk_categories + ) # var_sum disaggregated by category self.naccep = [] self.risks_kept = [] self.reinrisks_kept = [] - self.balance_ratio = simulation_parameters['insurers_balance_ratio'] - self.recursion_limit = simulation_parameters['insurers_recursion_limit'] - self.cash_left_by_categ = [self.cash for i in range(self.simulation_parameters["no_categories"])] + self.balance_ratio = simulation_parameters["insurers_balance_ratio"] + self.recursion_limit = simulation_parameters["insurers_recursion_limit"] + self.cash_left_by_categ = [ + self.cash for i in range(self.simulation_parameters["no_categories"]) + ] self.market_permanency_counter = 0 def iterate(self, time): @@ -120,17 +164,28 @@ def iterate(self, time): """realize due payments""" self.effect_payments(time) if isleconfig.verbose: - print(time, ":", self.id, len(self.underwritten_contracts), self.cash, self.operational) + print( + time, + ":", + self.id, + len(self.underwritten_contracts), + self.cash, + self.operational, + ) self.make_reinsurance_claims(time) """mature contracts""" if isleconfig.verbose: print("Number of underwritten contracts ", len(self.underwritten_contracts)) - maturing = [contract for contract in self.underwritten_contracts if contract.expiration <= time] + maturing = [ + contract + for contract in self.underwritten_contracts + if contract.expiration <= time + ] for contract in maturing: - self.underwritten_contracts.remove(contract) - contract.mature(time) + self.underwritten_contracts.remove(contract) + contract.mature(time) contracts_dissolved = len(maturing) """effect payments from contracts""" @@ -142,28 +197,49 @@ def iterate(self, time): new_nonproportional_risks, new_risks = self.get_newrisks_by_type() contracts_offered = len(new_risks) if isleconfig.verbose and contracts_offered < 2 * contracts_dissolved: - print("Something wrong; agent {0:d} receives too few new contracts {1:d} <= {2:d}".format( - self.id, contracts_offered, 2 * contracts_dissolved)) + print( + "Something wrong; agent {0:d} receives too few new contracts {1:d} <= {2:d}".format( + self.id, contracts_offered, 2 * contracts_dissolved + ) + ) """deal with non-proportional risks first as they must evaluate each request separately""" - [reinrisks_per_categ, number_reinrisks_categ] = self.risks_reinrisks_organizer(new_nonproportional_risks) + [ + reinrisks_per_categ, + number_reinrisks_categ, + ] = self.risks_reinrisks_organizer(new_nonproportional_risks) for repetition in range(self.recursion_limit): former_reinrisks_per_categ = copy.copy(reinrisks_per_categ) - [reinrisks_per_categ, not_accepted_reinrisks] = self.process_newrisks_reinsurer(reinrisks_per_categ, number_reinrisks_categ, time) #Here we process all the new reinrisks in order to keep the portfolio as balanced as possible. + [ + reinrisks_per_categ, + not_accepted_reinrisks, + ] = self.process_newrisks_reinsurer( + reinrisks_per_categ, number_reinrisks_categ, time + ) # Here we process all the new reinrisks in order to keep the portfolio as balanced as possible. if former_reinrisks_per_categ == reinrisks_per_categ: break self.simulation.return_reinrisks(not_accepted_reinrisks) - underwritten_risks = [{"value": contract.value, "category": contract.category, \ - "risk_factor": contract.risk_factor, "deductible": contract.deductible, \ - "excess": contract.excess, "insurancetype": contract.insurancetype, \ - "runtime": contract.runtime} for contract in self.underwritten_contracts if - contract.reinsurance_share != 1.0] + underwritten_risks = [ + { + "value": contract.value, + "category": contract.category, + "risk_factor": contract.risk_factor, + "deductible": contract.deductible, + "excess": contract.excess, + "insurancetype": contract.insurancetype, + "runtime": contract.runtime, + } + for contract in self.underwritten_contracts + if contract.reinsurance_share != 1.0 + ] """obtain risk model evaluation (VaR) for underwriting decisions and for capacity specific decisions""" # TODO: Enable reinsurance shares other than 0.0 and 1.0 - expected_profit, acceptable_by_category, cash_left_by_categ, var_per_risk_per_categ, self.excess_capital = self.riskmodel.evaluate(underwritten_risks, self.cash) - # TODO: resolve insurance reinsurance inconsistency (insurer underwrite after capacity decisions, reinsurers before). + expected_profit, acceptable_by_category, cash_left_by_categ, var_per_risk_per_categ, self.excess_capital = self.riskmodel.evaluate( + underwritten_risks, self.cash + ) + # TODO: resolve insurance reinsurance inconsistency (insurer underwrite after capacity decisions, reinsurers before). # This is currently so because it minimizes the number of times we need to run self.riskmodel.evaluate(). # It would also be more consistent if excess capital would be updated at the end of the iteration. @@ -179,17 +255,31 @@ def iterate(self, time): self.pay_dividends(time) """make underwriting decisions, category-wise""" - growth_limit = max(50, 2 * len(self.underwritten_contracts) + contracts_dissolved) + growth_limit = max( + 50, 2 * len(self.underwritten_contracts) + contracts_dissolved + ) if sum(acceptable_by_category) > growth_limit: - acceptable_by_category = np.asarray(acceptable_by_category).astype(np.double) - acceptable_by_category = acceptable_by_category * growth_limit / sum(acceptable_by_category) + acceptable_by_category = np.asarray(acceptable_by_category).astype( + np.double + ) + acceptable_by_category = ( + acceptable_by_category * growth_limit / sum(acceptable_by_category) + ) acceptable_by_category = np.int64(np.round(acceptable_by_category)) - [risks_per_categ, number_risks_categ] = self.risks_reinrisks_organizer(new_risks) + [risks_per_categ, number_risks_categ] = self.risks_reinrisks_organizer( + new_risks + ) for repetition in range(self.recursion_limit): former_risks_per_categ = copy.copy(risks_per_categ) - [risks_per_categ, not_accepted_risks] = self.process_newrisks_insurer(risks_per_categ, number_risks_categ, acceptable_by_category, - var_per_risk_per_categ, cash_left_by_categ, time) #Here we process all the new risks in order to keep the portfolio as balanced as possible. + [risks_per_categ, not_accepted_risks] = self.process_newrisks_insurer( + risks_per_categ, + number_risks_categ, + acceptable_by_category, + var_per_risk_per_categ, + cash_left_by_categ, + time, + ) # Here we process all the new risks in order to keep the portfolio as balanced as possible. if former_risks_per_categ == risks_per_categ: break self.simulation.return_risks(not_accepted_risks) @@ -201,7 +291,7 @@ def iterate(self, time): self.market_permanency(time) self.roll_over(time) - + self.estimated_var() def enter_illiquidity(self, time): @@ -222,7 +312,7 @@ def enter_bankruptcy(self, time): This method is used when a firm does not have enough cash to pay all its obligations. It is only called from the method self.enter_illiquidity() which is only called from the method self.effect_payments(). This method dissolves the firm through the method self.dissolve().""" - self.dissolve(time, 'record_bankruptcy') + self.dissolve(time, "record_bankruptcy") def market_exit(self, time): """Market_exit Method. @@ -238,7 +328,7 @@ def market_exit(self, time): for obligation in due: self.pay(obligation) self.obligations = [] - self.dissolve(time, 'record_market_exit') + self.dissolve(time, "record_market_exit") def dissolve(self, time, record): """Dissolve Method. @@ -253,14 +343,27 @@ def dissolve(self, time, record): next iteration. Finally the type of dissolution is recorded and the operational state is set to false. Different class variables are reset during the process: self.risks_kept, self.reinrisks_kept, self.excess_capital and self.profits_losses.""" - [contract.dissolve(time) for contract in self.underwritten_contracts] # removing (dissolving) all risks immediately after bankruptcy (may not be realistic, they might instead be bought by another company) + [ + contract.dissolve(time) for contract in self.underwritten_contracts + ] # removing (dissolving) all risks immediately after bankruptcy (may not be realistic, they might instead be bought by another company) self.simulation.return_risks(self.risks_kept) self.risks_kept = [] self.reinrisks_kept = [] - obligation = {"amount": self.cash, "recipient": self.simulation, "due_time": time, "purpose": "Dissolution"} - self.pay(obligation) # This MUST be the last obligation before the dissolution of the firm. - self.excess_capital = 0 # Excess of capital is 0 after bankruptcy or market exit. - self.profits_losses = 0 # Profits and losses are 0 after bankruptcy or market exit. + obligation = { + "amount": self.cash, + "recipient": self.simulation, + "due_time": time, + "purpose": "Dissolution", + } + self.pay( + obligation + ) # This MUST be the last obligation before the dissolution of the firm. + self.excess_capital = ( + 0 + ) # Excess of capital is 0 after bankruptcy or market exit. + self.profits_losses = ( + 0 + ) # Profits and losses are 0 after bankruptcy or market exit. if self.operational: method_to_call = getattr(self.simulation, record) method_to_call() @@ -278,7 +381,12 @@ def receive_obligation(self, amount, recipient, due_time, purpose): purpose: Type string, why they are being payed No return value Adds obligation (Type DataDict) to list of obligations owed by the firm.""" - obligation = {"amount": amount, "recipient": recipient, "due_time": due_time, "purpose": purpose} + obligation = { + "amount": amount, + "recipient": recipient, + "due_time": due_time, + "purpose": purpose, + } self.obligations.append(obligation) def effect_payments(self, time): @@ -288,8 +396,10 @@ def effect_payments(self, time): No return value Method checks firms list of obligations to see if ay are due for this time, then pays them. If the firm does not have enough cash then it enters illiquity, leaves the market, and matures all contracts.""" - due = [item for item in self.obligations if item["due_time"]<=time] - self.obligations = [item for item in self.obligations if item["due_time"]>time] + due = [item for item in self.obligations if item["due_time"] <= time] + self.obligations = [ + item for item in self.obligations if item["due_time"] > time + ] sum_due = sum([item["amount"] for item in due]) if sum_due > self.cash: self.obligations += due @@ -312,7 +422,7 @@ def pay(self, obligation): purpose = obligation["purpose"] if self.get_operational() and recipient.get_operational(): self.cash -= amount - if purpose is not 'dividend': + if purpose is not "dividend": self.profits_losses -= amount recipient.receive(amount) @@ -330,8 +440,8 @@ def pay_dividends(self, time): time: Type integer No return value If firm has positive profits will pay percentage of them as dividends. Currently pays to simulation.""" - self.receive_obligation(self.per_period_dividend, self.owner, time, 'dividend') - + self.receive_obligation(self.per_period_dividend, self.owner, time, "dividend") + def get_cash(self): """Method to return agents cash. Only used to calculate total sum of capital to recalculate market premium each iteration. @@ -356,7 +466,7 @@ def get_operational(self): No accepted values Returns Boolean""" return self.operational - + def get_pointer(self): """Method to get pointer. Returns self so renduant? Called only by resume.py""" return self @@ -373,7 +483,7 @@ def estimated_var(self): self.var_counter = 0 self.var_counter_per_risk = 0 self.var_sum = 0 - + if self.operational: for contract in self.underwritten_contracts: @@ -381,11 +491,16 @@ def estimated_var(self): self.var_category[contract.category] += contract.initial_VaR for category in range(len(self.counter_category)): - self.var_counter += self.counter_category[category] * self.riskmodel.inaccuracy[category] - self.var_sum += + self.var_category[category] + self.var_counter += ( + self.counter_category[category] + * self.riskmodel.inaccuracy[category] + ) + self.var_sum += +self.var_category[category] if not sum(self.counter_category) == 0: - self.var_counter_per_risk = self.var_counter / sum(self.counter_category) + self.var_counter_per_risk = self.var_counter / sum( + self.counter_category + ) else: self.var_counter_per_risk = 0 @@ -398,14 +513,26 @@ def get_newrisks_by_type(self): new_risks: Type list of DataDicts.""" new_risks = [] if self.is_insurer: - new_risks += self.simulation.solicit_insurance_requests(self.id, self.cash, self) + new_risks += self.simulation.solicit_insurance_requests( + self.id, self.cash, self + ) if self.is_reinsurer: - new_risks += self.simulation.solicit_reinsurance_requests(self.id, self.cash, self) - - new_nonproportional_risks = [risk for risk in new_risks if - risk.get("insurancetype") == 'excess-of-loss' and risk["owner"] is not self] - new_risks = [risk for risk in new_risks if - risk.get("insurancetype") in ['proportional', None] and risk["owner"] is not self] + new_risks += self.simulation.solicit_reinsurance_requests( + self.id, self.cash, self + ) + + new_nonproportional_risks = [ + risk + for risk in new_risks + if risk.get("insurancetype") == "excess-of-loss" + and risk["owner"] is not self + ] + new_risks = [ + risk + for risk in new_risks + if risk.get("insurancetype") in ["proportional", None] + and risk["owner"] is not self + ] return new_nonproportional_risks, new_risks def risks_reinrisks_organizer(self, new_risks): # @@ -417,11 +544,17 @@ def risks_reinrisks_organizer(self, new_risks): # number_risks_categ: Type list, elements are integers of total risks in each category """ - risks_per_categ = [[] for x in range(self.simulation_parameters["no_categories"])] - number_risks_categ = [[] for x in range(self.simulation_parameters["no_categories"])] + risks_per_categ = [ + [] for x in range(self.simulation_parameters["no_categories"]) + ] + number_risks_categ = [ + [] for x in range(self.simulation_parameters["no_categories"]) + ] for categ_id in range(self.simulation_parameters["no_categories"]): - risks_per_categ[categ_id] = [risk for risk in new_risks if risk["category"] == categ_id] + risks_per_categ[categ_id] = [ + risk for risk in new_risks if risk["category"] == categ_id + ] number_risks_categ[categ_id] = len(risks_per_categ[categ_id]) return risks_per_categ, number_risks_categ @@ -438,31 +571,52 @@ def balanced_portfolio(self, risk, cash_left_by_categ, var_per_risk): Boolean cash_left_by_categ: Type list of integers""" - cash_reserved_by_categ = self.cash - cash_left_by_categ # Calculates the cash already reserved by category + cash_reserved_by_categ = ( + self.cash - cash_left_by_categ + ) # Calculates the cash already reserved by category _, std_pre = get_mean_std(cash_reserved_by_categ) cash_reserved_by_categ_store = np.copy(cash_reserved_by_categ) - if risk.get("insurancetype") == 'excess-of-loss': - percentage_value_at_risk = self.riskmodel.getPPF(categ_id=risk["category"], tailSize=self.riskmodel.var_tail_prob) - expected_damage = percentage_value_at_risk * risk["value"] * risk["risk_factor"] \ - * self.riskmodel.inaccuracy[risk["category"]] - expected_claim = min(expected_damage, risk["value"] * risk["excess_fraction"]) - risk["value"] * risk["deductible_fraction"] + if risk.get("insurancetype") == "excess-of-loss": + percentage_value_at_risk = self.riskmodel.getPPF( + categ_id=risk["category"], tailSize=self.riskmodel.var_tail_prob + ) + expected_damage = ( + percentage_value_at_risk + * risk["value"] + * risk["risk_factor"] + * self.riskmodel.inaccuracy[risk["category"]] + ) + expected_claim = ( + min(expected_damage, risk["value"] * risk["excess_fraction"]) + - risk["value"] * risk["deductible_fraction"] + ) # record liquidity requirement and apply margin of safety for liquidity requirement - cash_reserved_by_categ_store[risk["category"]] += expected_claim * self.riskmodel.margin_of_safety #Here it is computed how the cash reserved by category would change if the new reinsurance risk was accepted + cash_reserved_by_categ_store[risk["category"]] += ( + expected_claim * self.riskmodel.margin_of_safety + ) # Here it is computed how the cash reserved by category would change if the new reinsurance risk was accepted else: - cash_reserved_by_categ_store[risk["category"]] += var_per_risk[risk["category"]] #Here it is computed how the cash reserved by category would change if the new insurance risk was accepted + cash_reserved_by_categ_store[risk["category"]] += var_per_risk[ + risk["category"] + ] # Here it is computed how the cash reserved by category would change if the new insurance risk was accepted - mean, std_post = get_mean_std(cash_reserved_by_categ_store) #Here it is computed the mean, std of the cash reserved by category after the new risk of reinrisk is accepted + mean, std_post = get_mean_std( + cash_reserved_by_categ_store + ) # Here it is computed the mean, std of the cash reserved by category after the new risk of reinrisk is accepted total_cash_reserved_by_categ_post = sum(cash_reserved_by_categ_store) - if (std_post * total_cash_reserved_by_categ_post/self.cash) <= (self.balance_ratio * mean) or std_post < std_pre: #The new risk is accepted is the standard deviation is reduced or the cash reserved by category is very well balanced. (std_post) <= (self.balance_ratio * mean) - for i in range(len(cash_left_by_categ)): #The balance condition is not taken into account if the cash reserve is far away from the limit. (total_cash_employed_by_categ_post/self.cash <<< 1) + if (std_post * total_cash_reserved_by_categ_post / self.cash) <= ( + self.balance_ratio * mean + ) or std_post < std_pre: # The new risk is accepted is the standard deviation is reduced or the cash reserved by category is very well balanced. (std_post) <= (self.balance_ratio * mean) + for i in range( + len(cash_left_by_categ) + ): # The balance condition is not taken into account if the cash reserve is far away from the limit. (total_cash_employed_by_categ_post/self.cash <<< 1) cash_left_by_categ[i] = self.cash - cash_reserved_by_categ_store[i] return True, cash_left_by_categ @@ -472,7 +626,9 @@ def balanced_portfolio(self, risk, cash_left_by_categ, var_per_risk): return False, cash_left_by_categ - def process_newrisks_reinsurer(self, reinrisks_per_categ, number_reinrisks_categ, time): + def process_newrisks_reinsurer( + self, reinrisks_per_categ, number_reinrisks_categ, time + ): """Method to decide if new risks are underwritten for the reinsurance firm. Accepts: reinrisks_per_categ: Type List of lists containing new reinsurance risks. @@ -486,31 +642,55 @@ def process_newrisks_reinsurer(self, reinrisks_per_categ, number_reinrisks_categ for iterion in range(max(number_reinrisks_categ)): for categ_id in range(self.simulation_parameters["no_categories"]): - if iterion < number_reinrisks_categ[categ_id] and reinrisks_per_categ[categ_id][iterion] is not None: + if ( + iterion < number_reinrisks_categ[categ_id] + and reinrisks_per_categ[categ_id][iterion] is not None + ): risk_to_insure = reinrisks_per_categ[categ_id][iterion] - underwritten_risks = [{"value": contract.value, "category": contract.category, \ - "risk_factor": contract.risk_factor, - "deductible": contract.deductible, \ - "excess": contract.excess, "insurancetype": contract.insurancetype, \ - "runtime_left": (contract.expiration - time)} for contract in - self.underwritten_contracts if contract.insurancetype == "excess-of-loss"] + underwritten_risks = [ + { + "value": contract.value, + "category": contract.category, + "risk_factor": contract.risk_factor, + "deductible": contract.deductible, + "excess": contract.excess, + "insurancetype": contract.insurancetype, + "runtime_left": (contract.expiration - time), + } + for contract in self.underwritten_contracts + if contract.insurancetype == "excess-of-loss" + ] accept, cash_left_by_categ, var_this_risk, self.excess_capital = self.riskmodel.evaluate( - underwritten_risks, self.cash, - risk_to_insure) + underwritten_risks, self.cash, risk_to_insure + ) if accept: - per_value_reinsurance_premium = self.np_reinsurance_premium_share * risk_to_insure[ - "periodized_total_premium"] * risk_to_insure["runtime"] * (self.simulation.get_market_reinpremium()/self.simulation.get_market_premium()) / risk_to_insure[ - "value"] # TODO: rename this to per_value_premium in insurancecontract.py to avoid confusion - [condition, cash_left_by_categ] = self.balanced_portfolio(risk_to_insure, cash_left_by_categ, None) #Here it is check whether the portfolio is balanced or not if the reinrisk (risk_to_insure) is underwritten. Return True if it is balanced. False otherwise. + per_value_reinsurance_premium = ( + self.np_reinsurance_premium_share + * risk_to_insure["periodized_total_premium"] + * risk_to_insure["runtime"] + * ( + self.simulation.get_market_reinpremium() + / self.simulation.get_market_premium() + ) + / risk_to_insure["value"] + ) # TODO: rename this to per_value_premium in insurancecontract.py to avoid confusion + [condition, cash_left_by_categ] = self.balanced_portfolio( + risk_to_insure, cash_left_by_categ, None + ) # Here it is check whether the portfolio is balanced or not if the reinrisk (risk_to_insure) is underwritten. Return True if it is balanced. False otherwise. if condition: - contract = ReinsuranceContract(self, risk_to_insure, time, per_value_reinsurance_premium, - risk_to_insure["runtime"], \ - self.default_contract_payment_period, \ - expire_immediately=self.simulation_parameters[ - "expire_immediately"], \ - initial_VaR=var_this_risk, \ - insurancetype=risk_to_insure[ - "insurancetype"]) # TODO: implement excess of loss for reinsurance contracts + contract = ReinsuranceContract( + self, + risk_to_insure, + time, + per_value_reinsurance_premium, + risk_to_insure["runtime"], + self.default_contract_payment_period, + expire_immediately=self.simulation_parameters[ + "expire_immediately" + ], + initial_VaR=var_this_risk, + insurancetype=risk_to_insure["insurancetype"], + ) # TODO: implement excess of loss for reinsurance contracts self.underwritten_contracts.append(contract) self.cash_left_by_categ = cash_left_by_categ reinrisks_per_categ[categ_id][iterion] = None @@ -521,11 +701,17 @@ def process_newrisks_reinsurer(self, reinrisks_per_categ, number_reinrisks_categ if reinrisk is not None: not_accepted_reinrisks.append(reinrisk) - - return reinrisks_per_categ, not_accepted_reinrisks - def process_newrisks_insurer(self, risks_per_categ, number_risks_categ, acceptable_by_category, var_per_risk_per_categ, cash_left_by_categ, time): + def process_newrisks_insurer( + self, + risks_per_categ, + number_risks_categ, + acceptable_by_category, + var_per_risk_per_categ, + cash_left_by_categ, + time, + ): """Method to decide if new risks are underwritten for the insurance firm. Accepts: risks_per_categ: Type List of lists containing new risks. @@ -545,36 +731,59 @@ def process_newrisks_insurer(self, risks_per_categ, number_risks_categ, acceptab _cached_rvs = self.contract_runtime_dist.rvs() for iter in range(max(number_risks_categ)): for categ_id in range(len(acceptable_by_category)): - if iter < number_risks_categ[categ_id] and acceptable_by_category[categ_id] > 0 and \ - risks_per_categ[categ_id][iter] is not None: + if ( + iter < number_risks_categ[categ_id] + and acceptable_by_category[categ_id] > 0 + and risks_per_categ[categ_id][iter] is not None + ): risk_to_insure = risks_per_categ[categ_id][iter] - if risk_to_insure.get("contract") is not None and risk_to_insure[ - "contract"].expiration > time: # required to rule out contracts that have exploded in the meantime - [condition, cash_left_by_categ] = self.balanced_portfolio(risk_to_insure, cash_left_by_categ, None) #Here it is check whether the portfolio is balanced or not if the reinrisk (risk_to_insure) is underwritten. Return True if it is balanced. False otherwise. + if ( + risk_to_insure.get("contract") is not None + and risk_to_insure["contract"].expiration > time + ): # required to rule out contracts that have exploded in the meantime + [condition, cash_left_by_categ] = self.balanced_portfolio( + risk_to_insure, cash_left_by_categ, None + ) # Here it is check whether the portfolio is balanced or not if the reinrisk (risk_to_insure) is underwritten. Return True if it is balanced. False otherwise. if condition: - contract = ReinsuranceContract(self, risk_to_insure, time, \ - self.simulation.get_reinsurance_market_premium(), - risk_to_insure["expiration"] - time, \ - self.default_contract_payment_period, \ - expire_immediately=self.simulation_parameters[ - "expire_immediately"], ) + contract = ReinsuranceContract( + self, + risk_to_insure, + time, + self.simulation.get_reinsurance_market_premium(), + risk_to_insure["expiration"] - time, + self.default_contract_payment_period, + expire_immediately=self.simulation_parameters[ + "expire_immediately" + ], + ) self.underwritten_contracts.append(contract) self.cash_left_by_categ = cash_left_by_categ risks_per_categ[categ_id][iter] = None else: - [condition, cash_left_by_categ] = self.balanced_portfolio(risk_to_insure, cash_left_by_categ, - var_per_risk_per_categ) #Here it is check whether the portfolio is balanced or not if the risk (risk_to_insure) is underwritten. Return True if it is balanced. False otherwise. + [condition, cash_left_by_categ] = self.balanced_portfolio( + risk_to_insure, cash_left_by_categ, var_per_risk_per_categ + ) # Here it is check whether the portfolio is balanced or not if the risk (risk_to_insure) is underwritten. Return True if it is balanced. False otherwise. if condition: - contract = InsuranceContract(self, risk_to_insure, time, self.simulation.get_market_premium(), \ - _cached_rvs, \ - self.default_contract_payment_period, \ - expire_immediately=self.simulation_parameters[ - "expire_immediately"], \ - initial_VaR=var_per_risk_per_categ[categ_id]) + contract = InsuranceContract( + self, + risk_to_insure, + time, + self.simulation.get_market_premium(), + _cached_rvs, + self.default_contract_payment_period, + expire_immediately=self.simulation_parameters[ + "expire_immediately" + ], + initial_VaR=var_per_risk_per_categ[categ_id], + ) self.underwritten_contracts.append(contract) self.cash_left_by_categ = cash_left_by_categ risks_per_categ[categ_id][iter] = None - acceptable_by_category[categ_id] -= 1 # TODO: allow different values per risk (i.e. sum over value (and reinsurance_share) or exposure instead of counting) + acceptable_by_category[ + categ_id + ] -= ( + 1 + ) # TODO: allow different values per risk (i.e. sum over value (and reinsurance_share) or exposure instead of counting) not_accepted_risks = [] for categ_id in range(len(acceptable_by_category)): @@ -591,7 +800,7 @@ def market_permanency(self, time): No return values. This method determines whether an insurer or reinsurer stays in the market. If it has very few risks underwritten or too much cash left for TOO LONG it eventually leaves the market. If it has very few risks - underwritten it cannot balance the portfolio so it makes sense to leave the market."""# + underwritten it cannot balance the portfolio so it makes sense to leave the market.""" # if not self.simulation_parameters["market_permanency_off"]: @@ -599,30 +808,62 @@ def market_permanency(self, time): avg_cash_left = get_mean(cash_left_by_categ) - if self.cash < self.simulation_parameters["cash_permanency_limit"]: #If their level of cash is so low that they cannot underwrite anything they also leave the market. + if ( + self.cash < self.simulation_parameters["cash_permanency_limit"] + ): # If their level of cash is so low that they cannot underwrite anything they also leave the market. self.market_exit(time) else: if self.is_insurer: - if len(self.underwritten_contracts) < self.simulation_parameters["insurance_permanency_contracts_limit"] or avg_cash_left / self.cash > self.simulation_parameters["insurance_permanency_ratio_limit"]: - #Insurers leave the market if they have contracts under the limit or an excess capital over the limit for too long. + if ( + len(self.underwritten_contracts) + < self.simulation_parameters[ + "insurance_permanency_contracts_limit" + ] + or avg_cash_left / self.cash + > self.simulation_parameters["insurance_permanency_ratio_limit"] + ): + # Insurers leave the market if they have contracts under the limit or an excess capital over the limit for too long. self.market_permanency_counter += 1 else: - self.market_permanency_counter = 0 #All these limits maybe should be parameters in isleconfig.py - - if self.market_permanency_counter >= self.simulation_parameters["insurance_permanency_time_constraint"]: # Here we determine how much is too long. + self.market_permanency_counter = ( + 0 + ) # All these limits maybe should be parameters in isleconfig.py + + if ( + self.market_permanency_counter + >= self.simulation_parameters[ + "insurance_permanency_time_constraint" + ] + ): # Here we determine how much is too long. self.market_exit(time) if self.is_reinsurer: - if len(self.underwritten_contracts) < self.simulation_parameters["reinsurance_permanency_contracts_limit"] or avg_cash_left / self.cash > self.simulation_parameters["reinsurance_permanency_ratio_limit"]: - #Reinsurers leave the market if they have contracts under the limit or an excess capital over the limit for too long. - - self.market_permanency_counter += 1 #Insurers and reinsurers potentially have different reasons to leave the market. That's why the code is duplicated here. + if ( + len(self.underwritten_contracts) + < self.simulation_parameters[ + "reinsurance_permanency_contracts_limit" + ] + or avg_cash_left / self.cash + > self.simulation_parameters[ + "reinsurance_permanency_ratio_limit" + ] + ): + # Reinsurers leave the market if they have contracts under the limit or an excess capital over the limit for too long. + + self.market_permanency_counter += ( + 1 + ) # Insurers and reinsurers potentially have different reasons to leave the market. That's why the code is duplicated here. else: self.market_permanency_counter = 0 - if self.market_permanency_counter >= self.simulation_parameters["reinsurance_permanency_time_constraint"]: # Here we determine how much is too long. + if ( + self.market_permanency_counter + >= self.simulation_parameters[ + "reinsurance_permanency_time_constraint" + ] + ): # Here we determine how much is too long. self.market_exit(time) def register_claim(self, claim): @@ -655,13 +896,22 @@ def roll_over(self, time): are created and destroyed every iteration. The main reason to implemented this method is to avoid a lack of coverage that appears, if contracts are allowed to mature and are evaluated again the next iteration.""" - maturing_next = [contract for contract in self.underwritten_contracts if contract.expiration == time + 1] + maturing_next = [ + contract + for contract in self.underwritten_contracts + if contract.expiration == time + 1 + ] if self.is_insurer is True: for contract in maturing_next: contract.roll_over_flag = 1 - if np.random.uniform(0,1,1) > self.simulation_parameters["insurance_retention"]: - self.simulation.return_risks([contract.risk_data]) # TODO: This is not a retention, so the roll_over_flag might be confusing in this case + if ( + np.random.uniform(0, 1, 1) + > self.simulation_parameters["insurance_retention"] + ): + self.simulation.return_risks( + [contract.risk_data] + ) # TODO: This is not a retention, so the roll_over_flag might be confusing in this case else: self.risks_kept.append(contract.risk_data) @@ -669,9 +919,13 @@ def roll_over(self, time): for reincontract in maturing_next: if reincontract.property_holder.operational: reincontract.roll_over_flag = 1 - #reinrisk = reincontract.property_holder.create_reinrisk(time, reincontract.category) - reinrisk = reincontract.property_holder.ask_reinsurance_non_proportional_by_category(time, reincontract.category, purpose='rollover') - if np.random.uniform(0,1,1) < self.simulation_parameters["reinsurance_retention"]: + # reinrisk = reincontract.property_holder.create_reinrisk(time, reincontract.category) + reinrisk = reincontract.property_holder.ask_reinsurance_non_proportional_by_category( + time, reincontract.category, purpose="rollover" + ) + if ( + np.random.uniform(0, 1, 1) + < self.simulation_parameters["reinsurance_retention"] + ): if reinrisk is not None: self.reinrisks_kept.append(reinrisk) - diff --git a/metaplotter.py b/metaplotter.py index 775b55e..858987e 100644 --- a/metaplotter.py +++ b/metaplotter.py @@ -5,11 +5,12 @@ import time import glob + def read_data(): # do not overwrite old pdfs - #if os.path.exists("data/fig_one_and_two_rm_comp.pdf"): + # if os.path.exists("data/fig_one_and_two_rm_comp.pdf"): # os.rename("data/fig_one_and_two_rm_comp.pdf", "data/fig_one_and_two_rm_comp_old_" + time.strftime('%Y_%b_%d_%H_%M') + ".pdf") - #if os.path.exists("data/fig_three_and_four_rm_comp.pdf"): + # if os.path.exists("data/fig_three_and_four_rm_comp.pdf"): # os.rename("data/fig_three_and_four_rm_comp.pdf", "data/fig_three_and_four_rm_comp_old_" + time.strftime('%Y_%b_%d_%H_%M') + ".pdf") upper_bound = 75 @@ -30,7 +31,12 @@ def read_data(): filenames_threes.sort() filenames_fours.sort() - assert len(filenames_ones) == len(filenames_twos) == len(filenames_threes) == len(filenames_fours) + assert ( + len(filenames_ones) + == len(filenames_twos) + == len(filenames_threes) + == len(filenames_fours) + ) all_filenames = filenames_ones + filenames_twos + filenames_threes + filenames_fours for filename in all_filenames: @@ -38,7 +44,7 @@ def read_data(): rfile = open(filename, "r") data = [eval(k) for k in rfile] rfile.close() - + # compute data series data_means = [] data_medians = [] @@ -53,21 +59,39 @@ def read_data(): data_medians = np.array(data_medians) data_q25 = np.array(data_q25) data_q75 = np.array(data_q75) - + # record data series timeseries_dict["mean"][filename] = data_means timeseries_dict["median"][filename] = data_medians timeseries_dict["quantile25"][filename] = data_q25 timeseries_dict["quantile75"][filename] = data_q75 return timeseries_dict - - -def plotting(output_label, timeseries_dict, riskmodelsetting1, riskmodelsetting2, series1, series2=None, additionalriskmodelsetting3=None, additionalriskmodelsetting4=None, plottype1="mean", plottype2="mean"): + +def plotting( + output_label, + timeseries_dict, + riskmodelsetting1, + riskmodelsetting2, + series1, + series2=None, + additionalriskmodelsetting3=None, + additionalriskmodelsetting4=None, + plottype1="mean", + plottype2="mean", +): # dictionaries colors = {"one": "red", "two": "blue", "three": "green", "four": "yellow"} - labels = {"contracts": "Contracts (Insurers)", "cash": "Liquidity (Insurers)", "operational": "Active Insurers", "premium": "Premium", "reincash": "Liquidity (Reinsurers)", "reincontracts": "Contracts (Reinsurers)", "reinoperational": "Active Reinsurers"} - + labels = { + "contracts": "Contracts (Insurers)", + "cash": "Liquidity (Insurers)", + "operational": "Active Insurers", + "premium": "Premium", + "reincash": "Liquidity (Reinsurers)", + "reincontracts": "Contracts (Reinsurers)", + "reinoperational": "Active Reinsurers", + } + # prepare labels, timeseries, etc. color1 = colors[riskmodelsetting1] color2 = colors[riskmodelsetting2] @@ -80,23 +104,33 @@ def plotting(output_label, timeseries_dict, riskmodelsetting1, riskmodelsetting2 plot_2_2 = "data/" + riskmodelsetting2 + "_" + series2 + ".dat" if additionalriskmodelsetting3 is not None: color3 = colors[additionalriskmodelsetting3] - label3 = str.upper(additionalriskmodelsetting3[0]) + additionalriskmodelsetting3[1:] + " riskmodels" + label3 = ( + str.upper(additionalriskmodelsetting3[0]) + + additionalriskmodelsetting3[1:] + + " riskmodels" + ) plot_1_3 = "data/" + additionalriskmodelsetting3 + "_" + series1 + ".dat" if series2 is not None: plot_2_3 = "data/" + additionalriskmodelsetting3 + "_" + series2 + ".dat" if additionalriskmodelsetting4 is not None: color4 = colors[additionalriskmodelsetting4] - label4 = str.upper(additionalriskmodelsetting4[0]) + additionalriskmodelsetting4[1:] + " riskmodels" + label4 = ( + str.upper(additionalriskmodelsetting4[0]) + + additionalriskmodelsetting4[1:] + + " riskmodels" + ) plot_1_4 = "data/" + additionalriskmodelsetting4 + "_" + series1 + ".dat" if series2 is not None: plot_2_4 = "data/" + additionalriskmodelsetting4 + "_" + series2 + ".dat" - + # Backup existing figures (so as not to overwrite them) outputfilename = "data/" + output_label + ".pdf" - backupfilename = "data/" + output_label + "_old_" + time.strftime('%Y_%b_%d_%H_%M') + ".pdf" + backupfilename = ( + "data/" + output_label + "_old_" + time.strftime("%Y_%b_%d_%H_%M") + ".pdf" + ) if os.path.exists(outputfilename): os.rename(outputfilename, backupfilename) - + # Plot and save fig = plt.figure() if series2 is not None: @@ -104,56 +138,190 @@ def plotting(output_label, timeseries_dict, riskmodelsetting1, riskmodelsetting2 else: ax0 = fig.add_subplot(111) if additionalriskmodelsetting3 is not None: - ax0.plot(range(len(timeseries_dict[plottype1][plot_1_3])), timeseries_dict[plottype1][plot_1_3], color=color3, label=label3) + ax0.plot( + range(len(timeseries_dict[plottype1][plot_1_3])), + timeseries_dict[plottype1][plot_1_3], + color=color3, + label=label3, + ) if additionalriskmodelsetting4 is not None: - ax0.plot(range(len(timeseries_dict[plottype1][plot_1_4])), timeseries_dict[plottype1][plot_1_4], color=color4, label=label4) - ax0.plot(range(len(timeseries_dict[plottype1][plot_1_1])), timeseries_dict[plottype1][plot_1_1], color=color1, label=label1) - ax0.plot(range(len(timeseries_dict[plottype1][plot_1_2])), timeseries_dict[plottype1][plot_1_2], color=color2, label=label2) - ax0.fill_between(range(len(timeseries_dict["quantile25"][plot_1_1])), timeseries_dict["quantile25"][plot_1_1], timeseries_dict["quantile75"][plot_1_1], facecolor=color1, alpha=0.25) - ax0.fill_between(range(len(timeseries_dict["quantile25"][plot_1_1])), timeseries_dict["quantile25"][plot_1_2], timeseries_dict["quantile75"][plot_1_2], facecolor=color2, alpha=0.25) - ax0.set_ylabel(labels[series1])#"Contracts") - ax0.legend(loc='best') + ax0.plot( + range(len(timeseries_dict[plottype1][plot_1_4])), + timeseries_dict[plottype1][plot_1_4], + color=color4, + label=label4, + ) + ax0.plot( + range(len(timeseries_dict[plottype1][plot_1_1])), + timeseries_dict[plottype1][plot_1_1], + color=color1, + label=label1, + ) + ax0.plot( + range(len(timeseries_dict[plottype1][plot_1_2])), + timeseries_dict[plottype1][plot_1_2], + color=color2, + label=label2, + ) + ax0.fill_between( + range(len(timeseries_dict["quantile25"][plot_1_1])), + timeseries_dict["quantile25"][plot_1_1], + timeseries_dict["quantile75"][plot_1_1], + facecolor=color1, + alpha=0.25, + ) + ax0.fill_between( + range(len(timeseries_dict["quantile25"][plot_1_1])), + timeseries_dict["quantile25"][plot_1_2], + timeseries_dict["quantile75"][plot_1_2], + facecolor=color2, + alpha=0.25, + ) + ax0.set_ylabel(labels[series1]) # "Contracts") + ax0.legend(loc="best") if series2 is not None: ax1 = fig.add_subplot(212) if additionalriskmodelsetting3 is not None: - ax1.plot(range(len(timeseries_dict[plottype2][plot_2_3])), timeseries_dict[plottype2][plot_2_3], color=color3, label=label3) + ax1.plot( + range(len(timeseries_dict[plottype2][plot_2_3])), + timeseries_dict[plottype2][plot_2_3], + color=color3, + label=label3, + ) if additionalriskmodelsetting4 is not None: - ax1.plot(range(len(timeseries_dict[plottype2][plot_2_4])), timeseries_dict[plottype2][plot_2_4], color=color4, label=label4) - ax1.plot(range(len(timeseries_dict[plottype2][plot_2_1])), timeseries_dict[plottype2][plot_2_1], color=color1, label=label1) - ax1.plot(range(len(timeseries_dict[plottype2][plot_2_2])), timeseries_dict[plottype2][plot_2_2], color=color2, label=label2) - ax1.fill_between(range(len(timeseries_dict["quantile25"][plot_2_1])), timeseries_dict["quantile25"][plot_2_1], timeseries_dict["quantile75"][plot_2_1], facecolor=color1, alpha=0.25) - ax1.fill_between(range(len(timeseries_dict["quantile25"][plot_2_1])), timeseries_dict["quantile25"][plot_2_2], timeseries_dict["quantile75"][plot_2_2], facecolor=color2, alpha=0.25) + ax1.plot( + range(len(timeseries_dict[plottype2][plot_2_4])), + timeseries_dict[plottype2][plot_2_4], + color=color4, + label=label4, + ) + ax1.plot( + range(len(timeseries_dict[plottype2][plot_2_1])), + timeseries_dict[plottype2][plot_2_1], + color=color1, + label=label1, + ) + ax1.plot( + range(len(timeseries_dict[plottype2][plot_2_2])), + timeseries_dict[plottype2][plot_2_2], + color=color2, + label=label2, + ) + ax1.fill_between( + range(len(timeseries_dict["quantile25"][plot_2_1])), + timeseries_dict["quantile25"][plot_2_1], + timeseries_dict["quantile75"][plot_2_1], + facecolor=color1, + alpha=0.25, + ) + ax1.fill_between( + range(len(timeseries_dict["quantile25"][plot_2_1])), + timeseries_dict["quantile25"][plot_2_2], + timeseries_dict["quantile75"][plot_2_2], + facecolor=color2, + alpha=0.25, + ) ax1.set_ylabel(labels[series2]) ax1.set_xlabel("Time") plt.savefig(outputfilename) plt.show() + timeseries = read_data() # for just two different riskmodel settings -plotting(output_label="fig_contracts_survival_1_2", timeseries_dict=timeseries, riskmodelsetting1="one", \ - riskmodelsetting2="two", series1="contracts", series2="operational", plottype1="mean", plottype2="median") -plotting(output_label="fig_reinsurers_contracts_survival_1_2", timeseries_dict=timeseries, riskmodelsetting1="one", \ - riskmodelsetting2="two", series1="reincontracts", series2="reinoperational", plottype1="mean", plottype2="median") -plotting(output_label="fig_premium_1_2", timeseries_dict=timeseries, riskmodelsetting1="one", riskmodelsetting2="two", \ - series1="premium", series2=None, plottype1="mean", plottype2=None) +plotting( + output_label="fig_contracts_survival_1_2", + timeseries_dict=timeseries, + riskmodelsetting1="one", + riskmodelsetting2="two", + series1="contracts", + series2="operational", + plottype1="mean", + plottype2="median", +) +plotting( + output_label="fig_reinsurers_contracts_survival_1_2", + timeseries_dict=timeseries, + riskmodelsetting1="one", + riskmodelsetting2="two", + series1="reincontracts", + series2="reinoperational", + plottype1="mean", + plottype2="median", +) +plotting( + output_label="fig_premium_1_2", + timeseries_dict=timeseries, + riskmodelsetting1="one", + riskmodelsetting2="two", + series1="premium", + series2=None, + plottype1="mean", + plottype2=None, +) raise SystemExit # for four different riskmodel settings -plotting(output_label="fig_contracts_survival_1_2", timeseries_dict=timeseries, riskmodelsetting1="one", \ - riskmodelsetting2="two", series1="contracts", series2="operational", additionalriskmodelsetting3="three", \ - additionalriskmodelsetting4="four", plottype1="mean", plottype2="median") -plotting(output_label="fig_contracts_survival_3_4", timeseries_dict=timeseries, riskmodelsetting1="three", \ - riskmodelsetting2="four", series1="contracts", series2="operational", additionalriskmodelsetting3="one", \ - additionalriskmodelsetting4="two", plottype1="mean", plottype2="median") -plotting(output_label="fig_reinsurers_contracts_survival_1_2", timeseries_dict=timeseries, riskmodelsetting1="one", \ - riskmodelsetting2="two", series1="reincontracts", series2="reinoperational", \ - additionalriskmodelsetting3="three", additionalriskmodelsetting4="four", plottype1="mean", plottype2="median") -plotting(output_label="fig_reinsurers_contracts_survival_3_4", timeseries_dict=timeseries, riskmodelsetting1="three", \ - riskmodelsetting2="four", series1="reincontracts", series2="reinoperational", \ - additionalriskmodelsetting3="one", additionalriskmodelsetting4="two", plottype1="mean", plottype2="median") -plotting(output_label="fig_premium_1_2", timeseries_dict=timeseries, riskmodelsetting1="one", riskmodelsetting2="two", \ - series1="premium", series2=None, additionalriskmodelsetting3="three", additionalriskmodelsetting4="four", \ - plottype1="mean", plottype2=None) - -#pdb.set_trace() +plotting( + output_label="fig_contracts_survival_1_2", + timeseries_dict=timeseries, + riskmodelsetting1="one", + riskmodelsetting2="two", + series1="contracts", + series2="operational", + additionalriskmodelsetting3="three", + additionalriskmodelsetting4="four", + plottype1="mean", + plottype2="median", +) +plotting( + output_label="fig_contracts_survival_3_4", + timeseries_dict=timeseries, + riskmodelsetting1="three", + riskmodelsetting2="four", + series1="contracts", + series2="operational", + additionalriskmodelsetting3="one", + additionalriskmodelsetting4="two", + plottype1="mean", + plottype2="median", +) +plotting( + output_label="fig_reinsurers_contracts_survival_1_2", + timeseries_dict=timeseries, + riskmodelsetting1="one", + riskmodelsetting2="two", + series1="reincontracts", + series2="reinoperational", + additionalriskmodelsetting3="three", + additionalriskmodelsetting4="four", + plottype1="mean", + plottype2="median", +) +plotting( + output_label="fig_reinsurers_contracts_survival_3_4", + timeseries_dict=timeseries, + riskmodelsetting1="three", + riskmodelsetting2="four", + series1="reincontracts", + series2="reinoperational", + additionalriskmodelsetting3="one", + additionalriskmodelsetting4="two", + plottype1="mean", + plottype2="median", +) +plotting( + output_label="fig_premium_1_2", + timeseries_dict=timeseries, + riskmodelsetting1="one", + riskmodelsetting2="two", + series1="premium", + series2=None, + additionalriskmodelsetting3="three", + additionalriskmodelsetting4="four", + plottype1="mean", + plottype2=None, +) + +# pdb.set_trace() diff --git a/metaplotter_pl_timescale.py b/metaplotter_pl_timescale.py index d261d11..f172949 100644 --- a/metaplotter_pl_timescale.py +++ b/metaplotter_pl_timescale.py @@ -5,11 +5,12 @@ import time import glob + def read_data(): # do not overwrite old pdfs - #if os.path.exists("data/fig_one_and_two_rm_comp.pdf"): + # if os.path.exists("data/fig_one_and_two_rm_comp.pdf"): # os.rename("data/fig_one_and_two_rm_comp.pdf", "data/fig_one_and_two_rm_comp_old_" + time.strftime('%Y_%b_%d_%H_%M') + ".pdf") - #if os.path.exists("data/fig_three_and_four_rm_comp.pdf"): + # if os.path.exists("data/fig_three_and_four_rm_comp.pdf"): # os.rename("data/fig_three_and_four_rm_comp.pdf", "data/fig_three_and_four_rm_comp_old_" + time.strftime('%Y_%b_%d_%H_%M') + ".pdf") upper_bound = 75 @@ -30,7 +31,7 @@ def read_data(): filenames_threes.sort() filenames_fours.sort() - #assert len(filenames_ones) == len(filenames_twos) == len(filenames_threes) == len(filenames_fours) + # assert len(filenames_ones) == len(filenames_twos) == len(filenames_threes) == len(filenames_fours) all_filenames = filenames_ones + filenames_twos + filenames_threes + filenames_fours for filename in all_filenames: @@ -38,7 +39,7 @@ def read_data(): rfile = open(filename, "r") data = [eval(k) for k in rfile] rfile.close() - + # compute data series data_means = [] data_medians = [] @@ -53,21 +54,41 @@ def read_data(): data_medians = np.array(data_medians) data_q25 = np.array(data_q25) data_q75 = np.array(data_q75) - + # record data series timeseries_dict["mean"][filename] = data_means timeseries_dict["median"][filename] = data_medians timeseries_dict["quantile25"][filename] = data_q25 timeseries_dict["quantile75"][filename] = data_q75 return timeseries_dict - - -def plotting(output_label, timeseries_dict, riskmodelsetting1, riskmodelsetting2, series1, series2=None, additionalriskmodelsetting3=None, additionalriskmodelsetting4=None, plottype1="mean", plottype2="mean"): + +def plotting( + output_label, + timeseries_dict, + riskmodelsetting1, + riskmodelsetting2, + series1, + series2=None, + additionalriskmodelsetting3=None, + additionalriskmodelsetting4=None, + plottype1="mean", + plottype2="mean", +): # dictionaries colors = {"one": "red", "two": "blue", "three": "green", "four": "yellow"} - labels = {"profitslosses": "Profits and Losses (Insurer)", "contracts": "Contracts (Insurers)", "cash": "Liquidity (Insurers)", "operational": "Active Insurers", "premium": "Premium", "reinprofitslosses": "Profits and Losses (Reinsurer)", "reincash": "Liquidity (Reinsurers)", "reincontracts": "Contracts (Reinsurers)", "reinoperational": "Active Reinsurers"} - + labels = { + "profitslosses": "Profits and Losses (Insurer)", + "contracts": "Contracts (Insurers)", + "cash": "Liquidity (Insurers)", + "operational": "Active Insurers", + "premium": "Premium", + "reinprofitslosses": "Profits and Losses (Reinsurer)", + "reincash": "Liquidity (Reinsurers)", + "reincontracts": "Contracts (Reinsurers)", + "reinoperational": "Active Reinsurers", + } + # prepare labels, timeseries, etc. color1 = colors[riskmodelsetting1] color2 = colors[riskmodelsetting2] @@ -80,23 +101,33 @@ def plotting(output_label, timeseries_dict, riskmodelsetting1, riskmodelsetting2 plot_2_2 = "data/" + riskmodelsetting2 + "_" + series2 + ".dat" if additionalriskmodelsetting3 is not None: color3 = colors[additionalriskmodelsetting3] - label3 = str.upper(additionalriskmodelsetting3[0]) + additionalriskmodelsetting3[1:] + " riskmodels" + label3 = ( + str.upper(additionalriskmodelsetting3[0]) + + additionalriskmodelsetting3[1:] + + " riskmodels" + ) plot_1_3 = "data/" + additionalriskmodelsetting3 + "_" + series1 + ".dat" if series2 is not None: plot_2_3 = "data/" + additionalriskmodelsetting3 + "_" + series2 + ".dat" if additionalriskmodelsetting4 is not None: color4 = colors[additionalriskmodelsetting4] - label4 = str.upper(additionalriskmodelsetting4[0]) + additionalriskmodelsetting4[1:] + " riskmodels" + label4 = ( + str.upper(additionalriskmodelsetting4[0]) + + additionalriskmodelsetting4[1:] + + " riskmodels" + ) plot_1_4 = "data/" + additionalriskmodelsetting4 + "_" + series1 + ".dat" if series2 is not None: plot_2_4 = "data/" + additionalriskmodelsetting4 + "_" + series2 + ".dat" - + # Backup existing figures (so as not to overwrite them) outputfilename = "data/" + output_label + ".pdf" - backupfilename = "data/" + output_label + "_old_" + time.strftime('%Y_%b_%d_%H_%M') + ".pdf" + backupfilename = ( + "data/" + output_label + "_old_" + time.strftime("%Y_%b_%d_%H_%M") + ".pdf" + ) if os.path.exists(outputfilename): os.rename(outputfilename, backupfilename) - + # Plot and save fig = plt.figure() if series2 is not None: @@ -105,39 +136,111 @@ def plotting(output_label, timeseries_dict, riskmodelsetting1, riskmodelsetting2 ax0 = fig.add_subplot(111) maxlen_plots = 0 if additionalriskmodelsetting3 is not None: - ax0.plot(range(len(timeseries_dict[plottype1][plot_1_3]))[200:], timeseries_dict[plottype1][plot_1_3][200:], color=color3, label=label3) + ax0.plot( + range(len(timeseries_dict[plottype1][plot_1_3]))[200:], + timeseries_dict[plottype1][plot_1_3][200:], + color=color3, + label=label3, + ) maxlen_plots = max(maxlen_plots, len(timeseries_dict[plottype1][plot_1_3])) if additionalriskmodelsetting4 is not None: - ax0.plot(range(len(timeseries_dict[plottype1][plot_1_4]))[200:], timeseries_dict[plottype1][plot_1_4][200:], color=color4, label=label4) + ax0.plot( + range(len(timeseries_dict[plottype1][plot_1_4]))[200:], + timeseries_dict[plottype1][plot_1_4][200:], + color=color4, + label=label4, + ) maxlen_plots = max(maxlen_plots, len(timeseries_dict[plottype1][plot_1_4])) - ax0.plot(range(len(timeseries_dict[plottype1][plot_1_1]))[200:], timeseries_dict[plottype1][plot_1_1][200:], color=color1, label=label1) - ax0.plot(range(len(timeseries_dict[plottype1][plot_1_2]))[200:], timeseries_dict[plottype1][plot_1_2][200:], color=color2, label=label2) - ax0.fill_between(range(len(timeseries_dict["quantile25"][plot_1_1]))[200:], timeseries_dict["quantile25"][plot_1_1][200:], timeseries_dict["quantile75"][plot_1_1][200:], facecolor=color1, alpha=0.25) - ax0.fill_between(range(len(timeseries_dict["quantile25"][plot_1_1]))[200:], timeseries_dict["quantile25"][plot_1_2][200:], timeseries_dict["quantile75"][plot_1_2][200:], facecolor=color2, alpha=0.25) - ax0.set_ylabel(labels[series1])#"Contracts") - maxlen_plots = max(maxlen_plots, len(timeseries_dict[plottype1][plot_1_1]), len(timeseries_dict[plottype1][plot_1_2])) - xticks = np.arange(200, maxlen_plots, step=120) + ax0.plot( + range(len(timeseries_dict[plottype1][plot_1_1]))[200:], + timeseries_dict[plottype1][plot_1_1][200:], + color=color1, + label=label1, + ) + ax0.plot( + range(len(timeseries_dict[plottype1][plot_1_2]))[200:], + timeseries_dict[plottype1][plot_1_2][200:], + color=color2, + label=label2, + ) + ax0.fill_between( + range(len(timeseries_dict["quantile25"][plot_1_1]))[200:], + timeseries_dict["quantile25"][plot_1_1][200:], + timeseries_dict["quantile75"][plot_1_1][200:], + facecolor=color1, + alpha=0.25, + ) + ax0.fill_between( + range(len(timeseries_dict["quantile25"][plot_1_1]))[200:], + timeseries_dict["quantile25"][plot_1_2][200:], + timeseries_dict["quantile75"][plot_1_2][200:], + facecolor=color2, + alpha=0.25, + ) + ax0.set_ylabel(labels[series1]) # "Contracts") + maxlen_plots = max( + maxlen_plots, + len(timeseries_dict[plottype1][plot_1_1]), + len(timeseries_dict[plottype1][plot_1_2]), + ) + xticks = np.arange(200, maxlen_plots, step=120) ax0.set_xticks(xticks) - ax0.set_xticklabels(["${0:d}$".format(int((xtc-200)/12)) for xtc in xticks]); + ax0.set_xticklabels(["${0:d}$".format(int((xtc - 200) / 12)) for xtc in xticks]) - ax0.legend(loc='best') + ax0.legend(loc="best") if series2 is not None: ax1 = fig.add_subplot(212) maxlen_plots = 0 if additionalriskmodelsetting3 is not None: - ax1.plot(range(len(timeseries_dict[plottype2][plot_2_3]))[200:], timeseries_dict[plottype2][plot_2_3][200:], color=color3, label=label3) + ax1.plot( + range(len(timeseries_dict[plottype2][plot_2_3]))[200:], + timeseries_dict[plottype2][plot_2_3][200:], + color=color3, + label=label3, + ) maxlen_plots = max(maxlen_plots, len(timeseries_dict[plottype1][plot_2_3])) if additionalriskmodelsetting4 is not None: - ax1.plot(range(len(timeseries_dict[plottype2][plot_2_4]))[200:], timeseries_dict[plottype2][plot_2_4][200:], color=color4, label=label4) + ax1.plot( + range(len(timeseries_dict[plottype2][plot_2_4]))[200:], + timeseries_dict[plottype2][plot_2_4][200:], + color=color4, + label=label4, + ) maxlen_plots = max(maxlen_plots, len(timeseries_dict[plottype1][plot_2_4])) - ax1.plot(range(len(timeseries_dict[plottype2][plot_2_1]))[200:], timeseries_dict[plottype2][plot_2_1][200:], color=color1, label=label1) - ax1.plot(range(len(timeseries_dict[plottype2][plot_2_2]))[200:], timeseries_dict[plottype2][plot_2_2][200:], color=color2, label=label2) - ax1.fill_between(range(len(timeseries_dict["quantile25"][plot_2_1]))[200:], timeseries_dict["quantile25"][plot_2_1][200:], timeseries_dict["quantile75"][plot_2_1][200:], facecolor=color1, alpha=0.25) - ax1.fill_between(range(len(timeseries_dict["quantile25"][plot_2_1]))[200:], timeseries_dict["quantile25"][plot_2_2][200:], timeseries_dict["quantile75"][plot_2_2][200:], facecolor=color2, alpha=0.25) - maxlen_plots = max(maxlen_plots, len(timeseries_dict[plottype1][plot_2_1]), len(timeseries_dict[plottype1][plot_2_2])) - xticks = np.arange(200, maxlen_plots, step=120) + ax1.plot( + range(len(timeseries_dict[plottype2][plot_2_1]))[200:], + timeseries_dict[plottype2][plot_2_1][200:], + color=color1, + label=label1, + ) + ax1.plot( + range(len(timeseries_dict[plottype2][plot_2_2]))[200:], + timeseries_dict[plottype2][plot_2_2][200:], + color=color2, + label=label2, + ) + ax1.fill_between( + range(len(timeseries_dict["quantile25"][plot_2_1]))[200:], + timeseries_dict["quantile25"][plot_2_1][200:], + timeseries_dict["quantile75"][plot_2_1][200:], + facecolor=color1, + alpha=0.25, + ) + ax1.fill_between( + range(len(timeseries_dict["quantile25"][plot_2_1]))[200:], + timeseries_dict["quantile25"][plot_2_2][200:], + timeseries_dict["quantile75"][plot_2_2][200:], + facecolor=color2, + alpha=0.25, + ) + maxlen_plots = max( + maxlen_plots, + len(timeseries_dict[plottype1][plot_2_1]), + len(timeseries_dict[plottype1][plot_2_2]), + ) + xticks = np.arange(200, maxlen_plots, step=120) ax1.set_xticks(xticks) - ax1.set_xticklabels(["${0:d}$".format(int((xtc-200)/12)) for xtc in xticks]); + ax1.set_xticklabels(["${0:d}$".format(int((xtc - 200) / 12)) for xtc in xticks]) ax1.set_ylabel(labels[series2]) ax1.set_xlabel("Years") else: @@ -145,32 +248,78 @@ def plotting(output_label, timeseries_dict, riskmodelsetting1, riskmodelsetting2 plt.savefig(outputfilename) plt.show() + timeseries = read_data() ## for just two different riskmodel settings -#plotting(output_label="fig_pl_survival_1_2", timeseries_dict=timeseries, riskmodelsetting1="one", \ +# plotting(output_label="fig_pl_survival_1_2", timeseries_dict=timeseries, riskmodelsetting1="one", \ # riskmodelsetting2="two", series1="profitslosses", series2="operational", plottype1="mean", plottype2="median") -#plotting(output_label="fig_reinsurers_pl_survival_1_2", timeseries_dict=timeseries, riskmodelsetting1="one", \ +# plotting(output_label="fig_reinsurers_pl_survival_1_2", timeseries_dict=timeseries, riskmodelsetting1="one", \ # riskmodelsetting2="two", series1="reinprofitslosses", series2="reinoperational", plottype1="mean", plottype2="median") -#plotting(output_label="fig_premium_1_2", timeseries_dict=timeseries, riskmodelsetting1="one", riskmodelsetting2="two", \ +# plotting(output_label="fig_premium_1_2", timeseries_dict=timeseries, riskmodelsetting1="one", riskmodelsetting2="two", \ # series1="premium", series2=None, plottype1="mean", plottype2=None) # -#raise SystemExit +# raise SystemExit # for four different riskmodel settings -plotting(output_label="fig_pl_survival_1_2", timeseries_dict=timeseries, riskmodelsetting1="one", \ - riskmodelsetting2="two", series1="profitslosses", series2="operational", additionalriskmodelsetting3="three", \ - additionalriskmodelsetting4="four", plottype1="mean", plottype2="median") -plotting(output_label="fig_pl_survival_3_4", timeseries_dict=timeseries, riskmodelsetting1="three", \ - riskmodelsetting2="four", series1="profitslosses", series2="operational", additionalriskmodelsetting3="one", \ - additionalriskmodelsetting4="two", plottype1="mean", plottype2="median") -plotting(output_label="fig_reinsurers_pl_survival_1_2", timeseries_dict=timeseries, riskmodelsetting1="one", \ - riskmodelsetting2="two", series1="reinprofitslosses", series2="reinoperational", \ - additionalriskmodelsetting3="three", additionalriskmodelsetting4="four", plottype1="mean", plottype2="median") -plotting(output_label="fig_reinsurers_pl_survival_3_4", timeseries_dict=timeseries, riskmodelsetting1="three", \ - riskmodelsetting2="four", series1="reinprofitslosses", series2="reinoperational", \ - additionalriskmodelsetting3="one", additionalriskmodelsetting4="two", plottype1="mean", plottype2="median") -plotting(output_label="fig_premium_1_2", timeseries_dict=timeseries, riskmodelsetting1="one", riskmodelsetting2="two", \ - series1="premium", series2=None, additionalriskmodelsetting3="three", additionalriskmodelsetting4="four", \ - plottype1="mean", plottype2=None) - -#pdb.set_trace() +plotting( + output_label="fig_pl_survival_1_2", + timeseries_dict=timeseries, + riskmodelsetting1="one", + riskmodelsetting2="two", + series1="profitslosses", + series2="operational", + additionalriskmodelsetting3="three", + additionalriskmodelsetting4="four", + plottype1="mean", + plottype2="median", +) +plotting( + output_label="fig_pl_survival_3_4", + timeseries_dict=timeseries, + riskmodelsetting1="three", + riskmodelsetting2="four", + series1="profitslosses", + series2="operational", + additionalriskmodelsetting3="one", + additionalriskmodelsetting4="two", + plottype1="mean", + plottype2="median", +) +plotting( + output_label="fig_reinsurers_pl_survival_1_2", + timeseries_dict=timeseries, + riskmodelsetting1="one", + riskmodelsetting2="two", + series1="reinprofitslosses", + series2="reinoperational", + additionalriskmodelsetting3="three", + additionalriskmodelsetting4="four", + plottype1="mean", + plottype2="median", +) +plotting( + output_label="fig_reinsurers_pl_survival_3_4", + timeseries_dict=timeseries, + riskmodelsetting1="three", + riskmodelsetting2="four", + series1="reinprofitslosses", + series2="reinoperational", + additionalriskmodelsetting3="one", + additionalriskmodelsetting4="two", + plottype1="mean", + plottype2="median", +) +plotting( + output_label="fig_premium_1_2", + timeseries_dict=timeseries, + riskmodelsetting1="one", + riskmodelsetting2="two", + series1="premium", + series2=None, + additionalriskmodelsetting3="three", + additionalriskmodelsetting4="four", + plottype1="mean", + plottype2=None, +) + +# pdb.set_trace() diff --git a/metaplotter_pl_timescale_additional_measures.py b/metaplotter_pl_timescale_additional_measures.py index 5b0c449..b22b452 100644 --- a/metaplotter_pl_timescale_additional_measures.py +++ b/metaplotter_pl_timescale_additional_measures.py @@ -5,11 +5,12 @@ import time import glob + def read_data(): # do not overwrite old pdfs - #if os.path.exists("data/fig_one_and_two_rm_comp.pdf"): + # if os.path.exists("data/fig_one_and_two_rm_comp.pdf"): # os.rename("data/fig_one_and_two_rm_comp.pdf", "data/fig_one_and_two_rm_comp_old_" + time.strftime('%Y_%b_%d_%H_%M') + ".pdf") - #if os.path.exists("data/fig_three_and_four_rm_comp.pdf"): + # if os.path.exists("data/fig_three_and_four_rm_comp.pdf"): # os.rename("data/fig_three_and_four_rm_comp.pdf", "data/fig_three_and_four_rm_comp_old_" + time.strftime('%Y_%b_%d_%H_%M') + ".pdf") upper_bound = 75 @@ -30,7 +31,7 @@ def read_data(): filenames_threes.sort() filenames_fours.sort() - #assert len(filenames_ones) == len(filenames_twos) == len(filenames_threes) == len(filenames_fours) + # assert len(filenames_ones) == len(filenames_twos) == len(filenames_threes) == len(filenames_fours) all_filenames = filenames_ones + filenames_twos + filenames_threes + filenames_fours for filename in all_filenames: @@ -38,7 +39,7 @@ def read_data(): rfile = open(filename, "r") data = [eval(k) for k in rfile] rfile.close() - + # compute data series data_means = [] data_medians = [] @@ -53,21 +54,45 @@ def read_data(): data_medians = np.array(data_medians) data_q25 = np.array(data_q25) data_q75 = np.array(data_q75) - + # record data series timeseries_dict["mean"][filename] = data_means timeseries_dict["median"][filename] = data_medians timeseries_dict["quantile25"][filename] = data_q25 timeseries_dict["quantile75"][filename] = data_q75 return timeseries_dict - - -def plotting(output_label, timeseries_dict, riskmodelsetting1, riskmodelsetting2, series1, series2=None, additionalriskmodelsetting3=None, additionalriskmodelsetting4=None, plottype1="mean", plottype2="mean"): + +def plotting( + output_label, + timeseries_dict, + riskmodelsetting1, + riskmodelsetting2, + series1, + series2=None, + additionalriskmodelsetting3=None, + additionalriskmodelsetting4=None, + plottype1="mean", + plottype2="mean", +): # dictionaries colors = {"one": "red", "two": "blue", "three": "green", "four": "yellow"} - labels = {"reinexcess_capital": "Excess Capital (Reinsurers)", "excess_capital": "Excess Capital (Insurers)", "cumulative_unrecovered_claims": "Uncovered Claims (cumulative)", "cumulative_bankruptcies": "Bankruptcies (cumulative)", "profitslosses": "Profits and Losses (Insurer)", "contracts": "Contracts (Insurers)", "cash": "Liquidity (Insurers)", "operational": "Active Insurers", "premium": "Premium", "reinprofitslosses": "Profits and Losses (Reinsurer)", "reincash": "Liquidity (Reinsurers)", "reincontracts": "Contracts (Reinsurers)", "reinoperational": "Active Reinsurers"} - + labels = { + "reinexcess_capital": "Excess Capital (Reinsurers)", + "excess_capital": "Excess Capital (Insurers)", + "cumulative_unrecovered_claims": "Uncovered Claims (cumulative)", + "cumulative_bankruptcies": "Bankruptcies (cumulative)", + "profitslosses": "Profits and Losses (Insurer)", + "contracts": "Contracts (Insurers)", + "cash": "Liquidity (Insurers)", + "operational": "Active Insurers", + "premium": "Premium", + "reinprofitslosses": "Profits and Losses (Reinsurer)", + "reincash": "Liquidity (Reinsurers)", + "reincontracts": "Contracts (Reinsurers)", + "reinoperational": "Active Reinsurers", + } + # prepare labels, timeseries, etc. color1 = colors[riskmodelsetting1] color2 = colors[riskmodelsetting2] @@ -80,23 +105,33 @@ def plotting(output_label, timeseries_dict, riskmodelsetting1, riskmodelsetting2 plot_2_2 = "data/" + riskmodelsetting2 + "_" + series2 + ".dat" if additionalriskmodelsetting3 is not None: color3 = colors[additionalriskmodelsetting3] - label3 = str.upper(additionalriskmodelsetting3[0]) + additionalriskmodelsetting3[1:] + " riskmodels" + label3 = ( + str.upper(additionalriskmodelsetting3[0]) + + additionalriskmodelsetting3[1:] + + " riskmodels" + ) plot_1_3 = "data/" + additionalriskmodelsetting3 + "_" + series1 + ".dat" if series2 is not None: plot_2_3 = "data/" + additionalriskmodelsetting3 + "_" + series2 + ".dat" if additionalriskmodelsetting4 is not None: color4 = colors[additionalriskmodelsetting4] - label4 = str.upper(additionalriskmodelsetting4[0]) + additionalriskmodelsetting4[1:] + " riskmodels" + label4 = ( + str.upper(additionalriskmodelsetting4[0]) + + additionalriskmodelsetting4[1:] + + " riskmodels" + ) plot_1_4 = "data/" + additionalriskmodelsetting4 + "_" + series1 + ".dat" if series2 is not None: plot_2_4 = "data/" + additionalriskmodelsetting4 + "_" + series2 + ".dat" - + # Backup existing figures (so as not to overwrite them) outputfilename = "data/" + output_label + ".pdf" - backupfilename = "data/" + output_label + "_old_" + time.strftime('%Y_%b_%d_%H_%M') + ".pdf" + backupfilename = ( + "data/" + output_label + "_old_" + time.strftime("%Y_%b_%d_%H_%M") + ".pdf" + ) if os.path.exists(outputfilename): os.rename(outputfilename, backupfilename) - + # Plot and save fig = plt.figure() if series2 is not None: @@ -105,39 +140,111 @@ def plotting(output_label, timeseries_dict, riskmodelsetting1, riskmodelsetting2 ax0 = fig.add_subplot(111) maxlen_plots = 0 if additionalriskmodelsetting3 is not None: - ax0.plot(range(len(timeseries_dict[plottype1][plot_1_3]))[200:], timeseries_dict[plottype1][plot_1_3][200:], color=color3, label=label3) + ax0.plot( + range(len(timeseries_dict[plottype1][plot_1_3]))[200:], + timeseries_dict[plottype1][plot_1_3][200:], + color=color3, + label=label3, + ) maxlen_plots = max(maxlen_plots, len(timeseries_dict[plottype1][plot_1_3])) if additionalriskmodelsetting4 is not None: - ax0.plot(range(len(timeseries_dict[plottype1][plot_1_4]))[200:], timeseries_dict[plottype1][plot_1_4][200:], color=color4, label=label4) + ax0.plot( + range(len(timeseries_dict[plottype1][plot_1_4]))[200:], + timeseries_dict[plottype1][plot_1_4][200:], + color=color4, + label=label4, + ) maxlen_plots = max(maxlen_plots, len(timeseries_dict[plottype1][plot_1_4])) - ax0.plot(range(len(timeseries_dict[plottype1][plot_1_1]))[200:], timeseries_dict[plottype1][plot_1_1][200:], color=color1, label=label1) - ax0.plot(range(len(timeseries_dict[plottype1][plot_1_2]))[200:], timeseries_dict[plottype1][plot_1_2][200:], color=color2, label=label2) - ax0.fill_between(range(len(timeseries_dict["quantile25"][plot_1_1]))[200:], timeseries_dict["quantile25"][plot_1_1][200:], timeseries_dict["quantile75"][plot_1_1][200:], facecolor=color1, alpha=0.25) - ax0.fill_between(range(len(timeseries_dict["quantile25"][plot_1_1]))[200:], timeseries_dict["quantile25"][plot_1_2][200:], timeseries_dict["quantile75"][plot_1_2][200:], facecolor=color2, alpha=0.25) - ax0.set_ylabel(labels[series1])#"Contracts") - maxlen_plots = max(maxlen_plots, len(timeseries_dict[plottype1][plot_1_1]), len(timeseries_dict[plottype1][plot_1_2])) - xticks = np.arange(200, maxlen_plots, step=120) + ax0.plot( + range(len(timeseries_dict[plottype1][plot_1_1]))[200:], + timeseries_dict[plottype1][plot_1_1][200:], + color=color1, + label=label1, + ) + ax0.plot( + range(len(timeseries_dict[plottype1][plot_1_2]))[200:], + timeseries_dict[plottype1][plot_1_2][200:], + color=color2, + label=label2, + ) + ax0.fill_between( + range(len(timeseries_dict["quantile25"][plot_1_1]))[200:], + timeseries_dict["quantile25"][plot_1_1][200:], + timeseries_dict["quantile75"][plot_1_1][200:], + facecolor=color1, + alpha=0.25, + ) + ax0.fill_between( + range(len(timeseries_dict["quantile25"][plot_1_1]))[200:], + timeseries_dict["quantile25"][plot_1_2][200:], + timeseries_dict["quantile75"][plot_1_2][200:], + facecolor=color2, + alpha=0.25, + ) + ax0.set_ylabel(labels[series1]) # "Contracts") + maxlen_plots = max( + maxlen_plots, + len(timeseries_dict[plottype1][plot_1_1]), + len(timeseries_dict[plottype1][plot_1_2]), + ) + xticks = np.arange(200, maxlen_plots, step=120) ax0.set_xticks(xticks) - ax0.set_xticklabels(["${0:d}$".format(int((xtc-200)/12)) for xtc in xticks]); + ax0.set_xticklabels(["${0:d}$".format(int((xtc - 200) / 12)) for xtc in xticks]) - ax0.legend(loc='best') + ax0.legend(loc="best") if series2 is not None: ax1 = fig.add_subplot(212) maxlen_plots = 0 if additionalriskmodelsetting3 is not None: - ax1.plot(range(len(timeseries_dict[plottype2][plot_2_3]))[200:], timeseries_dict[plottype2][plot_2_3][200:], color=color3, label=label3) + ax1.plot( + range(len(timeseries_dict[plottype2][plot_2_3]))[200:], + timeseries_dict[plottype2][plot_2_3][200:], + color=color3, + label=label3, + ) maxlen_plots = max(maxlen_plots, len(timeseries_dict[plottype1][plot_2_3])) if additionalriskmodelsetting4 is not None: - ax1.plot(range(len(timeseries_dict[plottype2][plot_2_4]))[200:], timeseries_dict[plottype2][plot_2_4][200:], color=color4, label=label4) + ax1.plot( + range(len(timeseries_dict[plottype2][plot_2_4]))[200:], + timeseries_dict[plottype2][plot_2_4][200:], + color=color4, + label=label4, + ) maxlen_plots = max(maxlen_plots, len(timeseries_dict[plottype1][plot_2_4])) - ax1.plot(range(len(timeseries_dict[plottype2][plot_2_1]))[200:], timeseries_dict[plottype2][plot_2_1][200:], color=color1, label=label1) - ax1.plot(range(len(timeseries_dict[plottype2][plot_2_2]))[200:], timeseries_dict[plottype2][plot_2_2][200:], color=color2, label=label2) - ax1.fill_between(range(len(timeseries_dict["quantile25"][plot_2_1]))[200:], timeseries_dict["quantile25"][plot_2_1][200:], timeseries_dict["quantile75"][plot_2_1][200:], facecolor=color1, alpha=0.25) - ax1.fill_between(range(len(timeseries_dict["quantile25"][plot_2_1]))[200:], timeseries_dict["quantile25"][plot_2_2][200:], timeseries_dict["quantile75"][plot_2_2][200:], facecolor=color2, alpha=0.25) - maxlen_plots = max(maxlen_plots, len(timeseries_dict[plottype1][plot_2_1]), len(timeseries_dict[plottype1][plot_2_2])) - xticks = np.arange(200, maxlen_plots, step=120) + ax1.plot( + range(len(timeseries_dict[plottype2][plot_2_1]))[200:], + timeseries_dict[plottype2][plot_2_1][200:], + color=color1, + label=label1, + ) + ax1.plot( + range(len(timeseries_dict[plottype2][plot_2_2]))[200:], + timeseries_dict[plottype2][plot_2_2][200:], + color=color2, + label=label2, + ) + ax1.fill_between( + range(len(timeseries_dict["quantile25"][plot_2_1]))[200:], + timeseries_dict["quantile25"][plot_2_1][200:], + timeseries_dict["quantile75"][plot_2_1][200:], + facecolor=color1, + alpha=0.25, + ) + ax1.fill_between( + range(len(timeseries_dict["quantile25"][plot_2_1]))[200:], + timeseries_dict["quantile25"][plot_2_2][200:], + timeseries_dict["quantile75"][plot_2_2][200:], + facecolor=color2, + alpha=0.25, + ) + maxlen_plots = max( + maxlen_plots, + len(timeseries_dict[plottype1][plot_2_1]), + len(timeseries_dict[plottype1][plot_2_2]), + ) + xticks = np.arange(200, maxlen_plots, step=120) ax1.set_xticks(xticks) - ax1.set_xticklabels(["${0:d}$".format(int((xtc-200)/12)) for xtc in xticks]); + ax1.set_xticklabels(["${0:d}$".format(int((xtc - 200) / 12)) for xtc in xticks]) ax1.set_ylabel(labels[series2]) ax1.set_xlabel("Years") else: @@ -145,43 +252,118 @@ def plotting(output_label, timeseries_dict, riskmodelsetting1, riskmodelsetting2 plt.savefig(outputfilename) plt.show() + timeseries = read_data() # for just two different riskmodel settings -#plotting(output_label="fig_pl_excap_1_2", timeseries_dict=timeseries, riskmodelsetting1="one", \ +# plotting(output_label="fig_pl_excap_1_2", timeseries_dict=timeseries, riskmodelsetting1="one", \ # riskmodelsetting2="two", series1="profitslosses", series2="excess_capital", plottype1="mean", plottype2="mean") -#plotting(output_label="fig_reinsurers_pl_excap_1_2", timeseries_dict=timeseries, riskmodelsetting1="one", \ +# plotting(output_label="fig_reinsurers_pl_excap_1_2", timeseries_dict=timeseries, riskmodelsetting1="one", \ # riskmodelsetting2="two", series1="reinprofitslosses", series2="reinexcess_capital", plottype1="mean", plottype2="mean") -#plotting(output_label="fig_bankruptcies_unrecovered_claims_1_2", timeseries_dict=timeseries, riskmodelsetting1="one", \ +# plotting(output_label="fig_bankruptcies_unrecovered_claims_1_2", timeseries_dict=timeseries, riskmodelsetting1="one", \ # riskmodelsetting2="two", series1="cumulative_bankruptcies", series2="cumulative_unrecovered_claims", plottype1="mean", plottype2="median") -#plotting(output_label="fig_premium_1_2", timeseries_dict=timeseries, riskmodelsetting1="one", riskmodelsetting2="two", \ +# plotting(output_label="fig_premium_1_2", timeseries_dict=timeseries, riskmodelsetting1="one", riskmodelsetting2="two", \ # series1="premium", series2=None, plottype1="mean", plottype2=None) # -#raise SystemExit +# raise SystemExit # for four different riskmodel settings -plotting(output_label="fig_pl_excap_1_2", timeseries_dict=timeseries, riskmodelsetting1="one", \ - riskmodelsetting2="two", series1="profitslosses", series2="excess_capital", additionalriskmodelsetting3="three", \ - additionalriskmodelsetting4="four", plottype1="mean", plottype2="mean") -plotting(output_label="fig_reinsurers_pl_excap_1_2", timeseries_dict=timeseries, riskmodelsetting1="one", \ - riskmodelsetting2="two", series1="reinprofitslosses", series2="reinexcess_capital", additionalriskmodelsetting3="three", \ - additionalriskmodelsetting4="four", plottype1="mean", plottype2="mean") -plotting(output_label="fig_bankruptcies_unrecovered_claims_1_2", timeseries_dict=timeseries, riskmodelsetting1="one", \ - riskmodelsetting2="two", series1="cumulative_bankruptcies", series2="cumulative_unrecovered_claims", additionalriskmodelsetting3="three", \ - additionalriskmodelsetting4="four", plottype1="mean", plottype2="median") -plotting(output_label="fig_premium_1_2", timeseries_dict=timeseries, riskmodelsetting1="one", riskmodelsetting2="two", \ - series1="premium", series2=None, additionalriskmodelsetting3="three", additionalriskmodelsetting4="four", plottype1="mean", plottype2=None) - -plotting(output_label="fig_pl_excap_3_4", timeseries_dict=timeseries, riskmodelsetting1="three", \ - riskmodelsetting2="four", series1="profitslosses", series2="excess_capital", additionalriskmodelsetting3="one", \ - additionalriskmodelsetting4="two", plottype1="mean", plottype2="mean") -plotting(output_label="fig_reinsurers_pl_excap_3_4", timeseries_dict=timeseries, riskmodelsetting1="three", \ - riskmodelsetting2="four", series1="reinprofitslosses", series2="reinexcess_capital", additionalriskmodelsetting3="one", \ - additionalriskmodelsetting4="two", plottype1="mean", plottype2="mean") -plotting(output_label="fig_bankruptcies_unrecovered_claims_3_4", timeseries_dict=timeseries, riskmodelsetting1="three", \ - riskmodelsetting2="four", series1="cumulative_bankruptcies", series2="cumulative_unrecovered_claims", additionalriskmodelsetting3="one", \ - additionalriskmodelsetting4="two", plottype1="mean", plottype2="median") -plotting(output_label="fig_premium_3_4", timeseries_dict=timeseries, riskmodelsetting1="three", riskmodelsetting2="four", \ - series1="premium", series2=None, additionalriskmodelsetting3="one", additionalriskmodelsetting4="two", plottype1="mean", plottype2=None) - - -#pdb.set_trace() +plotting( + output_label="fig_pl_excap_1_2", + timeseries_dict=timeseries, + riskmodelsetting1="one", + riskmodelsetting2="two", + series1="profitslosses", + series2="excess_capital", + additionalriskmodelsetting3="three", + additionalriskmodelsetting4="four", + plottype1="mean", + plottype2="mean", +) +plotting( + output_label="fig_reinsurers_pl_excap_1_2", + timeseries_dict=timeseries, + riskmodelsetting1="one", + riskmodelsetting2="two", + series1="reinprofitslosses", + series2="reinexcess_capital", + additionalriskmodelsetting3="three", + additionalriskmodelsetting4="four", + plottype1="mean", + plottype2="mean", +) +plotting( + output_label="fig_bankruptcies_unrecovered_claims_1_2", + timeseries_dict=timeseries, + riskmodelsetting1="one", + riskmodelsetting2="two", + series1="cumulative_bankruptcies", + series2="cumulative_unrecovered_claims", + additionalriskmodelsetting3="three", + additionalriskmodelsetting4="four", + plottype1="mean", + plottype2="median", +) +plotting( + output_label="fig_premium_1_2", + timeseries_dict=timeseries, + riskmodelsetting1="one", + riskmodelsetting2="two", + series1="premium", + series2=None, + additionalriskmodelsetting3="three", + additionalriskmodelsetting4="four", + plottype1="mean", + plottype2=None, +) + +plotting( + output_label="fig_pl_excap_3_4", + timeseries_dict=timeseries, + riskmodelsetting1="three", + riskmodelsetting2="four", + series1="profitslosses", + series2="excess_capital", + additionalriskmodelsetting3="one", + additionalriskmodelsetting4="two", + plottype1="mean", + plottype2="mean", +) +plotting( + output_label="fig_reinsurers_pl_excap_3_4", + timeseries_dict=timeseries, + riskmodelsetting1="three", + riskmodelsetting2="four", + series1="reinprofitslosses", + series2="reinexcess_capital", + additionalriskmodelsetting3="one", + additionalriskmodelsetting4="two", + plottype1="mean", + plottype2="mean", +) +plotting( + output_label="fig_bankruptcies_unrecovered_claims_3_4", + timeseries_dict=timeseries, + riskmodelsetting1="three", + riskmodelsetting2="four", + series1="cumulative_bankruptcies", + series2="cumulative_unrecovered_claims", + additionalriskmodelsetting3="one", + additionalriskmodelsetting4="two", + plottype1="mean", + plottype2="median", +) +plotting( + output_label="fig_premium_3_4", + timeseries_dict=timeseries, + riskmodelsetting1="three", + riskmodelsetting2="four", + series1="premium", + series2=None, + additionalriskmodelsetting3="one", + additionalriskmodelsetting4="two", + plottype1="mean", + plottype2=None, +) + + +# pdb.set_trace() diff --git a/plotter.py b/plotter.py index 593b95f..fd348d2 100755 --- a/plotter.py +++ b/plotter.py @@ -1,20 +1,20 @@ import matplotlib.pyplot as plt import numpy as np -rfile = open("data/history_logs.dat","r") +rfile = open("data/history_logs.dat", "r") data = [eval(k) for k in rfile] -contracts = data[0]['total_contracts'] -op = data[0]['total_operational'] -cash = data[0]['total_cash'] -pl = data[0]['total_profitslosses'] -reincontracts = data[0]['total_reincontracts'] -reinop = data[0]['total_reinoperational'] -reincash = data[0]['total_reincash'] -reinpl = data[0]['total_reinprofitslosses'] -premium = data[0]['market_premium'] -catbop = data[0]['total_catbondsoperational'] +contracts = data[0]["total_contracts"] +op = data[0]["total_operational"] +cash = data[0]["total_cash"] +pl = data[0]["total_profitslosses"] +reincontracts = data[0]["total_reincontracts"] +reinop = data[0]["total_reinoperational"] +reincash = data[0]["total_reincash"] +reinpl = data[0]["total_reinprofitslosses"] +premium = data[0]["market_premium"] +catbop = data[0]["total_catbondsoperational"] rfile.close() @@ -34,22 +34,22 @@ fig1 = plt.figure() ax0 = fig1.add_subplot(511) ax0.get_xaxis().set_visible(False) -ax0.plot(range(len(cs)), cs,"b") +ax0.plot(range(len(cs)), cs, "b") ax0.set_ylabel("Contracts") ax1 = fig1.add_subplot(512) ax1.get_xaxis().set_visible(False) -ax1.plot(range(len(os)), os,"b") +ax1.plot(range(len(os)), os, "b") ax1.set_ylabel("Active firms") ax2 = fig1.add_subplot(513) ax2.get_xaxis().set_visible(False) -ax2.plot(range(len(hs)), hs,"b") +ax2.plot(range(len(hs)), hs, "b") ax2.set_ylabel("Cash") ax3 = fig1.add_subplot(514) ax3.get_xaxis().set_visible(False) -ax3.plot(range(len(pls)), pls,"b") +ax3.plot(range(len(pls)), pls, "b") ax3.set_ylabel("Profits, Losses") ax9 = fig1.add_subplot(515) -ax9.plot(range(len(ps)), ps,"k") +ax9.plot(range(len(ps)), ps, "k") ax9.set_ylabel("Premium") ax9.set_xlabel("Time") plt.savefig("data/single_replication_pt1.pdf") @@ -57,22 +57,22 @@ fig2 = plt.figure() ax4 = fig2.add_subplot(511) ax4.get_xaxis().set_visible(False) -ax4.plot(range(len(cre)), cre,"r") +ax4.plot(range(len(cre)), cre, "r") ax4.set_ylabel("Contracts") ax5 = fig2.add_subplot(512) ax5.get_xaxis().set_visible(False) -ax5.plot(range(len(ore)), ore,"r") +ax5.plot(range(len(ore)), ore, "r") ax5.set_ylabel("Active reinfirms") ax6 = fig2.add_subplot(513) ax6.get_xaxis().set_visible(False) -ax6.plot(range(len(hre)), hre,"r") +ax6.plot(range(len(hre)), hre, "r") ax6.set_ylabel("Cash") ax7 = fig2.add_subplot(514) ax7.get_xaxis().set_visible(False) -ax7.plot(range(len(plre)), plre,"r") +ax7.plot(range(len(plre)), plre, "r") ax7.set_ylabel("Profits, Losses") ax8 = fig2.add_subplot(515) -ax8.plot(range(len(ocb)), ocb,"m") +ax8.plot(range(len(ocb)), ocb, "m") ax8.set_ylabel("Active cat bonds") ax8.set_xlabel("Time") diff --git a/plotter_pl_timescale.py b/plotter_pl_timescale.py index 4f517ff..26b001b 100644 --- a/plotter_pl_timescale.py +++ b/plotter_pl_timescale.py @@ -1,12 +1,14 @@ import matplotlib.pyplot as plt import numpy as np + def get_data(name): rfile = open(name, "r") out = [eval(k) for k in rfile] rfile.close() return out + contracts = get_data("data/contracts.dat") op = get_data("data/operational.dat") cash = get_data("data/cash.dat") @@ -30,7 +32,7 @@ def get_data(name): c_re = [] -o_re= [] +o_re = [] h_re = [] @@ -40,18 +42,18 @@ def get_data(name): p_e = [] -for i in range(len(contracts[0])): #for every time period i +for i in range(len(contracts[0])): # for every time period i cs = np.mean([item[i] for item in contracts]) - #pls = np.mean([item[i] for item in pl]) + # pls = np.mean([item[i] for item in pl]) os = np.median([item[i] for item in op]) hs = np.median([item[i] for item in cash]) c_s.append(cs) o_s.append(os) h_s.append(hs) - if i>0: - pls = np.mean([item[i]-item[i-1] for item in cash]) - plre = np.mean([item[i]-item[i-1] for item in reincash]) + if i > 0: + pls = np.mean([item[i] - item[i - 1] for item in cash]) + plre = np.mean([item[i] - item[i - 1] for item in reincash]) pl_s.append(pls) pl_re.append(plre) @@ -61,43 +63,43 @@ def get_data(name): c_re.append(cre) o_re.append(ore) h_re.append(hre) - + ocb = np.median([item[i] for item in catbop]) o_cb.append(ocb) - + p_s = np.median([item[i] for item in premium]) p_e.append(p_s) maxlen_plots = max(len(pl_s), len(pl_re), len(o_s), len(o_re), len(p_e)) -xticks = np.arange(200, maxlen_plots, step=120) +xticks = np.arange(200, maxlen_plots, step=120) fig0 = plt.figure() ax3 = fig0.add_subplot(511) -ax3.plot(range(len(pl_s))[200:], pl_s[200:],"b") +ax3.plot(range(len(pl_s))[200:], pl_s[200:], "b") ax3.set_ylabel("Profits, Losses") ax3.set_xticks(xticks) -ax3.set_xticklabels(["${0:d}$".format(int((xtc-200)/12)) for xtc in xticks]) +ax3.set_xticklabels(["${0:d}$".format(int((xtc - 200) / 12)) for xtc in xticks]) ax7 = fig0.add_subplot(512) -ax7.plot(range(len(pl_re))[200:], pl_re[200:],"r") +ax7.plot(range(len(pl_re))[200:], pl_re[200:], "r") ax7.set_ylabel("Profits, Losses (Reins.)") ax7.set_xticks(xticks) -ax7.set_xticklabels(["${0:d}$".format(int((xtc-200)/12)) for xtc in xticks]) +ax7.set_xticklabels(["${0:d}$".format(int((xtc - 200) / 12)) for xtc in xticks]) ax1 = fig0.add_subplot(513) -ax1.plot(range(len(o_s))[200:], o_s[200:],"b") +ax1.plot(range(len(o_s))[200:], o_s[200:], "b") ax1.set_ylabel("Active firms") ax1.set_xticks(xticks) -ax1.set_xticklabels(["${0:d}$".format(int((xtc-200)/12)) for xtc in xticks]) +ax1.set_xticklabels(["${0:d}$".format(int((xtc - 200) / 12)) for xtc in xticks]) ax5 = fig0.add_subplot(514) -ax5.plot(range(len(o_re))[200:], o_re[200:],"r") +ax5.plot(range(len(o_re))[200:], o_re[200:], "r") ax5.set_ylabel("Active reins. firms") ax5.set_xticks(xticks) -ax5.set_xticklabels(["${0:d}$".format(int((xtc-200)/12)) for xtc in xticks]) +ax5.set_xticklabels(["${0:d}$".format(int((xtc - 200) / 12)) for xtc in xticks]) ax9 = fig0.add_subplot(515) -ax9.plot(range(len(p_e))[200:], p_e[200:],"k") +ax9.plot(range(len(p_e))[200:], p_e[200:], "k") ax9.set_ylabel("Premium") ax9.set_xlabel("Years") ax9.set_xticks(xticks) -ax9.set_xticklabels(["${0:d}$".format(int((xtc-200)/12)) for xtc in xticks]) +ax9.set_xticklabels(["${0:d}$".format(int((xtc - 200) / 12)) for xtc in xticks]) plt.savefig("data/single_replication_new.pdf") diff --git a/reinsurancecontract.py b/reinsurancecontract.py index 22fbf10..ed868de 100644 --- a/reinsurancecontract.py +++ b/reinsurancecontract.py @@ -1,6 +1,7 @@ import numpy as np -from metainsurancecontract import MetaInsuranceContract +from metainsurancecontract import MetaInsuranceContract + class ReinsuranceContract(MetaInsuranceContract): """ReinsuranceContract class. @@ -9,18 +10,48 @@ class ReinsuranceContract(MetaInsuranceContract): and ReinsuranceContract objects. The signature of this class' constructor is the same as that of the InsuranceContract constructor. The class has two methods (explode, mature) that overwrite methods in InsuranceContract.""" - def __init__(self, insurer, properties, time, premium, runtime, payment_period, expire_immediately, initial_VaR=0.,\ - insurancetype="proportional", deductible_fraction=None, excess_fraction=None, reinsurance=0): - super(ReinsuranceContract, self).__init__(insurer, properties, time, premium, runtime, payment_period, \ - expire_immediately, initial_VaR, insurancetype, deductible_fraction, excess_fraction, reinsurance) - #self.is_reinsurancecontract = True - + + def __init__( + self, + insurer, + properties, + time, + premium, + runtime, + payment_period, + expire_immediately, + initial_VaR=0.0, + insurancetype="proportional", + deductible_fraction=None, + excess_fraction=None, + reinsurance=0, + ): + super(ReinsuranceContract, self).__init__( + insurer, + properties, + time, + premium, + runtime, + payment_period, + expire_immediately, + initial_VaR, + insurancetype, + deductible_fraction, + excess_fraction, + reinsurance, + ) + # self.is_reinsurancecontract = True + if self.insurancetype == "excess-of-loss": - self.property_holder.add_reinsurance(category=self.category, excess_fraction=self.excess_fraction, \ - deductible_fraction=self.deductible_fraction, contract=self) + self.property_holder.add_reinsurance( + category=self.category, + excess_fraction=self.excess_fraction, + deductible_fraction=self.deductible_fraction, + contract=self, + ) else: assert self.contract is not None - + def explode(self, time, damage_extent=None): """Explode method. Accepts arguments @@ -34,19 +65,25 @@ def explode(self, time, damage_extent=None): if self.insurancetype == "excess-of-loss" and damage_extent > self.deductible: claim = min(self.excess, damage_extent) - self.deductible - self.insurer.receive_obligation(claim, self.property_holder, time, 'claim') + self.insurer.receive_obligation(claim, self.property_holder, time, "claim") else: claim = min(self.excess, damage_extent) - self.deductible - self.insurer.receive_obligation(claim, self.property_holder, time + 1, 'claim') + self.insurer.receive_obligation( + claim, self.property_holder, time + 1, "claim" + ) # Reinsurer pays as soon as possible. - self.insurer.register_claim(claim) #Every reinsurance claim made is immediately registered. + self.insurer.register_claim( + claim + ) # Every reinsurance claim made is immediately registered. if self.expire_immediately: - self.current_claim += self.contract.claim # TODO: should proportional reinsurance claims be subject to excess_of_loss retrocession? If so, reorganize more straightforwardly - + self.current_claim += ( + self.contract.claim + ) # TODO: should proportional reinsurance claims be subject to excess_of_loss retrocession? If so, reorganize more straightforwardly + self.expiration = time - #self.terminating = True - + # self.terminating = True + def mature(self, time): """Mature method. Accepts arguments @@ -54,12 +91,15 @@ def mature(self, time): No return value. Removes any reinsurance functions this contract has and terminates any reinsurance contracts for this contract.""" - #self.terminating = True + # self.terminating = True self.terminate_reinsurance(time) - + if self.insurancetype == "excess-of-loss": - self.property_holder.delete_reinsurance(category=self.category, excess_fraction=self.excess_fraction, \ - deductible_fraction=self.deductible_fraction, contract=self) - else: #TODO: ? Instead: if self.insurancetype == "proportional": + self.property_holder.delete_reinsurance( + category=self.category, + excess_fraction=self.excess_fraction, + deductible_fraction=self.deductible_fraction, + contract=self, + ) + else: # TODO: ? Instead: if self.insurancetype == "proportional": self.contract.unreinsure() - diff --git a/reinsurancefirm.py b/reinsurancefirm.py index 1c1558b..ac2564f 100644 --- a/reinsurancefirm.py +++ b/reinsurancefirm.py @@ -1,9 +1,11 @@ -#from metainsuranceorg import MetaInsuranceOrg +# from metainsuranceorg import MetaInsuranceOrg from insurancefirm import InsuranceFirm + class ReinsuranceFirm(InsuranceFirm): """ReinsuranceFirm class. Inherits from InsuranceFirm.""" + def init(self, simulation_parameters, agent_parameters): """Constructor method. Accepts arguments diff --git a/resume.py b/resume.py index 49f374f..33ce1ec 100644 --- a/resume.py +++ b/resume.py @@ -6,7 +6,7 @@ import argparse import pickle import hashlib -import random +import random # import config file and apply configuration import isleconfig @@ -16,17 +16,46 @@ override_no_riskmodels = False # use argparse to handle command line arguments -parser = argparse.ArgumentParser(description='Model the Insurance sector') -parser.add_argument("--oneriskmodel", action="store_true", help="allow overriding the number of riskmodels from the standard config (with 1)") -parser.add_argument("--riskmodels", type=int, choices=[1,2,3,4], help="allow overriding the number of riskmodels from standard config (with 1 or other numbers)") -parser.add_argument("--replicid", type=int, help="if replication ID is given, pass this to the simulation so that the risk profile can be restored") -parser.add_argument("--replicating", action="store_true", help="if this is a simulation run designed to replicate another, override the config file parameter") -parser.add_argument("--randomseed", type=float, help="allow setting of numpy random seed") -parser.add_argument("--foreground", action="store_true", help="force foreground runs even if replication ID is given (which defaults to background runs)") -parser.add_argument("--shownetwork", action="store_true", help="show reinsurance relations as network") +parser = argparse.ArgumentParser(description="Model the Insurance sector") +parser.add_argument( + "--oneriskmodel", + action="store_true", + help="allow overriding the number of riskmodels from the standard config (with 1)", +) +parser.add_argument( + "--riskmodels", + type=int, + choices=[1, 2, 3, 4], + help="allow overriding the number of riskmodels from standard config (with 1 or other numbers)", +) +parser.add_argument( + "--replicid", + type=int, + help="if replication ID is given, pass this to the simulation so that the risk profile can be restored", +) +parser.add_argument( + "--replicating", + action="store_true", + help="if this is a simulation run designed to replicate another, override the config file parameter", +) +parser.add_argument( + "--randomseed", type=float, help="allow setting of numpy random seed" +) +parser.add_argument( + "--foreground", + action="store_true", + help="force foreground runs even if replication ID is given (which defaults to background runs)", +) +parser.add_argument( + "--shownetwork", action="store_true", help="show reinsurance relations as network" +) parser.add_argument("-p", "--showprogress", action="store_true", help="show timesteps") parser.add_argument("-v", "--verbose", action="store_true", help="more detailed output") -parser.add_argument("--save_iterations", type=int, help="number of iterations to iterate before saving world state") +parser.add_argument( + "--save_iterations", + type=int, + help="number of iterations to iterate before saving world state", +) args = parser.parse_args() if args.oneriskmodel: @@ -38,7 +67,9 @@ replic_ID = args.replicid if args.replicating: isleconfig.replicating = True - assert replic_ID is not None, "Error: Replication requires a replication ID to identify run to be replicated" + assert ( + replic_ID is not None + ), "Error: Replication requires a replication ID to identify run to be replicated" if args.randomseed: randomseed = args.randomseed seed = int(randomseed) @@ -64,7 +95,7 @@ # main function def main(): - + with open("data/simulation_save.pkl", "br") as rfile: d = pickle.load(rfile) simulation = d["simulation"] @@ -79,8 +110,12 @@ def main(): event_schedule_trunc = [] event_damage_trunc = [] for categ in range(simulation_parameters["no_categories"]): - event_schedule_trunc.append([event for event in event_schedule[categ] if event >= time]) - event_damage_trunc.append(event_damage[categ][-len(event_schedule_trunc[categ]):]) + event_schedule_trunc.append( + [event for event in event_schedule[categ] if event >= time] + ) + event_damage_trunc.append( + event_damage[categ][-len(event_schedule_trunc[categ]) :] + ) insurancefirms_group = list(simulation.insurancefirms) reinsurancefirms_group = list(simulation.reinsurancefirms) @@ -90,40 +125,58 @@ def main(): random.setstate(random_seed) for t in range(time, simulation_parameters["max_time"]): - + # create new agents # TODO: write method for this; this code block is executed almost identically 4 times if world.insurance_firm_market_entry(agent_type="InsuranceFirm"): parameters = [np.random.choice(world.agent_parameters["insurancefirm"])] parameters[0]["id"] = world.get_unique_insurer_id() - new_insurance_firm = simulation.build_agents(InsuranceFirm, - 'insurancefirm', - parameters=simulation_parameters, - agent_parameters=parameters) + new_insurance_firm = simulation.build_agents( + InsuranceFirm, + "insurancefirm", + parameters=simulation_parameters, + agent_parameters=parameters, + ) insurancefirms_group += new_insurance_firm new_insurancefirm_pointer = new_insurance_firm - world.accept_agents("insurancefirm", new_insurancefirm_pointer, new_insurance_firm, time=t) - + world.accept_agents( + "insurancefirm", new_insurancefirm_pointer, new_insurance_firm, time=t + ) + if world.insurance_firm_market_entry(agent_type="ReinsuranceFirm"): parameters = [np.random.choice(world.agent_parameters["reinsurancefirm"])] parameters[0]["id"] = world.get_unique_reinsurer_id() - new_reinsurance_firm = simulation.build_agents(ReinsuranceFirm, - 'reinsurancefirm', - parameters=simulation_parameters, - agent_parameters=parameters) + new_reinsurance_firm = simulation.build_agents( + ReinsuranceFirm, + "reinsurancefirm", + parameters=simulation_parameters, + agent_parameters=parameters, + ) reinsurancefirms_group += new_reinsurance_firm new_reinsurancefirm_pointer = new_reinsurance_firm - world.accept_agents("reinsurancefirm", new_reinsurancefirm_pointer, new_reinsurance_firm, time=t) - + world.accept_agents( + "reinsurancefirm", + new_reinsurancefirm_pointer, + new_reinsurance_firm, + time=t, + ) + # iterate simulation world.iterate(t) - + # log data world.save_data() - - if t > 0 and t//50 == t/50: - save_simulation(t, simulation, simulation_parameters, event_schedule, event_damage, exit_now=False) - #print("here") - + + if t > 0 and t // 50 == t / 50: + save_simulation( + t, + simulation, + simulation_parameters, + event_schedule, + event_damage, + exit_now=False, + ) + # print("here") + # finish simulation, write logs simulation.finalize() @@ -142,9 +195,11 @@ def save_simulation(t, sim, sim_param, event_schedule, event_damage, exit_now=Fa pickle.dump(d, wfile, protocol=pickle.HIGHEST_PROTOCOL) with open("data/simulation_resave.pkl", "br") as rfile: file_contents = rfile.read() - #print("\nSimulation hashes: ", hashlib.sha512(str(d).encode()).hexdigest(), "; ", hashlib.sha512(str(file_contents).encode()).hexdigest()) - # note that the hash over the dict is for some reason not identical between runs. The hash over the state saved to the file is. - print("\nSimulation hash: ", hashlib.sha512(str(file_contents).encode()).hexdigest()) + # print("\nSimulation hashes: ", hashlib.sha512(str(d).encode()).hexdigest(), "; ", hashlib.sha512(str(file_contents).encode()).hexdigest()) + # note that the hash over the dict is for some reason not identical between runs. The hash over the state saved to the file is. + print( + "\nSimulation hash: ", hashlib.sha512(str(file_contents).encode()).hexdigest() + ) if exit_now: exit(0) diff --git a/riskmodel.py b/riskmodel.py index ed259c0..d437c06 100644 --- a/riskmodel.py +++ b/riskmodel.py @@ -7,9 +7,20 @@ class RiskModel: - def __init__(self, damage_distribution, expire_immediately, cat_separation_distribution, norm_premium, \ - category_number, init_average_exposure, init_average_risk_factor, init_profit_estimate, \ - margin_of_safety, var_tail_prob, inaccuracy): + def __init__( + self, + damage_distribution, + expire_immediately, + cat_separation_distribution, + norm_premium, + category_number, + init_average_exposure, + init_average_risk_factor, + init_profit_estimate, + margin_of_safety, + var_tail_prob, + inaccuracy, + ): """Initialising method for RiskModel class. All accepted arguments are initialised in the __init__ method of insurancesimulation, and are mostly from isleconfig.py. """ self.cat_separation_distribution = cat_separation_distribution @@ -23,18 +34,20 @@ def __init__(self, damage_distribution, expire_immediately, cat_separation_distr self.margin_of_safety = margin_of_safety """damage_distribution is some scipy frozen rv distribution which is bound between 0 and 1 and indicates the share of risks suffering damage as part of any single catastrophic peril""" - self.damage_distribution = [damage_distribution for _ in range(self.category_number)] - self.damage_distribution_stack = [[] for _ in range(self.category_number)] + self.damage_distribution = [ + damage_distribution for _ in range(self.category_number) + ] + self.damage_distribution_stack = [[] for _ in range(self.category_number)] self.reinsurance_contract_stack = [[] for _ in range(self.category_number)] self.inaccuracy = inaccuracy - + def getPPF(self, categ_id, tailSize): """Method for getting quantile function of the damage distribution (value at risk) by category. Positional arguments: categ_id integer: category tailSize (float 1=>x=>0): quantile Returns value-at-risk.""" - return self.damage_distribution[categ_id].ppf(1-tailSize) + return self.damage_distribution[categ_id].ppf(1 - tailSize) def get_categ_risks(self, risks, categ_id): """Method takes list of all risks and only returns a list of all the risks belonging to the given category. @@ -46,7 +59,7 @@ def get_categ_risks(self, risks, categ_id): categ_risks = [risk for risk in risks if risk["category"] == categ_id] return categ_risks - def compute_expectation(self, categ_risks, categ_id): #TODO: more intuitive name? + def compute_expectation(self, categ_risks, categ_id): # TODO: more intuitive name? """Method to compute the average exposure and risk factor as well as the increase in expected profits for the risks in a given category. Accepts: @@ -61,25 +74,25 @@ def compute_expectation(self, categ_risks, categ_id): #TODO: more intuitive runtimes = [] for risk in categ_risks: # TODO: factor in excess instead of value? - exposures.append(risk["value"]-risk["deductible"]) + exposures.append(risk["value"] - risk["deductible"]) risk_factors.append(risk["risk_factor"]) runtimes.append(risk["runtime"]) average_exposure = np.mean(exposures) average_risk_factor = self.inaccuracy[categ_id] * np.mean(risk_factors) mean_runtime = np.mean(runtimes) - + if self.expire_immediately: incr_expected_profits = -1 # TODO: fix the norm_premium estimation - #incr_expected_profits = (self.norm_premium - (1 - scipy.stats.poisson(1 / self.cat_separation_distribution.mean() * \ + # incr_expected_profits = (self.norm_premium - (1 - scipy.stats.poisson(1 / self.cat_separation_distribution.mean() * \ # mean_runtime).pmf(0)) * self.damage_distribution[categ_id].mean() * average_risk_factor) * average_exposure * len(categ_risks) else: incr_expected_profits = -1 # TODO: expected profits should only be returned once the expire_immediately == False case is fixed - #incr_expected_profits = (self.norm_premium - mean_runtime / self.cat_separation_distribution[categ_id].mean() * self.damage_distribution.mean() * average_risk_factor) * average_exposure * len(categ_risks) - + # incr_expected_profits = (self.norm_premium - mean_runtime / self.cat_separation_distribution[categ_id].mean() * self.damage_distribution.mean() * average_risk_factor) * average_exposure * len(categ_risks) + return average_risk_factor, average_exposure, incr_expected_profits - + def evaluate_proportional(self, risks, cash): """Method to evaluate proportional type risks. Accepts: @@ -101,16 +114,18 @@ def evaluate_proportional(self, risks, cash): cash_left_by_category = np.copy(cash) expected_profits = 0 necessary_liquidity = 0 - + var_per_risk_per_categ = np.zeros(self.category_number) - + # compute acceptable risks by category for categ_id in range(self.category_number): # compute number of acceptable risks of this category categ_risks = self.get_categ_risks(risks=risks, categ_id=categ_id) - + if len(categ_risks) > 0: - average_risk_factor, average_exposure, incr_expected_profits = self.compute_expectation(categ_risks=categ_risks, categ_id=categ_id) + average_risk_factor, average_exposure, incr_expected_profits = self.compute_expectation( + categ_risks=categ_risks, categ_id=categ_id + ) else: average_risk_factor = self.init_average_risk_factor average_exposure = self.init_average_exposure @@ -118,15 +133,33 @@ def evaluate_proportional(self, risks, cash): # TODO: expected profits should only be returned once the expire_immediately == False case is fixed expected_profits += incr_expected_profits - + # compute value at risk - var_per_risk = self.getPPF(categ_id=categ_id, tailSize=self.var_tail_prob) * average_risk_factor * average_exposure * self.margin_of_safety - + var_per_risk = ( + self.getPPF(categ_id=categ_id, tailSize=self.var_tail_prob) + * average_risk_factor + * average_exposure + * self.margin_of_safety + ) + # record liquidity requirement and apply margin of safety for liquidity requirement - necessary_liquidity += var_per_risk * self.margin_of_safety * len(categ_risks) + necessary_liquidity += ( + var_per_risk * self.margin_of_safety * len(categ_risks) + ) if isleconfig.verbose: print(self.inaccuracy) - print("RISKMODEL: ", var_per_risk, " = PPF(0.02) * ", average_risk_factor, " * ", average_exposure, " vs. cash: ", cash[categ_id], "TOTAL_RISK_IN_CATEG: ", var_per_risk * len(categ_risks)) + print( + "RISKMODEL: ", + var_per_risk, + " = PPF(0.02) * ", + average_risk_factor, + " * ", + average_exposure, + " vs. cash: ", + cash[categ_id], + "TOTAL_RISK_IN_CATEG: ", + var_per_risk * len(categ_risks), + ) try: acceptable = int(math.floor(cash[categ_id] / var_per_risk)) remaining = acceptable - len(categ_risks) @@ -148,17 +181,28 @@ def evaluate_proportional(self, risks, cash): expected_profits = self.init_profit_estimate * cash[0] else: expected_profits /= necessary_liquidity - + max_cash_by_categ = max(cash_left_by_category) floored_cash_by_categ = cash_left_by_category.copy() floored_cash_by_categ[floored_cash_by_categ < 0] = 0 for categ_id in range(self.category_number): - remaining_acceptable_by_category[categ_id] = math.floor(remaining_acceptable_by_category[categ_id] * pow( - floored_cash_by_categ[categ_id] / max_cash_by_categ, 5)) + remaining_acceptable_by_category[categ_id] = math.floor( + remaining_acceptable_by_category[categ_id] + * pow(floored_cash_by_categ[categ_id] / max_cash_by_categ, 5) + ) if isleconfig.verbose: - print("RISKMODEL returns: ", expected_profits, remaining_acceptable_by_category) + print( + "RISKMODEL returns: ", + expected_profits, + remaining_acceptable_by_category, + ) - return expected_profits, remaining_acceptable_by_category, cash_left_by_category, var_per_risk_per_categ + return ( + expected_profits, + remaining_acceptable_by_category, + cash_left_by_category, + var_per_risk_per_categ, + ) def evaluate_excess_of_loss(self, risks, cash, offered_risk=None): """Method to evaluate excess-of-loss type risks. @@ -176,7 +220,7 @@ def evaluate_excess_of_loss(self, risks, cash, offered_risk=None): offered risk (if applicable) is then calculated (should only be one).""" cash_left_by_categ = np.copy(cash) assert len(cash_left_by_categ) == self.category_number - + # prepare variables additional_required = np.zeros(self.category_number) additional_var_per_categ = np.zeros(self.category_number) @@ -184,32 +228,52 @@ def evaluate_excess_of_loss(self, risks, cash, offered_risk=None): # values at risk and liquidity requirements by category for categ_id in range(self.category_number): categ_risks = self.get_categ_risks(risks=risks, categ_id=categ_id) - + # TODO: allow for different risk distributions for different categories - percentage_value_at_risk = self.getPPF(categ_id=categ_id, tailSize=self.var_tail_prob) - + percentage_value_at_risk = self.getPPF( + categ_id=categ_id, tailSize=self.var_tail_prob + ) + # compute liquidity requirements from existing contracts for risk in categ_risks: - expected_damage = percentage_value_at_risk * risk["value"] * risk["risk_factor"] * self.inaccuracy[categ_id] - expected_claim = min(expected_damage, risk["excess"]) - risk["deductible"] - + expected_damage = ( + percentage_value_at_risk + * risk["value"] + * risk["risk_factor"] + * self.inaccuracy[categ_id] + ) + expected_claim = ( + min(expected_damage, risk["excess"]) - risk["deductible"] + ) + # record liquidity requirement and apply margin of safety for liquidity requirement cash_left_by_categ[categ_id] -= expected_claim * self.margin_of_safety - + # compute additional liquidity requirements from newly offered contract - if (offered_risk is not None) and (offered_risk.get("category") == categ_id): - expected_damage_fraction = percentage_value_at_risk * offered_risk["risk_factor"] * self.inaccuracy[categ_id] - expected_claim_fraction = min(expected_damage_fraction, offered_risk["excess_fraction"]) - offered_risk["deductible_fraction"] + if (offered_risk is not None) and ( + offered_risk.get("category") == categ_id + ): + expected_damage_fraction = ( + percentage_value_at_risk + * offered_risk["risk_factor"] + * self.inaccuracy[categ_id] + ) + expected_claim_fraction = ( + min(expected_damage_fraction, offered_risk["excess_fraction"]) + - offered_risk["deductible_fraction"] + ) expected_claim_total = expected_claim_fraction * offered_risk["value"] - + # record liquidity requirement and apply margin of safety for liquidity requirement - additional_required[categ_id] += expected_claim_total * self.margin_of_safety + additional_required[categ_id] += ( + expected_claim_total * self.margin_of_safety + ) additional_var_per_categ[categ_id] += expected_claim_total - + # Additional value at risk should only occur in one category. Assert that this is the case. - assert sum(additional_var_per_categ > 0) <= 1 + assert sum(additional_var_per_categ > 0) <= 1 var_this_risk = max(additional_var_per_categ) - + return cash_left_by_categ, additional_required, var_this_risk def evaluate(self, risks, cash, offered_risk=None): @@ -237,7 +301,9 @@ def evaluate(self, risks, cash, offered_risk=None): results in two sets of return values being used. These return values are what is used to determine if risks are underwritten or not.""" # ensure that any risk to be considered supplied directly as argument is non-proportional/excess-of-loss - assert (offered_risk is None) or offered_risk.get("insurancetype") == "excess-of-loss" + assert (offered_risk is None) or offered_risk.get( + "insurancetype" + ) == "excess-of-loss" # construct cash_left_by_categ as a sequence, defining remaining liquidity by category if not isinstance(cash, (np.ndarray, list)): @@ -247,24 +313,44 @@ def evaluate(self, risks, cash, offered_risk=None): assert len(cash_left_by_categ) == self.category_number # sort current contracts - el_risks = [risk for risk in risks if risk["insurancetype"] == 'excess-of-loss'] - risks = [risk for risk in risks if risk["insurancetype"] == 'proportional'] + el_risks = [risk for risk in risks if risk["insurancetype"] == "excess-of-loss"] + risks = [risk for risk in risks if risk["insurancetype"] == "proportional"] # compute liquidity requirements and acceptable risks from existing contract if (offered_risk is not None) or (len(el_risks) > 0): - cash_left_by_categ, additional_required, var_this_risk = self.evaluate_excess_of_loss(el_risks, cash_left_by_categ, offered_risk) + cash_left_by_categ, additional_required, var_this_risk = self.evaluate_excess_of_loss( + el_risks, cash_left_by_categ, offered_risk + ) if (offered_risk is None) or (len(risks) > 0): - expected_profits_proportional, remaining_acceptable_by_categ, cash_left_by_categ, var_per_risk_per_categ = self.evaluate_proportional(risks, cash_left_by_categ) + expected_profits_proportional, remaining_acceptable_by_categ, cash_left_by_categ, var_per_risk_per_categ = self.evaluate_proportional( + risks, cash_left_by_categ + ) if offered_risk is None: # return numbers of remaining acceptable risks by category - return expected_profits_proportional, remaining_acceptable_by_categ, cash_left_by_categ, var_per_risk_per_categ, min(cash_left_by_categ) + return ( + expected_profits_proportional, + remaining_acceptable_by_categ, + cash_left_by_categ, + var_per_risk_per_categ, + min(cash_left_by_categ), + ) else: # return boolean value whether the offered excess_of_loss risk can be accepted if isleconfig.verbose: - print("REINSURANCE RISKMODEL", cash, cash_left_by_categ,(cash_left_by_categ - additional_required > 0).all()) + print( + "REINSURANCE RISKMODEL", + cash, + cash_left_by_categ, + (cash_left_by_categ - additional_required > 0).all(), + ) # if not (cash_left_by_categ - additional_required > 0).all(): # pdb.set_trace() - return (cash_left_by_categ - additional_required > 0).all(), cash_left_by_categ, var_this_risk, min(cash_left_by_categ) + return ( + (cash_left_by_categ - additional_required > 0).all(), + cash_left_by_categ, + var_this_risk, + min(cash_left_by_categ), + ) def add_reinsurance(self, categ_id, excess_fraction, deductible_fraction, contract): """Method to add any instance of reinsurance to risk models list of reinsurance contracts, and add damage @@ -276,13 +362,19 @@ def add_reinsurance(self, categ_id, excess_fraction, deductible_fraction, contra deductible_fraction: Type Decimal. contract: Type DataDict. No return values.""" - self.damage_distribution_stack[categ_id].append(self.damage_distribution[categ_id]) + self.damage_distribution_stack[categ_id].append( + self.damage_distribution[categ_id] + ) self.reinsurance_contract_stack[categ_id].append(contract) - self.damage_distribution[categ_id] = ReinsuranceDistWrapper(lower_bound=deductible_fraction, \ - upper_bound=excess_fraction, \ - dist=self.damage_distribution[categ_id]) + self.damage_distribution[categ_id] = ReinsuranceDistWrapper( + lower_bound=deductible_fraction, + upper_bound=excess_fraction, + dist=self.damage_distribution[categ_id], + ) - def delete_reinsurance(self, categ_id, excess_fraction, deductible_fraction, contract): + def delete_reinsurance( + self, categ_id, excess_fraction, deductible_fraction, contract + ): """Method to remove any instance of reinsurance to risk models list of reinsurance contracts, and remove its damage distribution from the stack of damage distributions per category. Only used in the delete_reinsurance method of insurancefirm. @@ -294,5 +386,6 @@ def delete_reinsurance(self, categ_id, excess_fraction, deductible_fraction, con No return values.""" assert self.reinsurance_contract_stack[categ_id][-1] == contract self.reinsurance_contract_stack[categ_id].pop() - self.damage_distribution[categ_id] = self.damage_distribution_stack[categ_id].pop() - + self.damage_distribution[categ_id] = self.damage_distribution_stack[ + categ_id + ].pop() diff --git a/setup.py b/setup.py index f7579e1..6e05b87 100644 --- a/setup.py +++ b/setup.py @@ -23,8 +23,7 @@ from distributiontruncated import TruncatedDistWrapper -class SetupSim(): - +class SetupSim: def __init__(self): self.simulation_parameters = isleconfig.simulation_parameters @@ -33,11 +32,16 @@ def __init__(self): self.max_time = self.simulation_parameters["max_time"] self.no_categories = self.simulation_parameters["no_categories"] - """set distribution""" - self.non_truncated = scipy.stats.pareto(b=2, loc=0, scale=0.25) #It is assumed that the damages of the catastrophes are drawn from a truncated Pareto distribution. - self.damage_distribution = TruncatedDistWrapper(lower_bound=0.25, upper_bound=1., dist=self.non_truncated) - self.cat_separation_distribution = scipy.stats.expon(0, self.simulation_parameters["event_time_mean_separation"]) #It is assumed that the time between catastrophes is exponentially distributed. + self.non_truncated = scipy.stats.pareto( + b=2, loc=0, scale=0.25 + ) # It is assumed that the damages of the catastrophes are drawn from a truncated Pareto distribution. + self.damage_distribution = TruncatedDistWrapper( + lower_bound=0.25, upper_bound=1.0, dist=self.non_truncated + ) + self.cat_separation_distribution = scipy.stats.expon( + 0, self.simulation_parameters["event_time_mean_separation"] + ) # It is assumed that the time between catastrophes is exponentially distributed. """"random seeds""" self.np_seed = [] @@ -45,19 +49,33 @@ def __init__(self): self.general_rc_event_schedule = [] self.general_rc_event_damage = [] - def schedule(self, replications): #This method returns the lists of schedule times and damages for an ensemble of replications of the model. The argument (replications) is the number of replications. + def schedule( + self, replications + ): # This method returns the lists of schedule times and damages for an ensemble of replications of the model. The argument (replications) is the number of replications. - general_rc_event_schedule = [] #In this list will be stored the lists of schedule times of catastrophes for an ensemble of simulations of the model. ([[Schedule 1], [Schedule 2], [Schedule 3],...,[Schedule N]]) - general_rc_event_damage = [] #In this list will be stored the lists of schedule damages of catastrophes for an ensemble of simulations of the model. ([[Schedule 1], [Schedule 2], [Schedule 3],...,[Schedule N]]) + general_rc_event_schedule = ( + [] + ) # In this list will be stored the lists of schedule times of catastrophes for an ensemble of simulations of the model. ([[Schedule 1], [Schedule 2], [Schedule 3],...,[Schedule N]]) + general_rc_event_damage = ( + [] + ) # In this list will be stored the lists of schedule damages of catastrophes for an ensemble of simulations of the model. ([[Schedule 1], [Schedule 2], [Schedule 3],...,[Schedule N]]) for i in range(replications): - rc_event_schedule = [] #In this list will be stored the lists of times when there will be catastrophes for every category of the model during a single run. ([[times for C1],[times for C2],[times for C3],[times for C4]]) - rc_event_damage = [] #In this list will be stored the lists of catastrophe damages for every category of the model during a single run. ([[damages for C1],[damages for C2],[damages for C3],[damages for C4]]) + rc_event_schedule = ( + [] + ) # In this list will be stored the lists of times when there will be catastrophes for every category of the model during a single run. ([[times for C1],[times for C2],[times for C3],[times for C4]]) + rc_event_damage = ( + [] + ) # In this list will be stored the lists of catastrophe damages for every category of the model during a single run. ([[damages for C1],[damages for C2],[damages for C3],[damages for C4]]) for j in range(self.no_categories): - event_schedule = [] #In this list will be stored the times when there will be a catastrophe related to a particular category. - event_damage = [] #In this list will be stored the damages of a catastrophe related to a particular category. + event_schedule = ( + [] + ) # In this list will be stored the times when there will be a catastrophe related to a particular category. + event_damage = ( + [] + ) # In this list will be stored the damages of a catastrophe related to a particular category. total = 0 - while (total < self.max_time): + while total < self.max_time: separation_time = self.cat_separation_distribution.rvs() total += int(math.ceil(separation_time)) if total < self.max_time: @@ -71,17 +89,21 @@ def schedule(self, replications): #This method returns the lists of schedule ti return self.general_rc_event_schedule, self.general_rc_event_damage - def seeds(self, replications): #This method returns the seeds required for an ensemble of replications of the model. The argument (replications) is the number of replications. + def seeds( + self, replications + ): # This method returns the seeds required for an ensemble of replications of the model. The argument (replications) is the number of replications. """draw random variates for random seeds""" for i in range(replications): - np_seed, random_seed = scipy.stats.randint.rvs(0, 2**16 - 1, size=2) + np_seed, random_seed = scipy.stats.randint.rvs(0, 2 ** 16 - 1, size=2) self.np_seed.append(np_seed) self.random_seed.append(random_seed) return self.np_seed, self.random_seed - def store(self, replications): #This method stores in a file the the schedules and random seeds required for an ensemble of replications of the model. The argument (replications) is the number of replications. - #With the information stored it is possible to replicate the entire behavior of the ensemble at a later time. + def store( + self, replications + ): # This method stores in a file the the schedules and random seeds required for an ensemble of replications of the model. The argument (replications) is the number of replications. + # With the information stored it is possible to replicate the entire behavior of the ensemble at a later time. event_schedules = [] for i in range(replications): @@ -97,7 +119,9 @@ def store(self, replications): #This method stores in a file the the schedules """ ensure that logging directory exists""" if not os.path.isdir("data"): - assert not os.path.exists("data"), "./data exists as regular file. This filename is required for the logging and event schedule directory" + assert not os.path.exists( + "data" + ), "./data exists as regular file. This filename is required for the logging and event schedule directory" os.makedirs("data") """Save as both pickle and txt""" @@ -106,15 +130,29 @@ def store(self, replications): #This method stores in a file the the schedules with open("./data/risk_event_schedules.txt", "w") as wfile: for rep_schedule in event_schedules: - wfile.write(str(rep_schedule).replace("\n","").replace("array", "np.array").replace("uint32", "np.uint32") + "\n") - - def obtain_ensemble(self, replications): #This method returns all the information (schedules and seeds) required to run an ensemble of simulations of the model. Since it also stores the information in a file it will be possible to replicate the ensemble at a later time. The argument (replications) is the number of replications. - #This method will be called either form ensemble.py or start.py - [general_rc_event_schedule, general_rc_event_damage] = self.schedule(replications) + wfile.write( + str(rep_schedule) + .replace("\n", "") + .replace("array", "np.array") + .replace("uint32", "np.uint32") + + "\n" + ) + + def obtain_ensemble( + self, replications + ): # This method returns all the information (schedules and seeds) required to run an ensemble of simulations of the model. Since it also stores the information in a file it will be possible to replicate the ensemble at a later time. The argument (replications) is the number of replications. + # This method will be called either form ensemble.py or start.py + [general_rc_event_schedule, general_rc_event_damage] = self.schedule( + replications + ) [np_seeds, random_seeds] = self.seeds(replications) self.store(replications) - return general_rc_event_schedule, general_rc_event_damage, np_seeds, random_seeds - + return ( + general_rc_event_schedule, + general_rc_event_damage, + np_seeds, + random_seeds, + ) diff --git a/start.py b/start.py index 4f05902..0a80017 100644 --- a/start.py +++ b/start.py @@ -19,61 +19,117 @@ """Creates data file for logs if does not exist""" if not os.path.isdir("data"): - assert not os.path.exists("data"), "./data exists as regular file. This filename is required for the logging directory" + assert not os.path.exists( + "data" + ), "./data exists as regular file. This filename is required for the logging directory" os.makedirs("data") -def main(simulation_parameters, rc_event_schedule, rc_event_damage, np_seed, random_seed, save_iter, requested_logs=None): +def main( + simulation_parameters, + rc_event_schedule, + rc_event_damage, + np_seed, + random_seed, + save_iter, + requested_logs=None, +): np.random.seed(np_seed) random.seed(random_seed) """create simulation and world objects""" - simulation_parameters['simulation'] = world = InsuranceSimulation(override_no_riskmodels, replic_ID, simulation_parameters, rc_event_schedule, rc_event_damage) + simulation_parameters["simulation"] = world = InsuranceSimulation( + override_no_riskmodels, + replic_ID, + simulation_parameters, + rc_event_schedule, + rc_event_damage, + ) simulation = world - + """create agents: insurance firms according to number in isleconfig.py, adds them all to the simulation instance""" - insurancefirms_group = simulation.build_agents(InsuranceFirm, 'insurancefirm', parameters=simulation_parameters, - agent_parameters=world.agent_parameters["insurancefirm"]) + insurancefirms_group = simulation.build_agents( + InsuranceFirm, + "insurancefirm", + parameters=simulation_parameters, + agent_parameters=world.agent_parameters["insurancefirm"], + ) insurancefirm_pointers = insurancefirms_group world.accept_agents("insurancefirm", insurancefirm_pointers, insurancefirms_group) """create agents: reinsurance firms according to number in isleconfig.py, adds them all to simulation instance""" - reinsurancefirms_group = simulation.build_agents(ReinsuranceFirm, 'reinsurancefirm', parameters=simulation_parameters, - agent_parameters=world.agent_parameters["reinsurancefirm"]) + reinsurancefirms_group = simulation.build_agents( + ReinsuranceFirm, + "reinsurancefirm", + parameters=simulation_parameters, + agent_parameters=world.agent_parameters["reinsurancefirm"], + ) reinsurancefirm_pointers = reinsurancefirms_group - world.accept_agents("reinsurancefirm", reinsurancefirm_pointers, reinsurancefirms_group) - + world.accept_agents( + "reinsurancefirm", reinsurancefirm_pointers, reinsurancefirms_group + ) + """Time iteration""" for t in range(simulation_parameters["max_time"]): - - "create new agents and accepts them based on probability" # TODO: write method for this; this code block is executed almost identically 4 times + + "create new agents and accepts them based on probability" # TODO: write method for this; this code block is executed almost identically 4 times if world.insurance_firm_market_entry(agent_type="InsuranceFirm"): - parameters = [world.agent_parameters["insurancefirm"][simulation.insurance_entry_index()]] + parameters = [ + world.agent_parameters["insurancefirm"][ + simulation.insurance_entry_index() + ] + ] parameters[0]["id"] = world.get_unique_insurer_id() - new_insurance_firm = simulation.build_agents(InsuranceFirm, 'insurancefirm', parameters=simulation_parameters, - agent_parameters=parameters) + new_insurance_firm = simulation.build_agents( + InsuranceFirm, + "insurancefirm", + parameters=simulation_parameters, + agent_parameters=parameters, + ) insurancefirms_group += new_insurance_firm new_insurancefirm_pointer = new_insurance_firm - world.accept_agents("insurancefirm", new_insurancefirm_pointer, new_insurance_firm, time=t) - + world.accept_agents( + "insurancefirm", new_insurancefirm_pointer, new_insurance_firm, time=t + ) + if world.insurance_firm_market_entry(agent_type="ReinsuranceFirm"): - parameters = [world.agent_parameters["reinsurancefirm"][simulation.reinsurance_entry_index()]] + parameters = [ + world.agent_parameters["reinsurancefirm"][ + simulation.reinsurance_entry_index() + ] + ] parameters[0]["id"] = world.get_unique_reinsurer_id() - new_reinsurance_firm = simulation.build_agents(ReinsuranceFirm, 'reinsurancefirm', parameters=simulation_parameters, - agent_parameters=parameters) + new_reinsurance_firm = simulation.build_agents( + ReinsuranceFirm, + "reinsurancefirm", + parameters=simulation_parameters, + agent_parameters=parameters, + ) reinsurancefirms_group += new_reinsurance_firm new_reinsurancefirm_pointer = new_reinsurance_firm - world.accept_agents("reinsurancefirm", new_reinsurancefirm_pointer, new_reinsurance_firm, time=t) - + world.accept_agents( + "reinsurancefirm", + new_reinsurancefirm_pointer, + new_reinsurance_firm, + time=t, + ) + "iterate simulation" world.iterate(t) - + "log data" world.save_data() - - if t%50 == save_iter: - save_simulation(t, simulation, simulation_parameters, rc_event_schedule, rc_event_damage, exit_now=False) - + + if t % 50 == save_iter: + save_simulation( + t, + simulation, + simulation_parameters, + rc_event_schedule, + rc_event_damage, + exit_now=False, + ) + """finish simulation, write logs""" simulation.finalize() @@ -94,9 +150,11 @@ def save_simulation(t, sim, sim_param, event_schedule, event_damage, exit_now=Fa pickle.dump(d, wfile, protocol=pickle.HIGHEST_PROTOCOL) with open("data/simulation_save.pkl", "br") as rfile: file_contents = rfile.read() - #print("\nSimulation hashes: ", hashlib.sha512(str(d).encode()).hexdigest(), "; ", hashlib.sha512(str(file_contents).encode()).hexdigest()) + # print("\nSimulation hashes: ", hashlib.sha512(str(d).encode()).hexdigest(), "; ", hashlib.sha512(str(file_contents).encode()).hexdigest()) # note that the hash over the dict is for some reason not identical between runs. The hash over the state saved to the file is. - print("\nSimulation hash: ", hashlib.sha512(str(file_contents).encode()).hexdigest()) + print( + "\nSimulation hash: ", hashlib.sha512(str(file_contents).encode()).hexdigest() + ) if exit_now: exit(0) @@ -104,22 +162,52 @@ def save_simulation(t, sim, sim_param, event_schedule, event_damage, exit_now=Fa """main entry point""" if __name__ == "__main__": """ use argparse to handle command line arguments""" - parser = argparse.ArgumentParser(description='Model the Insurance sector') - parser.add_argument("--oneriskmodel", action="store_true", - help="allow overriding the number of riskmodels from the standard config (with 1)") - parser.add_argument("--riskmodels", type=int, choices=[1, 2, 3, 4], - help="allow overriding the number of riskmodels from standard config (with 1 or other numbers)") - parser.add_argument("--replicid", type=int, - help="if replication ID is given, pass this to the simulation so that the risk profile can be restored") - parser.add_argument("--replicating", action="store_true", - help="if this is a simulation run designed to replicate another, override the config file parameter") - parser.add_argument("--randomseed", type=float, help="allow setting of numpy random seed") - parser.add_argument("--foreground", action="store_true", - help="force foreground runs even if replication ID is given (which defaults to background runs)") - parser.add_argument("--shownetwork", action="store_true", help="show reinsurance relations as network") - parser.add_argument("-p", "--showprogress", action="store_true", help="show timesteps") - parser.add_argument("-v", "--verbose", action="store_true", help="more detailed output") - parser.add_argument("--save_iterations", type=int, help="number of iterations to iterate before saving world state") + parser = argparse.ArgumentParser(description="Model the Insurance sector") + parser.add_argument( + "--oneriskmodel", + action="store_true", + help="allow overriding the number of riskmodels from the standard config (with 1)", + ) + parser.add_argument( + "--riskmodels", + type=int, + choices=[1, 2, 3, 4], + help="allow overriding the number of riskmodels from standard config (with 1 or other numbers)", + ) + parser.add_argument( + "--replicid", + type=int, + help="if replication ID is given, pass this to the simulation so that the risk profile can be restored", + ) + parser.add_argument( + "--replicating", + action="store_true", + help="if this is a simulation run designed to replicate another, override the config file parameter", + ) + parser.add_argument( + "--randomseed", type=float, help="allow setting of numpy random seed" + ) + parser.add_argument( + "--foreground", + action="store_true", + help="force foreground runs even if replication ID is given (which defaults to background runs)", + ) + parser.add_argument( + "--shownetwork", + action="store_true", + help="show reinsurance relations as network", + ) + parser.add_argument( + "-p", "--showprogress", action="store_true", help="show timesteps" + ) + parser.add_argument( + "-v", "--verbose", action="store_true", help="more detailed output" + ) + parser.add_argument( + "--save_iterations", + type=int, + help="number of iterations to iterate before saving world state", + ) args = parser.parse_args() if args.oneriskmodel: @@ -127,11 +215,13 @@ def save_simulation(t, sim, sim_param, event_schedule, event_damage, exit_now=Fa override_no_riskmodels = 1 if args.riskmodels: override_no_riskmodels = args.riskmodels - if args.replicid is not None: # TODO: this is broken, must be fixed or removed + if args.replicid is not None: # TODO: this is broken, must be fixed or removed replic_ID = args.replicid if args.replicating: isleconfig.replicating = True - assert replic_ID is not None, "Error: Replication requires a replication ID to identify run to be replicated" + assert ( + replic_ID is not None + ), "Error: Replication requires a replication ID to identify run to be replicated" if args.randomseed: randomseed = args.randomseed seed = int(randomseed) @@ -143,8 +233,9 @@ def save_simulation(t, sim, sim_param, event_schedule, event_damage, exit_now=Fa if args.shownetwork: isleconfig.show_network = True """Option requires reloading of InsuranceSimulation so that modules to show network can be loaded. - # TODO: change all module imports of the form "from module import class" to "import module". """ + # TODO: change all module imports of the form "from module import class" to "import module". """ import insurancesimulation + importlib.reload(insurancesimulation) from insurancesimulation import InsuranceSimulation if args.showprogress: @@ -155,19 +246,36 @@ def save_simulation(t, sim, sim_param, event_schedule, event_damage, exit_now=Fa save_iter = args.save_iterations else: save_iter = 20 - + from setup import SetupSim - setup = SetupSim() #Here the setup for the simulation is done. - [general_rc_event_schedule, general_rc_event_damage, np_seeds, random_seeds] = setup.obtain_ensemble(1) #Only one ensemble. This part will only be run locally (laptop). - log = main(simulation_parameters, general_rc_event_schedule[0], general_rc_event_damage[0], np_seeds[0], random_seeds[0], save_iter) - + setup = SetupSim() # Here the setup for the simulation is done. + [ + general_rc_event_schedule, + general_rc_event_damage, + np_seeds, + random_seeds, + ] = setup.obtain_ensemble( + 1 + ) # Only one ensemble. This part will only be run locally (laptop). + + log = main( + simulation_parameters, + general_rc_event_schedule[0], + general_rc_event_damage[0], + np_seeds[0], + random_seeds[0], + save_iter, + ) + """ Restore the log at the end of the single simulation run for saving and for potential further study """ - is_background = (not isleconfig.force_foreground) and (isleconfig.replicating or (replic_ID in locals())) + is_background = (not isleconfig.force_foreground) and ( + isleconfig.replicating or (replic_ID in locals()) + ) L = logger.Logger() L.restore_logger_object(list(log)) L.save_log(is_background) - + """ Obtain calibration score """ CS = calibrationscore.CalibrationScore(L) score = CS.test_all() diff --git a/visualisation.py b/visualisation.py index 2ce6814..8790d02 100644 --- a/visualisation.py +++ b/visualisation.py @@ -5,9 +5,18 @@ import argparse - class TimeSeries(object): - def __init__(self, series_list, title="",xlabel="Time", colour='k', axlst=None, fig=None, percentiles=None, alpha=0.7): + def __init__( + self, + series_list, + title="", + xlabel="Time", + colour="k", + axlst=None, + fig=None, + percentiles=None, + alpha=0.7, + ): self.series_list = series_list self.size = len(series_list) self.xlabel = xlabel @@ -15,22 +24,32 @@ def __init__(self, series_list, title="",xlabel="Time", colour='k', axlst=None, self.alpha = alpha self.percentiles = percentiles self.title = title - self.timesteps = [t for t in range(len(series_list[0][0]))] # assume all data series are the same size + self.timesteps = [ + t for t in range(len(series_list[0][0])) + ] # assume all data series are the same size if axlst is not None and fig is not None: self.axlst = axlst self.fig = fig else: - self.fig, self.axlst = plt.subplots(self.size,sharex=True) + self.fig, self.axlst = plt.subplots(self.size, sharex=True) - #self.plot() # we create the object when we want the plot so call plot() in the constructor + # self.plot() # we create the object when we want the plot so call plot() in the constructor def plot(self): - for i, (series, series_label, fill_lower, fill_upper) in enumerate(self.series_list): - self.axlst[i].plot(self.timesteps, series,color=self.colour) + for i, (series, series_label, fill_lower, fill_upper) in enumerate( + self.series_list + ): + self.axlst[i].plot(self.timesteps, series, color=self.colour) self.axlst[i].set_ylabel(series_label) if fill_lower is not None and fill_upper is not None: - self.axlst[i].fill_between(self.timesteps, fill_lower, fill_upper, color=self.colour, alpha=self.alpha) - self.axlst[self.size-1].set_xlabel(self.xlabel) + self.axlst[i].fill_between( + self.timesteps, + fill_lower, + fill_upper, + color=self.colour, + alpha=self.alpha, + ) + self.axlst[self.size - 1].set_xlabel(self.xlabel) self.fig.suptitle(self.title) return self.fig, self.axlst @@ -38,20 +57,24 @@ def save(self, filename): self.fig.savefig("{filename}".format(filename=filename)) return + class InsuranceFirmAnimation(object): - '''class takes in a run of insurance data and produces animations ''' + """class takes in a run of insurance data and produces animations """ + def __init__(self, data): self.data = data self.fig, self.ax = plt.subplots() self.stream = self.data_stream() - self.ani = animation.FuncAnimation(self.fig, self.update, repeat=False, interval=100,) - #init_func=self.setup_plot) + self.ani = animation.FuncAnimation( + self.fig, self.update, repeat=False, interval=100 + ) + # init_func=self.setup_plot) def setup_plot(self): # initial drawing of the plot - casharr,idarr = next(self.stream) - self.pie = self.ax.pie(casharr, labels=idarr,autopct='%1.0f%%') - return self.pie, + casharr, idarr = next(self.stream) + self.pie = self.ax.pie(casharr, labels=idarr, autopct="%1.0f%%") + return (self.pie,) def data_stream(self): # unpack data in a format ready for update() @@ -62,59 +85,81 @@ def data_stream(self): if operational: casharr.append(cash) idarr.append(id) - yield casharr,idarr + yield casharr, idarr def update(self, i): # clear plot and redraw self.ax.clear() - self.ax.axis('equal') - casharr,idarr = next(self.stream) - self.pie = self.ax.pie(casharr, labels=idarr,autopct='%1.0f%%') - self.ax.set_title("Timestep : {:,.0f} | Total cash : {:,.0f}".format(i,sum(casharr))) - return self.pie, + self.ax.axis("equal") + casharr, idarr = next(self.stream) + self.pie = self.ax.pie(casharr, labels=idarr, autopct="%1.0f%%") + self.ax.set_title( + "Timestep : {:,.0f} | Total cash : {:,.0f}".format(i, sum(casharr)) + ) + return (self.pie,) - def save(self,filename): - self.ani.save(filename, writer='ffmpeg', dpi=80) + def save(self, filename): + self.ani.save(filename, writer="ffmpeg", dpi=80) def show(self): plt.show() + class visualisation(object): def __init__(self, history_logs_list): self.history_logs_list = history_logs_list # unused data in history_logs - #self.excess_capital = history_logs['total_excess_capital'] - #self.reinexcess_capital = history_logs['total_reinexcess_capital'] - #self.diffvar = history_logs['market_diffvar'] - #self.cumulative_bankruptcies = history_logs['cumulative_bankruptcies'] - #self.cumulative_unrecovered_claims = history_logs['cumulative_unrecovered_claims'] + # self.excess_capital = history_logs['total_excess_capital'] + # self.reinexcess_capital = history_logs['total_reinexcess_capital'] + # self.diffvar = history_logs['market_diffvar'] + # self.cumulative_bankruptcies = history_logs['cumulative_bankruptcies'] + # self.cumulative_unrecovered_claims = history_logs['cumulative_unrecovered_claims'] return def insurer_pie_animation(self, run=0): data = self.history_logs_list[run] - insurance_cash = np.array(data['insurance_firms_cash']) + insurance_cash = np.array(data["insurance_firms_cash"]) self.ins_pie_anim = InsuranceFirmAnimation(insurance_cash) return self.ins_pie_anim def reinsurer_pie_animation(self, run=0): data = self.history_logs_list[run] - reinsurance_cash = np.array(data['reinsurance_firms_cash']) + reinsurance_cash = np.array(data["reinsurance_firms_cash"]) self.reins_pie_anim = InsuranceFirmAnimation(reinsurance_cash) return self.reins_pie_anim - def insurer_time_series(self, runs=None, axlst=None, fig=None, title="Insurer", colour='black', percentiles=[25,75]): + def insurer_time_series( + self, + runs=None, + axlst=None, + fig=None, + title="Insurer", + colour="black", + percentiles=[25, 75], + ): # runs should be a list of the indexes you want included in the ensemble for consideration if runs: data = [self.history_logs_list[x] for x in runs] else: data = self.history_logs_list - + # Take the element-wise means/medians of the ensemble set (axis=0) - contracts_agg = [history_logs['total_contracts'] for history_logs in self.history_logs_list] - profitslosses_agg = [history_logs['total_profitslosses'] for history_logs in self.history_logs_list] - operational_agg = [history_logs['total_operational'] for history_logs in self.history_logs_list] - cash_agg = [history_logs['total_cash'] for history_logs in self.history_logs_list] - premium_agg = [history_logs['market_premium'] for history_logs in self.history_logs_list] + contracts_agg = [ + history_logs["total_contracts"] for history_logs in self.history_logs_list + ] + profitslosses_agg = [ + history_logs["total_profitslosses"] + for history_logs in self.history_logs_list + ] + operational_agg = [ + history_logs["total_operational"] for history_logs in self.history_logs_list + ] + cash_agg = [ + history_logs["total_cash"] for history_logs in self.history_logs_list + ] + premium_agg = [ + history_logs["market_premium"] for history_logs in self.history_logs_list + ] contracts = np.mean(contracts_agg, axis=0) profitslosses = np.mean(profitslosses_agg, axis=0) @@ -122,16 +167,56 @@ def insurer_time_series(self, runs=None, axlst=None, fig=None, title="Insurer", cash = np.median(cash_agg, axis=0) premium = np.median(premium_agg, axis=0) - self.ins_time_series = TimeSeries([ - (contracts, 'Contracts', np.percentile(contracts_agg,percentiles[0], axis=0), np.percentile(contracts_agg, percentiles[1], axis=0)), - (profitslosses, 'Profitslosses', np.percentile(profitslosses_agg,percentiles[0], axis=0), np.percentile(profitslosses_agg, percentiles[1], axis=0)), - (operational, 'Operational', np.percentile(operational_agg,percentiles[0], axis=0), np.percentile(operational_agg, percentiles[1], axis=0)), - (cash, 'Cash', np.percentile(cash_agg,percentiles[0], axis=0), np.percentile(cash_agg, percentiles[1], axis=0)), - (premium, "Premium", np.percentile(premium_agg,percentiles[0], axis=0), np.percentile(premium_agg, percentiles[1], axis=0)), - ],title=title, xlabel = "Time", axlst=axlst, fig=fig, colour=colour).plot() + self.ins_time_series = TimeSeries( + [ + ( + contracts, + "Contracts", + np.percentile(contracts_agg, percentiles[0], axis=0), + np.percentile(contracts_agg, percentiles[1], axis=0), + ), + ( + profitslosses, + "Profitslosses", + np.percentile(profitslosses_agg, percentiles[0], axis=0), + np.percentile(profitslosses_agg, percentiles[1], axis=0), + ), + ( + operational, + "Operational", + np.percentile(operational_agg, percentiles[0], axis=0), + np.percentile(operational_agg, percentiles[1], axis=0), + ), + ( + cash, + "Cash", + np.percentile(cash_agg, percentiles[0], axis=0), + np.percentile(cash_agg, percentiles[1], axis=0), + ), + ( + premium, + "Premium", + np.percentile(premium_agg, percentiles[0], axis=0), + np.percentile(premium_agg, percentiles[1], axis=0), + ), + ], + title=title, + xlabel="Time", + axlst=axlst, + fig=fig, + colour=colour, + ).plot() return self.ins_time_series - def reinsurer_time_series(self, runs=None, axlst=None, fig=None, title="Reinsurer", colour='black', percentiles=[25,75]): + def reinsurer_time_series( + self, + runs=None, + axlst=None, + fig=None, + title="Reinsurer", + colour="black", + percentiles=[25, 75], + ): # runs should be a list of the indexes you want included in the ensemble for consideration if runs: data = [self.history_logs_list[x] for x in runs] @@ -139,11 +224,25 @@ def reinsurer_time_series(self, runs=None, axlst=None, fig=None, title="Reinsure data = self.history_logs_list # Take the element-wise means/medians of the ensemble set (axis=0) - reincontracts_agg = [history_logs['total_reincontracts'] for history_logs in self.history_logs_list] - reinprofitslosses_agg = [history_logs['total_reinprofitslosses'] for history_logs in self.history_logs_list] - reinoperational_agg = [history_logs['total_reinoperational'] for history_logs in self.history_logs_list] - reincash_agg = [history_logs['total_reincash'] for history_logs in self.history_logs_list] - catbonds_number_agg = [history_logs['total_catbondsoperational'] for history_logs in self.history_logs_list] + reincontracts_agg = [ + history_logs["total_reincontracts"] + for history_logs in self.history_logs_list + ] + reinprofitslosses_agg = [ + history_logs["total_reinprofitslosses"] + for history_logs in self.history_logs_list + ] + reinoperational_agg = [ + history_logs["total_reinoperational"] + for history_logs in self.history_logs_list + ] + reincash_agg = [ + history_logs["total_reincash"] for history_logs in self.history_logs_list + ] + catbonds_number_agg = [ + history_logs["total_catbondsoperational"] + for history_logs in self.history_logs_list + ] reincontracts = np.mean(reincontracts_agg, axis=0) reinprofitslosses = np.mean(reinprofitslosses_agg, axis=0) @@ -151,73 +250,169 @@ def reinsurer_time_series(self, runs=None, axlst=None, fig=None, title="Reinsure reincash = np.median(reincash_agg, axis=0) catbonds_number = np.median(catbonds_number_agg, axis=0) - self.reins_time_series = TimeSeries([ - (reincontracts, 'Contracts', np.percentile(reincontracts_agg,percentiles[0], axis=0), np.percentile(reincontracts_agg, percentiles[1], axis=0)), - (reinprofitslosses, 'Profitslosses', np.percentile(reinprofitslosses_agg,percentiles[0], axis=0), np.percentile(reinprofitslosses_agg, percentiles[1], axis=0)), - (reinoperational, 'Operational', np.percentile(reinoperational_agg,percentiles[0], axis=0), np.percentile(reinoperational_agg, percentiles[1], axis=0)), - (reincash, 'Cash', np.percentile(reincash_agg,percentiles[0], axis=0), np.percentile(reincash_agg, percentiles[1], axis=0)), - (catbonds_number, "Activate Cat Bonds", np.percentile(catbonds_number_agg,percentiles[0], axis=0), np.percentile(catbonds_number_agg, percentiles[1], axis=0)), - ],title= title, xlabel = "Time", axlst=axlst, fig=fig, colour=colour).plot() + self.reins_time_series = TimeSeries( + [ + ( + reincontracts, + "Contracts", + np.percentile(reincontracts_agg, percentiles[0], axis=0), + np.percentile(reincontracts_agg, percentiles[1], axis=0), + ), + ( + reinprofitslosses, + "Profitslosses", + np.percentile(reinprofitslosses_agg, percentiles[0], axis=0), + np.percentile(reinprofitslosses_agg, percentiles[1], axis=0), + ), + ( + reinoperational, + "Operational", + np.percentile(reinoperational_agg, percentiles[0], axis=0), + np.percentile(reinoperational_agg, percentiles[1], axis=0), + ), + ( + reincash, + "Cash", + np.percentile(reincash_agg, percentiles[0], axis=0), + np.percentile(reincash_agg, percentiles[1], axis=0), + ), + ( + catbonds_number, + "Activate Cat Bonds", + np.percentile(catbonds_number_agg, percentiles[0], axis=0), + np.percentile(catbonds_number_agg, percentiles[1], axis=0), + ), + ], + title=title, + xlabel="Time", + axlst=axlst, + fig=fig, + colour=colour, + ).plot() return self.reins_time_series def metaplotter_timescale(self): # Take the element-wise means/medians of the ensemble set (axis=0) - contracts = np.mean([history_logs['total_contracts'] for history_logs in self.history_logs_list],axis=0) - profitslosses = np.mean([history_logs['total_profitslosses'] for history_logs in self.history_logs_list],axis=0) - operational = np.median([history_logs['total_operational'] for history_logs in self.history_logs_list],axis=0) - cash = np.median([history_logs['total_cash'] for history_logs in self.history_logs_list],axis=0) - premium = np.median([history_logs['market_premium'] for history_logs in self.history_logs_list],axis=0) - reincontracts = np.mean([history_logs['total_reincontracts'] for history_logs in self.history_logs_list],axis=0) - reinprofitslosses = np.mean([history_logs['total_reinprofitslosses'] for history_logs in self.history_logs_list],axis=0) - reinoperational = np.median([history_logs['total_reinoperational'] for history_logs in self.history_logs_list],axis=0) - reincash = np.median([history_logs['total_reincash'] for history_logs in self.history_logs_list],axis=0) - catbonds_number = np.median([history_logs['total_catbondsoperational'] for history_logs in self.history_logs_list],axis=0) + contracts = np.mean( + [ + history_logs["total_contracts"] + for history_logs in self.history_logs_list + ], + axis=0, + ) + profitslosses = np.mean( + [ + history_logs["total_profitslosses"] + for history_logs in self.history_logs_list + ], + axis=0, + ) + operational = np.median( + [ + history_logs["total_operational"] + for history_logs in self.history_logs_list + ], + axis=0, + ) + cash = np.median( + [history_logs["total_cash"] for history_logs in self.history_logs_list], + axis=0, + ) + premium = np.median( + [history_logs["market_premium"] for history_logs in self.history_logs_list], + axis=0, + ) + reincontracts = np.mean( + [ + history_logs["total_reincontracts"] + for history_logs in self.history_logs_list + ], + axis=0, + ) + reinprofitslosses = np.mean( + [ + history_logs["total_reinprofitslosses"] + for history_logs in self.history_logs_list + ], + axis=0, + ) + reinoperational = np.median( + [ + history_logs["total_reinoperational"] + for history_logs in self.history_logs_list + ], + axis=0, + ) + reincash = np.median( + [history_logs["total_reincash"] for history_logs in self.history_logs_list], + axis=0, + ) + catbonds_number = np.median( + [ + history_logs["total_catbondsoperational"] + for history_logs in self.history_logs_list + ], + axis=0, + ) return def show(self): plt.show() return + class compare_riskmodels(object): - def __init__(self,vis_list, colour_list): + def __init__(self, vis_list, colour_list): # take in list of visualisation objects and call their plot methods self.vis_list = vis_list self.colour_list = colour_list - - def create_insurer_timeseries(self, fig=None, axlst=None, percentiles=[25,75]): + + def create_insurer_timeseries(self, fig=None, axlst=None, percentiles=[25, 75]): # create the time series for each object in turn and superpose them? fig = axlst = None - for vis,colour in zip(self.vis_list, self.colour_list): - (fig, axlst) = vis.insurer_time_series(fig=fig, axlst=axlst, colour=colour, percentiles=percentiles) + for vis, colour in zip(self.vis_list, self.colour_list): + (fig, axlst) = vis.insurer_time_series( + fig=fig, axlst=axlst, colour=colour, percentiles=percentiles + ) - def create_reinsurer_timeseries(self, fig=None, axlst=None, percentiles=[25,75]): + def create_reinsurer_timeseries(self, fig=None, axlst=None, percentiles=[25, 75]): # create the time series for each object in turn and superpose them? fig = axlst = None - for vis,colour in zip(self.vis_list, self.colour_list): - (fig, axlst) = vis.reinsurer_time_series(fig=fig, axlst=axlst, colour=colour, percentiles=percentiles) + for vis, colour in zip(self.vis_list, self.colour_list): + (fig, axlst) = vis.reinsurer_time_series( + fig=fig, axlst=axlst, colour=colour, percentiles=percentiles + ) def show(self): plt.show() + def save(self): # logic to save plots pass - -if __name__ == "__main__": +if __name__ == "__main__": + # use argparse to handle command line arguments - parser = argparse.ArgumentParser(description='Model the Insurance sector') - parser.add_argument("--single", action="store_true", help="plot time series of a single run of the insurance model") - parser.add_argument("--comparison", action="store_true", help="plot the result of an ensemble of replicatons of the insurance model") + parser = argparse.ArgumentParser(description="Model the Insurance sector") + parser.add_argument( + "--single", + action="store_true", + help="plot time series of a single run of the insurance model", + ) + parser.add_argument( + "--comparison", + action="store_true", + help="plot the result of an ensemble of replicatons of the insurance model", + ) args = parser.parse_args() - if args.single: # load in data from the history_logs dictionarywith open("data/history_logs.dat","r") as rfile: - with open("data/history_logs.dat","r") as rfile: - history_logs_list = [eval(k) for k in rfile] # one dict on each line + with open("data/history_logs.dat", "r") as rfile: + history_logs_list = [eval(k) for k in rfile] # one dict on each line # first create visualisation object, then create graph/animation objects as necessary vis = visualisation(history_logs_list) vis.insurer_pie_animation() @@ -227,26 +422,27 @@ def save(self): vis.show() N = len(history_logs_list) - if args.comparison: # for each run, generate an animation and time series for insurer and reinsurer # TODO: provide some way for these to be lined up nicely rather than having to manually arrange screen - #for i in range(N): + # for i in range(N): # vis.insurer_pie_animation(run=i) # vis.insurer_time_series(runs=[i]) # vis.reinsurer_pie_animation(run=i) # vis.reinsurer_time_series(runs=[i]) # vis.show() vis_list = [] - filenames = ["./data/"+x+"_history_logs.dat" for x in ["one","two","three","four"]] + filenames = [ + "./data/" + x + "_history_logs.dat" for x in ["one", "two", "three", "four"] + ] for filename in filenames: - with open(filename,'r') as rfile: - history_logs_list = [eval(k) for k in rfile] # one dict on each line + with open(filename, "r") as rfile: + history_logs_list = [eval(k) for k in rfile] # one dict on each line vis_list.append(visualisation(history_logs_list)) - colour_list = ['blue', 'yellow', 'red', 'green'] + colour_list = ["blue", "yellow", "red", "green"] cmp_rsk = compare_riskmodels(vis_list, colour_list) - cmp_rsk.create_insurer_timeseries(percentiles=[10,90]) - cmp_rsk.create_reinsurer_timeseries(percentiles=[10,90]) + cmp_rsk.create_insurer_timeseries(percentiles=[10, 90]) + cmp_rsk.create_reinsurer_timeseries(percentiles=[10, 90]) cmp_rsk.show() diff --git a/visualization_network.py b/visualization_network.py index b0497d1..cfe2d7a 100644 --- a/visualization_network.py +++ b/visualization_network.py @@ -2,32 +2,43 @@ import matplotlib.pyplot as plt import numpy as np -class ReinsuranceNetwork(): + +class ReinsuranceNetwork: def __init__(self): """Initialising method for ReinsuranceNetwork. No accepted values. This created the figure that the network will be displayed on so only called once, and only if show_network is True.""" - self.figure = plt.figure(num=None, figsize=(10, 8), dpi=100, facecolor='w', edgecolor='k') + self.figure = plt.figure( + num=None, figsize=(10, 8), dpi=100, facecolor="w", edgecolor="k" + ) def compute_measures(self): """Method to obtain the network distribution and print it. No accepted values. No return values.""" - #degrees = self.network.degree() + # degrees = self.network.degree() degree_distr = dict(self.network.degree()).values() in_degree_distr = dict(self.network.in_degree()).values() out_degree_distr = dict(self.network.out_degree()).values() is_connected = nx.is_weakly_connected(self.network) - #is_connected = nx.is_strongly_connected(self.network) # must always be False + # is_connected = nx.is_strongly_connected(self.network) # must always be False try: node_centralities = nx.eigenvector_centrality(self.network) except: node_centralities = nx.betweenness_centrality(self.network) # TODO: and more, choose more meaningful ones... - - print("Graph is connected: ", is_connected, "\nIn degrees ", in_degree_distr, "\nOut degrees", out_degree_distr, \ - "\nCentralities", node_centralities) + + print( + "Graph is connected: ", + is_connected, + "\nIn degrees ", + in_degree_distr, + "\nOut degrees", + out_degree_distr, + "\nCentralities", + node_centralities, + ) def update(self, insurancefirms, reinsurancefirms, catbonds): """Method to update the network. @@ -45,7 +56,11 @@ def update(self, insurancefirms, reinsurancefirms, catbonds): """obtain lists of operational entities""" op_entities = {} self.num_entities = {} - for firmtype, firmlist in [("insurers", self.insurancefirms), ("reinsurers", self.reinsurancefirms), ("catbonds", self.catbonds)]: + for firmtype, firmlist in [ + ("insurers", self.insurancefirms), + ("reinsurers", self.reinsurancefirms), + ("catbonds", self.catbonds), + ]: op_firmtype = [firm for firm in firmlist if firm.operational] op_entities[firmtype] = op_firmtype self.num_entities[firmtype] = len(op_firmtype) @@ -53,14 +68,20 @@ def update(self, insurancefirms, reinsurancefirms, catbonds): self.network_size = sum(self.num_entities.values()) """Create weighted adjacency matrix and category edge labels""" - weights_matrix = np.zeros(self.network_size ** 2).reshape(self.network_size, self.network_size) + weights_matrix = np.zeros(self.network_size ** 2).reshape( + self.network_size, self.network_size + ) self.edge_labels = {} - for idx_to, firm in enumerate(op_entities["insurers"] + op_entities["reinsurers"]): + for idx_to, firm in enumerate( + op_entities["insurers"] + op_entities["reinsurers"] + ): eolrs = firm.get_excess_of_loss_reinsurance() for eolr in eolrs: # pdb.set_trace() try: - idx_from = self.num_entities["insurers"] + (op_entities["reinsurers"] + op_entities["catbonds"]).index(eolr["reinsurer"]) + idx_from = self.num_entities["insurers"] + ( + op_entities["reinsurers"] + op_entities["catbonds"] + ).index(eolr["reinsurer"]) weights_matrix[idx_from][idx_to] = eolr["value"] self.edge_labels[idx_to, idx_from] = eolr["category"] except ValueError: @@ -70,8 +91,12 @@ def update(self, insurancefirms, reinsurancefirms, catbonds): adj_matrix = np.sign(weights_matrix) """define network""" - self.network = nx.from_numpy_array(weights_matrix, create_using=nx.DiGraph()) # weighted - self.network_unweighted = nx.from_numpy_array(adj_matrix, create_using=nx.DiGraph()) # unweighted + self.network = nx.from_numpy_array( + weights_matrix, create_using=nx.DiGraph() + ) # weighted + self.network_unweighted = nx.from_numpy_array( + adj_matrix, create_using=nx.DiGraph() + ) # unweighted def visualize(self): """Method to add the network to the figure initialised in __init__. @@ -81,12 +106,23 @@ def visualize(self): corresponding to the category being reinsured, and adds a legend to indicate which node is insurer, reinsurer, or CatBond. This method allows the figure to be updated without a new figure being created or stopping the program.""" - plt.ion() # Turns on interactive graph mode. + plt.ion() # Turns on interactive graph mode. firmtypes = np.ones(self.network_size) - firmtypes[self.num_entities["insurers"]:self.num_entities["insurers"]+self.num_entities["reinsurers"]] = 0.5 - firmtypes[self.num_entities["insurers"]+self.num_entities["reinsurers"]:] = 1.3 - print("Number of insurers: %i, Number of Reinsurers: %i, CatBonds: %i" - % (self.num_entities["insurers"], self.num_entities["reinsurers"], self.num_entities['catbonds'])) + firmtypes[ + self.num_entities["insurers"] : self.num_entities["insurers"] + + self.num_entities["reinsurers"] + ] = 0.5 + firmtypes[ + self.num_entities["insurers"] + self.num_entities["reinsurers"] : + ] = 1.3 + print( + "Number of insurers: %i, Number of Reinsurers: %i, CatBonds: %i" + % ( + self.num_entities["insurers"], + self.num_entities["reinsurers"], + self.num_entities["catbonds"], + ) + ) # Either this or below create a network, this one has id's but no key. # pos = nx.spring_layout(self.network_unweighted) @@ -95,16 +131,53 @@ def visualize(self): "Draw Network" pos = nx.spring_layout(self.network_unweighted) - nx.draw_networkx_nodes(self.network_unweighted, pos, list(range(self.num_entities["insurers"])), - node_color='b', node_size=50, alpha=0.9, label='Insurer') - nx.draw_networkx_nodes(self.network_unweighted, pos, list(range(self.num_entities["insurers"], self.num_entities["insurers"]+self.num_entities["reinsurers"])), - node_color='r', node_size=50, alpha=0.9, label='Reinsurer') - nx.draw_networkx_nodes(self.network_unweighted, pos, list(range(self.num_entities["insurers"] + self.num_entities["reinsurers"], self.num_entities["insurers"] + self.num_entities["reinsurers"] + self.num_entities['catbonds'])), - node_color='g', node_size=50, alpha=0.9, label='CatBond') - nx.draw_networkx_edges(self.network_unweighted, pos, width=1.0, alpha=0.5, node_size=50) - nx.draw_networkx_edge_labels(self.network_unweighted, pos, self.edge_labels, font_size=5) - plt.legend(scatterpoints=1, loc='upper right') - plt.axis('off') + nx.draw_networkx_nodes( + self.network_unweighted, + pos, + list(range(self.num_entities["insurers"])), + node_color="b", + node_size=50, + alpha=0.9, + label="Insurer", + ) + nx.draw_networkx_nodes( + self.network_unweighted, + pos, + list( + range( + self.num_entities["insurers"], + self.num_entities["insurers"] + self.num_entities["reinsurers"], + ) + ), + node_color="r", + node_size=50, + alpha=0.9, + label="Reinsurer", + ) + nx.draw_networkx_nodes( + self.network_unweighted, + pos, + list( + range( + self.num_entities["insurers"] + self.num_entities["reinsurers"], + self.num_entities["insurers"] + + self.num_entities["reinsurers"] + + self.num_entities["catbonds"], + ) + ), + node_color="g", + node_size=50, + alpha=0.9, + label="CatBond", + ) + nx.draw_networkx_edges( + self.network_unweighted, pos, width=1.0, alpha=0.5, node_size=50 + ) + nx.draw_networkx_edge_labels( + self.network_unweighted, pos, self.edge_labels, font_size=5 + ) + plt.legend(scatterpoints=1, loc="upper right") + plt.axis("off") plt.show() """Update figure""" From d8c22ad4e81a31826cf91eac2e6bca17260c09b7 Mon Sep 17 00:00:00 2001 From: Chris Hughes Date: Fri, 12 Jul 2019 14:34:00 +0100 Subject: [PATCH 034/125] Cleanup after merge --- catbond.py | 7 ++++--- ensemble.py | 1 + insurancesimulation.py | 9 +++++---- isleconfig.py | 10 +++++----- metainsuranceorg.py | 11 ++++------- riskmodel.py | 2 +- 6 files changed, 20 insertions(+), 20 deletions(-) diff --git a/catbond.py b/catbond.py index 1e0ddfb..8d55262 100644 --- a/catbond.py +++ b/catbond.py @@ -2,10 +2,10 @@ from metainsuranceorg import MetaInsuranceOrg - class CatBond(MetaInsuranceOrg): def __init__( - self, simulation, per_period_premium, owner): + self, simulation, per_period_premium, owner, interest_rate=0 + ): """Initialising methods. Accepts: simulation: Type class @@ -36,7 +36,8 @@ def iterate(self, time): No return values For each time iteration this is called from insurancesimulation to perform duties: interest payments, pay obligations, mature the contract if ended, make payments.""" - self.simulation.bank.award_interest(self, self.cash) + # QUERY: Shouldn't the interest on the cat bond be paid by the issuer, not the bank/market? + self.obtain_yield(time) self.effect_payments(time) if isleconfig.verbose: print( diff --git a/ensemble.py b/ensemble.py index 65423b3..8c53dd0 100644 --- a/ensemble.py +++ b/ensemble.py @@ -5,6 +5,7 @@ import copy import os +# noinspection PyUnresolvedReferences from sandman2.api import operation, Session import isleconfig diff --git a/insurancesimulation.py b/insurancesimulation.py index 251fe36..ca5cd87 100644 --- a/insurancesimulation.py +++ b/insurancesimulation.py @@ -1,4 +1,5 @@ from distributiontruncated import TruncatedDistWrapper +import visualization_network import numpy as np import scipy.stats import math @@ -165,7 +166,7 @@ def __init__( self.risks_counter[item["category"]] + 1 ) - self.inaccuracy = self.get_all_riskmodel_combinations( + self.inaccuracy = self._get_all_riskmodel_combinations( self.simulation_parameters["no_categories"], self.simulation_parameters["riskmodel_inaccuracy_parameter"], ) @@ -408,12 +409,12 @@ def iterate(self, t): # adjust market premiums sum_capital = sum([agent.get_cash() for agent in self.insurancefirms]) - self.adjust_market_premium(capital=sum_capital) + self._adjust_market_premium(capital=sum_capital) sum_capital = sum([agent.get_cash() for agent in self.reinsurancefirms]) - self.adjust_reinsurance_market_premium(capital=sum_capital) + self._adjust_reinsurance_market_premium(capital=sum_capital) # Pay obligations - self.effect_payments(t) + self._effect_payments(t) # identify perils and effect claims for categ_id in range(len(self.rc_event_schedule)): diff --git a/isleconfig.py b/isleconfig.py index d000f7b..6166f07 100644 --- a/isleconfig.py +++ b/isleconfig.py @@ -10,9 +10,9 @@ simulation_parameters = { "no_categories": 4, - "no_insurancefirms": 200, - "no_reinsurancefirms": 5, - "no_riskmodels": 3, + "no_insurancefirms": 20, + "no_reinsurancefirms": 4, + "no_riskmodels": 2, # values >=1; inaccuracy higher with higher values "riskmodel_inaccuracy_parameter": 2, # values >=1; factor of additional liquidity beyond value at risk @@ -28,7 +28,7 @@ "mean_contract_runtime": 12, "contract_runtime_halfspread": 2, "default_contract_payment_period": 3, - "max_time": 1000, + "max_time": 300, "money_supply": 2000000000, "event_time_mean_separation": 100 / 3.0, "expire_immediately": False, @@ -99,5 +99,5 @@ "reinsurance_limit": 0.1, "upper_price_limit": 1.2, "lower_price_limit": 0.85, - "no_risks": 200000, + "no_risks": 20000, } diff --git a/metainsuranceorg.py b/metainsuranceorg.py index fd0d81d..bd732cb 100644 --- a/metainsuranceorg.py +++ b/metainsuranceorg.py @@ -149,10 +149,7 @@ def __init__(self, simulation_parameters, agent_parameters): self.balance_ratio = simulation_parameters["insurers_balance_ratio"] self.recursion_limit = simulation_parameters["insurers_recursion_limit"] # QUERY: Should this have to sum to self.cash - self.cash_left_by_categ = - self.cash * np.ones( - self.simulation_parameters["no_categories"] - ) + self.cash_left_by_categ = self.cash * np.ones(self.simulation_parameters["no_categories"]) self.market_permanency_counter = 0 def iterate(self, time): @@ -412,13 +409,13 @@ def receive_obligation(self, amount, recipient, due_time, purpose): } self.obligations.append(obligation) - def effect_payments(self, time):"""Method for checking if any payments are due. + def effect_payments(self, time): + """Method for checking if any payments are due. Accepts: time: Type Integer No return value Method checks firms list of obligations to see if ay are due for this time, then pays them. If the firm does not have enough cash then it enters illiquity, leaves the market, and matures all contracts.""" - # TODO: don't really want to be reconstructing lists every time (unless the oblications are naturally sorted by # time, in which case this could be done slightly better). Low priority, but something to consider due = [item for item in self.obligations if item["due_time"] <= time] @@ -817,7 +814,7 @@ def process_newrisks_insurer( they should be underwritten or not. It is done in this way to maintain the portfolio as balanced as possible. For that reason we process risk[C1], risk[C2], risk[C3], risk[C4], risk[C1], risk[C2], ... and so forth. If risks are accepted then a contract is written.""" -_cached_rvs = self.contract_runtime_dist.rvs() + _cached_rvs = self.contract_runtime_dist.rvs() for risk_index in range(max(number_risks_categ)): for categ_id in range(len(acceptable_by_category)): if ( diff --git a/riskmodel.py b/riskmodel.py index aee3f9e..4ce47a8 100644 --- a/riskmodel.py +++ b/riskmodel.py @@ -205,7 +205,7 @@ def evaluate_proportional(self, risks, cash): ) def evaluate_excess_of_loss(self, risks, cash, offered_risk=None): - """Method to evaluate excess-of-loss type risks. + """Method to evaluate excess-of-loss type risks. Accepts: risks: Type List of DataDicts. cash: Type List. Gives cash available for each category. From 444bc28ae62035cfb8d66f6d82a20bf33ad6bb32 Mon Sep 17 00:00:00 2001 From: Chris Hughes Date: Fri, 12 Jul 2019 16:27:32 +0100 Subject: [PATCH 035/125] Moved agent creating code into insurancesimulation.py from start.py --- insurancefirm.py | 14 ++--- insurancesimulation.py | 123 +++++++++++++++++++++-------------------- start.py | 81 ++++----------------------- 3 files changed, 81 insertions(+), 137 deletions(-) diff --git a/insurancefirm.py b/insurancefirm.py index 2144876..3766313 100644 --- a/insurancefirm.py +++ b/insurancefirm.py @@ -1,5 +1,5 @@ from metainsuranceorg import MetaInsuranceOrg -from catbond import CatBond +import catbond import numpy as np from reinsurancecontract import ReinsuranceContract import isleconfig @@ -397,13 +397,13 @@ def issue_cat_bond(self, time, categ_id, per_value_per_period_premium=0): ] ) # TODO: or is it range(1, risk["runtime"]+1)? # catbond = CatBond(self.simulation, per_period_premium) - catbond = CatBond( + new_catbond = catbond.CatBond( self.simulation, per_period_premium, self.interest_rate ) # TODO: shift obtain_yield method to insurancesimulation, thereby making it unnecessary to drag parameters like self.interest_rate from instance to instance and from class to class """add contract; contract is a quasi-reinsurance contract""" contract = ReinsuranceContract( - catbond, + new_catbond, risk, time, 0, @@ -415,20 +415,20 @@ def issue_cat_bond(self, time, categ_id, per_value_per_period_premium=0): ) # per_value_reinsurance_premium = 0 because the insurance firm does not continue to make payments to the cat bond. Only once. - catbond.set_contract(contract) + new_catbond.set_contract(contract) """sell cat bond (to self.simulation)""" self.simulation.receive_obligation(var_this_risk, self, time, "bond") - catbond.set_owner(self.simulation) + new_catbond.set_owner(self.simulation) """hand cash over to cat bond such that var_this_risk is covered""" obligation = { "amount": var_this_risk + total_premium, - "recipient": catbond, + "recipient": new_catbond, "due_time": time, "purpose": "bond", } self.pay(obligation) # TODO: is var_this_risk the correct amount? """register catbond""" - self.simulation.accept_agents("catbond", [catbond], time=time) + self.simulation.add_agents(catbond.CatBond, "catbond", [new_catbond]) def make_reinsurance_claims(self, time): """Method to make reinsurance claims. diff --git a/insurancesimulation.py b/insurancesimulation.py index ca5cd87..b8e480f 100644 --- a/insurancesimulation.py +++ b/insurancesimulation.py @@ -1,5 +1,7 @@ from distributiontruncated import TruncatedDistWrapper import visualization_network +import insurancefirm +import reinsurancefirm import numpy as np import scipy.stats import math @@ -316,69 +318,62 @@ def initialize_agent_parameters( } ) - def add_agents(self, agent_class, agent_class_string): - # TODO: implement this to merge build_agents and accept_agents - pass - - def build_agents( - self, agent_class, agent_class_string, parameters, agent_parameters - ): - """Method for building new agents, only used for re/insurance firms. Loops through the agent parameters for each - initialised agent to create an instance of them using re/insurancefirm. - Accepts: - Agent_class: class of agent, either InsuranceFirm or ReinsuranceFirm. - agent_class_string: String Type containing string of agent class. Not used. - parameters: DataDict, contains config parameters. - agent_parameters: DataDict of agent parameters. - Returns: - agents: List Type, list of agent class instances created by loop""" - assert parameters == self.simulation_parameters - agents = [] - for ap in agent_parameters: - agents.append(agent_class(parameters, ap)) - return agents - - def accept_agents(self, agent_class_string, agents, time=0): - """Method to 'accept' agents in that it adds agent to relevant list of agents kept by simulation - instance, also adds agent to logger. Also takes created agents initial cash out of economy. - Accepts: - agent_class_string: String Type. - agents: List type of agent class instances. - agent_group: List type of agent class instances. - time: Integer type, not used - Returns: - None""" - if agent_class_string == "insurancefirm": - try: + def add_agents(self, agent_class, agent_class_string, agents=None, n=1): + """Method for building agents and adding them to the simulation. Can also add pre-made catbond agents directly + Accepts: + agent_class: class of the agent, InsuranceFirm, ReinsuranceFirm or CatBond + agent_class_string: string of the same, "insurancefirm", "reinsurancefirm" or "catbond" + agents: if adding directly, a list of the agents to add + n: int of number of agents to add + Returns: + None""" + if agents: + # We're probably just adding a catbond + if agent_class_string == "catbond": + assert len(agents) == n + self.catbonds += agents + else: + raise ValueError("Only catbonds may be passed directly") + else: + # We need to create and input the agents + if agent_class_string == "insurancefirm": + if not self.insurancefirms: + # There aren't any other firms yet, add the first ones + assert len(self.agent_parameters["insurancefirm"]) == n + agent_parameters = self.agent_parameters["insurancefirm"] + else: + # We are adding new agents to an existing simulation + agent_parameters = [self.agent_parameters["insurancefirm"][self.insurance_entry_index()] for _ in range(n)] + for ap in agent_parameters: + ap["id"] = self.get_unique_insurer_id() + agents = [agent_class(self.simulation_parameters, ap) for ap in agent_parameters] + # We've made the agents, add them to the simulation self.insurancefirms += agents - self.insurancefirms_group = agents - except: # QUERY: Why? - print(sys.exc_info()) - pdb.set_trace() - # fix self.history_logs['individual_contracts'] list - for agent in agents: - self.logger.add_insurance_agent() - # remove new agent cash from simulation cash to ensure stock flow consistency - total_new_agent_cash = sum([agent.cash for agent in agents]) - self._reduce_money_supply(total_new_agent_cash) - elif agent_class_string == "reinsurancefirm": - try: + for agent in agents: + self.logger.add_insurance_agent() + + elif agent_class_string == "reinsurancefirm": + # Much the same as above + if not self.reinsurancefirms: + assert len(self.agent_parameters["reinsurancefirm"]) == n + agent_parameters = self.agent_parameters["reinsurancefirm"] + else: + agent_parameters = [self.agent_parameters["reinsurancefirm"][self.reinsurance_entry_index()] for _ in range(n)] + for ap in agent_parameters: + ap["id"] = self.get_unique_reinsurer_id() + # QUERY: This was written but not actually used in the original implementation - should it be? + # ap["initial_cash"] = self.reinsurance_capital_entry() + agents = [agent_class(self.simulation_parameters, ap) for ap in agent_parameters] self.reinsurancefirms += agents - self.reinsurancefirms_group = agents - except: - print(sys.exc_info()) - pdb.set_trace() - # remove new agent cash from simulation cash to ensure stock flow consistency + + elif agent_class_string == "catbond": + raise ValueError(f"Catbonds must be built before being added") + else: + raise ValueError(f"Unrecognised agent type {agent_class_string}") + + # Keep the total amount of money constant total_new_agent_cash = sum([agent.cash for agent in agents]) self._reduce_money_supply(total_new_agent_cash) - elif agent_class_string == "catbond": - try: - self.catbonds += agents - except: - print(sys.exc_info()) - pdb.set_trace() - else: - raise ValueError(f"Error: Unexpected agent class used {agent_class_string}") def delete_agents(self, agent_class_string, agents): """Method for deleting catbonds as it is only agent that is allowed to be removed @@ -405,6 +400,16 @@ def iterate(self, t): if isleconfig.showprogress: print(f"\rTime: {t}", end="") + if self.firm_enters_market(agent_type="InsuranceFirm"): + self.add_agents(insurancefirm.InsuranceFirm, + "insurancefirm", + n=1) + + if self.firm_enters_market(agent_type="ReinsuranceFirm"): + self.add_agents(reinsurancefirm.ReinsuranceFirm, + "reinsurancefirm", + n=1) + self.reset_pls() # adjust market premiums diff --git a/start.py b/start.py index c20339c..d621b91 100644 --- a/start.py +++ b/start.py @@ -13,6 +13,7 @@ import isleconfig import logger import reinsurancefirm +import copy simulation_parameters = isleconfig.simulation_parameters filepath = None @@ -35,7 +36,7 @@ def main( rc_event_damage, np_seed, random_seed, - save_iter: int, + save_iter: int, replic_ID, requested_logs=None, ): @@ -52,77 +53,15 @@ def main( rc_event_damage, ) - # create agents: insurance firms - insurancefirms_group = simulation.build_agents( - insurancefirm.InsuranceFirm, - "insurancefirm", - parameters=simulation_parameters, - agent_parameters=simulation.agent_parameters["insurancefirm"], - ) - - simulation.accept_agents("insurancefirm", insurancefirms_group) - - # create agents: reinsurance firms - reinsurancefirms_group = simulation.build_agents( - reinsurancefirm.ReinsuranceFirm, - "reinsurancefirm", - parameters=simulation_parameters, - agent_parameters=simulation.agent_parameters["reinsurancefirm"], - ) - simulation.accept_agents("reinsurancefirm", reinsurancefirms_group) + simulation.add_agents(insurancefirm.InsuranceFirm, + "insurancefirm", + n=simulation_parameters["no_insurancefirms"]) + simulation.add_agents(reinsurancefirm.ReinsuranceFirm, + "reinsurancefirm", + n=simulation_parameters["no_reinsurancefirms"]) - # time iteration for t in range(simulation_parameters["max_time"]): - # create new agents # TODO: write method for this; this code block is executed almost identically 4 times - # In fact this should probably all go in insurancesimulation.py, as part of simulation.iterate(t) - if simulation.firm_enters_market(agent_type="InsuranceFirm"): - parameters = [ - np.random.choice(simulation.agent_parameters["insurancefirm"]) - ] # QUERY Which of these should be used? - parameters = [ - simulation.agent_parameters["insurancefirm"][ - simulation.insurance_entry_index() - ] - ] - # QUERY: As far as I can tell, there are only {no_riskmodels} distinct values for parameters, why does - # simulation.agent_parameters["insurancefirm"] need to have length {no_insurancefirms}? - # Also why do the new insurers always use the least popular risk model? - parameters[0]["id"] = simulation.get_unique_insurer_id() - new_insurance_firm = simulation.build_agents( - insurancefirm.InsuranceFirm, - "insurancefirm", - parameters=simulation_parameters, - agent_parameters=parameters, - ) - insurancefirms_group += new_insurance_firm - simulation.accept_agents("insurancefirm", new_insurance_firm, time=t) - - if simulation.firm_enters_market(agent_type="ReinsuranceFirm"): - parameters = [ - np.random.choice(simulation.agent_parameters["reinsurancefirm"]) - ] - # The reinsurance firms do just pick a random riskmodel when they are created. It is weighted by the initial - # distribution, I think # QUERY: is this right? - parameters[0]["initial_cash"] = simulation.reinsurance_capital_entry() - # Since the value of the reinrisks varies overtime it makes sense that the market entry of reinsures - # depends on those values. The method world.reinsurance_capital_entry() determines the capital - # market entry of reinsurers. - parameters = [ - simulation.agent_parameters["reinsurancefirm"][ - simulation.reinsurance_entry_index() - ] - ] - parameters[0]["id"] = simulation.get_unique_reinsurer_id() - new_reinsurance_firm = simulation.build_agents( - reinsurancefirm.ReinsuranceFirm, - "reinsurancefirm", - parameters=simulation_parameters, - agent_parameters=parameters, - ) - reinsurancefirms_group += new_reinsurance_firm - simulation.accept_agents("reinsurancefirm", new_reinsurance_firm, time=t) - - # iterate simulation + # Main time iteration loop simulation.iterate(t) # log data @@ -131,7 +70,7 @@ def main( if t % save_iter == 0 and t > 0: save_simulation(t, simulation, simulation_parameters, exit_now=False) - # finish simulation, write logs + # Finish simulation, write logs simulation.finalize() # It is required to return this list to download all the data generated by a single run of the model from the cloud. From d8fcce6ca6415dcc376a9e65e7ea5bd956b91345 Mon Sep 17 00:00:00 2001 From: KloskaT Date: Mon, 15 Jul 2019 11:39:45 +0100 Subject: [PATCH 036/125] Fixed error where reinsurance were initialized using insurance firm IDs --- insurancesimulation.py | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/insurancesimulation.py b/insurancesimulation.py index 51a5d67..ee1f522 100644 --- a/insurancesimulation.py +++ b/insurancesimulation.py @@ -171,13 +171,18 @@ def initialize_agent_parameters(self, firmtype, simulation_parameters, risk_mode reinsurance_level_upperbound = simulation_parameters["reinsurance_reinsurance_levels_upper_bound"] for i in range(no_firms): + if firmtype == "insurancefirm": + unique_id = self.get_unique_insurer_id() + elif firmtype == "reinsurancefirm": + unique_id = self.get_unique_reinsurer_id() + if simulation_parameters['static_non-proportional_reinsurance_levels']: reinsurance_level = simulation_parameters["default_non-proportional_reinsurance_deductible"] else: reinsurance_level = np.random.uniform(reinsurance_level_lowerbound, reinsurance_level_upperbound) riskmodel_config = risk_model_configurations[i % len(risk_model_configurations)] - self.agent_parameters[firmtype].append({'id': self.get_unique_insurer_id(), 'initial_cash': simulation_parameters[initial_cash], + self.agent_parameters[firmtype].append({'id': unique_id, 'initial_cash': simulation_parameters[initial_cash], 'riskmodel_config': riskmodel_config, 'norm_premium': self.norm_premium, 'profit_target': simulation_parameters["norm_profit_markup"], 'initial_acceptance_threshold': simulation_parameters["initial_acceptance_threshold"], @@ -335,7 +340,7 @@ def iterate(self, t): if reinsurer.riskmodel.inaccuracy == self.inaccuracy[i]: self.reinsurance_models_counter[i] += 1 - network_division = 2 # How often network is updated. + network_division = 1 # How often network is updated. if isleconfig.show_network and t % network_division == 0 and t > 0: if t == network_division: self.RN = visualization_network.ReinsuranceNetwork() # Only creates once instance so only one figure. @@ -389,7 +394,7 @@ def save_data(self): current_log['insurance_firms_cash'] = insurance_firms current_log['reinsurance_firms_cash'] = reinsurance_firms current_log['market_diffvar'] = self.compute_market_diffvar() - + current_log['individual_contracts'] = [] individual_contracts_no = [len(insurancefirm.underwritten_contracts) for insurancefirm in self.insurancefirms] for i in range(len(individual_contracts_no)): From c82025aab13577542668b0e2af3f94106d753a7c Mon Sep 17 00:00:00 2001 From: Chris Hughes Date: Fri, 12 Jul 2019 17:33:11 +0100 Subject: [PATCH 037/125] Attempting to get resume working, not successfully (pickle?!) --- calibrationscore.py | 2 +- catbond.py | 4 +- distributiontruncated.py | 4 +- ensemble.py | 1 + insurancecontract.py | 1 + insurancefirm.py | 26 ++++-- insurancesimulation.py | 75 +++++++++------- isleconfig.py | 9 +- logger.py | 12 +-- metainsurancecontract.py | 16 ++-- metainsuranceorg.py | 45 ++++------ metaplotter.py | 2 - metaplotter_pl_timescale.py | 2 - reinsurancefirm.py | 2 +- resume.py | 174 ------------------------------------ riskmodel.py | 2 +- setup_simulation.py | 12 +-- start.py | 125 +++++++++++++++++--------- visualisation.py | 4 +- visualization_network.py | 3 +- 20 files changed, 199 insertions(+), 322 deletions(-) delete mode 100644 resume.py diff --git a/calibrationscore.py b/calibrationscore.py index b0a86a6..f9cc48c 100644 --- a/calibrationscore.py +++ b/calibrationscore.py @@ -30,7 +30,7 @@ def __init__(self, L): def test_all(self): """Method to test all calibration tests. No arguments. - Returns combined calibration score as float \in [0,1].""" + Returns combined calibration score as float in [0,1].""" """Compute score components""" scores = { diff --git a/catbond.py b/catbond.py index 8d55262..9967efd 100644 --- a/catbond.py +++ b/catbond.py @@ -3,9 +3,7 @@ class CatBond(MetaInsuranceOrg): - def __init__( - self, simulation, per_period_premium, owner, interest_rate=0 - ): + def __init__(self, simulation, per_period_premium, owner, interest_rate=0): """Initialising methods. Accepts: simulation: Type class diff --git a/distributiontruncated.py b/distributiontruncated.py index 28c030f..6f6a15b 100644 --- a/distributiontruncated.py +++ b/distributiontruncated.py @@ -56,7 +56,9 @@ def rvs(self, size=1): # Sample RVs from the original distribution and then throw out the ones that are outside the bounds. init_sample_size = int(ceil(size / self.normalizing_factor * 1.1)) sample = self.dist.rvs(size=init_sample_size) - sample = np.logical_and(self.lower_bound <= sample, sample <= self.upper_bound) + sample = sample[ + np.logical_and(self.lower_bound <= sample, sample <= self.upper_bound) + ] while len(sample) < size: sample = np.append(sample, self.rvs(size - len(sample))) return sample[:size] diff --git a/ensemble.py b/ensemble.py index 8c53dd0..cf42574 100644 --- a/ensemble.py +++ b/ensemble.py @@ -5,6 +5,7 @@ import copy import os + # noinspection PyUnresolvedReferences from sandman2.api import operation, Session diff --git a/insurancecontract.py b/insurancecontract.py index 93ad6a5..a400821 100644 --- a/insurancecontract.py +++ b/insurancecontract.py @@ -8,6 +8,7 @@ class InsuranceContract(MetaInsuranceContract): and ReinsuranceContract objects. The signature of this class' constructor is the same as that of the InsuranceContract constructor. The class has two methods (explode, mature) that overwrite methods in InsuranceContract.""" + def __init__( self, insurer, diff --git a/insurancefirm.py b/insurancefirm.py index 3766313..fadde73 100644 --- a/insurancefirm.py +++ b/insurancefirm.py @@ -15,7 +15,7 @@ def __init__(self, simulation_parameters, agent_parameters): Signature is identical to constructor method of parent class. Constructor calls parent constructor and only overwrites boolean indicators of insurer and reinsurer role of the object.""" - super(InsuranceFirm, self).__init__(simulation_parameters, agent_parameters) + super().__init__(simulation_parameters, agent_parameters) self.is_insurer = True self.is_reinsurer = False @@ -114,10 +114,14 @@ def increase_capacity(self, time, max_var): market premium is above its average premium, otherwise firm is 'forced' to get a catbond or reinsurance. Only implemented for non-proportional(excess of loss) reinsurance. Only issues one reinsurance or catbond per iteration unless not enough capacity to meet target.""" - assert self.simulation_reinsurance_type == 'non-proportional' - '''get prices''' - reinsurance_price = self.simulation.get_reinsurance_premium(self.np_reinsurance_deductible_fraction) - cat_bond_price = self.simulation.get_cat_bond_price(self.np_reinsurance_deductible_fraction) + assert self.simulation_reinsurance_type == "non-proportional" + """get prices""" + reinsurance_price = self.simulation.get_reinsurance_premium( + self.np_reinsurance_deductible_fraction + ) + cat_bond_price = self.simulation.get_cat_bond_price( + self.np_reinsurance_deductible_fraction + ) capacity = None if not reinsurance_price == cat_bond_price == float("inf"): categ_ids = [ @@ -130,8 +134,16 @@ def increase_capacity(self, time, max_var): while len(categ_ids) >= 1: categ_id = categ_ids.pop() capacity = self.get_capacity(max_var) - if self.capacity_target < capacity: # just one per iteration, unless capital target is unmatched - if self.increase_capacity_by_category(time, categ_id, reinsurance_price=reinsurance_price, cat_bond_price=cat_bond_price, force=False): + if ( + self.capacity_target < capacity + ): # just one per iteration, unless capital target is unmatched + if self.increase_capacity_by_category( + time, + categ_id, + reinsurance_price=reinsurance_price, + cat_bond_price=cat_bond_price, + force=False, + ): categ_ids = [] else: self.increase_capacity_by_category( diff --git a/insurancesimulation.py b/insurancesimulation.py index b8e480f..79ce5d4 100644 --- a/insurancesimulation.py +++ b/insurancesimulation.py @@ -5,7 +5,6 @@ import numpy as np import scipy.stats import math -import sys, pdb import isleconfig import random import copy @@ -343,10 +342,18 @@ def add_agents(self, agent_class, agent_class_string, agents=None, n=1): agent_parameters = self.agent_parameters["insurancefirm"] else: # We are adding new agents to an existing simulation - agent_parameters = [self.agent_parameters["insurancefirm"][self.insurance_entry_index()] for _ in range(n)] + agent_parameters = [ + self.agent_parameters["insurancefirm"][ + self.insurance_entry_index() + ] + for _ in range(n) + ] for ap in agent_parameters: ap["id"] = self.get_unique_insurer_id() - agents = [agent_class(self.simulation_parameters, ap) for ap in agent_parameters] + agents = [ + agent_class(self.simulation_parameters, ap) + for ap in agent_parameters + ] # We've made the agents, add them to the simulation self.insurancefirms += agents for agent in agents: @@ -358,12 +365,20 @@ def add_agents(self, agent_class, agent_class_string, agents=None, n=1): assert len(self.agent_parameters["reinsurancefirm"]) == n agent_parameters = self.agent_parameters["reinsurancefirm"] else: - agent_parameters = [self.agent_parameters["reinsurancefirm"][self.reinsurance_entry_index()] for _ in range(n)] + agent_parameters = [ + self.agent_parameters["reinsurancefirm"][ + self.reinsurance_entry_index() + ] + for _ in range(n) + ] for ap in agent_parameters: ap["id"] = self.get_unique_reinsurer_id() # QUERY: This was written but not actually used in the original implementation - should it be? # ap["initial_cash"] = self.reinsurance_capital_entry() - agents = [agent_class(self.simulation_parameters, ap) for ap in agent_parameters] + agents = [ + agent_class(self.simulation_parameters, ap) + for ap in agent_parameters + ] self.reinsurancefirms += agents elif agent_class_string == "catbond": @@ -401,14 +416,10 @@ def iterate(self, t): print(f"\rTime: {t}", end="") if self.firm_enters_market(agent_type="InsuranceFirm"): - self.add_agents(insurancefirm.InsuranceFirm, - "insurancefirm", - n=1) + self.add_agents(insurancefirm.InsuranceFirm, "insurancefirm", n=1) if self.firm_enters_market(agent_type="ReinsuranceFirm"): - self.add_agents(reinsurancefirm.ReinsuranceFirm, - "reinsurancefirm", - n=1) + self.add_agents(reinsurancefirm.ReinsuranceFirm, "reinsurancefirm", n=1) self.reset_pls() @@ -512,8 +523,8 @@ def save_data(self): ) total_excess_capital = sum( [ - insurancefirm.get_excess_capital() - for insurancefirm in self.insurancefirms + firm.get_excess_capital() + for firm in self.insurancefirms ] ) total_profitslosses = sum( @@ -521,47 +532,47 @@ def save_data(self): ) total_contracts_no = sum( [ - len(insurancefirm.underwritten_contracts) - for insurancefirm in self.insurancefirms + len(firm.underwritten_contracts) + for firm in self.insurancefirms ] ) total_reincash_no = sum( - [reinsurancefirm.cash for reinsurancefirm in self.reinsurancefirms] + [firm.cash for firm in self.reinsurancefirms] ) total_reinexcess_capital = sum( [ - reinsurancefirm.get_excess_capital() - for reinsurancefirm in self.reinsurancefirms + firm.get_excess_capital() + for firm in self.reinsurancefirms ] ) total_reinprofitslosses = sum( [ - reinsurancefirm.get_profitslosses() - for reinsurancefirm in self.reinsurancefirms + firm.get_profitslosses() + for firm in self.reinsurancefirms ] ) total_reincontracts_no = sum( [ - len(reinsurancefirm.underwritten_contracts) - for reinsurancefirm in self.reinsurancefirms + len(firm.underwritten_contracts) + for firm in self.reinsurancefirms ] ) operational_no = sum( - [insurancefirm.operational for insurancefirm in self.insurancefirms] + [firm.operational for firm in self.insurancefirms] ) reinoperational_no = sum( - [reinsurancefirm.operational for reinsurancefirm in self.reinsurancefirms] + [firm.operational for firm in self.reinsurancefirms] ) catbondsoperational_no = sum([catbond.operational for catbond in self.catbonds]) """ collect agent-level data """ insurance_firms = [ - (insurancefirm.cash, insurancefirm.id, insurancefirm.operational) - for insurancefirm in self.insurancefirms + (firm.cash, firm.id, firm.operational) + for firm in self.insurancefirms ] reinsurance_firms = [ - (reinsurancefirm.cash, reinsurancefirm.id, reinsurancefirm.operational) - for reinsurancefirm in self.reinsurancefirms + (firm.cash, firm.id, firm.operational) + for firm in self.reinsurancefirms ] """ prepare dict """ @@ -593,8 +604,8 @@ def save_data(self): current_log["market_diffvar"] = self.compute_market_diffvar() current_log["individual_contracts"] = [ - len(insurancefirm.underwritten_contracts) - for insurancefirm in self.insurancefirms + len(firm.underwritten_contracts) + for firm in self.insurancefirms ] """ call to Logger object """ @@ -631,7 +642,7 @@ def _inflict_peril(self, categ_id, damage, t): if isleconfig.verbose: print("**** PERIL", damage) damagevalues = np.random.beta( - 1, 1.0 / damage - 1, size=self.risks_counter[categ_id] + a=1, b=1.0 / damage - 1, size=self.risks_counter[categ_id] ) uniformvalues = np.random.uniform(0, 1, size=self.risks_counter[categ_id]) [ @@ -907,7 +918,7 @@ def solicit_insurance_requests(self, insurer_id, cash, insurer): return risks_to_be_sent - def solicit_reinsurance_requests(self, id, cash, reinsurer): + def solicit_reinsurance_requests(self, cash, reinsurer): """Method for determining which reinsurance risks are to be assessed by firms based on reinsurer weights Accepts: id: Type integer diff --git a/isleconfig.py b/isleconfig.py index 6166f07..62aed05 100644 --- a/isleconfig.py +++ b/isleconfig.py @@ -3,10 +3,11 @@ force_foreground = False verbose = False showprogress = False -show_network = ( - False -) # Should network be visualized? This should be False by default, to be overridden by commandline arguments -slim_log = True # Should logs be small in ensemble runs (only aggregated level data)? +# Should network be visualized? This should be False by default, to be overridden by commandline arguments +show_network = False +# Should logs be small in ensemble runs (only aggregated level data)? +slim_log = True + simulation_parameters = { "no_categories": 4, diff --git a/logger.py b/logger.py index b07d3ad..a77e8da 100644 --- a/logger.py +++ b/logger.py @@ -93,24 +93,20 @@ def record_data(self, data_dict): data_dict["individual_contracts"][i] ) - def obtain_log( - self, requested_logs - ): # This function allows to return in a list all the data generated by the model. There is no other way to transfer it back from the cloud. + def obtain_log(self, requested_logs=None): """Method to transfer entire log (self.history_log as well as risk event schedule). This is used to transfer the log to master core from work cores in ensemble runs in the cloud. - No arguments. + Arguments: + requested_logs: a list of the names of the logs requested (as strings) Returns list (listified dict).""" - # LOG_DEFAULT is a list and thus mutable, so we don't want to have it as a default value - if requested_logs is None: - requested_logs = LOG_DEFAULT """Include environment variables (number of risk models and risk event schedule)""" self.history_logs["number_riskmodels"] = self.number_riskmodels self.history_logs["rc_event_damage_initial"] = self.rc_event_damage_initial self.history_logs["rc_event_schedule_initial"] = self.rc_event_schedule_initial """Parse logs to be returned""" - if requested_logs == None: + if requested_logs is None: requested_logs = LOG_DEFAULT log = {name: self.history_logs[name] for name in requested_logs} diff --git a/metainsurancecontract.py b/metainsurancecontract.py index a4f0018..8cec74c 100644 --- a/metainsurancecontract.py +++ b/metainsurancecontract.py @@ -56,17 +56,21 @@ def __init__( self.initial_VaR = initial_var # set deductible from argument, risk property or default value, whichever first is not None default_deductible_fraction = 0.0 - self.deductible_fraction = deductible_fraction if deductible_fraction is not None else properties.get( - "deductible_fraction", default_deductible_fraction - ) + self.deductible_fraction = ( + deductible_fraction + if deductible_fraction is not None + else properties.get("deductible_fraction", default_deductible_fraction) + ) self.deductible = self.deductible_fraction * self.value # set excess from argument, risk property or default value, whichever first is not None default_excess_fraction = 1.0 - self.excess_fraction = excess_fraction if excess_fraction is not None else properties.get( - "excess_fraction", default_excess_fraction - ) + self.excess_fraction = ( + excess_fraction + if excess_fraction is not None + else properties.get("excess_fraction", default_excess_fraction) + ) self.excess = self.excess_fraction * self.value diff --git a/metainsuranceorg.py b/metainsuranceorg.py index bd732cb..bc6ef15 100644 --- a/metainsuranceorg.py +++ b/metainsuranceorg.py @@ -149,7 +149,9 @@ def __init__(self, simulation_parameters, agent_parameters): self.balance_ratio = simulation_parameters["insurers_balance_ratio"] self.recursion_limit = simulation_parameters["insurers_recursion_limit"] # QUERY: Should this have to sum to self.cash - self.cash_left_by_categ = self.cash * np.ones(self.simulation_parameters["no_categories"]) + self.cash_left_by_categ = self.cash * np.ones( + self.simulation_parameters["no_categories"] + ) self.market_permanency_counter = 0 def iterate(self, time): @@ -189,7 +191,6 @@ def iterate(self, time): """adjust liquidity, borrow or invest""" # Not implemented - self.market_permanency(time) self.roll_over(time) @@ -509,16 +510,6 @@ def get_excess_capital(self): Returns agents excess capital""" return self.excess_capital - def logme(self): - self.log("cash", self.cash) - self.log("underwritten_contracts", self.underwritten_contracts) - self.log("operational", self.operational) - - def log(self, *args): - raise NotImplementedError( - "The log method should have been overridden by the subclass" - ) - def number_underwritten_contracts(self): return len(self.underwritten_contracts) @@ -587,9 +578,7 @@ def get_newrisks_by_type(self): self.id, self.cash, self ) if self.is_reinsurer: - new_risks += self.simulation.solicit_reinsurance_requests( - self.id, self.cash, self - ) + new_risks += self.simulation.solicit_reinsurance_requests(self.cash, self) new_nonproportional_risks = [ risk @@ -605,7 +594,6 @@ def get_newrisks_by_type(self): ] return new_nonproportional_risks, new_risks - def increase_capacity(self, time, var_by_category): raise NotImplementedError( "Method is not implemented in MetaInsuranceOrg, just in inheriting InsuranceFirm instances" @@ -621,24 +609,24 @@ def adjust_capacity_target(self, time): "Method not implemented. adjust_capacity_target method should be implemented in inheriting classes" ) - def risks_reinrisks_organizer(self, new_risks): #This method organizes the new risks received by the insurer (or reinsurer) + def risks_reinrisks_organizer(self, new_risks): """This method organizes the new risks received by the insurer (or reinsurer) by category. Accepts: new_risks: Type list of DataDicts Returns: - risks_per_catgegory: Type list of categories, each contains risks originating from that category. + risks_by_category: Type list of categories, each contains risks originating from that category. number_risks_categ: Type list, elements are integers of total risks in each category""" - risks_per_categ = [[] for x in range(self.simulation_parameters["no_categories"])] #This method organizes the new risks received by the insurer (or reinsurer) by category in the nested list "risks_per_categ". - number_risks_categ = [[] for x in range(self.simulation_parameters["no_categories"])] #This method also counts the new risks received by the insurer (or reinsurer) by category in the list "number_risks_categ". + risks_by_category = [[] for _ in range(self.simulation_parameters["no_categories"])] + number_risks_categ = np.zeros(self.simulation_parameters["no_categories"], dtype=np.int_) for categ_id in range(self.simulation_parameters["no_categories"]): - risks_per_categ[categ_id] = [ + risks_by_category[categ_id] = [ risk for risk in new_risks if risk["category"] == categ_id ] - number_risks_categ[categ_id] = len(risks_per_categ[categ_id]) + number_risks_categ[categ_id] = len(risks_by_category[categ_id]) - # The method returns both risks_per_categ and number_risks_categ. - return risks_per_categ, number_risks_categ + # The method returns both risks_by_category and number_risks_categ. + return risks_by_category, number_risks_categ def balanced_portfolio(self, risk, cash_left_by_categ, var_per_risk): """This method decides whether the portfolio is balanced enough to accept a new risk or not. If it is balanced @@ -725,8 +713,13 @@ def process_newrisks_reinsurer( risks are accepted then a contract is written.""" for iterion in range(max(number_reinrisks_categ)): - for categ_id in range(self.simulation_parameters["no_categories"]): #Here we take only one risk per category at a time to achieve risk[C1], risk[C2], risk[C3], risk[C4], risk[C1], risk[C2], ... if possible. - if iterion < number_reinrisks_categ[categ_id] and reinrisks_per_categ[categ_id][iterion] is not None: + for categ_id in range( + self.simulation_parameters["no_categories"] + ): # Here we take only one risk per category at a time to achieve risk[C1], risk[C2], risk[C3], risk[C4], risk[C1], risk[C2], ... if possible. + if ( + iterion < number_reinrisks_categ[categ_id] + and reinrisks_per_categ[categ_id][iterion] is not None + ): risk_to_insure = reinrisks_per_categ[categ_id][iterion] underwritten_risks = [ { diff --git a/metaplotter.py b/metaplotter.py index 1ec39d3..40a6050 100644 --- a/metaplotter.py +++ b/metaplotter.py @@ -6,8 +6,6 @@ import os - - def read_data(): # do not overwrite old pdfs # if os.path.exists("data/fig_one_and_two_rm_comp.pdf"): diff --git a/metaplotter_pl_timescale.py b/metaplotter_pl_timescale.py index 9b5029b..48eb650 100644 --- a/metaplotter_pl_timescale.py +++ b/metaplotter_pl_timescale.py @@ -6,8 +6,6 @@ import os - - def read_data(): # do not overwrite old pdfs # if os.path.exists("data/fig_one_and_two_rm_comp.pdf"): diff --git a/reinsurancefirm.py b/reinsurancefirm.py index 8e6e609..5e1043c 100644 --- a/reinsurancefirm.py +++ b/reinsurancefirm.py @@ -11,6 +11,6 @@ def __init__(self, simulation_parameters, agent_parameters): Signature is identical to constructor method of parent class. Constructor calls parent constructor and only overwrites boolean indicators of insurer and reinsurer role of the object.""" - super(ReinsuranceFirm, self).__init__(simulation_parameters, agent_parameters) + super().__init__(simulation_parameters, agent_parameters) self.is_insurer = False self.is_reinsurer = True diff --git a/resume.py b/resume.py deleted file mode 100644 index 6770536..0000000 --- a/resume.py +++ /dev/null @@ -1,174 +0,0 @@ -# import common packages -import argparse -import hashlib -import numpy as np -import pickle -import random - -# import config file and apply configuration -import isleconfig -# TODO: this whole module could be an argument for start.py - - -simulation_parameters = isleconfig.simulation_parameters -replic_ID = None -override_no_riskmodels = False - -# use argparse to handle command line arguments -parser = argparse.ArgumentParser(description="Model the Insurance sector") -parser.add_argument("--abce", action="store_true", help="[REMOVED] use abce") -parser.add_argument( - "--oneriskmodel", - action="store_true", - help="allow overriding the number of riskmodels from the standard config (with 1)", -) -parser.add_argument( - "--riskmodels", - type=int, - choices=[1, 2, 3, 4], - help="allow overriding the number of riskmodels from standard config (with 1 or other numbers)", -) -parser.add_argument( - "--replicid", - type=int, - help="if replication ID is given, pass this to the simulation so that the risk profile can be restored", -) -parser.add_argument( - "--replicating", - action="store_true", - help="if this is a simulation run designed to replicate another, override the config file parameter", -) -parser.add_argument( - "--randomseed", type=float, help="allow setting of numpy random seed" -) -parser.add_argument( - "--foreground", - action="store_true", - help="force foreground runs even if replication ID is given (which defaults to background runs)", -) -parser.add_argument("-p", "--showprogress", action="store_true", help="show timesteps") -parser.add_argument("-v", "--verbose", action="store_true", help="more detailed output") -args = parser.parse_args() - -if args.abce: - raise Exception("ABCE is not and will not be supported") -if args.oneriskmodel: - isleconfig.oneriskmodel = True - override_no_riskmodels = 1 -if args.riskmodels: - override_no_riskmodels = args.riskmodels -if args.replicid is not None: - replic_ID = args.replicid -if args.replicating: - isleconfig.replicating = True - assert ( - replic_ID is not None - ), "Error: Replication requires a replication ID to identify run to be replicated" -if args.randomseed: - randomseed = args.randomseed - seed = int(randomseed) -else: - np.random.seed() - seed = np.random.randint(0, 2 ** 31 - 1) -if args.foreground: - isleconfig.force_foreground = True -if args.showprogress: - isleconfig.showprogress = True -if args.verbose: - isleconfig.verbose = True - -# import isle modules - -from insurancefirm import InsuranceFirm -from reinsurancefirm import ReinsuranceFirm - - -# main function -def main(): # TODO: this script should probably be an argument for start.py - with open("data/simulation_save.pkl", "br") as rfile: - d = pickle.load(rfile) - simulation = d["simulation"] - world = simulation - np_seed = d["np_seed"] - random_seed = d["random_seed"] - time = d["time"] - simulation_parameters = d["simulation_parameters"] - - insurancefirms_group = list(simulation.insurancefirms) - reinsurancefirms_group = list(simulation.reinsurancefirms) - - # np.random.seed(seed) - np.random.set_state(np_seed) - random.setstate(random_seed) - - # time iteration - for t in range(time, simulation_parameters["max_time"]): - - # abce time step - simulation.advance_round(t) - - # create new agents # TODO: write method for this; this code block is executed almost identically 4 times - if world.firm_enters_market(agent_type="InsuranceFirm"): - parameters = [np.random.choice(world.agent_parameters["insurancefirm"])] - parameters[0]["id"] = world.get_unique_insurer_id() - new_insurance_firm = simulation.build_agents( - InsuranceFirm, - "insurancefirm", - parameters=simulation_parameters, - agent_parameters=parameters, - ) - insurancefirms_group += new_insurance_firm - new_insurancefirm_pointer = new_insurance_firm - world.accept_agents("insurancefirm", new_insurancefirm_pointer, time=t) - - if world.firm_enters_market(agent_type="ReinsuranceFirm"): - parameters = [np.random.choice(world.agent_parameters["reinsurancefirm"])] - parameters[0]["id"] = world.get_unique_reinsurer_id() - new_reinsurance_firm = simulation.build_agents( - ReinsuranceFirm, - "reinsurancefirm", - parameters=simulation_parameters, - agent_parameters=parameters, - ) - reinsurancefirms_group += new_reinsurance_firm - new_reinsurancefirm_pointer = new_reinsurance_firm - world.accept_agents("reinsurancefirm", new_reinsurancefirm_pointer, time=t) - - # iterate simulation - world.iterate(t) - - # log data - world.save_data() - - if t > 0 and t // 50 == t / 50: - save_simulation(t, simulation, simulation_parameters, exit_now=False) - # print("here") - - # finish simulation, write logs - simulation.finalize() - - -# save function -def save_simulation(t, sim, sim_param, exit_now=False): - d = {} - d["np_seed"] = np.random.get_state() - d["random_seed"] = random.getstate() - d["time"] = t - d["simulation"] = sim - d["simulation_parameters"] = sim_param - with open("data/simulation_resave.pkl", "bw") as wfile: - pickle.dump(d, wfile, protocol=pickle.HIGHEST_PROTOCOL) - with open("data/simulation_resave.pkl", "br") as rfile: - file_contents = rfile.read() - # print("\nSimulation hashes: ", hashlib.sha512(str(d).encode()).hexdigest(), "; ", hashlib.sha512(str(file_contents).encode()).hexdigest()) - # note that the hash over the dict is for some reason not identical between runs. The hash over the state saved to the file is. - print( - "\nSimulation hash: ", hashlib.sha512(str(file_contents).encode()).hexdigest() - ) - if exit_now: - exit(0) - - -# main entry point -if __name__ == "__main__": - main() diff --git a/riskmodel.py b/riskmodel.py index 4ce47a8..79b16bd 100644 --- a/riskmodel.py +++ b/riskmodel.py @@ -278,6 +278,7 @@ def evaluate_excess_of_loss(self, risks, cash, offered_risk=None): return cash_left_by_categ, additional_required, var_this_risk + # noinspection PyUnboundLocalVariable def evaluate(self, risks, cash, offered_risk=None): """Method to evaluate given risks and the offered risk. Accepts: @@ -318,7 +319,6 @@ def evaluate(self, risks, cash, offered_risk=None): el_risks = [risk for risk in risks if risk["insurancetype"] == "excess-of-loss"] risks = [risk for risk in risks if risk["insurancetype"] == "proportional"] # compute liquidity requirements and acceptable risks from existing contract - # TODO: Consider edge cases here if (offered_risk is not None) or (len(el_risks) > 0): cash_left_by_categ, additional_required, var_this_risk = self.evaluate_excess_of_loss( el_risks, cash_left_by_categ, offered_risk diff --git a/setup_simulation.py b/setup_simulation.py index 69ed1f3..3fa638d 100644 --- a/setup_simulation.py +++ b/setup_simulation.py @@ -135,16 +135,6 @@ def store(self): with open("./data/" + self.filepath, "wb") as wfile: pickle.dump(event_schedules, wfile, protocol=pickle.HIGHEST_PROTOCOL) - # with open("./data/risk_event_schedules.txt", "w") as wfile: # QUERY: what's this for? Why do we need the txt? - # for rep_schedule in event_schedules: - # wfile.write( - # str(rep_schedule) - # .replace("\n", "") - # .replace("array", "np.array") - # .replace("uint32", "np.uint32") - # + "\n" - # ) - def recall(self): assert ( self.np_seed @@ -177,7 +167,7 @@ def obtain_ensemble(self, replications, filepath=None, overwrite=False): # Not replicating another run, so we are writing to the file given self.replications = replications if filepath is None and not self.overwrite: - print("No explicit path given, automatically overwriting default path") + print("No explicit path given, automatically overwriting default path for initial state") self.overwrite = True self.schedule(replications) self.seeds(replications) diff --git a/start.py b/start.py index d621b91..79ac1f4 100644 --- a/start.py +++ b/start.py @@ -5,15 +5,16 @@ import os import pickle import random +import copy import calibrationscore import insurancefirm import insurancesimulation + # import config file and apply configuration import isleconfig import logger import reinsurancefirm -import copy simulation_parameters = isleconfig.simulation_parameters filepath = None @@ -39,36 +40,54 @@ def main( save_iter: int, replic_ID, requested_logs=None, + resume=False, ): - np.random.seed(np_seed) - random.seed(random_seed) - - simulation_parameters[ - "simulation" - ] = simulation = insurancesimulation.InsuranceSimulation( - override_no_riskmodels, - replic_ID, - simulation_parameters, - rc_event_schedule, - rc_event_damage, - ) + if not resume: + np.random.seed(np_seed) + random.seed(random_seed) - simulation.add_agents(insurancefirm.InsuranceFirm, - "insurancefirm", - n=simulation_parameters["no_insurancefirms"]) - simulation.add_agents(reinsurancefirm.ReinsuranceFirm, - "reinsurancefirm", - n=simulation_parameters["no_reinsurancefirms"]) + simulation_parameters[ + "simulation" + ] = simulation = insurancesimulation.InsuranceSimulation( + override_no_riskmodels, + replic_ID, + simulation_parameters, + rc_event_schedule, + rc_event_damage, + ) - for t in range(simulation_parameters["max_time"]): + simulation.add_agents( + insurancefirm.InsuranceFirm, + "insurancefirm", + n=simulation_parameters["no_insurancefirms"], + ) + simulation.add_agents( + reinsurancefirm.ReinsuranceFirm, + "reinsurancefirm", + n=simulation_parameters["no_reinsurancefirms"], + ) + time = 0 + else: + d = load_simulation() + np.random.set_state(d["np_seed"]) + random.setstate(d["random_seed"]) + time = d["time"] + simulation = d["simulation"] + simulation_parameters = d["simulation_parameters"] + for key in d["isleconfig"]: + isleconfig.__dict__[key] = d["isleconfig"][key] + simulation = copy.deepcopy(simulation) + for t in range(time, simulation_parameters["max_time"]): # Main time iteration loop simulation.iterate(t) # log data simulation.save_data() - if t % save_iter == 0 and t > 0: - save_simulation(t, simulation, simulation_parameters, exit_now=False) + # Don't save at t=0 or if the simulation has just finished + if t % save_iter == 0 and 0 < t < simulation_parameters["max_time"]: + # Need to use t+1 as resume will start at time saved + save_simulation(t + 1, simulation, simulation_parameters, exit_now=False) # Finish simulation, write logs simulation.finalize() @@ -77,7 +96,6 @@ def main( return simulation.obtain_log(requested_logs) -# save function def save_simulation(t, sim, sim_param, exit_now=False): d = { "np_seed": np.random.get_state(), @@ -85,20 +103,34 @@ def save_simulation(t, sim, sim_param, exit_now=False): "time": t, "simulation": sim, "simulation_parameters": sim_param, + "isleconfig": {}, } + for key in isleconfig.__dict__: + if not key.startswith("__"): + d["isleconfig"][key] = isleconfig.__dict__[key] + with open("data/simulation_save.pkl", "bw") as wfile: pickle.dump(d, wfile, protocol=pickle.HIGHEST_PROTOCOL) with open("data/simulation_save.pkl", "br") as rfile: file_contents = rfile.read() - # print("\nSimulation hashes: ", hashlib.sha512(str(d).encode()).hexdigest(), "; ", hashlib.sha512(str(file_contents).encode()).hexdigest()) - # note that the hash over the dict is for some reason not identical between runs. The hash over the state saved to the file is. - print( - "\nSimulation hash: ", hashlib.sha512(str(file_contents).encode()).hexdigest() - ) + print("\nSaved simulation with hash:", hashlib.sha512(str(file_contents).encode()).hexdigest()) + if exit_now: exit(0) +def load_simulation(): + # TODO: Fix! This doesn't work, the retrieved file is different to the saved one. + with open("data/simulation_save.pkl", "br") as rfile: + print( + "\nLoading simulation with hash:", + hashlib.sha512(str(rfile.read()).encode()).hexdigest(), + ) + rfile.seek(0) + file_contents = pickle.load(rfile) + return file_contents + + # main entry point if __name__ == "__main__": @@ -107,6 +139,12 @@ def save_simulation(t, sim, sim_param, exit_now=False): parser.add_argument( "--abce", action="store_true", help="[REMOVED] ABCE no longer supported" ) + parser.add_argument( + "--resume", + action="store_true", + help="Resume the simulation from a previous save in ./data/simulation_save.pkl. " + "All other arguments will be ignored", + ) parser.add_argument( "--oneriskmodel", action="store_true", @@ -133,7 +171,7 @@ def save_simulation(t, sim, sim_param, exit_now=False): "--replicating", action="store_true", help="if this is a simulation run designed to replicate another, override the config file parameter. " - "You probably want to specify the --file to read from.", + "You probably want to specify the --file to read from.", ) parser.add_argument( "-o", @@ -199,19 +237,25 @@ def save_simulation(t, sim, sim_param, exit_now=False): if args.save_iterations: save_iter = args.save_iterations else: - save_iter = 200 + save_iter = 100 - from setup_simulation import SetupSim + if not args.resume: + from setup_simulation import SetupSim - setup = SetupSim() # Here the setup for the simulation is done. + setup = SetupSim() # Here the setup for the simulation is done. - [ - general_rc_event_schedule, - general_rc_event_damage, - np_seeds, - random_seeds, - ] = setup.obtain_ensemble(1, filepath, overwrite) - # Only one ensemble. This part will only be run locally (laptop). + # Only one ensemble. This part will only be run locally (laptop). + [ + general_rc_event_schedule, + general_rc_event_damage, + np_seeds, + random_seeds, + ] = setup.obtain_ensemble(1, filepath, overwrite) + else: + # We are resuming, so all of the necessary setup will be loaded from a file + general_rc_event_schedule = ( + general_rc_event_damage + ) = np_seeds = random_seeds = [None] # Run the main program # Note that we pass the filepath as the replic_ID @@ -222,7 +266,8 @@ def save_simulation(t, sim, sim_param, exit_now=False): np_seeds[0], random_seeds[0], save_iter, - filepath, + replic_ID=1, + resume=args.resume, ) replic_ID = filepath diff --git a/visualisation.py b/visualisation.py index da6a75f..d399c33 100644 --- a/visualisation.py +++ b/visualisation.py @@ -82,10 +82,10 @@ def data_stream(self): for timestep in self.data: casharr = [] idarr = [] - for (cash, id, operational) in timestep: + for (cash, firm_id, operational) in timestep: if operational: casharr.append(cash) - idarr.append(id) + idarr.append(firm_id) yield casharr, idarr def update(self, i): diff --git a/visualization_network.py b/visualization_network.py index cfe2d7a..e0b4826 100644 --- a/visualization_network.py +++ b/visualization_network.py @@ -25,7 +25,8 @@ def compute_measures(self): # is_connected = nx.is_strongly_connected(self.network) # must always be False try: node_centralities = nx.eigenvector_centrality(self.network) - except: + except nx.NetworkXException: + # TODO: be more specific about this exception if possible node_centralities = nx.betweenness_centrality(self.network) # TODO: and more, choose more meaningful ones... From 8470ae385fb21872c931b9507139330842dfe4f3 Mon Sep 17 00:00:00 2001 From: Chris Hughes Date: Tue, 16 Jul 2019 09:22:17 +0100 Subject: [PATCH 038/125] Minor tweaks --- insurancesimulation.py | 11 ++++++----- utils.py | 4 ++-- 2 files changed, 8 insertions(+), 7 deletions(-) diff --git a/insurancesimulation.py b/insurancesimulation.py index 79ce5d4..8b87398 100644 --- a/insurancesimulation.py +++ b/insurancesimulation.py @@ -69,6 +69,9 @@ def __init__( self.cat_separation_distribution = scipy.stats.expon( 0, simulation_parameters["event_time_mean_separation"] ) + + # Risk factors represent, for example, the earthquake risk for a particular house (compare to the value) + # TODO: Implement! Think about insureres rejecting risks under certain situations (high risk factor) self.risk_factor_lower_bound = simulation_parameters["risk_factor_lower_bound"] self.risk_factor_spread = ( simulation_parameters["risk_factor_upper_bound"] @@ -288,7 +291,7 @@ def initialize_agent_parameters( ] self.agent_parameters[firmtype].append( { - "id": self.get_unique_insurer_id(), + "id": self.get_unique_insurer_id(), # TODO: Fix "initial_cash": simulation_parameters[initial_cash], "riskmodel_config": riskmodel_config, "norm_premium": self.norm_premium, @@ -645,10 +648,8 @@ def _inflict_peril(self, categ_id, damage, t): a=1, b=1.0 / damage - 1, size=self.risks_counter[categ_id] ) uniformvalues = np.random.uniform(0, 1, size=self.risks_counter[categ_id]) - [ + for i, contract in enumerate(affected_contracts): contract.explode(t, uniformvalues[i], damagevalues[i]) - for i, contract in enumerate(affected_contracts) - ] def receive_obligation(self, amount, recipient, due_time, purpose): """Method for adding obligation to list that is resolved at the start if each iteration of simulation. Only @@ -955,7 +956,6 @@ def return_reinrisks(self, not_accepted_risks): Returns None""" self.not_accepted_reinrisks += not_accepted_risks - # QUERY: What does this represent? def _get_all_riskmodel_combinations(self, n, rm_factor): """Method for calculating riskmodels for each category based on the risk model inaccuracy parameter, and is used purely to assign inaccuracy. Currently all equal and overwritten immediately. @@ -1046,6 +1046,7 @@ def compute_market_diffvar(self): ) totalreal = len([firm for firm in self.insurancefirms if firm.operational]) + # Real VaR is 1 for each firm totalina += sum( [ diff --git a/utils.py b/utils.py index 92cad72..22efa3e 100644 --- a/utils.py +++ b/utils.py @@ -2,7 +2,7 @@ import numpy as np -class constant_gen(stats.rv_continuous): +class ConstantGen(stats.rv_continuous): def _pdf(self, x, *args): a = np.float_(x == 0) a[a == 1.0] = np.float_("inf") @@ -18,4 +18,4 @@ def _rvs(self, *args): return np.zeros(shape=self._size) -constant = constant_gen(name="constant") +constant = ConstantGen(name="constant") From 25fb93ba7d782e45bd0772e040490cc1662a043c Mon Sep 17 00:00:00 2001 From: KloskaT Date: Tue, 16 Jul 2019 10:39:39 +0100 Subject: [PATCH 039/125] Added animated pie chart for percent contracts on same figure, for both insurance and reinsurance contracts. Can also save files as mp4. Also can now see events on time series and pie charts. --- insurancesimulation.py | 11 ++- logger.py | 24 ++++-- visualisation.py | 183 ++++++++++++++++++++++++++++------------- 3 files changed, 156 insertions(+), 62 deletions(-) diff --git a/insurancesimulation.py b/insurancesimulation.py index ee1f522..eec5803 100644 --- a/insurancesimulation.py +++ b/insurancesimulation.py @@ -49,7 +49,7 @@ def __init__(self, override_no_riskmodels, replic_ID, simulation_parameters, rc_ self.risk_factor_distribution = scipy.stats.uniform(loc=self.risk_factor_lower_bound, scale=self.risk_factor_spread) if not simulation_parameters["risk_factors_present"]: self.risk_factor_distribution = scipy.stats.uniform(loc=1.0, scale=0) - self.risk_value_distribution = scipy.stats.uniform(loc=1000, scale=0) #TODO is this correct? + self.risk_value_distribution = scipy.stats.uniform(loc=1000, scale=0) risk_factor_mean = self.risk_factor_distribution.mean() if np.isnan(risk_factor_mean): # unfortunately scipy.stats.mean is not well-defined if scale = 0 risk_factor_mean = self.risk_factor_distribution.rvs() @@ -239,6 +239,8 @@ def accept_agents(self, agent_class_string, agents, agent_group=None, time=0): except: print(sys.exc_info()) pdb.set_trace() + for agent in agents: + self.logger.add_reinsurance_agent() # remove new agent cash from simulation cash to ensure stock flow consistency new_agent_cash = sum([agent.cash for agent in agents]) self.reduce_money_supply(new_agent_cash) @@ -400,6 +402,11 @@ def save_data(self): for i in range(len(individual_contracts_no)): current_log['individual_contracts'].append(individual_contracts_no[i]) + current_log['reinsurance_contracts'] = [] + reinsurance_contracts_no = [len(reinsurancefirm.underwritten_contracts) for reinsurancefirm in self.reinsurancefirms] + for i in range(len(reinsurance_contracts_no)): + current_log['reinsurance_contracts'].append(reinsurance_contracts_no[i]) + """ call to Logger object """ self.logger.record_data(current_log) @@ -684,7 +691,7 @@ def return_reinrisks(self, not_accepted_risks): def get_all_riskmodel_combinations(self, rm_factor): """Method for calculating riskmodels for each category based on the risk model inaccuracy parameter, and is - used purely to assign inaccuracy. Currently all equal and overwritten immediately. + used purely to assign inaccuracy. Undervalues one risk category and overestimates all the rest. Accepts: rm_factor: Type Integer = risk model inaccuracy parameter Returns: diff --git a/logger.py b/logger.py index 8fe928f..93ce229 100644 --- a/logger.py +++ b/logger.py @@ -10,7 +10,7 @@ 'total_reincontracts total_reinoperational total_catbondsoperational market_premium ' 'market_reinpremium cumulative_bankruptcies cumulative_market_exits cumulative_unrecovered_claims ' 'cumulative_claims insurance_firms_cash reinsurance_firms_cash market_diffvar ' - 'rc_event_schedule_initial rc_event_damage_initial number_riskmodels' + 'rc_event_schedule_initial rc_event_damage_initial number_riskmodels individual_contracts reinsurance_contracts' ).split(' ') class Logger(): @@ -46,7 +46,7 @@ def __init__(self, no_riskmodels=None, rc_event_schedule_initial=None, rc_event_ self.history_logs[_v] = [] """Variables pertaining to individual insurance firms""" - self.history_logs['individual_contracts'] = [] # TODO: Should there not be a similar record for reinsurance + self.history_logs['individual_contracts'] = [] self.history_logs['insurance_firms_cash'] = [] """Variables pertaining to reinsurance sector""" @@ -58,6 +58,7 @@ def __init__(self, no_riskmodels=None, rc_event_schedule_initial=None, rc_event_ """Variables pertaining to individual reinsurance firms""" self.history_logs['reinsurance_firms_cash'] = [] + self.history_logs['reinsurance_contracts'] = [] """Variables pertaining to cat bonds""" self.history_logs['total_catbondsoperational'] = [] @@ -73,11 +74,14 @@ def record_data(self, data_dict): data_dict: Type dict. Data with the same keys as are used in self.history_log(). Returns None.""" for key in data_dict.keys(): - if key != "individual_contracts": + if key != "individual_contracts" and key != 'reinsurance_contracts': self.history_logs[key].append(data_dict[key]) - else: + if key == "individual_contracts": for i in range(len(data_dict["individual_contracts"])): self.history_logs['individual_contracts'][i].append(data_dict["individual_contracts"][i]) + if key == "reinsurance_contracts": + for i in range(len(data_dict["reinsurance_contracts"])): + self.history_logs["reinsurance_contracts"][i].append(data_dict["reinsurance_contracts"][i]) def obtain_log(self, requested_logs=LOG_DEFAULT): #This function allows to return in a list all the data generated by the model. There is no other way to transfer it back from the cloud. """Method to transfer entire log (self.history_log as well as risk event schedule). This is @@ -115,7 +119,6 @@ def restore_logger_object(self, log): self.rc_event_schedule_initial = log["rc_event_schedule_initial"] self.rc_event_damage_initial = log["rc_event_damage_initial"] self.number_riskmodels = log["number_riskmodels"] - del log["rc_event_schedule_initial"], log["rc_event_damage_initial"], log["number_riskmodels"] """Restore history log""" self.history_logs = log @@ -175,3 +178,14 @@ def add_insurance_agent(self): zeroes_to_append = [] self.history_logs['individual_contracts'].append(zeroes_to_append) + def add_reinsurance_agent(self): + """Method for adding an additional insurer agent to the history log. This is necessary to keep the number + of individual insurance firm logs constant in time. + No arguments. + Returns None.""" + if len(self.history_logs['reinsurance_contracts']) > 0: + zeroes_to_append = list(np.zeros(len(self.history_logs['reinsurance_contracts'][0]), dtype=int)) + else: + zeroes_to_append = [] + self.history_logs['reinsurance_contracts'].append(zeroes_to_append) + diff --git a/visualisation.py b/visualisation.py index 2ce6814..5cecccd 100644 --- a/visualisation.py +++ b/visualisation.py @@ -1,13 +1,12 @@ # file to visualise data from a single and ensemble runs import numpy as np +import argparse import matplotlib.pyplot as plt import matplotlib.animation as animation -import argparse - class TimeSeries(object): - def __init__(self, series_list, title="",xlabel="Time", colour='k', axlst=None, fig=None, percentiles=None, alpha=0.7): + def __init__(self, series_list, event_schedule, damage_schedule, title="",xlabel="Time", colour='k', axlst=None, fig=None, percentiles=None, alpha=0.7): self.series_list = series_list self.size = len(series_list) self.xlabel = xlabel @@ -16,90 +15,157 @@ def __init__(self, series_list, title="",xlabel="Time", colour='k', axlst=None, self.percentiles = percentiles self.title = title self.timesteps = [t for t in range(len(series_list[0][0]))] # assume all data series are the same size + self.events_schedule = event_schedule + self.damage_schedule = damage_schedule if axlst is not None and fig is not None: self.axlst = axlst self.fig = fig else: self.fig, self.axlst = plt.subplots(self.size,sharex=True) - #self.plot() # we create the object when we want the plot so call plot() in the constructor - - def plot(self): + def plot(self, schedule=False): + event_categ_colours = ['r', 'b', 'g', 'fuchsia'] for i, (series, series_label, fill_lower, fill_upper) in enumerate(self.series_list): self.axlst[i].plot(self.timesteps, series,color=self.colour) self.axlst[i].set_ylabel(series_label) + if fill_lower is not None and fill_upper is not None: self.axlst[i].fill_between(self.timesteps, fill_lower, fill_upper, color=self.colour, alpha=self.alpha) + + if schedule: # Plots vertical lines for events if set. + for categ in range(len(self.events_schedule)): + for event_time in self.events_schedule[categ]: + index = self.events_schedule[categ].index(event_time) + if self.damage_schedule[categ][index] > 0.5: # Only plots line if event is significant + self.axlst[i].axvline(event_time, color=event_categ_colours[categ], alpha=self.damage_schedule[categ][index]) self.axlst[self.size-1].set_xlabel(self.xlabel) self.fig.suptitle(self.title) + return self.fig, self.axlst def save(self, filename): self.fig.savefig("{filename}".format(filename=filename)) return + class InsuranceFirmAnimation(object): - '''class takes in a run of insurance data and produces animations ''' - def __init__(self, data): - self.data = data - self.fig, self.ax = plt.subplots() + """Initialising method for the animation of insurance firm data. + Accepts: + cash_data: Type List of List of Lists: Contains the operational, ID and cash for each firm for each time. + insure_contracts: Type List of Lists. Contains number of underwritten contracts for each firm for each time. + event_schedule: Type List of Lists. Contains event times by category. + type: Type String. Used to specify which file to save to. + save: Type Boolean + perils: Type Boolean. For if screen should flash during peril time. + No return values. + This class takes the cash and contract data of each firm over all time and produces an animation showing how the + proportion of each for all operational firms changes with time. Allows it to be saved as an MP4.""" + def __init__(self, cash_data, insure_contracts, event_schedule, type, save=False, perils=False): + # Converts list of events by category into list of all events. + self.perils_condition = perils + self.all_event_times = [] + for categ in event_schedule: + self.all_event_times += categ + self.all_event_times.sort() + + # Setting data and creating pie chart animations. + self.cash_data = cash_data + self.insurance_contracts = insure_contracts + self.event_times_per_categ = event_schedule + + # If animation is saved or not + self.save_condition = save + self.type = type + + def animate(self): + self.pies = [0, 0] + self.fig, (self.ax1, self.ax2) = plt.subplots(1, 2) self.stream = self.data_stream() - self.ani = animation.FuncAnimation(self.fig, self.update, repeat=False, interval=100,) - #init_func=self.setup_plot) - - def setup_plot(self): - # initial drawing of the plot - casharr,idarr = next(self.stream) - self.pie = self.ax.pie(casharr, labels=idarr,autopct='%1.0f%%') - return self.pie, + self.animate = animation.FuncAnimation(self.fig, self.update, repeat=False, interval=20, save_count=98) + if self.save_condition: + self.save() def data_stream(self): - # unpack data in a format ready for update() - for timestep in self.data: - casharr = [] - idarr = [] + """Method to get the next set of firm data. + No accpeted values + Yields: + firm_cash_list: Type List. Contains the cash of each firm. + firm_id_list: Type List. Contains the unique ID of each firm. + firm_contract_list: Type List. Contains the number of underwritten contracts for each firm. + This iterates once every time it is called from the update method as it gets the next frame of data for the pie + charts.""" + t = 0 + for timestep in self.cash_data: + firm_cash_list = [] + firm_id_list = [] + firm_contract_list = [] for (cash, id, operational) in timestep: if operational: - casharr.append(cash) - idarr.append(id) - yield casharr,idarr + firm_id_list.append(id) + firm_cash_list.append(cash) + firm_contract_list.append(self.insurance_contracts[id][t]) + yield firm_cash_list, firm_id_list, firm_contract_list + t += 1 def update(self, i): - # clear plot and redraw - self.ax.clear() - self.ax.axis('equal') - casharr,idarr = next(self.stream) - self.pie = self.ax.pie(casharr, labels=idarr,autopct='%1.0f%%') - self.ax.set_title("Timestep : {:,.0f} | Total cash : {:,.0f}".format(i,sum(casharr))) - return self.pie, + """Method to update the animation frame. + Accepts: + i: Type Integer, iteration number. + Returns: + self.pies: Type List. + This method is called or each iteration of the FuncAnimation and clears and redraws the pie charts onto the + axis, getting data from data_stream method. Can also be set such that the figure flashes red at an event time.""" + self.ax1.clear() + self.ax2.clear() + self.ax1.axis('equal') + self.ax2.axis('equal') + cash_list, id_list, con_list = next(self.stream) + self.pies[0] = self.ax1.pie(cash_list, labels=id_list, autopct='%1.0f%%') + self.ax1.set_title("Total cash : {:,.0f}".format(sum(cash_list))) + self.pies[1] = self.ax2.pie(con_list, labels=id_list, autopct='%1.0f%%') + self.ax2.set_title("Total contracts : {:,.0f}".format(sum(con_list))) + self.fig.suptitle("%s Timestep : %i" % (self.type, i)) + if self.perils_condition: + if i == self.all_event_times[0]: + self.fig.set_facecolor('r') + self.all_event_times = self.all_event_times[1:] + else: + self.fig.set_facecolor('w') + return self.pies - def save(self,filename): - self.ani.save(filename, writer='ffmpeg', dpi=80) + def save(self): + """Method to save the animation as mp4. Dependant on type of firm. + No accepted values. + No return values.""" + if self.type == "Insurance Firm": + self.animate.save("data/animated_insurfirm_pie.mp4", writer="ffmpeg", dpi=200, fps=10) + elif self.type == "Reinsurance Firm": + self.animate.save("data/animated_reinsurefirm_pie.mp4", writer="ffmpeg", dpi=200, fps=10) + else: + print("Incorrect Type for Saving") - def show(self): - plt.show() class visualisation(object): def __init__(self, history_logs_list): self.history_logs_list = history_logs_list - # unused data in history_logs - #self.excess_capital = history_logs['total_excess_capital'] - #self.reinexcess_capital = history_logs['total_reinexcess_capital'] - #self.diffvar = history_logs['market_diffvar'] - #self.cumulative_bankruptcies = history_logs['cumulative_bankruptcies'] - #self.cumulative_unrecovered_claims = history_logs['cumulative_unrecovered_claims'] return def insurer_pie_animation(self, run=0): data = self.history_logs_list[run] insurance_cash = np.array(data['insurance_firms_cash']) - self.ins_pie_anim = InsuranceFirmAnimation(insurance_cash) + contract_data = self.history_logs_list[0]['individual_contracts'] + event_schedule = self.history_logs_list[0]["rc_event_schedule_initial"] + self.ins_pie_anim = InsuranceFirmAnimation(insurance_cash, contract_data, event_schedule, 'Insurance Firm', save=True) + self.ins_pie_anim.animate() return self.ins_pie_anim def reinsurer_pie_animation(self, run=0): data = self.history_logs_list[run] reinsurance_cash = np.array(data['reinsurance_firms_cash']) - self.reins_pie_anim = InsuranceFirmAnimation(reinsurance_cash) + contract_data = self.history_logs_list[0]['reinsurance_contracts'] + event_schedule = self.history_logs_list[0]["rc_event_schedule_initial"] + self.reins_pie_anim = InsuranceFirmAnimation(reinsurance_cash, contract_data, event_schedule, 'Reinsurance Firm', save=True) + self.reins_pie_anim.animate() return self.reins_pie_anim def insurer_time_series(self, runs=None, axlst=None, fig=None, title="Insurer", colour='black', percentiles=[25,75]): @@ -108,7 +174,7 @@ def insurer_time_series(self, runs=None, axlst=None, fig=None, title="Insurer", data = [self.history_logs_list[x] for x in runs] else: data = self.history_logs_list - + # Take the element-wise means/medians of the ensemble set (axis=0) contracts_agg = [history_logs['total_contracts'] for history_logs in self.history_logs_list] profitslosses_agg = [history_logs['total_profitslosses'] for history_logs in self.history_logs_list] @@ -122,13 +188,16 @@ def insurer_time_series(self, runs=None, axlst=None, fig=None, title="Insurer", cash = np.median(cash_agg, axis=0) premium = np.median(premium_agg, axis=0) + events = self.history_logs_list[0]["rc_event_schedule_initial"] + damages = self.history_logs_list[0]['rc_event_damage_initial'] + self.ins_time_series = TimeSeries([ (contracts, 'Contracts', np.percentile(contracts_agg,percentiles[0], axis=0), np.percentile(contracts_agg, percentiles[1], axis=0)), (profitslosses, 'Profitslosses', np.percentile(profitslosses_agg,percentiles[0], axis=0), np.percentile(profitslosses_agg, percentiles[1], axis=0)), (operational, 'Operational', np.percentile(operational_agg,percentiles[0], axis=0), np.percentile(operational_agg, percentiles[1], axis=0)), (cash, 'Cash', np.percentile(cash_agg,percentiles[0], axis=0), np.percentile(cash_agg, percentiles[1], axis=0)), - (premium, "Premium", np.percentile(premium_agg,percentiles[0], axis=0), np.percentile(premium_agg, percentiles[1], axis=0)), - ],title=title, xlabel = "Time", axlst=axlst, fig=fig, colour=colour).plot() + (premium, "Premium", np.percentile(premium_agg,percentiles[0], axis=0), np.percentile(premium_agg, percentiles[1], axis=0))], events, damages, title=title, xlabel="Time", axlst=axlst, fig=fig, colour=colour) + self.ins_time_series.plot(schedule=True) return self.ins_time_series def reinsurer_time_series(self, runs=None, axlst=None, fig=None, title="Reinsurer", colour='black', percentiles=[25,75]): @@ -151,13 +220,17 @@ def reinsurer_time_series(self, runs=None, axlst=None, fig=None, title="Reinsure reincash = np.median(reincash_agg, axis=0) catbonds_number = np.median(catbonds_number_agg, axis=0) + events = self.history_logs_list[0]["rc_event_schedule_initial"] + damages = self.history_logs_list[0]['rc_event_damage_initial'] + self.reins_time_series = TimeSeries([ (reincontracts, 'Contracts', np.percentile(reincontracts_agg,percentiles[0], axis=0), np.percentile(reincontracts_agg, percentiles[1], axis=0)), (reinprofitslosses, 'Profitslosses', np.percentile(reinprofitslosses_agg,percentiles[0], axis=0), np.percentile(reinprofitslosses_agg, percentiles[1], axis=0)), (reinoperational, 'Operational', np.percentile(reinoperational_agg,percentiles[0], axis=0), np.percentile(reinoperational_agg, percentiles[1], axis=0)), (reincash, 'Cash', np.percentile(reincash_agg,percentiles[0], axis=0), np.percentile(reincash_agg, percentiles[1], axis=0)), (catbonds_number, "Activate Cat Bonds", np.percentile(catbonds_number_agg,percentiles[0], axis=0), np.percentile(catbonds_number_agg, percentiles[1], axis=0)), - ],title= title, xlabel = "Time", axlst=axlst, fig=fig, colour=colour).plot() + ], events, damages, title=title, xlabel="Time", axlst=axlst, fig=fig, colour=colour) + self.reins_time_series.plot() return self.reins_time_series def metaplotter_timescale(self): @@ -178,6 +251,7 @@ def show(self): plt.show() return + class compare_riskmodels(object): def __init__(self,vis_list, colour_list): # take in list of visualisation objects and call their plot methods @@ -198,36 +272,35 @@ def create_reinsurer_timeseries(self, fig=None, axlst=None, percentiles=[25,75]) def show(self): plt.show() + def save(self): # logic to save plots pass -if __name__ == "__main__": - +if __name__ == "__main__": # use argparse to handle command line arguments parser = argparse.ArgumentParser(description='Model the Insurance sector') parser.add_argument("--single", action="store_true", help="plot time series of a single run of the insurance model") parser.add_argument("--comparison", action="store_true", help="plot the result of an ensemble of replicatons of the insurance model") args = parser.parse_args() - - + args.single = True if args.single: # load in data from the history_logs dictionarywith open("data/history_logs.dat","r") as rfile: with open("data/history_logs.dat","r") as rfile: history_logs_list = [eval(k) for k in rfile] # one dict on each line + # first create visualisation object, then create graph/animation objects as necessary vis = visualisation(history_logs_list) vis.insurer_pie_animation() vis.reinsurer_pie_animation() - vis.insurer_time_series() - vis.reinsurer_time_series() + # vis.insurer_time_series() + # vis.reinsurer_time_series() vis.show() N = len(history_logs_list) - if args.comparison: # for each run, generate an animation and time series for insurer and reinsurer From 361f02ae194c93be2baf6e582f7018aa772f2b05 Mon Sep 17 00:00:00 2001 From: Chris Hughes Date: Tue, 16 Jul 2019 11:09:08 +0100 Subject: [PATCH 040/125] Minor tweaks --- insurancecontract.py | 3 +-- insurancesimulation.py | 2 +- metainsurancecontract.py | 4 ++-- metainsuranceorg.py | 2 -- riskmodel.py | 4 ++++ 5 files changed, 8 insertions(+), 7 deletions(-) diff --git a/insurancecontract.py b/insurancecontract.py index a400821..eb7035e 100644 --- a/insurancecontract.py +++ b/insurancecontract.py @@ -24,7 +24,7 @@ def __init__( excess_fraction=None, reinsurance=0, ): - super(InsuranceContract, self).__init__( + super().__init__( insurer, properties, time, @@ -52,7 +52,6 @@ def explode(self, time, uniform_value, damage_extent): No return value. For registering damage and creating resulting claims (and payment obligations).""" # np.mean(np.random.beta(1, 1./mu -1, size=90000)) - # if np.random.uniform(0, 1) < self.risk_factor: if uniform_value < self.risk_factor: claim = min(self.excess, damage_extent * self.value) - self.deductible self.insurer.register_claim( diff --git a/insurancesimulation.py b/insurancesimulation.py index 8b87398..3b32e4a 100644 --- a/insurancesimulation.py +++ b/insurancesimulation.py @@ -1046,7 +1046,7 @@ def compute_market_diffvar(self): ) totalreal = len([firm for firm in self.insurancefirms if firm.operational]) - # Real VaR is 1 for each firm + # Real VaR is 1 for each firm, we think totalina += sum( [ diff --git a/metainsurancecontract.py b/metainsurancecontract.py index 8cec74c..627ed35 100644 --- a/metainsurancecontract.py +++ b/metainsurancecontract.py @@ -27,8 +27,8 @@ def __init__( initial_var: Type float. Initial value at risk. Used only to compute true and estimated value at risk. optional: insurancetype: Type string. The type of this contract, especially "proportional" vs "excess_of_loss" - deductible: Type float (or int) - excess: Type float (or int or None) + deductible_fraction: Type float (or int) + excess_fraction: Type float (or int or None) reinsurance: Type float (or int). The value that is being reinsured. Returns InsuranceContract. Creates InsuranceContract, saves parameters. Creates obligation for premium payment. Includes contract diff --git a/metainsuranceorg.py b/metainsuranceorg.py index bc6ef15..3f038de 100644 --- a/metainsuranceorg.py +++ b/metainsuranceorg.py @@ -852,8 +852,6 @@ def process_newrisks_insurer( # to insure it # Here it is check whether the portfolio is balanced or not if the risk (risk_to_insure) is # underwritten. Return True if it is balanced. False otherwise. - # QUERY: Why is the premium always the market premium? Isn't the setting of premiums an - # important part of the risk model? No if condition: contract = InsuranceContract( self, diff --git a/riskmodel.py b/riskmodel.py index 79b16bd..7993542 100644 --- a/riskmodel.py +++ b/riskmodel.py @@ -142,6 +142,7 @@ def evaluate_proportional(self, risks, cash): * average_exposure * self.margin_of_safety ) + # QUERY: Is the margin of safety appiled twice? (above and below) # record liquidity requirement and apply margin of safety for liquidity requirement necessary_liquidity += ( @@ -244,6 +245,9 @@ def evaluate_excess_of_loss(self, risks, cash, offered_risk=None): * risk["risk_factor"] * self.inaccuracy[categ_id] ) + # QUERY: This doesn't look accurate to me - E(f(X)) != f(E(X)) in general + + # QUERY: Isn't this wrong? expected_claim = ( min(expected_damage, risk["excess"]) - risk["deductible"] ) From bbc1b08a605678488532c8b46a3abfcb6ccfde7f Mon Sep 17 00:00:00 2001 From: Chris Hughes Date: Tue, 16 Jul 2019 11:37:10 +0100 Subject: [PATCH 041/125] Fixed bug due to slightly improper merge --- insurancesimulation.py | 57 ++---- logger.py | 133 ++++++++------ metainsuranceorg.py | 8 +- setup_simulation.py | 4 +- start.py | 5 +- visualisation.py | 402 ++++++++++++++++++++++++++++++++--------- 6 files changed, 419 insertions(+), 190 deletions(-) diff --git a/insurancesimulation.py b/insurancesimulation.py index bf6527c..266e64d 100644 --- a/insurancesimulation.py +++ b/insurancesimulation.py @@ -388,6 +388,8 @@ def add_agents(self, agent_class, agent_class_string, agents=None, n=1): for ap in agent_parameters ] self.reinsurancefirms += agents + for agent in agents: + self.logger.add_reinsurance_agent() elif agent_class_string == "catbond": raise ValueError(f"Catbonds must be built before being added") @@ -510,7 +512,7 @@ def iterate(self, t): if reinsurer.riskmodel.inaccuracy == self.inaccuracy[i]: self.reinsurance_models_counter[i] += 1 - network_division = 1 # How often network is updated. + network_division = 1 # How often network is updated. if isleconfig.show_network and t % network_division == 0 and t > 0: if t == network_division: self.RN = ( @@ -530,57 +532,34 @@ def save_data(self): [insurancefirm.cash for insurancefirm in self.insurancefirms] ) total_excess_capital = sum( - [ - firm.get_excess_capital() - for firm in self.insurancefirms - ] + [firm.get_excess_capital() for firm in self.insurancefirms] ) total_profitslosses = sum( [insurancefirm.get_profitslosses() for insurancefirm in self.insurancefirms] ) total_contracts_no = sum( - [ - len(firm.underwritten_contracts) - for firm in self.insurancefirms - ] - ) - total_reincash_no = sum( - [firm.cash for firm in self.reinsurancefirms] + [len(firm.underwritten_contracts) for firm in self.insurancefirms] ) + total_reincash_no = sum([firm.cash for firm in self.reinsurancefirms]) total_reinexcess_capital = sum( - [ - firm.get_excess_capital() - for firm in self.reinsurancefirms - ] + [firm.get_excess_capital() for firm in self.reinsurancefirms] ) total_reinprofitslosses = sum( - [ - firm.get_profitslosses() - for firm in self.reinsurancefirms - ] + [firm.get_profitslosses() for firm in self.reinsurancefirms] ) total_reincontracts_no = sum( - [ - len(firm.underwritten_contracts) - for firm in self.reinsurancefirms - ] - ) - operational_no = sum( - [firm.operational for firm in self.insurancefirms] - ) - reinoperational_no = sum( - [firm.operational for firm in self.reinsurancefirms] + [len(firm.underwritten_contracts) for firm in self.reinsurancefirms] ) + operational_no = sum([firm.operational for firm in self.insurancefirms]) + reinoperational_no = sum([firm.operational for firm in self.reinsurancefirms]) catbondsoperational_no = sum([catbond.operational for catbond in self.catbonds]) """ collect agent-level data """ insurance_firms = [ - (firm.cash, firm.id, firm.operational) - for firm in self.insurancefirms + (firm.cash, firm.id, firm.operational) for firm in self.insurancefirms ] reinsurance_firms = [ - (firm.cash, firm.id, firm.operational) - for firm in self.reinsurancefirms + (firm.cash, firm.id, firm.operational) for firm in self.reinsurancefirms ] """ prepare dict """ @@ -612,14 +591,12 @@ def save_data(self): current_log["market_diffvar"] = self.compute_market_diffvar() current_log["individual_contracts"] = [ - len(firm.underwritten_contracts) - for firm in self.insurancefirms + len(firm.underwritten_contracts) for firm in self.insurancefirms ] - current_log['reinsurance_contracts'] = [] - reinsurance_contracts_no = [len(reinsurancefirm.underwritten_contracts) for reinsurancefirm in self.reinsurancefirms] - for i in range(len(reinsurance_contracts_no)): - current_log['reinsurance_contracts'].append(reinsurance_contracts_no[i]) + current_log["reinsurance_contracts"] = [ + len(firm.underwritten_contracts) for firm in self.reinsurancefirms + ] """ call to Logger object """ self.logger.record_data(current_log) diff --git a/logger.py b/logger.py index 93ce229..d45d2ea 100644 --- a/logger.py +++ b/logger.py @@ -5,16 +5,22 @@ import listify LOG_DEFAULT = ( - 'total_cash total_excess_capital total_profitslosses total_contracts ' - 'total_operational total_reincash total_reinexcess_capital total_reinprofitslosses ' - 'total_reincontracts total_reinoperational total_catbondsoperational market_premium ' - 'market_reinpremium cumulative_bankruptcies cumulative_market_exits cumulative_unrecovered_claims ' - 'cumulative_claims insurance_firms_cash reinsurance_firms_cash market_diffvar ' - 'rc_event_schedule_initial rc_event_damage_initial number_riskmodels individual_contracts reinsurance_contracts' -).split(' ') - -class Logger(): - def __init__(self, no_riskmodels=None, rc_event_schedule_initial=None, rc_event_damage_initial=None): + "total_cash total_excess_capital total_profitslosses total_contracts " + "total_operational total_reincash total_reinexcess_capital total_reinprofitslosses " + "total_reincontracts total_reinoperational total_catbondsoperational market_premium " + "market_reinpremium cumulative_bankruptcies cumulative_market_exits cumulative_unrecovered_claims " + "cumulative_claims insurance_firms_cash reinsurance_firms_cash market_diffvar " + "rc_event_schedule_initial rc_event_damage_initial number_riskmodels individual_contracts reinsurance_contracts" +).split(" ") + + +class Logger: + def __init__( + self, + no_riskmodels=None, + rc_event_schedule_initial=None, + rc_event_damage_initial=None, + ): """Constructor. Prepares history_logs atribute as dict for the logs. Records initial event schedule of simulation run. Arguments @@ -22,86 +28,94 @@ def __init__(self, no_riskmodels=None, rc_event_schedule_initial=None, rc_event_ rc_event_schedule_initial: list of lists of int. Times of risk events by category rc_event_damage_initial: list of arrays (or lists) of float. Damage by peril for each category as share of total possible damage (maximum insured or excess). - Returns class instance.""" - + Returns class instance.""" + """Record number of riskmodels""" self.number_riskmodels = no_riskmodels - + """Record initial event schedule""" self.rc_event_schedule_initial = rc_event_schedule_initial self.rc_event_damage_initial = rc_event_damage_initial """Prepare history log dict""" self.history_logs = {} - + """Variables pertaining to insurance sector""" # TODO: should we not have `cumulative_bankruptcies` and # `cumulative_market_exits` for both insurance firms and reinsurance firms? # `cumulative_claims`: Here are stored the total cumulative claims received # by the whole insurance sector until a certain time. - insurance_sector = ('total_cash total_excess_capital total_profitslosses ' - 'total_contracts total_operational cumulative_bankruptcies ' - 'cumulative_market_exits cumulative_claims cumulative_unrecovered_claims').split(' ') + insurance_sector = ( + "total_cash total_excess_capital total_profitslosses " + "total_contracts total_operational cumulative_bankruptcies " + "cumulative_market_exits cumulative_claims cumulative_unrecovered_claims" + ).split(" ") for _v in insurance_sector: self.history_logs[_v] = [] - + """Variables pertaining to individual insurance firms""" - self.history_logs['individual_contracts'] = [] - self.history_logs['insurance_firms_cash'] = [] - + self.history_logs["individual_contracts"] = [] + self.history_logs["insurance_firms_cash"] = [] + """Variables pertaining to reinsurance sector""" - self.history_logs['total_reincash'] = [] - self.history_logs['total_reinexcess_capital'] = [] - self.history_logs['total_reinprofitslosses'] = [] - self.history_logs['total_reincontracts'] = [] - self.history_logs['total_reinoperational'] = [] + self.history_logs["total_reincash"] = [] + self.history_logs["total_reinexcess_capital"] = [] + self.history_logs["total_reinprofitslosses"] = [] + self.history_logs["total_reincontracts"] = [] + self.history_logs["total_reinoperational"] = [] """Variables pertaining to individual reinsurance firms""" - self.history_logs['reinsurance_firms_cash'] = [] - self.history_logs['reinsurance_contracts'] = [] + self.history_logs["reinsurance_firms_cash"] = [] + self.history_logs["reinsurance_contracts"] = [] """Variables pertaining to cat bonds""" - self.history_logs['total_catbondsoperational'] = [] + self.history_logs["total_catbondsoperational"] = [] """Variables pertaining to premiums""" - self.history_logs['market_premium'] = [] - self.history_logs['market_reinpremium'] = [] - self.history_logs['market_diffvar'] = [] - + self.history_logs["market_premium"] = [] + self.history_logs["market_reinpremium"] = [] + self.history_logs["market_diffvar"] = [] + def record_data(self, data_dict): """Method to record data for one period Arguments data_dict: Type dict. Data with the same keys as are used in self.history_log(). - Returns None.""" + Returns None.""" for key in data_dict.keys(): - if key != "individual_contracts" and key != 'reinsurance_contracts': + if key != "individual_contracts" and key != "reinsurance_contracts": self.history_logs[key].append(data_dict[key]) if key == "individual_contracts": for i in range(len(data_dict["individual_contracts"])): - self.history_logs['individual_contracts'][i].append(data_dict["individual_contracts"][i]) + self.history_logs["individual_contracts"][i].append( + data_dict["individual_contracts"][i] + ) if key == "reinsurance_contracts": for i in range(len(data_dict["reinsurance_contracts"])): - self.history_logs["reinsurance_contracts"][i].append(data_dict["reinsurance_contracts"][i]) + self.history_logs["reinsurance_contracts"][i].append( + data_dict["reinsurance_contracts"][i] + ) - def obtain_log(self, requested_logs=LOG_DEFAULT): #This function allows to return in a list all the data generated by the model. There is no other way to transfer it back from the cloud. + def obtain_log( + self, requested_logs=LOG_DEFAULT + ): # This function allows to return in a list all the data generated by the model. There is no other way to transfer it back from the cloud. """Method to transfer entire log (self.history_log as well as risk event schedule). This is used to transfer the log to master core from work cores in ensemble runs in the cloud. No arguments. Returns list (listified dict).""" - + """Include environment variables (number of risk models and risk event schedule)""" self.history_logs["number_riskmodels"] = self.number_riskmodels self.history_logs["rc_event_damage_initial"] = self.rc_event_damage_initial self.history_logs["rc_event_schedule_initial"] = self.rc_event_schedule_initial - + """Parse logs to be returned""" if requested_logs == None: requested_logs = LOG_DEFAULT log = {name: self.history_logs[name] for name in requested_logs} - + """Convert to list and return""" return listify.listify(log) - + def restore_logger_object(self, log): """Method to restore logger object. A log can be restored later. It can also be restored on a different machine. This is useful in the case of ensemble runs to move the log to @@ -114,12 +128,12 @@ def restore_logger_object(self, log): """Restore dict""" log = listify.delistify(log) - + """Extract environment variables (number of risk models and risk event schedule)""" self.rc_event_schedule_initial = log["rc_event_schedule_initial"] self.rc_event_damage_initial = log["rc_event_damage_initial"] self.number_riskmodels = log["number_riskmodels"] - + """Restore history log""" self.history_logs = log @@ -129,18 +143,18 @@ def save_log(self, background_run): Arguments: background_run: Type bool. Is this an ensemble run (true) or not (false). Returns None.""" - + """Prepare writing tasks""" if background_run: to_log = self.replication_log_prepare() else: to_log = self.single_log_prepare() - + """Write to disk""" for filename, data, operation_character in to_log: with open(filename, operation_character) as wfile: wfile.write(str(data) + "\n") - + def replication_log_prepare(self): """Method to prepare writing tasks for ensemble run saving. No arguments @@ -153,7 +167,7 @@ def replication_log_prepare(self): to_log = [] to_log.append(("data/" + fpf + "_history_logs.dat", self.history_logs, "a")) return to_log - + def single_log_prepare(self): """Method to prepare writing tasks for single run saving. No arguments @@ -164,28 +178,31 @@ def single_log_prepare(self): to_log = [] to_log.append(("data/history_logs.dat", self.history_logs, "w")) return to_log - - def add_insurance_agent(self): + + def add_insurance_agent(self): """Method for adding an additional insurer agent to the history log. This is necessary to keep the number of individual insurance firm logs constant in time. No arguments. Returns None.""" - # TODO: should this not also be done for self.history_logs['insurance_firms_cash'] and + # TODO: should this not also be done for self.history_logs['insurance_firms_cash'] and # self.history_logs['reinsurance_firms_cash'] - if len(self.history_logs['individual_contracts']) > 0: - zeroes_to_append = list(np.zeros(len(self.history_logs['individual_contracts'][0]), dtype=int)) + if len(self.history_logs["individual_contracts"]) > 0: + zeroes_to_append = list( + np.zeros(len(self.history_logs["individual_contracts"][0]), dtype=int) + ) else: zeroes_to_append = [] - self.history_logs['individual_contracts'].append(zeroes_to_append) + self.history_logs["individual_contracts"].append(zeroes_to_append) def add_reinsurance_agent(self): """Method for adding an additional insurer agent to the history log. This is necessary to keep the number of individual insurance firm logs constant in time. No arguments. Returns None.""" - if len(self.history_logs['reinsurance_contracts']) > 0: - zeroes_to_append = list(np.zeros(len(self.history_logs['reinsurance_contracts'][0]), dtype=int)) + if len(self.history_logs["reinsurance_contracts"]) > 0: + zeroes_to_append = list( + np.zeros(len(self.history_logs["reinsurance_contracts"][0]), dtype=int) + ) else: zeroes_to_append = [] - self.history_logs['reinsurance_contracts'].append(zeroes_to_append) - + self.history_logs["reinsurance_contracts"].append(zeroes_to_append) diff --git a/metainsuranceorg.py b/metainsuranceorg.py index 3f038de..c2979a3 100644 --- a/metainsuranceorg.py +++ b/metainsuranceorg.py @@ -616,8 +616,12 @@ def risks_reinrisks_organizer(self, new_risks): Returns: risks_by_category: Type list of categories, each contains risks originating from that category. number_risks_categ: Type list, elements are integers of total risks in each category""" - risks_by_category = [[] for _ in range(self.simulation_parameters["no_categories"])] - number_risks_categ = np.zeros(self.simulation_parameters["no_categories"], dtype=np.int_) + risks_by_category = [ + [] for _ in range(self.simulation_parameters["no_categories"]) + ] + number_risks_categ = np.zeros( + self.simulation_parameters["no_categories"], dtype=np.int_ + ) for categ_id in range(self.simulation_parameters["no_categories"]): risks_by_category[categ_id] = [ diff --git a/setup_simulation.py b/setup_simulation.py index 3fa638d..6e4ab6f 100644 --- a/setup_simulation.py +++ b/setup_simulation.py @@ -167,7 +167,9 @@ def obtain_ensemble(self, replications, filepath=None, overwrite=False): # Not replicating another run, so we are writing to the file given self.replications = replications if filepath is None and not self.overwrite: - print("No explicit path given, automatically overwriting default path for initial state") + print( + "No explicit path given, automatically overwriting default path for initial state" + ) self.overwrite = True self.schedule(replications) self.seeds(replications) diff --git a/start.py b/start.py index 79ac1f4..f534ca2 100644 --- a/start.py +++ b/start.py @@ -113,7 +113,10 @@ def save_simulation(t, sim, sim_param, exit_now=False): pickle.dump(d, wfile, protocol=pickle.HIGHEST_PROTOCOL) with open("data/simulation_save.pkl", "br") as rfile: file_contents = rfile.read() - print("\nSaved simulation with hash:", hashlib.sha512(str(file_contents).encode()).hexdigest()) + print( + "\nSaved simulation with hash:", + hashlib.sha512(str(file_contents).encode()).hexdigest(), + ) if exit_now: exit(0) diff --git a/visualisation.py b/visualisation.py index 5cecccd..2fae350 100644 --- a/visualisation.py +++ b/visualisation.py @@ -6,7 +6,19 @@ class TimeSeries(object): - def __init__(self, series_list, event_schedule, damage_schedule, title="",xlabel="Time", colour='k', axlst=None, fig=None, percentiles=None, alpha=0.7): + def __init__( + self, + series_list, + event_schedule, + damage_schedule, + title="", + xlabel="Time", + colour="k", + axlst=None, + fig=None, + percentiles=None, + alpha=0.7, + ): self.series_list = series_list self.size = len(series_list) self.xlabel = xlabel @@ -14,31 +26,47 @@ def __init__(self, series_list, event_schedule, damage_schedule, title="",xlabel self.alpha = alpha self.percentiles = percentiles self.title = title - self.timesteps = [t for t in range(len(series_list[0][0]))] # assume all data series are the same size + self.timesteps = [ + t for t in range(len(series_list[0][0])) + ] # assume all data series are the same size self.events_schedule = event_schedule self.damage_schedule = damage_schedule if axlst is not None and fig is not None: self.axlst = axlst self.fig = fig else: - self.fig, self.axlst = plt.subplots(self.size,sharex=True) + self.fig, self.axlst = plt.subplots(self.size, sharex=True) def plot(self, schedule=False): - event_categ_colours = ['r', 'b', 'g', 'fuchsia'] - for i, (series, series_label, fill_lower, fill_upper) in enumerate(self.series_list): - self.axlst[i].plot(self.timesteps, series,color=self.colour) + event_categ_colours = ["r", "b", "g", "fuchsia"] + for i, (series, series_label, fill_lower, fill_upper) in enumerate( + self.series_list + ): + self.axlst[i].plot(self.timesteps, series, color=self.colour) self.axlst[i].set_ylabel(series_label) if fill_lower is not None and fill_upper is not None: - self.axlst[i].fill_between(self.timesteps, fill_lower, fill_upper, color=self.colour, alpha=self.alpha) - - if schedule: # Plots vertical lines for events if set. + self.axlst[i].fill_between( + self.timesteps, + fill_lower, + fill_upper, + color=self.colour, + alpha=self.alpha, + ) + + if schedule: # Plots vertical lines for events if set. for categ in range(len(self.events_schedule)): for event_time in self.events_schedule[categ]: index = self.events_schedule[categ].index(event_time) - if self.damage_schedule[categ][index] > 0.5: # Only plots line if event is significant - self.axlst[i].axvline(event_time, color=event_categ_colours[categ], alpha=self.damage_schedule[categ][index]) - self.axlst[self.size-1].set_xlabel(self.xlabel) + if ( + self.damage_schedule[categ][index] > 0.5 + ): # Only plots line if event is significant + self.axlst[i].axvline( + event_time, + color=event_categ_colours[categ], + alpha=self.damage_schedule[categ][index], + ) + self.axlst[self.size - 1].set_xlabel(self.xlabel) self.fig.suptitle(self.title) return self.fig, self.axlst @@ -60,7 +88,16 @@ class InsuranceFirmAnimation(object): No return values. This class takes the cash and contract data of each firm over all time and produces an animation showing how the proportion of each for all operational firms changes with time. Allows it to be saved as an MP4.""" - def __init__(self, cash_data, insure_contracts, event_schedule, type, save=False, perils=False): + + def __init__( + self, + cash_data, + insure_contracts, + event_schedule, + type, + save=False, + perils=False, + ): # Converts list of events by category into list of all events. self.perils_condition = perils self.all_event_times = [] @@ -81,7 +118,9 @@ def animate(self): self.pies = [0, 0] self.fig, (self.ax1, self.ax2) = plt.subplots(1, 2) self.stream = self.data_stream() - self.animate = animation.FuncAnimation(self.fig, self.update, repeat=False, interval=20, save_count=98) + self.animate = animation.FuncAnimation( + self.fig, self.update, repeat=False, interval=20, save_count=98 + ) if self.save_condition: self.save() @@ -117,20 +156,20 @@ def update(self, i): axis, getting data from data_stream method. Can also be set such that the figure flashes red at an event time.""" self.ax1.clear() self.ax2.clear() - self.ax1.axis('equal') - self.ax2.axis('equal') + self.ax1.axis("equal") + self.ax2.axis("equal") cash_list, id_list, con_list = next(self.stream) - self.pies[0] = self.ax1.pie(cash_list, labels=id_list, autopct='%1.0f%%') + self.pies[0] = self.ax1.pie(cash_list, labels=id_list, autopct="%1.0f%%") self.ax1.set_title("Total cash : {:,.0f}".format(sum(cash_list))) - self.pies[1] = self.ax2.pie(con_list, labels=id_list, autopct='%1.0f%%') + self.pies[1] = self.ax2.pie(con_list, labels=id_list, autopct="%1.0f%%") self.ax2.set_title("Total contracts : {:,.0f}".format(sum(con_list))) self.fig.suptitle("%s Timestep : %i" % (self.type, i)) if self.perils_condition: if i == self.all_event_times[0]: - self.fig.set_facecolor('r') + self.fig.set_facecolor("r") self.all_event_times = self.all_event_times[1:] else: - self.fig.set_facecolor('w') + self.fig.set_facecolor("w") return self.pies def save(self): @@ -138,9 +177,13 @@ def save(self): No accepted values. No return values.""" if self.type == "Insurance Firm": - self.animate.save("data/animated_insurfirm_pie.mp4", writer="ffmpeg", dpi=200, fps=10) + self.animate.save( + "data/animated_insurfirm_pie.mp4", writer="ffmpeg", dpi=200, fps=10 + ) elif self.type == "Reinsurance Firm": - self.animate.save("data/animated_reinsurefirm_pie.mp4", writer="ffmpeg", dpi=200, fps=10) + self.animate.save( + "data/animated_reinsurefirm_pie.mp4", writer="ffmpeg", dpi=200, fps=10 + ) else: print("Incorrect Type for Saving") @@ -152,23 +195,39 @@ def __init__(self, history_logs_list): def insurer_pie_animation(self, run=0): data = self.history_logs_list[run] - insurance_cash = np.array(data['insurance_firms_cash']) - contract_data = self.history_logs_list[0]['individual_contracts'] + insurance_cash = np.array(data["insurance_firms_cash"]) + contract_data = self.history_logs_list[0]["individual_contracts"] event_schedule = self.history_logs_list[0]["rc_event_schedule_initial"] - self.ins_pie_anim = InsuranceFirmAnimation(insurance_cash, contract_data, event_schedule, 'Insurance Firm', save=True) + self.ins_pie_anim = InsuranceFirmAnimation( + insurance_cash, contract_data, event_schedule, "Insurance Firm", save=True + ) self.ins_pie_anim.animate() return self.ins_pie_anim def reinsurer_pie_animation(self, run=0): data = self.history_logs_list[run] - reinsurance_cash = np.array(data['reinsurance_firms_cash']) - contract_data = self.history_logs_list[0]['reinsurance_contracts'] + reinsurance_cash = np.array(data["reinsurance_firms_cash"]) + contract_data = self.history_logs_list[0]["reinsurance_contracts"] event_schedule = self.history_logs_list[0]["rc_event_schedule_initial"] - self.reins_pie_anim = InsuranceFirmAnimation(reinsurance_cash, contract_data, event_schedule, 'Reinsurance Firm', save=True) + self.reins_pie_anim = InsuranceFirmAnimation( + reinsurance_cash, + contract_data, + event_schedule, + "Reinsurance Firm", + save=True, + ) self.reins_pie_anim.animate() return self.reins_pie_anim - def insurer_time_series(self, runs=None, axlst=None, fig=None, title="Insurer", colour='black', percentiles=[25,75]): + def insurer_time_series( + self, + runs=None, + axlst=None, + fig=None, + title="Insurer", + colour="black", + percentiles=[25, 75], + ): # runs should be a list of the indexes you want included in the ensemble for consideration if runs: data = [self.history_logs_list[x] for x in runs] @@ -176,11 +235,22 @@ def insurer_time_series(self, runs=None, axlst=None, fig=None, title="Insurer", data = self.history_logs_list # Take the element-wise means/medians of the ensemble set (axis=0) - contracts_agg = [history_logs['total_contracts'] for history_logs in self.history_logs_list] - profitslosses_agg = [history_logs['total_profitslosses'] for history_logs in self.history_logs_list] - operational_agg = [history_logs['total_operational'] for history_logs in self.history_logs_list] - cash_agg = [history_logs['total_cash'] for history_logs in self.history_logs_list] - premium_agg = [history_logs['market_premium'] for history_logs in self.history_logs_list] + contracts_agg = [ + history_logs["total_contracts"] for history_logs in self.history_logs_list + ] + profitslosses_agg = [ + history_logs["total_profitslosses"] + for history_logs in self.history_logs_list + ] + operational_agg = [ + history_logs["total_operational"] for history_logs in self.history_logs_list + ] + cash_agg = [ + history_logs["total_cash"] for history_logs in self.history_logs_list + ] + premium_agg = [ + history_logs["market_premium"] for history_logs in self.history_logs_list + ] contracts = np.mean(contracts_agg, axis=0) profitslosses = np.mean(profitslosses_agg, axis=0) @@ -189,18 +259,61 @@ def insurer_time_series(self, runs=None, axlst=None, fig=None, title="Insurer", premium = np.median(premium_agg, axis=0) events = self.history_logs_list[0]["rc_event_schedule_initial"] - damages = self.history_logs_list[0]['rc_event_damage_initial'] - - self.ins_time_series = TimeSeries([ - (contracts, 'Contracts', np.percentile(contracts_agg,percentiles[0], axis=0), np.percentile(contracts_agg, percentiles[1], axis=0)), - (profitslosses, 'Profitslosses', np.percentile(profitslosses_agg,percentiles[0], axis=0), np.percentile(profitslosses_agg, percentiles[1], axis=0)), - (operational, 'Operational', np.percentile(operational_agg,percentiles[0], axis=0), np.percentile(operational_agg, percentiles[1], axis=0)), - (cash, 'Cash', np.percentile(cash_agg,percentiles[0], axis=0), np.percentile(cash_agg, percentiles[1], axis=0)), - (premium, "Premium", np.percentile(premium_agg,percentiles[0], axis=0), np.percentile(premium_agg, percentiles[1], axis=0))], events, damages, title=title, xlabel="Time", axlst=axlst, fig=fig, colour=colour) + damages = self.history_logs_list[0]["rc_event_damage_initial"] + + self.ins_time_series = TimeSeries( + [ + ( + contracts, + "Contracts", + np.percentile(contracts_agg, percentiles[0], axis=0), + np.percentile(contracts_agg, percentiles[1], axis=0), + ), + ( + profitslosses, + "Profitslosses", + np.percentile(profitslosses_agg, percentiles[0], axis=0), + np.percentile(profitslosses_agg, percentiles[1], axis=0), + ), + ( + operational, + "Operational", + np.percentile(operational_agg, percentiles[0], axis=0), + np.percentile(operational_agg, percentiles[1], axis=0), + ), + ( + cash, + "Cash", + np.percentile(cash_agg, percentiles[0], axis=0), + np.percentile(cash_agg, percentiles[1], axis=0), + ), + ( + premium, + "Premium", + np.percentile(premium_agg, percentiles[0], axis=0), + np.percentile(premium_agg, percentiles[1], axis=0), + ), + ], + events, + damages, + title=title, + xlabel="Time", + axlst=axlst, + fig=fig, + colour=colour, + ) self.ins_time_series.plot(schedule=True) return self.ins_time_series - def reinsurer_time_series(self, runs=None, axlst=None, fig=None, title="Reinsurer", colour='black', percentiles=[25,75]): + def reinsurer_time_series( + self, + runs=None, + axlst=None, + fig=None, + title="Reinsurer", + colour="black", + percentiles=[25, 75], + ): # runs should be a list of the indexes you want included in the ensemble for consideration if runs: data = [self.history_logs_list[x] for x in runs] @@ -208,11 +321,25 @@ def reinsurer_time_series(self, runs=None, axlst=None, fig=None, title="Reinsure data = self.history_logs_list # Take the element-wise means/medians of the ensemble set (axis=0) - reincontracts_agg = [history_logs['total_reincontracts'] for history_logs in self.history_logs_list] - reinprofitslosses_agg = [history_logs['total_reinprofitslosses'] for history_logs in self.history_logs_list] - reinoperational_agg = [history_logs['total_reinoperational'] for history_logs in self.history_logs_list] - reincash_agg = [history_logs['total_reincash'] for history_logs in self.history_logs_list] - catbonds_number_agg = [history_logs['total_catbondsoperational'] for history_logs in self.history_logs_list] + reincontracts_agg = [ + history_logs["total_reincontracts"] + for history_logs in self.history_logs_list + ] + reinprofitslosses_agg = [ + history_logs["total_reinprofitslosses"] + for history_logs in self.history_logs_list + ] + reinoperational_agg = [ + history_logs["total_reinoperational"] + for history_logs in self.history_logs_list + ] + reincash_agg = [ + history_logs["total_reincash"] for history_logs in self.history_logs_list + ] + catbonds_number_agg = [ + history_logs["total_catbondsoperational"] + for history_logs in self.history_logs_list + ] reincontracts = np.mean(reincontracts_agg, axis=0) reinprofitslosses = np.mean(reinprofitslosses_agg, axis=0) @@ -221,30 +348,115 @@ def reinsurer_time_series(self, runs=None, axlst=None, fig=None, title="Reinsure catbonds_number = np.median(catbonds_number_agg, axis=0) events = self.history_logs_list[0]["rc_event_schedule_initial"] - damages = self.history_logs_list[0]['rc_event_damage_initial'] - - self.reins_time_series = TimeSeries([ - (reincontracts, 'Contracts', np.percentile(reincontracts_agg,percentiles[0], axis=0), np.percentile(reincontracts_agg, percentiles[1], axis=0)), - (reinprofitslosses, 'Profitslosses', np.percentile(reinprofitslosses_agg,percentiles[0], axis=0), np.percentile(reinprofitslosses_agg, percentiles[1], axis=0)), - (reinoperational, 'Operational', np.percentile(reinoperational_agg,percentiles[0], axis=0), np.percentile(reinoperational_agg, percentiles[1], axis=0)), - (reincash, 'Cash', np.percentile(reincash_agg,percentiles[0], axis=0), np.percentile(reincash_agg, percentiles[1], axis=0)), - (catbonds_number, "Activate Cat Bonds", np.percentile(catbonds_number_agg,percentiles[0], axis=0), np.percentile(catbonds_number_agg, percentiles[1], axis=0)), - ], events, damages, title=title, xlabel="Time", axlst=axlst, fig=fig, colour=colour) + damages = self.history_logs_list[0]["rc_event_damage_initial"] + + self.reins_time_series = TimeSeries( + [ + ( + reincontracts, + "Contracts", + np.percentile(reincontracts_agg, percentiles[0], axis=0), + np.percentile(reincontracts_agg, percentiles[1], axis=0), + ), + ( + reinprofitslosses, + "Profitslosses", + np.percentile(reinprofitslosses_agg, percentiles[0], axis=0), + np.percentile(reinprofitslosses_agg, percentiles[1], axis=0), + ), + ( + reinoperational, + "Operational", + np.percentile(reinoperational_agg, percentiles[0], axis=0), + np.percentile(reinoperational_agg, percentiles[1], axis=0), + ), + ( + reincash, + "Cash", + np.percentile(reincash_agg, percentiles[0], axis=0), + np.percentile(reincash_agg, percentiles[1], axis=0), + ), + ( + catbonds_number, + "Activate Cat Bonds", + np.percentile(catbonds_number_agg, percentiles[0], axis=0), + np.percentile(catbonds_number_agg, percentiles[1], axis=0), + ), + ], + events, + damages, + title=title, + xlabel="Time", + axlst=axlst, + fig=fig, + colour=colour, + ) self.reins_time_series.plot() return self.reins_time_series def metaplotter_timescale(self): # Take the element-wise means/medians of the ensemble set (axis=0) - contracts = np.mean([history_logs['total_contracts'] for history_logs in self.history_logs_list],axis=0) - profitslosses = np.mean([history_logs['total_profitslosses'] for history_logs in self.history_logs_list],axis=0) - operational = np.median([history_logs['total_operational'] for history_logs in self.history_logs_list],axis=0) - cash = np.median([history_logs['total_cash'] for history_logs in self.history_logs_list],axis=0) - premium = np.median([history_logs['market_premium'] for history_logs in self.history_logs_list],axis=0) - reincontracts = np.mean([history_logs['total_reincontracts'] for history_logs in self.history_logs_list],axis=0) - reinprofitslosses = np.mean([history_logs['total_reinprofitslosses'] for history_logs in self.history_logs_list],axis=0) - reinoperational = np.median([history_logs['total_reinoperational'] for history_logs in self.history_logs_list],axis=0) - reincash = np.median([history_logs['total_reincash'] for history_logs in self.history_logs_list],axis=0) - catbonds_number = np.median([history_logs['total_catbondsoperational'] for history_logs in self.history_logs_list],axis=0) + contracts = np.mean( + [ + history_logs["total_contracts"] + for history_logs in self.history_logs_list + ], + axis=0, + ) + profitslosses = np.mean( + [ + history_logs["total_profitslosses"] + for history_logs in self.history_logs_list + ], + axis=0, + ) + operational = np.median( + [ + history_logs["total_operational"] + for history_logs in self.history_logs_list + ], + axis=0, + ) + cash = np.median( + [history_logs["total_cash"] for history_logs in self.history_logs_list], + axis=0, + ) + premium = np.median( + [history_logs["market_premium"] for history_logs in self.history_logs_list], + axis=0, + ) + reincontracts = np.mean( + [ + history_logs["total_reincontracts"] + for history_logs in self.history_logs_list + ], + axis=0, + ) + reinprofitslosses = np.mean( + [ + history_logs["total_reinprofitslosses"] + for history_logs in self.history_logs_list + ], + axis=0, + ) + reinoperational = np.median( + [ + history_logs["total_reinoperational"] + for history_logs in self.history_logs_list + ], + axis=0, + ) + reincash = np.median( + [history_logs["total_reincash"] for history_logs in self.history_logs_list], + axis=0, + ) + catbonds_number = np.median( + [ + history_logs["total_catbondsoperational"] + for history_logs in self.history_logs_list + ], + axis=0, + ) return def show(self): @@ -253,22 +465,26 @@ def show(self): class compare_riskmodels(object): - def __init__(self,vis_list, colour_list): + def __init__(self, vis_list, colour_list): # take in list of visualisation objects and call their plot methods self.vis_list = vis_list self.colour_list = colour_list - - def create_insurer_timeseries(self, fig=None, axlst=None, percentiles=[25,75]): + + def create_insurer_timeseries(self, fig=None, axlst=None, percentiles=[25, 75]): # create the time series for each object in turn and superpose them? fig = axlst = None - for vis,colour in zip(self.vis_list, self.colour_list): - (fig, axlst) = vis.insurer_time_series(fig=fig, axlst=axlst, colour=colour, percentiles=percentiles) + for vis, colour in zip(self.vis_list, self.colour_list): + (fig, axlst) = vis.insurer_time_series( + fig=fig, axlst=axlst, colour=colour, percentiles=percentiles + ) - def create_reinsurer_timeseries(self, fig=None, axlst=None, percentiles=[25,75]): + def create_reinsurer_timeseries(self, fig=None, axlst=None, percentiles=[25, 75]): # create the time series for each object in turn and superpose them? fig = axlst = None - for vis,colour in zip(self.vis_list, self.colour_list): - (fig, axlst) = vis.reinsurer_time_series(fig=fig, axlst=axlst, colour=colour, percentiles=percentiles) + for vis, colour in zip(self.vis_list, self.colour_list): + (fig, axlst) = vis.reinsurer_time_series( + fig=fig, axlst=axlst, colour=colour, percentiles=percentiles + ) def show(self): plt.show() @@ -276,21 +492,29 @@ def show(self): def save(self): # logic to save plots pass - + if __name__ == "__main__": # use argparse to handle command line arguments - parser = argparse.ArgumentParser(description='Model the Insurance sector') - parser.add_argument("--single", action="store_true", help="plot time series of a single run of the insurance model") - parser.add_argument("--comparison", action="store_true", help="plot the result of an ensemble of replicatons of the insurance model") + parser = argparse.ArgumentParser(description="Model the Insurance sector") + parser.add_argument( + "--single", + action="store_true", + help="plot time series of a single run of the insurance model", + ) + parser.add_argument( + "--comparison", + action="store_true", + help="plot the result of an ensemble of replicatons of the insurance model", + ) args = parser.parse_args() args.single = True if args.single: # load in data from the history_logs dictionarywith open("data/history_logs.dat","r") as rfile: - with open("data/history_logs.dat","r") as rfile: - history_logs_list = [eval(k) for k in rfile] # one dict on each line + with open("data/history_logs.dat", "r") as rfile: + history_logs_list = [eval(k) for k in rfile] # one dict on each line # first create visualisation object, then create graph/animation objects as necessary vis = visualisation(history_logs_list) @@ -305,21 +529,23 @@ def save(self): # for each run, generate an animation and time series for insurer and reinsurer # TODO: provide some way for these to be lined up nicely rather than having to manually arrange screen - #for i in range(N): + # for i in range(N): # vis.insurer_pie_animation(run=i) # vis.insurer_time_series(runs=[i]) # vis.reinsurer_pie_animation(run=i) # vis.reinsurer_time_series(runs=[i]) # vis.show() vis_list = [] - filenames = ["./data/"+x+"_history_logs.dat" for x in ["one","two","three","four"]] + filenames = [ + "./data/" + x + "_history_logs.dat" for x in ["one", "two", "three", "four"] + ] for filename in filenames: - with open(filename,'r') as rfile: - history_logs_list = [eval(k) for k in rfile] # one dict on each line + with open(filename, "r") as rfile: + history_logs_list = [eval(k) for k in rfile] # one dict on each line vis_list.append(visualisation(history_logs_list)) - colour_list = ['blue', 'yellow', 'red', 'green'] + colour_list = ["blue", "yellow", "red", "green"] cmp_rsk = compare_riskmodels(vis_list, colour_list) - cmp_rsk.create_insurer_timeseries(percentiles=[10,90]) - cmp_rsk.create_reinsurer_timeseries(percentiles=[10,90]) + cmp_rsk.create_insurer_timeseries(percentiles=[10, 90]) + cmp_rsk.create_reinsurer_timeseries(percentiles=[10, 90]) cmp_rsk.show() From 6c7bc050ffc693b1f321af260700c0d9c2512a0a Mon Sep 17 00:00:00 2001 From: Chris Hughes Date: Tue, 16 Jul 2019 14:19:20 +0100 Subject: [PATCH 042/125] Implemented scaling of insurance (not reinsurance) premiums and risk model inaccuracy based on firm size. A firm that holds many risks will have a less inaccurate model and charge higher premiums than a firm that holds few risks. --- insurancefirm.py | 8 +++--- insurancesimulation.py | 37 +++++++++++++++++++++++--- isleconfig.py | 6 +++++ metainsuranceorg.py | 59 +++++++++++++++++++++++++++++++++++------- 4 files changed, 94 insertions(+), 16 deletions(-) diff --git a/insurancefirm.py b/insurancefirm.py index fadde73..974365a 100644 --- a/insurancefirm.py +++ b/insurancefirm.py @@ -98,8 +98,10 @@ def get_capacity(self, max_var): if max_var < self.cash: reinsurance_var_estimate = self.get_reinsurance_var_estimate(max_var) return self.cash + reinsurance_var_estimate - # else: # (This point is only reached when insurer is in severe financial difficulty. Ensure insurer recovers complete coverage.) - return self.cash + else: + # (This point is only reached when insurer is in severe financial difficulty. + # Ensure insurer recovers complete coverage.) + return self.cash def increase_capacity(self, time, max_var): """Method to increase the capacity of the firm. @@ -163,7 +165,7 @@ def increase_capacity_by_category( ): """Method to increase capacity. Only called by increase_capacity. Accepts: - time: Type Integer> + time: Type Integer categ_id: Type integer. reinsurance_price: Type Decimal. cat_bond_price: Type Decimal. diff --git a/insurancesimulation.py b/insurancesimulation.py index 266e64d..bb32c8e 100644 --- a/insurancesimulation.py +++ b/insurancesimulation.py @@ -244,6 +244,7 @@ def __init__( self.reinsurance_models_counter = np.zeros( self.simulation_parameters["no_categories"] ) + self._time = None def initialize_agent_parameters( self, firmtype, simulation_parameters, risk_model_configurations @@ -419,6 +420,8 @@ def iterate(self, t): Accepts: t: Integer, current time step Returns None""" + + self._time = t if isleconfig.verbose: print() print(t, ": ", len(self.risks)) @@ -499,7 +502,7 @@ def iterate(self, t): for insurer in self.insurancefirms: if insurer.operational: for i in range(len(self.inaccuracy)): - if insurer.riskmodel.inaccuracy == self.inaccuracy[i]: + if np.array_equal(insurer.riskmodel.inaccuracy, self.inaccuracy[i]): self.insurance_models_counter[i] += 1 self.reinsurance_models_counter = np.zeros( @@ -509,7 +512,9 @@ def iterate(self, t): for reinsurer in self.reinsurancefirms: for i in range(len(self.inaccuracy)): if reinsurer.operational: - if reinsurer.riskmodel.inaccuracy == self.inaccuracy[i]: + if np.array_equal( + reinsurer.riskmodel.inaccuracy, self.inaccuracy[i] + ): self.reinsurance_models_counter[i] += 1 network_division = 1 # How often network is updated. @@ -956,7 +961,7 @@ def _get_all_riskmodel_combinations(self, n, rm_factor): self.simulation_parameters["no_categories"] ) riskmodel_combination[i] = 1 / rm_factor - riskmodels.append(riskmodel_combination.tolist()) + riskmodels.append(riskmodel_combination) return riskmodels def firm_enters_market(self, prob=-1, agent_type="InsuranceFirm"): @@ -1135,3 +1140,29 @@ def reset_pls(self): for catbond in self.catbonds: catbond.reset_pl() + + def get_risk_share(self, firm): + """Method to determine the total percentage of risks in the market that are held by a particular firm. + + For insurers uses insurance risks, for reinsurers uses reinsurance risks + Calculates the + Accepts: + firm: an insurance or reinsurance firm + Returns: + proportion: type Float, the proportion of risks held by the given firm """ + if firm.is_insurer: + total = self.simulation_parameters["no_risks"] + elif firm.is_reinsurer: + total = sum( + [ + reinfirm.number_underwritten_contracts() + for reinfirm in self.reinsurancefirms + ] + + [len(self.reinrisks)] + ) + else: + raise ValueError("Firm is neither insurer or reinsurer, which is odd") + if total == 0: + return 0 + else: + return firm.number_underwritten_contracts() / total diff --git a/isleconfig.py b/isleconfig.py index 62aed05..4775043 100644 --- a/isleconfig.py +++ b/isleconfig.py @@ -101,4 +101,10 @@ "upper_price_limit": 1.2, "lower_price_limit": 0.85, "no_risks": 20000, + # Determines the maximum upscaling of premiums based on insurer size - set to 1 to disable scaled premiums. + # Values between 0 and 1 will make premiums decrease for bigger insurers + "max_scale_premiums": 2, + # Determines the minimum fraction of inaccuracy that insurers can achieve - a value of 0 means the biggest insurers + # can be perfectly accurate, a value of 1 disables changes in inaccuracy based on size + "scale_inaccuracy": 0.5, } diff --git a/metainsuranceorg.py b/metainsuranceorg.py index c2979a3..831a0cf 100644 --- a/metainsuranceorg.py +++ b/metainsuranceorg.py @@ -93,6 +93,13 @@ def __init__(self, simulation_parameters, agent_parameters): * simulation_parameters["margin_increase"] ) + self.max_inaccuracy = rm_config["inaccuracy_by_categ"] + self.min_inaccuracy = self.max_inaccuracy * isleconfig.simulation_parameters[ + "scale_inaccuracy" + ] + np.ones(len(self.max_inaccuracy)) * ( + 1 - isleconfig.simulation_parameters["scale_inaccuracy"] + ) + self.riskmodel = RiskModel( damage_distribution=rm_config["damage_distribution"], expire_immediately=rm_config["expire_immediately"], @@ -104,7 +111,7 @@ def __init__(self, simulation_parameters, agent_parameters): init_profit_estimate=rm_config["norm_profit_markup"], margin_of_safety=margin_of_safety_correction, var_tail_prob=rm_config["var_tail_prob"], - inaccuracy=rm_config["inaccuracy_by_categ"], + inaccuracy=self.max_inaccuracy, ) self.category_reinsurance = [ @@ -153,6 +160,8 @@ def __init__(self, simulation_parameters, agent_parameters): self.simulation_parameters["no_categories"] ) self.market_permanency_counter = 0 + # The share of all risks that this firm holds + self.risk_share = 0 def iterate(self, time): """Method that iterates each firm by one time step. @@ -186,6 +195,11 @@ def iterate(self, time): for contract in self.underwritten_contracts: contract.check_payment_due(time) + """Check what proportion of the risk market we hold and then update the riskmodel accordingly""" + self.update_risk_share() + self.adjust_riskmodel() + + """Collect and process new risks""" self.collect_process_evaluate_risks(time, contracts_dissolved) """adjust liquidity, borrow or invest""" @@ -205,9 +219,8 @@ def collect_process_evaluate_risks(self, time, contracts_dissolved): contracts_offered = len(new_risks) if isleconfig.verbose and contracts_offered < 2 * contracts_dissolved: print( - "Something wrong; agent {0:d} receives too few new contracts {1:d} <= {2:d}".format( - self.id, contracts_offered, 2 * contracts_dissolved - ) + f"Something wrong; agent {self.id} receives too few new contracts {contracts_offered} " + f"<= {2 * contracts_dissolved}" ) """deal with non-proportional risks first as they must evaluate each request separatly, @@ -717,9 +730,9 @@ def process_newrisks_reinsurer( risks are accepted then a contract is written.""" for iterion in range(max(number_reinrisks_categ)): - for categ_id in range( - self.simulation_parameters["no_categories"] - ): # Here we take only one risk per category at a time to achieve risk[C1], risk[C2], risk[C3], risk[C4], risk[C1], risk[C2], ... if possible. + for categ_id in range(self.simulation_parameters["no_categories"]): + # Here we take only one risk per category at a time to achieve risk[C1], risk[C2], risk[C3], + # risk[C4], risk[C1], risk[C2], ... if possible. if ( iterion < number_reinrisks_categ[categ_id] and reinrisks_per_categ[categ_id][iterion] is not None @@ -755,6 +768,7 @@ def process_newrisks_reinsurer( ) / risk_to_insure["value"] ) + # Here it is check whether the portfolio is balanced or not if the reinrisk # (risk_to_insure) is underwritten. Return True if it is balanced. False otherwise. condition, cash_left_by_categ = self.balanced_portfolio( @@ -836,7 +850,7 @@ def process_newrisks_insurer( self, risk_to_insure, time, - self.simulation.get_reinsurance_market_premium(), + self.insurance_premium(), risk_to_insure["expiration"] - time, self.default_contract_payment_period, expire_immediately=self.simulation_parameters[ @@ -846,8 +860,7 @@ def process_newrisks_insurer( self.underwritten_contracts.append(contract) self.cash_left_by_categ = cash_left_by_categ risks_per_categ[categ_id][risk_index] = None - # TODO: move this to insurancecontract (ca. line 14) -> DONE - # TODO: do not write into other object's properties, use setter -> DONE + else: [condition, cash_left_by_categ] = self.balanced_portfolio( risk_to_insure, cash_left_by_categ, var_per_risk_per_categ @@ -1027,3 +1040,29 @@ def make_reinsurance_claims(self, time): "MetaInsuranceOrg does not implement make_reinsurance_claims, " "it should have been overridden" ) + + def update_risk_share(self): + """Updates own value for share of all risks held by this firm""" + self.risk_share = self.simulation.get_risk_share(self) + + def insurance_premium(self): + """Returns the premium this firm will charge for insurance. + + Returns the market premium multiplied by a factor that scales linearly with self.risk_share between 1 and + the max permissble adjustment""" + max_adjustment = isleconfig.simulation_parameters["max_scale_premiums"] + return self.simulation.get_market_premium() * ( + 1 * (1 - self.risk_share) + max_adjustment * self.risk_share + ) + + def adjust_riskmodel(self): + """Adjusts the inaccuracy parameter in the risk model under use depending on the share of risks held + + Shrinks the risk model towards the best available risk model (as determined by "scale_inaccuracy" in isleconfig) + by the share of risk this firm holds. + """ + if isleconfig.simulation_parameters["scale_inaccuracy"] != 1: + self.riskmodel.inaccuracy = ( + self.max_inaccuracy * (1 - self.risk_share) + + self.min_inaccuracy * self.risk_share + ) From 465adf3a394a0ed7c97a1a14f2467e8c8d572bd6 Mon Sep 17 00:00:00 2001 From: Chris Hughes Date: Tue, 16 Jul 2019 11:37:10 +0100 Subject: [PATCH 043/125] Fixed bug due to slightly improper merge --- insurancesimulation.py | 60 ++---- logger.py | 133 ++++++++------ metainsuranceorg.py | 12 +- setup_simulation.py | 4 +- start.py | 5 +- visualisation.py | 402 ++++++++++++++++++++++++++++++++--------- 6 files changed, 421 insertions(+), 195 deletions(-) diff --git a/insurancesimulation.py b/insurancesimulation.py index bf6527c..7cc8262 100644 --- a/insurancesimulation.py +++ b/insurancesimulation.py @@ -388,6 +388,8 @@ def add_agents(self, agent_class, agent_class_string, agents=None, n=1): for ap in agent_parameters ] self.reinsurancefirms += agents + for agent in agents: + self.logger.add_reinsurance_agent() elif agent_class_string == "catbond": raise ValueError(f"Catbonds must be built before being added") @@ -510,7 +512,7 @@ def iterate(self, t): if reinsurer.riskmodel.inaccuracy == self.inaccuracy[i]: self.reinsurance_models_counter[i] += 1 - network_division = 1 # How often network is updated. + network_division = 1 # How often network is updated. if isleconfig.show_network and t % network_division == 0 and t > 0: if t == network_division: self.RN = ( @@ -530,57 +532,34 @@ def save_data(self): [insurancefirm.cash for insurancefirm in self.insurancefirms] ) total_excess_capital = sum( - [ - firm.get_excess_capital() - for firm in self.insurancefirms - ] + [firm.get_excess_capital() for firm in self.insurancefirms] ) total_profitslosses = sum( [insurancefirm.get_profitslosses() for insurancefirm in self.insurancefirms] ) total_contracts_no = sum( - [ - len(firm.underwritten_contracts) - for firm in self.insurancefirms - ] - ) - total_reincash_no = sum( - [firm.cash for firm in self.reinsurancefirms] + [len(firm.underwritten_contracts) for firm in self.insurancefirms] ) + total_reincash_no = sum([firm.cash for firm in self.reinsurancefirms]) total_reinexcess_capital = sum( - [ - firm.get_excess_capital() - for firm in self.reinsurancefirms - ] + [firm.get_excess_capital() for firm in self.reinsurancefirms] ) total_reinprofitslosses = sum( - [ - firm.get_profitslosses() - for firm in self.reinsurancefirms - ] + [firm.get_profitslosses() for firm in self.reinsurancefirms] ) total_reincontracts_no = sum( - [ - len(firm.underwritten_contracts) - for firm in self.reinsurancefirms - ] - ) - operational_no = sum( - [firm.operational for firm in self.insurancefirms] - ) - reinoperational_no = sum( - [firm.operational for firm in self.reinsurancefirms] + [len(firm.underwritten_contracts) for firm in self.reinsurancefirms] ) + operational_no = sum([firm.operational for firm in self.insurancefirms]) + reinoperational_no = sum([firm.operational for firm in self.reinsurancefirms]) catbondsoperational_no = sum([catbond.operational for catbond in self.catbonds]) """ collect agent-level data """ insurance_firms = [ - (firm.cash, firm.id, firm.operational) - for firm in self.insurancefirms + (firm.cash, firm.id, firm.operational) for firm in self.insurancefirms ] reinsurance_firms = [ - (firm.cash, firm.id, firm.operational) - for firm in self.reinsurancefirms + (firm.cash, firm.id, firm.operational) for firm in self.reinsurancefirms ] """ prepare dict """ @@ -612,14 +591,12 @@ def save_data(self): current_log["market_diffvar"] = self.compute_market_diffvar() current_log["individual_contracts"] = [ - len(firm.underwritten_contracts) - for firm in self.insurancefirms + len(firm.underwritten_contracts) for firm in self.insurancefirms ] - current_log['reinsurance_contracts'] = [] - reinsurance_contracts_no = [len(reinsurancefirm.underwritten_contracts) for reinsurancefirm in self.reinsurancefirms] - for i in range(len(reinsurance_contracts_no)): - current_log['reinsurance_contracts'].append(reinsurance_contracts_no[i]) + current_log["reinsurance_contracts"] = [ + len(firm.underwritten_contracts) for firm in self.reinsurancefirms + ] """ call to Logger object """ self.logger.record_data(current_log) @@ -909,10 +886,9 @@ def get_reinrisks(self): np.random.shuffle(self.reinrisks) return self.reinrisks - def solicit_insurance_requests(self, insurer_id, cash, insurer): + def solicit_insurance_requests(self, cash, insurer): """Method for determining which risks are to be assessed by firms based on insurer weights Accepts: - id: Type integer cash: Type Integer insurer: Type firm metainsuranceorg instance Returns: diff --git a/logger.py b/logger.py index 93ce229..d45d2ea 100644 --- a/logger.py +++ b/logger.py @@ -5,16 +5,22 @@ import listify LOG_DEFAULT = ( - 'total_cash total_excess_capital total_profitslosses total_contracts ' - 'total_operational total_reincash total_reinexcess_capital total_reinprofitslosses ' - 'total_reincontracts total_reinoperational total_catbondsoperational market_premium ' - 'market_reinpremium cumulative_bankruptcies cumulative_market_exits cumulative_unrecovered_claims ' - 'cumulative_claims insurance_firms_cash reinsurance_firms_cash market_diffvar ' - 'rc_event_schedule_initial rc_event_damage_initial number_riskmodels individual_contracts reinsurance_contracts' -).split(' ') - -class Logger(): - def __init__(self, no_riskmodels=None, rc_event_schedule_initial=None, rc_event_damage_initial=None): + "total_cash total_excess_capital total_profitslosses total_contracts " + "total_operational total_reincash total_reinexcess_capital total_reinprofitslosses " + "total_reincontracts total_reinoperational total_catbondsoperational market_premium " + "market_reinpremium cumulative_bankruptcies cumulative_market_exits cumulative_unrecovered_claims " + "cumulative_claims insurance_firms_cash reinsurance_firms_cash market_diffvar " + "rc_event_schedule_initial rc_event_damage_initial number_riskmodels individual_contracts reinsurance_contracts" +).split(" ") + + +class Logger: + def __init__( + self, + no_riskmodels=None, + rc_event_schedule_initial=None, + rc_event_damage_initial=None, + ): """Constructor. Prepares history_logs atribute as dict for the logs. Records initial event schedule of simulation run. Arguments @@ -22,86 +28,94 @@ def __init__(self, no_riskmodels=None, rc_event_schedule_initial=None, rc_event_ rc_event_schedule_initial: list of lists of int. Times of risk events by category rc_event_damage_initial: list of arrays (or lists) of float. Damage by peril for each category as share of total possible damage (maximum insured or excess). - Returns class instance.""" - + Returns class instance.""" + """Record number of riskmodels""" self.number_riskmodels = no_riskmodels - + """Record initial event schedule""" self.rc_event_schedule_initial = rc_event_schedule_initial self.rc_event_damage_initial = rc_event_damage_initial """Prepare history log dict""" self.history_logs = {} - + """Variables pertaining to insurance sector""" # TODO: should we not have `cumulative_bankruptcies` and # `cumulative_market_exits` for both insurance firms and reinsurance firms? # `cumulative_claims`: Here are stored the total cumulative claims received # by the whole insurance sector until a certain time. - insurance_sector = ('total_cash total_excess_capital total_profitslosses ' - 'total_contracts total_operational cumulative_bankruptcies ' - 'cumulative_market_exits cumulative_claims cumulative_unrecovered_claims').split(' ') + insurance_sector = ( + "total_cash total_excess_capital total_profitslosses " + "total_contracts total_operational cumulative_bankruptcies " + "cumulative_market_exits cumulative_claims cumulative_unrecovered_claims" + ).split(" ") for _v in insurance_sector: self.history_logs[_v] = [] - + """Variables pertaining to individual insurance firms""" - self.history_logs['individual_contracts'] = [] - self.history_logs['insurance_firms_cash'] = [] - + self.history_logs["individual_contracts"] = [] + self.history_logs["insurance_firms_cash"] = [] + """Variables pertaining to reinsurance sector""" - self.history_logs['total_reincash'] = [] - self.history_logs['total_reinexcess_capital'] = [] - self.history_logs['total_reinprofitslosses'] = [] - self.history_logs['total_reincontracts'] = [] - self.history_logs['total_reinoperational'] = [] + self.history_logs["total_reincash"] = [] + self.history_logs["total_reinexcess_capital"] = [] + self.history_logs["total_reinprofitslosses"] = [] + self.history_logs["total_reincontracts"] = [] + self.history_logs["total_reinoperational"] = [] """Variables pertaining to individual reinsurance firms""" - self.history_logs['reinsurance_firms_cash'] = [] - self.history_logs['reinsurance_contracts'] = [] + self.history_logs["reinsurance_firms_cash"] = [] + self.history_logs["reinsurance_contracts"] = [] """Variables pertaining to cat bonds""" - self.history_logs['total_catbondsoperational'] = [] + self.history_logs["total_catbondsoperational"] = [] """Variables pertaining to premiums""" - self.history_logs['market_premium'] = [] - self.history_logs['market_reinpremium'] = [] - self.history_logs['market_diffvar'] = [] - + self.history_logs["market_premium"] = [] + self.history_logs["market_reinpremium"] = [] + self.history_logs["market_diffvar"] = [] + def record_data(self, data_dict): """Method to record data for one period Arguments data_dict: Type dict. Data with the same keys as are used in self.history_log(). - Returns None.""" + Returns None.""" for key in data_dict.keys(): - if key != "individual_contracts" and key != 'reinsurance_contracts': + if key != "individual_contracts" and key != "reinsurance_contracts": self.history_logs[key].append(data_dict[key]) if key == "individual_contracts": for i in range(len(data_dict["individual_contracts"])): - self.history_logs['individual_contracts'][i].append(data_dict["individual_contracts"][i]) + self.history_logs["individual_contracts"][i].append( + data_dict["individual_contracts"][i] + ) if key == "reinsurance_contracts": for i in range(len(data_dict["reinsurance_contracts"])): - self.history_logs["reinsurance_contracts"][i].append(data_dict["reinsurance_contracts"][i]) + self.history_logs["reinsurance_contracts"][i].append( + data_dict["reinsurance_contracts"][i] + ) - def obtain_log(self, requested_logs=LOG_DEFAULT): #This function allows to return in a list all the data generated by the model. There is no other way to transfer it back from the cloud. + def obtain_log( + self, requested_logs=LOG_DEFAULT + ): # This function allows to return in a list all the data generated by the model. There is no other way to transfer it back from the cloud. """Method to transfer entire log (self.history_log as well as risk event schedule). This is used to transfer the log to master core from work cores in ensemble runs in the cloud. No arguments. Returns list (listified dict).""" - + """Include environment variables (number of risk models and risk event schedule)""" self.history_logs["number_riskmodels"] = self.number_riskmodels self.history_logs["rc_event_damage_initial"] = self.rc_event_damage_initial self.history_logs["rc_event_schedule_initial"] = self.rc_event_schedule_initial - + """Parse logs to be returned""" if requested_logs == None: requested_logs = LOG_DEFAULT log = {name: self.history_logs[name] for name in requested_logs} - + """Convert to list and return""" return listify.listify(log) - + def restore_logger_object(self, log): """Method to restore logger object. A log can be restored later. It can also be restored on a different machine. This is useful in the case of ensemble runs to move the log to @@ -114,12 +128,12 @@ def restore_logger_object(self, log): """Restore dict""" log = listify.delistify(log) - + """Extract environment variables (number of risk models and risk event schedule)""" self.rc_event_schedule_initial = log["rc_event_schedule_initial"] self.rc_event_damage_initial = log["rc_event_damage_initial"] self.number_riskmodels = log["number_riskmodels"] - + """Restore history log""" self.history_logs = log @@ -129,18 +143,18 @@ def save_log(self, background_run): Arguments: background_run: Type bool. Is this an ensemble run (true) or not (false). Returns None.""" - + """Prepare writing tasks""" if background_run: to_log = self.replication_log_prepare() else: to_log = self.single_log_prepare() - + """Write to disk""" for filename, data, operation_character in to_log: with open(filename, operation_character) as wfile: wfile.write(str(data) + "\n") - + def replication_log_prepare(self): """Method to prepare writing tasks for ensemble run saving. No arguments @@ -153,7 +167,7 @@ def replication_log_prepare(self): to_log = [] to_log.append(("data/" + fpf + "_history_logs.dat", self.history_logs, "a")) return to_log - + def single_log_prepare(self): """Method to prepare writing tasks for single run saving. No arguments @@ -164,28 +178,31 @@ def single_log_prepare(self): to_log = [] to_log.append(("data/history_logs.dat", self.history_logs, "w")) return to_log - - def add_insurance_agent(self): + + def add_insurance_agent(self): """Method for adding an additional insurer agent to the history log. This is necessary to keep the number of individual insurance firm logs constant in time. No arguments. Returns None.""" - # TODO: should this not also be done for self.history_logs['insurance_firms_cash'] and + # TODO: should this not also be done for self.history_logs['insurance_firms_cash'] and # self.history_logs['reinsurance_firms_cash'] - if len(self.history_logs['individual_contracts']) > 0: - zeroes_to_append = list(np.zeros(len(self.history_logs['individual_contracts'][0]), dtype=int)) + if len(self.history_logs["individual_contracts"]) > 0: + zeroes_to_append = list( + np.zeros(len(self.history_logs["individual_contracts"][0]), dtype=int) + ) else: zeroes_to_append = [] - self.history_logs['individual_contracts'].append(zeroes_to_append) + self.history_logs["individual_contracts"].append(zeroes_to_append) def add_reinsurance_agent(self): """Method for adding an additional insurer agent to the history log. This is necessary to keep the number of individual insurance firm logs constant in time. No arguments. Returns None.""" - if len(self.history_logs['reinsurance_contracts']) > 0: - zeroes_to_append = list(np.zeros(len(self.history_logs['reinsurance_contracts'][0]), dtype=int)) + if len(self.history_logs["reinsurance_contracts"]) > 0: + zeroes_to_append = list( + np.zeros(len(self.history_logs["reinsurance_contracts"][0]), dtype=int) + ) else: zeroes_to_append = [] - self.history_logs['reinsurance_contracts'].append(zeroes_to_append) - + self.history_logs["reinsurance_contracts"].append(zeroes_to_append) diff --git a/metainsuranceorg.py b/metainsuranceorg.py index 3f038de..d5a9592 100644 --- a/metainsuranceorg.py +++ b/metainsuranceorg.py @@ -574,9 +574,7 @@ def get_newrisks_by_type(self): new_risks: Type list of DataDicts.""" new_risks = [] if self.is_insurer: - new_risks += self.simulation.solicit_insurance_requests( - self.id, self.cash, self - ) + new_risks += self.simulation.solicit_insurance_requests(self.cash, self) if self.is_reinsurer: new_risks += self.simulation.solicit_reinsurance_requests(self.cash, self) @@ -616,8 +614,12 @@ def risks_reinrisks_organizer(self, new_risks): Returns: risks_by_category: Type list of categories, each contains risks originating from that category. number_risks_categ: Type list, elements are integers of total risks in each category""" - risks_by_category = [[] for _ in range(self.simulation_parameters["no_categories"])] - number_risks_categ = np.zeros(self.simulation_parameters["no_categories"], dtype=np.int_) + risks_by_category = [ + [] for _ in range(self.simulation_parameters["no_categories"]) + ] + number_risks_categ = np.zeros( + self.simulation_parameters["no_categories"], dtype=np.int_ + ) for categ_id in range(self.simulation_parameters["no_categories"]): risks_by_category[categ_id] = [ diff --git a/setup_simulation.py b/setup_simulation.py index 3fa638d..6e4ab6f 100644 --- a/setup_simulation.py +++ b/setup_simulation.py @@ -167,7 +167,9 @@ def obtain_ensemble(self, replications, filepath=None, overwrite=False): # Not replicating another run, so we are writing to the file given self.replications = replications if filepath is None and not self.overwrite: - print("No explicit path given, automatically overwriting default path for initial state") + print( + "No explicit path given, automatically overwriting default path for initial state" + ) self.overwrite = True self.schedule(replications) self.seeds(replications) diff --git a/start.py b/start.py index 79ac1f4..f534ca2 100644 --- a/start.py +++ b/start.py @@ -113,7 +113,10 @@ def save_simulation(t, sim, sim_param, exit_now=False): pickle.dump(d, wfile, protocol=pickle.HIGHEST_PROTOCOL) with open("data/simulation_save.pkl", "br") as rfile: file_contents = rfile.read() - print("\nSaved simulation with hash:", hashlib.sha512(str(file_contents).encode()).hexdigest()) + print( + "\nSaved simulation with hash:", + hashlib.sha512(str(file_contents).encode()).hexdigest(), + ) if exit_now: exit(0) diff --git a/visualisation.py b/visualisation.py index 5cecccd..2fae350 100644 --- a/visualisation.py +++ b/visualisation.py @@ -6,7 +6,19 @@ class TimeSeries(object): - def __init__(self, series_list, event_schedule, damage_schedule, title="",xlabel="Time", colour='k', axlst=None, fig=None, percentiles=None, alpha=0.7): + def __init__( + self, + series_list, + event_schedule, + damage_schedule, + title="", + xlabel="Time", + colour="k", + axlst=None, + fig=None, + percentiles=None, + alpha=0.7, + ): self.series_list = series_list self.size = len(series_list) self.xlabel = xlabel @@ -14,31 +26,47 @@ def __init__(self, series_list, event_schedule, damage_schedule, title="",xlabel self.alpha = alpha self.percentiles = percentiles self.title = title - self.timesteps = [t for t in range(len(series_list[0][0]))] # assume all data series are the same size + self.timesteps = [ + t for t in range(len(series_list[0][0])) + ] # assume all data series are the same size self.events_schedule = event_schedule self.damage_schedule = damage_schedule if axlst is not None and fig is not None: self.axlst = axlst self.fig = fig else: - self.fig, self.axlst = plt.subplots(self.size,sharex=True) + self.fig, self.axlst = plt.subplots(self.size, sharex=True) def plot(self, schedule=False): - event_categ_colours = ['r', 'b', 'g', 'fuchsia'] - for i, (series, series_label, fill_lower, fill_upper) in enumerate(self.series_list): - self.axlst[i].plot(self.timesteps, series,color=self.colour) + event_categ_colours = ["r", "b", "g", "fuchsia"] + for i, (series, series_label, fill_lower, fill_upper) in enumerate( + self.series_list + ): + self.axlst[i].plot(self.timesteps, series, color=self.colour) self.axlst[i].set_ylabel(series_label) if fill_lower is not None and fill_upper is not None: - self.axlst[i].fill_between(self.timesteps, fill_lower, fill_upper, color=self.colour, alpha=self.alpha) - - if schedule: # Plots vertical lines for events if set. + self.axlst[i].fill_between( + self.timesteps, + fill_lower, + fill_upper, + color=self.colour, + alpha=self.alpha, + ) + + if schedule: # Plots vertical lines for events if set. for categ in range(len(self.events_schedule)): for event_time in self.events_schedule[categ]: index = self.events_schedule[categ].index(event_time) - if self.damage_schedule[categ][index] > 0.5: # Only plots line if event is significant - self.axlst[i].axvline(event_time, color=event_categ_colours[categ], alpha=self.damage_schedule[categ][index]) - self.axlst[self.size-1].set_xlabel(self.xlabel) + if ( + self.damage_schedule[categ][index] > 0.5 + ): # Only plots line if event is significant + self.axlst[i].axvline( + event_time, + color=event_categ_colours[categ], + alpha=self.damage_schedule[categ][index], + ) + self.axlst[self.size - 1].set_xlabel(self.xlabel) self.fig.suptitle(self.title) return self.fig, self.axlst @@ -60,7 +88,16 @@ class InsuranceFirmAnimation(object): No return values. This class takes the cash and contract data of each firm over all time and produces an animation showing how the proportion of each for all operational firms changes with time. Allows it to be saved as an MP4.""" - def __init__(self, cash_data, insure_contracts, event_schedule, type, save=False, perils=False): + + def __init__( + self, + cash_data, + insure_contracts, + event_schedule, + type, + save=False, + perils=False, + ): # Converts list of events by category into list of all events. self.perils_condition = perils self.all_event_times = [] @@ -81,7 +118,9 @@ def animate(self): self.pies = [0, 0] self.fig, (self.ax1, self.ax2) = plt.subplots(1, 2) self.stream = self.data_stream() - self.animate = animation.FuncAnimation(self.fig, self.update, repeat=False, interval=20, save_count=98) + self.animate = animation.FuncAnimation( + self.fig, self.update, repeat=False, interval=20, save_count=98 + ) if self.save_condition: self.save() @@ -117,20 +156,20 @@ def update(self, i): axis, getting data from data_stream method. Can also be set such that the figure flashes red at an event time.""" self.ax1.clear() self.ax2.clear() - self.ax1.axis('equal') - self.ax2.axis('equal') + self.ax1.axis("equal") + self.ax2.axis("equal") cash_list, id_list, con_list = next(self.stream) - self.pies[0] = self.ax1.pie(cash_list, labels=id_list, autopct='%1.0f%%') + self.pies[0] = self.ax1.pie(cash_list, labels=id_list, autopct="%1.0f%%") self.ax1.set_title("Total cash : {:,.0f}".format(sum(cash_list))) - self.pies[1] = self.ax2.pie(con_list, labels=id_list, autopct='%1.0f%%') + self.pies[1] = self.ax2.pie(con_list, labels=id_list, autopct="%1.0f%%") self.ax2.set_title("Total contracts : {:,.0f}".format(sum(con_list))) self.fig.suptitle("%s Timestep : %i" % (self.type, i)) if self.perils_condition: if i == self.all_event_times[0]: - self.fig.set_facecolor('r') + self.fig.set_facecolor("r") self.all_event_times = self.all_event_times[1:] else: - self.fig.set_facecolor('w') + self.fig.set_facecolor("w") return self.pies def save(self): @@ -138,9 +177,13 @@ def save(self): No accepted values. No return values.""" if self.type == "Insurance Firm": - self.animate.save("data/animated_insurfirm_pie.mp4", writer="ffmpeg", dpi=200, fps=10) + self.animate.save( + "data/animated_insurfirm_pie.mp4", writer="ffmpeg", dpi=200, fps=10 + ) elif self.type == "Reinsurance Firm": - self.animate.save("data/animated_reinsurefirm_pie.mp4", writer="ffmpeg", dpi=200, fps=10) + self.animate.save( + "data/animated_reinsurefirm_pie.mp4", writer="ffmpeg", dpi=200, fps=10 + ) else: print("Incorrect Type for Saving") @@ -152,23 +195,39 @@ def __init__(self, history_logs_list): def insurer_pie_animation(self, run=0): data = self.history_logs_list[run] - insurance_cash = np.array(data['insurance_firms_cash']) - contract_data = self.history_logs_list[0]['individual_contracts'] + insurance_cash = np.array(data["insurance_firms_cash"]) + contract_data = self.history_logs_list[0]["individual_contracts"] event_schedule = self.history_logs_list[0]["rc_event_schedule_initial"] - self.ins_pie_anim = InsuranceFirmAnimation(insurance_cash, contract_data, event_schedule, 'Insurance Firm', save=True) + self.ins_pie_anim = InsuranceFirmAnimation( + insurance_cash, contract_data, event_schedule, "Insurance Firm", save=True + ) self.ins_pie_anim.animate() return self.ins_pie_anim def reinsurer_pie_animation(self, run=0): data = self.history_logs_list[run] - reinsurance_cash = np.array(data['reinsurance_firms_cash']) - contract_data = self.history_logs_list[0]['reinsurance_contracts'] + reinsurance_cash = np.array(data["reinsurance_firms_cash"]) + contract_data = self.history_logs_list[0]["reinsurance_contracts"] event_schedule = self.history_logs_list[0]["rc_event_schedule_initial"] - self.reins_pie_anim = InsuranceFirmAnimation(reinsurance_cash, contract_data, event_schedule, 'Reinsurance Firm', save=True) + self.reins_pie_anim = InsuranceFirmAnimation( + reinsurance_cash, + contract_data, + event_schedule, + "Reinsurance Firm", + save=True, + ) self.reins_pie_anim.animate() return self.reins_pie_anim - def insurer_time_series(self, runs=None, axlst=None, fig=None, title="Insurer", colour='black', percentiles=[25,75]): + def insurer_time_series( + self, + runs=None, + axlst=None, + fig=None, + title="Insurer", + colour="black", + percentiles=[25, 75], + ): # runs should be a list of the indexes you want included in the ensemble for consideration if runs: data = [self.history_logs_list[x] for x in runs] @@ -176,11 +235,22 @@ def insurer_time_series(self, runs=None, axlst=None, fig=None, title="Insurer", data = self.history_logs_list # Take the element-wise means/medians of the ensemble set (axis=0) - contracts_agg = [history_logs['total_contracts'] for history_logs in self.history_logs_list] - profitslosses_agg = [history_logs['total_profitslosses'] for history_logs in self.history_logs_list] - operational_agg = [history_logs['total_operational'] for history_logs in self.history_logs_list] - cash_agg = [history_logs['total_cash'] for history_logs in self.history_logs_list] - premium_agg = [history_logs['market_premium'] for history_logs in self.history_logs_list] + contracts_agg = [ + history_logs["total_contracts"] for history_logs in self.history_logs_list + ] + profitslosses_agg = [ + history_logs["total_profitslosses"] + for history_logs in self.history_logs_list + ] + operational_agg = [ + history_logs["total_operational"] for history_logs in self.history_logs_list + ] + cash_agg = [ + history_logs["total_cash"] for history_logs in self.history_logs_list + ] + premium_agg = [ + history_logs["market_premium"] for history_logs in self.history_logs_list + ] contracts = np.mean(contracts_agg, axis=0) profitslosses = np.mean(profitslosses_agg, axis=0) @@ -189,18 +259,61 @@ def insurer_time_series(self, runs=None, axlst=None, fig=None, title="Insurer", premium = np.median(premium_agg, axis=0) events = self.history_logs_list[0]["rc_event_schedule_initial"] - damages = self.history_logs_list[0]['rc_event_damage_initial'] - - self.ins_time_series = TimeSeries([ - (contracts, 'Contracts', np.percentile(contracts_agg,percentiles[0], axis=0), np.percentile(contracts_agg, percentiles[1], axis=0)), - (profitslosses, 'Profitslosses', np.percentile(profitslosses_agg,percentiles[0], axis=0), np.percentile(profitslosses_agg, percentiles[1], axis=0)), - (operational, 'Operational', np.percentile(operational_agg,percentiles[0], axis=0), np.percentile(operational_agg, percentiles[1], axis=0)), - (cash, 'Cash', np.percentile(cash_agg,percentiles[0], axis=0), np.percentile(cash_agg, percentiles[1], axis=0)), - (premium, "Premium", np.percentile(premium_agg,percentiles[0], axis=0), np.percentile(premium_agg, percentiles[1], axis=0))], events, damages, title=title, xlabel="Time", axlst=axlst, fig=fig, colour=colour) + damages = self.history_logs_list[0]["rc_event_damage_initial"] + + self.ins_time_series = TimeSeries( + [ + ( + contracts, + "Contracts", + np.percentile(contracts_agg, percentiles[0], axis=0), + np.percentile(contracts_agg, percentiles[1], axis=0), + ), + ( + profitslosses, + "Profitslosses", + np.percentile(profitslosses_agg, percentiles[0], axis=0), + np.percentile(profitslosses_agg, percentiles[1], axis=0), + ), + ( + operational, + "Operational", + np.percentile(operational_agg, percentiles[0], axis=0), + np.percentile(operational_agg, percentiles[1], axis=0), + ), + ( + cash, + "Cash", + np.percentile(cash_agg, percentiles[0], axis=0), + np.percentile(cash_agg, percentiles[1], axis=0), + ), + ( + premium, + "Premium", + np.percentile(premium_agg, percentiles[0], axis=0), + np.percentile(premium_agg, percentiles[1], axis=0), + ), + ], + events, + damages, + title=title, + xlabel="Time", + axlst=axlst, + fig=fig, + colour=colour, + ) self.ins_time_series.plot(schedule=True) return self.ins_time_series - def reinsurer_time_series(self, runs=None, axlst=None, fig=None, title="Reinsurer", colour='black', percentiles=[25,75]): + def reinsurer_time_series( + self, + runs=None, + axlst=None, + fig=None, + title="Reinsurer", + colour="black", + percentiles=[25, 75], + ): # runs should be a list of the indexes you want included in the ensemble for consideration if runs: data = [self.history_logs_list[x] for x in runs] @@ -208,11 +321,25 @@ def reinsurer_time_series(self, runs=None, axlst=None, fig=None, title="Reinsure data = self.history_logs_list # Take the element-wise means/medians of the ensemble set (axis=0) - reincontracts_agg = [history_logs['total_reincontracts'] for history_logs in self.history_logs_list] - reinprofitslosses_agg = [history_logs['total_reinprofitslosses'] for history_logs in self.history_logs_list] - reinoperational_agg = [history_logs['total_reinoperational'] for history_logs in self.history_logs_list] - reincash_agg = [history_logs['total_reincash'] for history_logs in self.history_logs_list] - catbonds_number_agg = [history_logs['total_catbondsoperational'] for history_logs in self.history_logs_list] + reincontracts_agg = [ + history_logs["total_reincontracts"] + for history_logs in self.history_logs_list + ] + reinprofitslosses_agg = [ + history_logs["total_reinprofitslosses"] + for history_logs in self.history_logs_list + ] + reinoperational_agg = [ + history_logs["total_reinoperational"] + for history_logs in self.history_logs_list + ] + reincash_agg = [ + history_logs["total_reincash"] for history_logs in self.history_logs_list + ] + catbonds_number_agg = [ + history_logs["total_catbondsoperational"] + for history_logs in self.history_logs_list + ] reincontracts = np.mean(reincontracts_agg, axis=0) reinprofitslosses = np.mean(reinprofitslosses_agg, axis=0) @@ -221,30 +348,115 @@ def reinsurer_time_series(self, runs=None, axlst=None, fig=None, title="Reinsure catbonds_number = np.median(catbonds_number_agg, axis=0) events = self.history_logs_list[0]["rc_event_schedule_initial"] - damages = self.history_logs_list[0]['rc_event_damage_initial'] - - self.reins_time_series = TimeSeries([ - (reincontracts, 'Contracts', np.percentile(reincontracts_agg,percentiles[0], axis=0), np.percentile(reincontracts_agg, percentiles[1], axis=0)), - (reinprofitslosses, 'Profitslosses', np.percentile(reinprofitslosses_agg,percentiles[0], axis=0), np.percentile(reinprofitslosses_agg, percentiles[1], axis=0)), - (reinoperational, 'Operational', np.percentile(reinoperational_agg,percentiles[0], axis=0), np.percentile(reinoperational_agg, percentiles[1], axis=0)), - (reincash, 'Cash', np.percentile(reincash_agg,percentiles[0], axis=0), np.percentile(reincash_agg, percentiles[1], axis=0)), - (catbonds_number, "Activate Cat Bonds", np.percentile(catbonds_number_agg,percentiles[0], axis=0), np.percentile(catbonds_number_agg, percentiles[1], axis=0)), - ], events, damages, title=title, xlabel="Time", axlst=axlst, fig=fig, colour=colour) + damages = self.history_logs_list[0]["rc_event_damage_initial"] + + self.reins_time_series = TimeSeries( + [ + ( + reincontracts, + "Contracts", + np.percentile(reincontracts_agg, percentiles[0], axis=0), + np.percentile(reincontracts_agg, percentiles[1], axis=0), + ), + ( + reinprofitslosses, + "Profitslosses", + np.percentile(reinprofitslosses_agg, percentiles[0], axis=0), + np.percentile(reinprofitslosses_agg, percentiles[1], axis=0), + ), + ( + reinoperational, + "Operational", + np.percentile(reinoperational_agg, percentiles[0], axis=0), + np.percentile(reinoperational_agg, percentiles[1], axis=0), + ), + ( + reincash, + "Cash", + np.percentile(reincash_agg, percentiles[0], axis=0), + np.percentile(reincash_agg, percentiles[1], axis=0), + ), + ( + catbonds_number, + "Activate Cat Bonds", + np.percentile(catbonds_number_agg, percentiles[0], axis=0), + np.percentile(catbonds_number_agg, percentiles[1], axis=0), + ), + ], + events, + damages, + title=title, + xlabel="Time", + axlst=axlst, + fig=fig, + colour=colour, + ) self.reins_time_series.plot() return self.reins_time_series def metaplotter_timescale(self): # Take the element-wise means/medians of the ensemble set (axis=0) - contracts = np.mean([history_logs['total_contracts'] for history_logs in self.history_logs_list],axis=0) - profitslosses = np.mean([history_logs['total_profitslosses'] for history_logs in self.history_logs_list],axis=0) - operational = np.median([history_logs['total_operational'] for history_logs in self.history_logs_list],axis=0) - cash = np.median([history_logs['total_cash'] for history_logs in self.history_logs_list],axis=0) - premium = np.median([history_logs['market_premium'] for history_logs in self.history_logs_list],axis=0) - reincontracts = np.mean([history_logs['total_reincontracts'] for history_logs in self.history_logs_list],axis=0) - reinprofitslosses = np.mean([history_logs['total_reinprofitslosses'] for history_logs in self.history_logs_list],axis=0) - reinoperational = np.median([history_logs['total_reinoperational'] for history_logs in self.history_logs_list],axis=0) - reincash = np.median([history_logs['total_reincash'] for history_logs in self.history_logs_list],axis=0) - catbonds_number = np.median([history_logs['total_catbondsoperational'] for history_logs in self.history_logs_list],axis=0) + contracts = np.mean( + [ + history_logs["total_contracts"] + for history_logs in self.history_logs_list + ], + axis=0, + ) + profitslosses = np.mean( + [ + history_logs["total_profitslosses"] + for history_logs in self.history_logs_list + ], + axis=0, + ) + operational = np.median( + [ + history_logs["total_operational"] + for history_logs in self.history_logs_list + ], + axis=0, + ) + cash = np.median( + [history_logs["total_cash"] for history_logs in self.history_logs_list], + axis=0, + ) + premium = np.median( + [history_logs["market_premium"] for history_logs in self.history_logs_list], + axis=0, + ) + reincontracts = np.mean( + [ + history_logs["total_reincontracts"] + for history_logs in self.history_logs_list + ], + axis=0, + ) + reinprofitslosses = np.mean( + [ + history_logs["total_reinprofitslosses"] + for history_logs in self.history_logs_list + ], + axis=0, + ) + reinoperational = np.median( + [ + history_logs["total_reinoperational"] + for history_logs in self.history_logs_list + ], + axis=0, + ) + reincash = np.median( + [history_logs["total_reincash"] for history_logs in self.history_logs_list], + axis=0, + ) + catbonds_number = np.median( + [ + history_logs["total_catbondsoperational"] + for history_logs in self.history_logs_list + ], + axis=0, + ) return def show(self): @@ -253,22 +465,26 @@ def show(self): class compare_riskmodels(object): - def __init__(self,vis_list, colour_list): + def __init__(self, vis_list, colour_list): # take in list of visualisation objects and call their plot methods self.vis_list = vis_list self.colour_list = colour_list - - def create_insurer_timeseries(self, fig=None, axlst=None, percentiles=[25,75]): + + def create_insurer_timeseries(self, fig=None, axlst=None, percentiles=[25, 75]): # create the time series for each object in turn and superpose them? fig = axlst = None - for vis,colour in zip(self.vis_list, self.colour_list): - (fig, axlst) = vis.insurer_time_series(fig=fig, axlst=axlst, colour=colour, percentiles=percentiles) + for vis, colour in zip(self.vis_list, self.colour_list): + (fig, axlst) = vis.insurer_time_series( + fig=fig, axlst=axlst, colour=colour, percentiles=percentiles + ) - def create_reinsurer_timeseries(self, fig=None, axlst=None, percentiles=[25,75]): + def create_reinsurer_timeseries(self, fig=None, axlst=None, percentiles=[25, 75]): # create the time series for each object in turn and superpose them? fig = axlst = None - for vis,colour in zip(self.vis_list, self.colour_list): - (fig, axlst) = vis.reinsurer_time_series(fig=fig, axlst=axlst, colour=colour, percentiles=percentiles) + for vis, colour in zip(self.vis_list, self.colour_list): + (fig, axlst) = vis.reinsurer_time_series( + fig=fig, axlst=axlst, colour=colour, percentiles=percentiles + ) def show(self): plt.show() @@ -276,21 +492,29 @@ def show(self): def save(self): # logic to save plots pass - + if __name__ == "__main__": # use argparse to handle command line arguments - parser = argparse.ArgumentParser(description='Model the Insurance sector') - parser.add_argument("--single", action="store_true", help="plot time series of a single run of the insurance model") - parser.add_argument("--comparison", action="store_true", help="plot the result of an ensemble of replicatons of the insurance model") + parser = argparse.ArgumentParser(description="Model the Insurance sector") + parser.add_argument( + "--single", + action="store_true", + help="plot time series of a single run of the insurance model", + ) + parser.add_argument( + "--comparison", + action="store_true", + help="plot the result of an ensemble of replicatons of the insurance model", + ) args = parser.parse_args() args.single = True if args.single: # load in data from the history_logs dictionarywith open("data/history_logs.dat","r") as rfile: - with open("data/history_logs.dat","r") as rfile: - history_logs_list = [eval(k) for k in rfile] # one dict on each line + with open("data/history_logs.dat", "r") as rfile: + history_logs_list = [eval(k) for k in rfile] # one dict on each line # first create visualisation object, then create graph/animation objects as necessary vis = visualisation(history_logs_list) @@ -305,21 +529,23 @@ def save(self): # for each run, generate an animation and time series for insurer and reinsurer # TODO: provide some way for these to be lined up nicely rather than having to manually arrange screen - #for i in range(N): + # for i in range(N): # vis.insurer_pie_animation(run=i) # vis.insurer_time_series(runs=[i]) # vis.reinsurer_pie_animation(run=i) # vis.reinsurer_time_series(runs=[i]) # vis.show() vis_list = [] - filenames = ["./data/"+x+"_history_logs.dat" for x in ["one","two","three","four"]] + filenames = [ + "./data/" + x + "_history_logs.dat" for x in ["one", "two", "three", "four"] + ] for filename in filenames: - with open(filename,'r') as rfile: - history_logs_list = [eval(k) for k in rfile] # one dict on each line + with open(filename, "r") as rfile: + history_logs_list = [eval(k) for k in rfile] # one dict on each line vis_list.append(visualisation(history_logs_list)) - colour_list = ['blue', 'yellow', 'red', 'green'] + colour_list = ["blue", "yellow", "red", "green"] cmp_rsk = compare_riskmodels(vis_list, colour_list) - cmp_rsk.create_insurer_timeseries(percentiles=[10,90]) - cmp_rsk.create_reinsurer_timeseries(percentiles=[10,90]) + cmp_rsk.create_insurer_timeseries(percentiles=[10, 90]) + cmp_rsk.create_reinsurer_timeseries(percentiles=[10, 90]) cmp_rsk.show() From 1130cab0572b75010eb2a43b231e518e29fe990e Mon Sep 17 00:00:00 2001 From: Chris Hughes Date: Tue, 16 Jul 2019 15:37:07 +0100 Subject: [PATCH 044/125] Lots of little aesthetic cleanups --- calibrationscore.py | 6 +-- catbond.py | 5 +++ distributionreinsurance.py | 22 +++++------ distributiontruncated.py | 12 +++--- insurancefirm.py | 65 ++++++++++++++++--------------- insurancesimulation.py | 79 ++++++++++++++++++-------------------- isleconfig.py | 9 +++-- logger.py | 1 - metainsurancecontract.py | 8 ++-- metainsuranceorg.py | 61 ++++++++++++++++------------- reinsurancecontract.py | 5 +-- riskmodel.py | 67 ++++++++++++++++++++++---------- 12 files changed, 187 insertions(+), 153 deletions(-) diff --git a/calibrationscore.py b/calibrationscore.py index f9cc48c..415610a 100644 --- a/calibrationscore.py +++ b/calibrationscore.py @@ -39,16 +39,14 @@ def test_all(self): """Print components""" print("\n") for cond_name, score in scores.items(): - print("{0:47s}: {1:8f}".format(cond_name, score)) + print(f"{cond_name:47s}: {score:8f}") """Compute combined score""" self.calibration_score = self.combine_scores( np.array([*scores.values()], dtype=object) ) """Print combined score""" print( - "\n Total calibration score: {0:8f}".format( - self.calibration_score - ) + f"\n Total calibration score: {self.calibration_score:8f}" ) """Return""" return self.calibration_score diff --git a/catbond.py b/catbond.py index 9967efd..adf8c0b 100644 --- a/catbond.py +++ b/catbond.py @@ -1,8 +1,13 @@ import isleconfig from metainsuranceorg import MetaInsuranceOrg +# TODO: This and MetaInsuranceOrg should probably both subclass something simple - a MetaAgent, say. MetaInsuranceOrg +# can do more than a CatBond should be able to! + +# noinspection PyAbstractClass class CatBond(MetaInsuranceOrg): + # noinspection PyMissingConstructor def __init__(self, simulation, per_period_premium, owner, interest_rate=0): """Initialising methods. Accepts: diff --git a/distributionreinsurance.py b/distributionreinsurance.py index 61b425c..904a93c 100644 --- a/distributionreinsurance.py +++ b/distributionreinsurance.py @@ -21,11 +21,11 @@ def __init__(self, dist, lower_bound=None, upper_bound=None): def pdf(self, x): x = np.array(x, ndmin=1) r = map( - lambda Y: self.dist.pdf(Y) - if Y < self.lower_bound + lambda y: self.dist.pdf(y) + if y < self.lower_bound else np.inf - if Y == self.lower_bound - else self.dist.pdf(Y + self.upper_bound - self.lower_bound), + if y == self.lower_bound + else self.dist.pdf(y + self.upper_bound - self.lower_bound), x, ) r = np.array(list(r)) @@ -37,9 +37,9 @@ def pdf(self, x): def cdf(self, x): x = np.array(x, ndmin=1) r = map( - lambda Y: self.dist.cdf(Y) - if Y < self.lower_bound - else self.dist.cdf(Y + self.upper_bound - self.lower_bound), + lambda y: self.dist.cdf(y) + if y < self.lower_bound + else self.dist.cdf(y + self.upper_bound - self.lower_bound), x, ) r = np.array(list(r)) @@ -52,11 +52,11 @@ def ppf(self, x): x = np.array(x, ndmin=1) assert (x >= 0).all() and (x <= 1).all() r = map( - lambda Y: self.dist.ppf(Y) - if Y <= self.dist.cdf(self.lower_bound) + lambda y: self.dist.ppf(y) + if y <= self.dist.cdf(self.lower_bound) else self.dist.ppf(self.dist.cdf(self.lower_bound)) - if Y <= self.dist.cdf(self.upper_bound) - else self.dist.ppf(Y) - self.upper_bound + self.lower_bound, + if y <= self.dist.cdf(self.upper_bound) + else self.dist.ppf(y) - self.upper_bound + self.lower_bound, x, ) r = np.array(list(r)) diff --git a/distributiontruncated.py b/distributiontruncated.py index 6f6a15b..e4ceb0f 100644 --- a/distributiontruncated.py +++ b/distributiontruncated.py @@ -17,8 +17,8 @@ def __init__(self, dist, lower_bound=0.0, upper_bound=1.0): def pdf(self, x): x = np.array(x, ndmin=1) r = map( - lambda Y: self.dist.pdf(Y) / self.normalizing_factor - if (self.lower_bound <= Y <= self.upper_bound) + lambda y: self.dist.pdf(y) / self.normalizing_factor + if (self.lower_bound <= y <= self.upper_bound) else 0, x, ) @@ -31,11 +31,11 @@ def pdf(self, x): def cdf(self, x): x = np.array(x, ndmin=1) r = map( - lambda Y: 0 - if Y < self.lower_bound + lambda y: 0 + if y < self.lower_bound else 1 - if Y > self.upper_bound - else (self.dist.cdf(Y) - self.dist.cdf(self.lower_bound)) + if y > self.upper_bound + else (self.dist.cdf(y) - self.dist.cdf(self.lower_bound)) / self.normalizing_factor, x, ) diff --git a/insurancefirm.py b/insurancefirm.py index 974365a..3bf46a0 100644 --- a/insurancefirm.py +++ b/insurancefirm.py @@ -62,8 +62,8 @@ def adjust_capacity_target(self, max_var): Accepts: max_var: Type Decimal. No return values. - This method decides to increase/decrease the capacity target dependant on if the ratio of capacity target to max - VaR is above/below a predetermined limit.""" + This method decides to increase/decrease the capacity target dependant on if the ratio of + capacity target to max VaR is above/below a predetermined limit.""" reinsurance_var_estimate = self.get_reinsurance_var_estimate(max_var) if max_var + reinsurance_var_estimate == 0: # TODO: why is this being called with max_var = 0 anyway? @@ -92,9 +92,9 @@ def get_capacity(self, max_var): max_var: Type Decimal. Returns: self.cash (+ reinsurance_VaR_estimate): Type Decimal. - This method is called by increase_capacity to get the real capacity of the firm. If the firm has enough money to - cover its max value at risk then its capacity is its cash + the reinsurance VaR estimate, otherwise the firm is - recovering from some losses and so capacity is just cash.""" + This method is called by increase_capacity to get the real capacity of the firm. If the firm has + enough money to cover its max value at risk then its capacity is its cash + the reinsurance VaR + estimate, otherwise the firm is recovering from some losses and so capacity is just cash.""" if max_var < self.cash: reinsurance_var_estimate = self.get_reinsurance_var_estimate(max_var) return self.cash + reinsurance_var_estimate @@ -176,9 +176,7 @@ def increase_capacity_by_category( only issue if market premium is greater than firms average premium.""" if isleconfig.verbose: print( - "IF {0:d} increasing capacity in period {1:d}, cat bond price: {2:f}, reinsurance premium {3:f}".format( - self.id, time, cat_bond_price, reinsurance_price - ) + f"IF {self.id:d} increasing capacity in period {time:d}, cat bond price: {cat_bond_price:f}, reinsurance premium {reinsurance_price:f}" ) if not force: actual_premium = self.get_average_premium(categ_id) @@ -188,13 +186,11 @@ def increase_capacity_by_category( """on the basis of prices decide for obtaining reinsurance or for issuing cat bond""" if reinsurance_price > cat_bond_price: if isleconfig.verbose: - print("IF {0:d} issuing Cat bond in period {1:d}".format(self.id, time)) + print(f"IF {self.id:d} issuing Cat bond in period {time:d}") self.issue_cat_bond(time, categ_id) else: if isleconfig.verbose: - print( - "IF {0:d} getting reinsurance in period {1:d}".format(self.id, time) - ) + print(f"IF {self.id:d} getting reinsurance in period {time:d}") self.ask_reinsurance_non_proportional_by_category(time, categ_id) return True @@ -243,7 +239,7 @@ def ask_reinsurance_non_proportional(self, time): if self.category_reinsurance[categ_id] is None: self.ask_reinsurance_non_proportional_by_category(time, categ_id) - def characterize_underwritten_risks_by_category(self, time, categ_id): + def characterize_underwritten_risks_by_category(self, categ_id): """Method to characterise associated risks in a given category in terms of value, number, avg risk factor, and total premium per iteration. Accepts: @@ -283,9 +279,12 @@ def ask_reinsurance_non_proportional_by_category( and, assuming firms has underwritten risks in category, creates new reinsurance risk with values based on firms existing underwritten risks. If the method was called to create a new risks then it is appended to list of 'reinrisks', otherwise used for creating the risk when a reinsurance contract rolls over.""" - total_value, avg_risk_factor, number_risks, periodized_total_premium = self.characterize_underwritten_risks_by_category( - time, categ_id - ) + [ + total_value, + avg_risk_factor, + number_risks, + periodized_total_premium, + ] = self.characterize_underwritten_risks_by_category(categ_id) if number_risks > 0: risk = { "value": total_value, @@ -358,20 +357,14 @@ def add_reinsurance(self, category, excess_fraction, deductible_fraction, contra ) self.category_reinsurance[category] = contract - def delete_reinsurance( - self, category, excess_fraction, deductible_fraction, contract - ): + def delete_reinsurance(self, category, contract): """Method called by reinsurancecontract to delete the reinsurance contract from the firms counter for the given category, used so that another reinsurance contract can be issued for that category if needed. Accepts: category: Type Integer. - excess_fraction: Type Decimal. Value of excess. - deductible_fraction: Type Decimal. Value of deductible. contract: Type Class. Reinsurance contract issued to firm. No return values.""" - self.riskmodel.delete_reinsurance( - category, excess_fraction, deductible_fraction, contract - ) + self.riskmodel.delete_reinsurance(category, contract) self.category_reinsurance[category] = None def issue_cat_bond(self, time, categ_id, per_value_per_period_premium=0): @@ -384,9 +377,12 @@ def issue_cat_bond(self, time, categ_id, per_value_per_period_premium=0): Method is only called by increase_capacity_by_category method when CatBond prices are lower than reinsurance. It then creates the CatBond as a quasi-reinsurance contract that is paid for immediately (by simulation) with no premium payments.""" - total_value, avg_risk_factor, number_risks, periodized_total_premium = self.characterize_underwritten_risks_by_category( - time, categ_id - ) + [ + total_value, + avg_risk_factor, + number_risks, + _, + ] = self.characterize_underwritten_risks_by_category(categ_id) if number_risks > 0: risk = { "value": total_value, @@ -411,9 +407,11 @@ def issue_cat_bond(self, time, categ_id, per_value_per_period_premium=0): ] ) # TODO: or is it range(1, risk["runtime"]+1)? # catbond = CatBond(self.simulation, per_period_premium) + # TODO: shift obtain_yield method to insurancesimulation, thereby making it unnecessary to drag + # parameters like self.interest_rate from instance to instance and from class to class new_catbond = catbond.CatBond( self.simulation, per_period_premium, self.interest_rate - ) # TODO: shift obtain_yield method to insurancesimulation, thereby making it unnecessary to drag parameters like self.interest_rate from instance to instance and from class to class + ) """add contract; contract is a quasi-reinsurance contract""" contract = ReinsuranceContract( @@ -427,7 +425,7 @@ def issue_cat_bond(self, time, categ_id, per_value_per_period_premium=0): initial_var=var_this_risk, insurancetype=risk["insurancetype"], ) - # per_value_reinsurance_premium = 0 because the insurance firm does not continue to make payments to the cat bond. Only once. + # per_value_reinsurance_premium = 0 because the insurance firm make only one payment to catbond new_catbond.set_contract(contract) """sell cat bond (to self.simulation)""" @@ -493,9 +491,12 @@ def get_excess_of_loss_reinsurance(self): def create_reinrisk(self, time, categ_id): """Proceed with creation of reinsurance risk only if category is not empty.""" - total_value, avg_risk_factor, number_risks, periodized_total_premium = self.characterize_underwritten_risks_by_category( - time, categ_id - ) + [ + total_value, + avg_risk_factor, + number_risks, + periodized_total_premium, + ] = self.characterize_underwritten_risks_by_category(categ_id) if number_risks > 0: # TODO: make runtime into a parameter risk = { diff --git a/insurancesimulation.py b/insurancesimulation.py index b695372..0ef360c 100644 --- a/insurancesimulation.py +++ b/insurancesimulation.py @@ -28,7 +28,7 @@ class InsuranceSimulation: def __init__( self, override_no_riskmodels, - replic_ID, + replic_id, simulation_parameters, rc_event_schedule, rc_event_damage, @@ -53,11 +53,11 @@ def __init__( self.number_riskmodels = simulation_parameters["no_riskmodels"] # save parameters - if (replic_ID is None) or isleconfig.force_foreground: + if (replic_id is None) or isleconfig.force_foreground: self.background_run = False else: self.background_run = True - self.replic_ID = replic_ID + self.replic_id = replic_id self.simulation_parameters = simulation_parameters "Unpacks parameters and sets distributions" @@ -171,8 +171,7 @@ def __init__( ) self.inaccuracy = self._get_all_riskmodel_combinations( - self.simulation_parameters["no_categories"], - self.simulation_parameters["riskmodel_inaccuracy_parameter"], + self.simulation_parameters["riskmodel_inaccuracy_parameter"] ) self.inaccuracy = random.sample( @@ -205,6 +204,9 @@ def __init__( # firms (why can't we just get that from the instances of InsuranceFirm) or a list of the *possible* parameter # values for insurance firms (in which case why does it have the length it does)? self.agent_parameters = {"insurancefirm": [], "reinsurancefirm": []} + self.insurer_id_counter = 0 + self.reinsurer_id_counter = 0 + self.initialize_agent_parameters( "insurancefirm", simulation_parameters, risk_model_configurations ) @@ -245,6 +247,7 @@ def __init__( self.simulation_parameters["no_categories"] ) self._time = None + self.RN = None def initialize_agent_parameters( self, firmtype, simulation_parameters, risk_model_configurations @@ -254,7 +257,6 @@ def initialize_agent_parameters( Creates the agent parameters of both firm types for the initial number specified in isleconfig.py Returns None""" if firmtype == "insurancefirm": - self.insurer_id_counter = 0 no_firms = simulation_parameters["no_insurancefirms"] initial_cash = "initial_agent_cash" reinsurance_level_lowerbound = simulation_parameters[ @@ -265,7 +267,6 @@ def initialize_agent_parameters( ] elif firmtype == "reinsurancefirm": - self.reinsurer_id_counter = 0 no_firms = simulation_parameters["no_reinsurancefirms"] initial_cash = "initial_reinagent_cash" reinsurance_level_lowerbound = simulation_parameters[ @@ -282,6 +283,8 @@ def initialize_agent_parameters( unique_id = self.get_unique_insurer_id() elif firmtype == "reinsurancefirm": unique_id = self.get_unique_reinsurer_id() + else: + raise ValueError(f"Firm type {firmtype} not recognised") if simulation_parameters["static_non-proportional_reinsurance_levels"]: reinsurance_level = simulation_parameters[ @@ -365,7 +368,7 @@ def add_agents(self, agent_class, agent_class_string, agents=None, n=1): ] # We've made the agents, add them to the simulation self.insurancefirms += agents - for agent in agents: + for _ in agents: self.logger.add_insurance_agent() elif agent_class_string == "reinsurancefirm": @@ -389,7 +392,7 @@ def add_agents(self, agent_class, agent_class_string, agents=None, n=1): for ap in agent_parameters ] self.reinsurancefirms += agents - for agent in agents: + for _ in agents: self.logger.add_reinsurance_agent() elif agent_class_string == "catbond": @@ -533,14 +536,12 @@ def save_data(self): Returns None.""" """ collect data """ - total_cash_no = sum( - [insurancefirm.cash for insurancefirm in self.insurancefirms] - ) + total_cash_no = sum([firm.cash for firm in self.insurancefirms]) total_excess_capital = sum( [firm.get_excess_capital() for firm in self.insurancefirms] ) total_profitslosses = sum( - [insurancefirm.get_profitslosses() for insurancefirm in self.insurancefirms] + [firm.get_profitslosses() for firm in self.insurancefirms] ) total_contracts_no = sum( [len(firm.underwritten_contracts) for firm in self.insurancefirms] @@ -681,7 +682,6 @@ def _pay(self, obligation): Returns None""" amount = obligation["amount"] recipient = obligation["recipient"] - purpose = obligation["purpose"] if not self.money_supply > amount: warnings.warn("Something wrong: economy out of money", RuntimeWarning) if recipient.get_operational(): @@ -709,9 +709,7 @@ def _reset_reinsurance_weights(self): self.not_accepted_reinrisks = [] operational_reinfirms = [ - reinsurancefirm - for reinsurancefirm in self.reinsurancefirms - if reinsurancefirm.operational + firm for firm in self.reinsurancefirms if firm.operational ] operational_no = len(operational_reinfirms) @@ -742,15 +740,9 @@ def _reset_insurance_weights(self): """Method for clearing and setting insurance weights dependant on how many insurance companies exist and how many insurance risks are offered. This determined which risks are sent to metainsuranceorg iteration.""" - operational_no = sum( - [insurancefirm.operational for insurancefirm in self.insurancefirms] - ) + operational_no = sum([firm.operational for firm in self.insurancefirms]) - operational_firms = [ - insurancefirm - for insurancefirm in self.insurancefirms - if insurancefirm.operational - ] + operational_firms = [firm for firm in self.insurancefirms if firm.operational] risks_no = len(self.risks) @@ -780,10 +772,10 @@ def _adjust_market_premium(self, capital): Accepts arguments capital: Type float. The total capital (cash) available in the insurance market (insurance only). No return value. - This method adjusts the premium charged by insurance firms for the risks covered. The premium reduces linearly - with the capital available in the insurance market and viceversa. The premium reduces until it reaches a minimum - below which no insurer is willing to reduce further the price. This method is only called in the self.iterate() - method of this class.""" + This method adjusts the premium charged by insurance firms for the risks covered. The premium reduces + linearly with the capital available in the insurance market and viceversa. The premium reduces until it + reaches a minimum below which no insurer is willing to reduce further the price. This method is only called + in the self.iterate() method of this class.""" self.market_premium = self.norm_premium * ( self.simulation_parameters["upper_price_limit"] - self.simulation_parameters["premium_sensitivity"] @@ -804,10 +796,10 @@ def _adjust_reinsurance_market_premium(self, capital): Accepts arguments capital: Type float. The total capital (cash) available in the reinsurance market (reinsurance only). No return value. - This method adjusts the premium charged by reinsurance firms for the risks covered. The premium reduces linearly - with the capital available in the reinsurance market and viceversa. The premium reduces until it reaches a minimum - below which no reinsurer is willing to reduce further the price. This method is only called in the self.iterate() - method of this class.""" + This method adjusts the premium charged by reinsurance firms for the risks covered. The premium reduces + linearly with the capital available in the reinsurance market and viceversa. The premium reduces until it + reaches a minimum below which no reinsurer is willing to reduce further the price. This method is only + called in the self.iterate() method of this class.""" self.reinsurance_market_premium = self.norm_premium * ( self.simulation_parameters["upper_price_limit"] - self.simulation_parameters["reinpremium_sensitivity"] @@ -856,8 +848,9 @@ def get_reinsurance_premium(self, np_reinsurance_deductible_fraction): ) def get_cat_bond_price(self, np_reinsurance_deductible_fraction): - """Method to calculate and return catbond price. If catbonds are not desired will return infinity so no catbonds - will be issued. Otherwise calculates based on reinsurance market premium, catbond premium, deductible fraction. + """Method to calculate and return catbond price. If catbonds are not desired will return infinity so no + catbonds will be issued. Otherwise calculates based on reinsurance market premium, catbond premium, + deductible fraction. Accepts: np_reinsurance_deductible_fraction: Type Integer Returns: @@ -891,7 +884,7 @@ def get_reinrisks(self): np.random.shuffle(self.reinrisks) return self.reinrisks - def solicit_insurance_requests(self, cash, insurer): + def solicit_insurance_requests(self, insurer): """Method for determining which risks are to be assessed by firms based on insurer weights Accepts: cash: Type Integer @@ -910,7 +903,7 @@ def solicit_insurance_requests(self, cash, insurer): return risks_to_be_sent - def solicit_reinsurance_requests(self, cash, reinsurer): + def solicit_reinsurance_requests(self, reinsurer): """Method for determining which reinsurance risks are to be assessed by firms based on reinsurer weights Accepts: id: Type integer @@ -947,7 +940,7 @@ def return_reinrisks(self, not_accepted_risks): Returns None""" self.not_accepted_reinrisks += not_accepted_risks - def _get_all_riskmodel_combinations(self, n, rm_factor): + def _get_all_riskmodel_combinations(self, rm_factor): """Method for calculating riskmodels for each category based on the risk model inaccuracy parameter, and is used purely to assign inaccuracy. Undervalues one risk category and overestimates all the rest. Accepts: @@ -999,8 +992,9 @@ def record_market_exit(self): """Record_market_exit Method. Accepts no arguments. No return value. - This method is used to record the firms that leave the market due to underperforming conditions. It is only called - from the method dissolve() from the class metainsuranceorg.py after the dissolution of the firm.""" + This method is used to record the firms that leave the market due to underperforming conditions. It is + only called from the method dissolve() from the class metainsuranceorg.py after the dissolution of the + firm.""" self.cumulative_market_exits += 1 def record_unrecovered_claims(self, loss): @@ -1092,6 +1086,7 @@ def reinsurance_entry_index(self): 0 : self.simulation_parameters["no_riskmodels"] ].argmin() + # noinspection PyMethodMayBeStatic def get_operational(self): """Method to return if simulation is operational. Always true. Used only in pay methods above and metainsuranceorg. @@ -1131,8 +1126,8 @@ def reset_pls(self): Accepts no arguments. No return value. This method reset all the profits and losses of all insurance firms, reinsurance firms and catbonds.""" - for insurancefirm in self.insurancefirms: - insurancefirm.reset_pl() + for firm in self.insurancefirms: + firm.reset_pl() for reininsurancefirm in self.reinsurancefirms: reininsurancefirm.reset_pl() diff --git a/isleconfig.py b/isleconfig.py index 4775043..e45855d 100644 --- a/isleconfig.py +++ b/isleconfig.py @@ -21,7 +21,7 @@ "margin_increase": 0, # "margin_increase" modifies the margin of safety depending on the number of risks models available in the market. # When it is 0 all risk models have the same margin of safety. - "value_at_risk_tail_probability": 0.005, + "value_at_risk_tail_probability": 0.02, # values <1, >0, usually close to 0; tail probability at which the value at risk is taken by the risk models "norm_profit_markup": 0.15, "rein_norm_profit_markup": 0.15, @@ -102,9 +102,10 @@ "lower_price_limit": 0.85, "no_risks": 20000, # Determines the maximum upscaling of premiums based on insurer size - set to 1 to disable scaled premiums. - # Values between 0 and 1 will make premiums decrease for bigger insurers - "max_scale_premiums": 2, + # High values will give bigger insurers more money + # Values between 0 and 1 will make premiums decrease for bigger insurers. + "max_scale_premiums": 1.2, # Determines the minimum fraction of inaccuracy that insurers can achieve - a value of 0 means the biggest insurers # can be perfectly accurate, a value of 1 disables changes in inaccuracy based on size - "scale_inaccuracy": 0.5, + "scale_inaccuracy": 0.3, } diff --git a/logger.py b/logger.py index d45d2ea..23ad149 100644 --- a/logger.py +++ b/logger.py @@ -1,7 +1,6 @@ """Logging class. Handles records of a single simulation run. Can save and reload. """ import numpy as np -import pdb import listify LOG_DEFAULT = ( diff --git a/metainsurancecontract.py b/metainsurancecontract.py index 627ed35..bbb256b 100644 --- a/metainsurancecontract.py +++ b/metainsurancecontract.py @@ -24,7 +24,7 @@ def __init__( payment_period: Type integer. expire_immediately: Type boolean. True if the contract expires with the first risk event. False if multiple risk events are covered. - initial_var: Type float. Initial value at risk. Used only to compute true and estimated value at risk. + initial_var: Type float. Initial value at risk. Used only to compute true and estimated VaR. optional: insurancetype: Type string. The type of this contract, especially "proportional" vs "excess_of_loss" deductible_fraction: Type float (or int) @@ -111,7 +111,8 @@ def check_payment_due(self, time): Accepts: time: Type integer No return values. - This method checks if a scheduled premium payment is due, pays it to the insurer, and removes from schedule.""" + This method checks if a scheduled premium payment is due, pays it to the insurer, + and removes from schedule.""" if len(self.payment_times) > 0 and time >= self.payment_times[-1]: # Create obligation for premium payment self.property_holder.receive_obligation( @@ -128,7 +129,8 @@ def get_and_reset_current_claim(self): Returns: self.category: Type integer. Which category the contracted risk is in. current_claim: Type decimal - self.insurancetype == proportional: Type Boolean. Returns True if insurance is proportional and vice versa. + self.insurancetype == "proportional": Type Boolean. Returns True if insurance is + proportional and vice versa. This method retuns the current claim, then resets it, and also indicates the type of insurance.""" current_claim = self.current_claim self.current_claim = 0 diff --git a/metainsuranceorg.py b/metainsuranceorg.py index 833cadb..acae0d2 100644 --- a/metainsuranceorg.py +++ b/metainsuranceorg.py @@ -30,8 +30,8 @@ def __init__(self, simulation_parameters, agent_parameters): Accepts: Simulation_parameters: Type DataDict agent_parameters: Type DataDict - Constructor creates general instance of an insurance company which is inherited by the reinsurance and - insurance firm classes. Initialises all necessary values provided by config file.""" + Constructor creates general instance of an insurance company which is inherited by the reinsurance + and insurance firm classes. Initialises all necessary values provided by config file.""" self.simulation = simulation_parameters["simulation"] self.simulation_parameters = simulation_parameters self.contract_runtime_dist = scipy.stats.randint( @@ -115,7 +115,7 @@ def __init__(self, simulation_parameters, agent_parameters): ) self.category_reinsurance = [ - None for i in range(self.simulation_no_risk_categories) + None for _ in range(self.simulation_no_risk_categories) ] if self.simulation_reinsurance_type == "non-proportional": if agent_parameters["non-proportional_reinsurance_level"] is not None: @@ -168,9 +168,9 @@ def iterate(self, time): Accepts: Time: Type Integer No return value - For each time step this method obtains every firms interest payments, pays obligations, claim reinsurance, - matures necessary contracts. Check condition for operational firms (as never removed) so only operational - firms receive new risks to evaluate, pay dividends, adjust capacity.""" + For each time step this method obtains every firms interest payments, pays obligations, claim + reinsurance, matures necessary contracts. Check condition for operational firms (as never removed) + so only operational firms receive new risks to evaluate, pay dividends, adjust capacity.""" """obtain investments yield""" self.obtain_yield(time) @@ -269,10 +269,16 @@ def collect_process_evaluate_risks(self, time, contracts_dissolved): """obtain risk model evaluation (VaR) for underwriting decisions and for capacity specific decisions""" # TODO: Enable reinsurance shares other than 0.0 and 1.0 - expected_profit, acceptable_by_category, cash_left_by_categ, var_per_risk_per_categ, self.excess_capital = self.riskmodel.evaluate( - underwritten_risks, self.cash - ) - # TODO: resolve insurance reinsurance inconsistency (insurer underwrite after capacity decisions, reinsurers before). + [ + _, + acceptable_by_category, + cash_left_by_categ, + var_per_risk_per_categ, + self.excess_capital, + ] = self.riskmodel.evaluate(underwritten_risks, self.cash) + # TODO: resolve insurance reinsurance inconsistency (insurer underwrite after capacity decisions, + # reinsurers before). + # This is currently so because it minimizes the number of times we need to run self.riskmodel.evaluate(). # It would also be more consistent if excess capital would be updated at the end of the iteration. """handle adjusting capacity target and capacity""" @@ -305,7 +311,8 @@ def collect_process_evaluate_risks(self, time, contracts_dissolved): ) for repetition in range(self.recursion_limit): - # TODO: find an efficient way to stop the recursion if there are no more risks to accept or if it is not accepting any more over several iterations. + # TODO: find an efficient way to stop the recursion if there are no more risks to accept or if it is + # not accepting any more over several iterations. Done, maybe? former_risks_per_categ = copy.copy(risks_per_categ) # Here we process all the new risks in order to keep the portfolio as balanced as possible. risks_per_categ, not_accepted_risks = self.process_newrisks_insurer( @@ -330,9 +337,9 @@ def enter_illiquidity(self, time): Accepts arguments time: Type integer. The current time. No return value. - This method is called when a firm does not have enough cash to pay all its obligations. It is only called from - the method self._effect_payments() which is called at the beginning of the self.iterate() method of this class. - This method formalizes the bankruptcy through the method self.enter_bankruptcy().""" + This method is called when a firm does not have enough cash to pay all its obligations. It is only called + from the method self._effect_payments() which is called at the beginning of the self.iterate() method of + this class. This method formalizes the bankruptcy through the method self.enter_bankruptcy().""" self.enter_bankruptcy(time) def enter_bankruptcy(self, time): @@ -350,11 +357,11 @@ def market_exit(self, time): Accepts arguments time: Type integer. The current time. No return value. - This method is called when a firms wants to leave the market because it feels that it has been underperforming - for too many periods. It is only called from the method self.market_permanency() that it is run in the main iterate - method of this class. It needs to be different from the method self.enter_bankruptcy() because in this case - all the obligations can be paid. After paying all the obligations this method dissolves the firm through the - method self.dissolve().""" + This method is called when a firms wants to leave the market because it feels that it has been + underperforming for too many periods. It is only called from the method self.market_permanency() that it is + run in the main iterate method of this class. It needs to be different from the method + self.enter_bankruptcy() because in this case all the obligations can be paid. After paying all the + obligations this method dissolves the firm through the method self.dissolve().""" due = [item for item in self.obligations] for obligation in due: self.pay(obligation) @@ -369,11 +376,12 @@ def dissolve(self, time, record): the dissolution of the firm.So far it can be either 'record_bankruptcy' or 'record_market_exit'. No return value. This method dissolves the firm. It is called from the methods self.enter_bankruptcy() and self.market_exit() - of this class (metainsuranceorg.py). First of all it dissolves all the contracts currently held (those in self.underwritten_contracts). + of this class (metainsuranceorg.py). First of all it dissolves all the contracts currently held (those in + self.underwritten_contracts). Next all the cash currently available is transferred to insurancesimulation.py through an obligation in the next iteration. Finally the type of dissolution is recorded and the operational state is set to false. - Different class variables are reset during the process: self.risks_kept, self.reinrisks_kept, self.excess_capital - and self.profits_losses.""" + Different class variables are reset during the process: self.risks_kept, self.reinrisks_kept, + self.excess_capital and self.profits_losses.""" for contract in self.underwritten_contracts: contract.dissolve(time) # removing (dissolving) all risks immediately after bankruptcy (may not be realistic, @@ -478,7 +486,8 @@ def pay_dividends(self, time): Accepts: time: Type integer No return value - If firm has positive profits will pay percentage of them as dividends. Currently pays to simulation.""" + If firm has positive profits will pay percentage of them as dividends. Currently pays to simulation. + """ self.receive_obligation(self.per_period_dividend, self.owner, time, "dividend") @@ -587,9 +596,9 @@ def get_newrisks_by_type(self): new_risks: Type list of DataDicts.""" new_risks = [] if self.is_insurer: - new_risks += self.simulation.solicit_insurance_requests(self.cash, self) + new_risks += self.simulation.solicit_insurance_requests(self) if self.is_reinsurer: - new_risks += self.simulation.solicit_reinsurance_requests(self.cash, self) + new_risks += self.simulation.solicit_reinsurance_requests(self) new_nonproportional_risks = [ risk @@ -719,7 +728,7 @@ def process_newrisks_reinsurer( """Method to decide if new risks are underwritten for the reinsurance firm. Accepts: reinrisks_per_categ: Type List of lists containing new reinsurance risks. - number_reinrisks_per_categ: Type List of integers, contains number of new reinsurance risks per category. + number_reinrisks_per_categ: Type List of integers, contains number of new reinsurance risks by category. time: Type integer No return values. This method processes one by one the reinrisks contained in reinrisks_per_categ in order to decide whether diff --git a/reinsurancecontract.py b/reinsurancecontract.py index 9e8164d..f09047f 100644 --- a/reinsurancecontract.py +++ b/reinsurancecontract.py @@ -102,10 +102,7 @@ def mature(self, time): if self.insurancetype == "excess-of-loss": self.property_holder.delete_reinsurance( - category=self.category, - excess_fraction=self.excess_fraction, - deductible_fraction=self.deductible_fraction, - contract=self, + category=self.category, contract=self ) else: # TODO: ? Instead: if self.insurancetype == "proportional": self.contract.unreinsure() diff --git a/riskmodel.py b/riskmodel.py index 7993542..2341558 100644 --- a/riskmodel.py +++ b/riskmodel.py @@ -24,7 +24,7 @@ def __init__( self.cat_separation_distribution = cat_separation_distribution self.norm_premium = norm_premium # QUERY: Whis is this passed as an argument and then ignored? - self.var_tail_prob = 0.02 + self.var_tail_prob = var_tail_prob self.expire_immediately = expire_immediately self.category_number = category_number self.init_average_exposure = init_average_exposure @@ -86,12 +86,35 @@ def compute_expectation(self, categ_risks, categ_id): # TODO: more intuitive na if self.expire_immediately: incr_expected_profits = -1 # TODO: fix the norm_premium estimation - # incr_expected_profits = (self.norm_premium - (1 - scipy.stats.poisson(1 / self.cat_separation_distribution.mean() * \ - # mean_runtime).pmf(0)) * self.damage_distribution[categ_id].mean() * average_risk_factor) * average_exposure * len(categ_risks) + # incr_expected_profits = ( + # ( + # self.norm_premium + # - ( + # 1 + # - scipy.stats.poisson( + # 1 / self.cat_separation_distribution.mean() * mean_runtime + # ).pmf(0) + # ) + # * self.damage_distribution[categ_id].mean() + # * average_risk_factor + # ) + # * average_exposure + # * len(categ_risks) + # ) else: incr_expected_profits = -1 # TODO: expected profits should only be returned once the expire_immediately == False case is fixed - # incr_expected_profits = (self.norm_premium - mean_runtime / self.cat_separation_distribution[categ_id].mean() * self.damage_distribution.mean() * average_risk_factor) * average_exposure * len(categ_risks) + # incr_expected_profits = ( + # ( + # self.norm_premium + # - mean_runtime + # / self.cat_separation_distribution[categ_id].mean() + # * self.damage_distribution.mean() + # * average_risk_factor + # ) + # * average_exposure + # * len(categ_risks) + # ) return average_risk_factor, average_exposure, incr_expected_profits @@ -102,8 +125,10 @@ def evaluate_proportional(self, risks, cash): cash: Type List. Gives cash available for each category. Returns: expected_profits: Type Decimal (Currently returns None) - remaining_acceptable_by_category: Type List of Integers. Number of risks that would not be covered by firms cash. - cash_left_by_category: Type List of Integers. Firms expected cash left if underwriting the risks from that category. + remaining_acceptable_by_category: Type List of Integers. Number of risks that would not be covered by + firms cash. + cash_left_by_category: Type List of Integers. Firms expected cash left if underwriting the risks from + that category. var_per_risk_per_categ: List of Integers. Average VaR per category. This method iterates through the risks in each category and calculates the average VaR, how many could be underwritten according to their average VaR, how much cash would be left per category if all risks were @@ -185,7 +210,7 @@ def evaluate_proportional(self, risks, cash): max_cash_by_categ = max(cash_left_by_category) floored_cash_by_categ = cash_left_by_category.copy() floored_cash_by_categ[floored_cash_by_categ < 0] = 0 - remaining_acceptable_by_category_old = remaining_acceptable_by_category.copy() + # remaining_acceptable_by_category_old = remaining_acceptable_by_category.copy() for categ_id in range(self.category_number): # QUERY: Where does this come from? remaining_acceptable_by_category[categ_id] = math.floor( @@ -292,16 +317,19 @@ def evaluate(self, risks, cash, offered_risk=None): offered_risk: Type DataDict or defaults to None. (offered_risk = None) Returns: expected_profits_proportional: Type Decimal (Currently returns None) - remaining_acceptable_by_categ: Type List of Integers. Number of risks that would not be covered by firms cash. - cash_left_by_categ: Type List of Integers. Firms expected cash left if underwriting the risks from that category. + remaining_acceptable_by_categ: Type List of Integers. Number of risks that would not be covered by + firms cash. + cash_left_by_categ: Type List of Integers. Firms expected cash left if underwriting the risks from that + category. var_per_risk_per_categ: List of Integers. Average VaR per category min(cash_left_by_categ): Type Decimal. Minimum (offered_risk != None) Returns: - (cash_left_by_categ - additional_required > 0).all(): Type Boolean. Returns True only if all categories have - enough to cover the additional capital to insure risk. + (cash_left_by_categ - additional_required > 0).all(): Type Boolean. Returns True only if all categories + have enough to cover the additional capital to insure risk. cash_left_by_categ: Type List of Decimals. Cash left per category if all risks claimed. var_this_risk: Type Decimal. Expected claim of offered risk. - min(cash_left_by_categ): Type Decimal. Minimum value of cash left in a category after covering all expected claims. + min(cash_left_by_categ): Type Decimal. Minimum value of cash left in a category after covering all + expected claims. This method organises all risks by insurance type then delegates then to respective methods (evaluate_prop/evaluate_excess_of_loss). Excess of loss risks are processed one at a time and are admitted using the offered_risk argument, whereas proportional risks are processed all at once leaving offered_risk = 0. This @@ -328,9 +356,12 @@ def evaluate(self, risks, cash, offered_risk=None): el_risks, cash_left_by_categ, offered_risk ) if (offered_risk is None) or (len(risks) > 0): - expected_profits_proportional, remaining_acceptable_by_categ, cash_left_by_categ, var_per_risk_per_categ = self.evaluate_proportional( - risks, cash_left_by_categ - ) + [ + expected_profits_proportional, + remaining_acceptable_by_categ, + cash_left_by_categ, + var_per_risk_per_categ, + ] = self.evaluate_proportional(risks, cash_left_by_categ) if offered_risk is None: # return numbers of remaining acceptable risks by category return ( @@ -378,16 +409,12 @@ def add_reinsurance(self, categ_id, excess_fraction, deductible_fraction, contra dist=self.damage_distribution[categ_id], ) - def delete_reinsurance( - self, categ_id, excess_fraction, deductible_fraction, contract - ): + def delete_reinsurance(self, categ_id, contract): """Method to remove any instance of reinsurance to risk models list of reinsurance contracts, and remove its damage distribution from the stack of damage distributions per category. Only used in the delete_reinsurance method of insurancefirm. Accepts: categ_id: Type Integer. - excess_fraction: Type Decimal. - deductible_fraction: Type Decimal. contract: Type DataDict. No return values.""" assert self.reinsurance_contract_stack[categ_id][-1] == contract From 8d225faa17159d64ac8df15669fd201caa6f829a Mon Sep 17 00:00:00 2001 From: Chris Hughes Date: Tue, 16 Jul 2019 15:37:07 +0100 Subject: [PATCH 045/125] Lots of little aesthetic cleanups --- calibrationscore.py | 6 +-- catbond.py | 5 +++ distributionreinsurance.py | 22 +++++------ distributiontruncated.py | 12 +++--- insurancefirm.py | 65 ++++++++++++++++--------------- insurancesimulation.py | 79 ++++++++++++++++++-------------------- isleconfig.py | 9 +++-- logger.py | 1 - metainsurancecontract.py | 8 ++-- metainsuranceorg.py | 71 +++++++++++++++++++++------------- plotter.py | 1 + reinsurancecontract.py | 5 +-- riskmodel.py | 67 ++++++++++++++++++++++---------- 13 files changed, 197 insertions(+), 154 deletions(-) diff --git a/calibrationscore.py b/calibrationscore.py index f9cc48c..415610a 100644 --- a/calibrationscore.py +++ b/calibrationscore.py @@ -39,16 +39,14 @@ def test_all(self): """Print components""" print("\n") for cond_name, score in scores.items(): - print("{0:47s}: {1:8f}".format(cond_name, score)) + print(f"{cond_name:47s}: {score:8f}") """Compute combined score""" self.calibration_score = self.combine_scores( np.array([*scores.values()], dtype=object) ) """Print combined score""" print( - "\n Total calibration score: {0:8f}".format( - self.calibration_score - ) + f"\n Total calibration score: {self.calibration_score:8f}" ) """Return""" return self.calibration_score diff --git a/catbond.py b/catbond.py index 9967efd..adf8c0b 100644 --- a/catbond.py +++ b/catbond.py @@ -1,8 +1,13 @@ import isleconfig from metainsuranceorg import MetaInsuranceOrg +# TODO: This and MetaInsuranceOrg should probably both subclass something simple - a MetaAgent, say. MetaInsuranceOrg +# can do more than a CatBond should be able to! + +# noinspection PyAbstractClass class CatBond(MetaInsuranceOrg): + # noinspection PyMissingConstructor def __init__(self, simulation, per_period_premium, owner, interest_rate=0): """Initialising methods. Accepts: diff --git a/distributionreinsurance.py b/distributionreinsurance.py index 61b425c..904a93c 100644 --- a/distributionreinsurance.py +++ b/distributionreinsurance.py @@ -21,11 +21,11 @@ def __init__(self, dist, lower_bound=None, upper_bound=None): def pdf(self, x): x = np.array(x, ndmin=1) r = map( - lambda Y: self.dist.pdf(Y) - if Y < self.lower_bound + lambda y: self.dist.pdf(y) + if y < self.lower_bound else np.inf - if Y == self.lower_bound - else self.dist.pdf(Y + self.upper_bound - self.lower_bound), + if y == self.lower_bound + else self.dist.pdf(y + self.upper_bound - self.lower_bound), x, ) r = np.array(list(r)) @@ -37,9 +37,9 @@ def pdf(self, x): def cdf(self, x): x = np.array(x, ndmin=1) r = map( - lambda Y: self.dist.cdf(Y) - if Y < self.lower_bound - else self.dist.cdf(Y + self.upper_bound - self.lower_bound), + lambda y: self.dist.cdf(y) + if y < self.lower_bound + else self.dist.cdf(y + self.upper_bound - self.lower_bound), x, ) r = np.array(list(r)) @@ -52,11 +52,11 @@ def ppf(self, x): x = np.array(x, ndmin=1) assert (x >= 0).all() and (x <= 1).all() r = map( - lambda Y: self.dist.ppf(Y) - if Y <= self.dist.cdf(self.lower_bound) + lambda y: self.dist.ppf(y) + if y <= self.dist.cdf(self.lower_bound) else self.dist.ppf(self.dist.cdf(self.lower_bound)) - if Y <= self.dist.cdf(self.upper_bound) - else self.dist.ppf(Y) - self.upper_bound + self.lower_bound, + if y <= self.dist.cdf(self.upper_bound) + else self.dist.ppf(y) - self.upper_bound + self.lower_bound, x, ) r = np.array(list(r)) diff --git a/distributiontruncated.py b/distributiontruncated.py index 6f6a15b..e4ceb0f 100644 --- a/distributiontruncated.py +++ b/distributiontruncated.py @@ -17,8 +17,8 @@ def __init__(self, dist, lower_bound=0.0, upper_bound=1.0): def pdf(self, x): x = np.array(x, ndmin=1) r = map( - lambda Y: self.dist.pdf(Y) / self.normalizing_factor - if (self.lower_bound <= Y <= self.upper_bound) + lambda y: self.dist.pdf(y) / self.normalizing_factor + if (self.lower_bound <= y <= self.upper_bound) else 0, x, ) @@ -31,11 +31,11 @@ def pdf(self, x): def cdf(self, x): x = np.array(x, ndmin=1) r = map( - lambda Y: 0 - if Y < self.lower_bound + lambda y: 0 + if y < self.lower_bound else 1 - if Y > self.upper_bound - else (self.dist.cdf(Y) - self.dist.cdf(self.lower_bound)) + if y > self.upper_bound + else (self.dist.cdf(y) - self.dist.cdf(self.lower_bound)) / self.normalizing_factor, x, ) diff --git a/insurancefirm.py b/insurancefirm.py index 974365a..3bf46a0 100644 --- a/insurancefirm.py +++ b/insurancefirm.py @@ -62,8 +62,8 @@ def adjust_capacity_target(self, max_var): Accepts: max_var: Type Decimal. No return values. - This method decides to increase/decrease the capacity target dependant on if the ratio of capacity target to max - VaR is above/below a predetermined limit.""" + This method decides to increase/decrease the capacity target dependant on if the ratio of + capacity target to max VaR is above/below a predetermined limit.""" reinsurance_var_estimate = self.get_reinsurance_var_estimate(max_var) if max_var + reinsurance_var_estimate == 0: # TODO: why is this being called with max_var = 0 anyway? @@ -92,9 +92,9 @@ def get_capacity(self, max_var): max_var: Type Decimal. Returns: self.cash (+ reinsurance_VaR_estimate): Type Decimal. - This method is called by increase_capacity to get the real capacity of the firm. If the firm has enough money to - cover its max value at risk then its capacity is its cash + the reinsurance VaR estimate, otherwise the firm is - recovering from some losses and so capacity is just cash.""" + This method is called by increase_capacity to get the real capacity of the firm. If the firm has + enough money to cover its max value at risk then its capacity is its cash + the reinsurance VaR + estimate, otherwise the firm is recovering from some losses and so capacity is just cash.""" if max_var < self.cash: reinsurance_var_estimate = self.get_reinsurance_var_estimate(max_var) return self.cash + reinsurance_var_estimate @@ -176,9 +176,7 @@ def increase_capacity_by_category( only issue if market premium is greater than firms average premium.""" if isleconfig.verbose: print( - "IF {0:d} increasing capacity in period {1:d}, cat bond price: {2:f}, reinsurance premium {3:f}".format( - self.id, time, cat_bond_price, reinsurance_price - ) + f"IF {self.id:d} increasing capacity in period {time:d}, cat bond price: {cat_bond_price:f}, reinsurance premium {reinsurance_price:f}" ) if not force: actual_premium = self.get_average_premium(categ_id) @@ -188,13 +186,11 @@ def increase_capacity_by_category( """on the basis of prices decide for obtaining reinsurance or for issuing cat bond""" if reinsurance_price > cat_bond_price: if isleconfig.verbose: - print("IF {0:d} issuing Cat bond in period {1:d}".format(self.id, time)) + print(f"IF {self.id:d} issuing Cat bond in period {time:d}") self.issue_cat_bond(time, categ_id) else: if isleconfig.verbose: - print( - "IF {0:d} getting reinsurance in period {1:d}".format(self.id, time) - ) + print(f"IF {self.id:d} getting reinsurance in period {time:d}") self.ask_reinsurance_non_proportional_by_category(time, categ_id) return True @@ -243,7 +239,7 @@ def ask_reinsurance_non_proportional(self, time): if self.category_reinsurance[categ_id] is None: self.ask_reinsurance_non_proportional_by_category(time, categ_id) - def characterize_underwritten_risks_by_category(self, time, categ_id): + def characterize_underwritten_risks_by_category(self, categ_id): """Method to characterise associated risks in a given category in terms of value, number, avg risk factor, and total premium per iteration. Accepts: @@ -283,9 +279,12 @@ def ask_reinsurance_non_proportional_by_category( and, assuming firms has underwritten risks in category, creates new reinsurance risk with values based on firms existing underwritten risks. If the method was called to create a new risks then it is appended to list of 'reinrisks', otherwise used for creating the risk when a reinsurance contract rolls over.""" - total_value, avg_risk_factor, number_risks, periodized_total_premium = self.characterize_underwritten_risks_by_category( - time, categ_id - ) + [ + total_value, + avg_risk_factor, + number_risks, + periodized_total_premium, + ] = self.characterize_underwritten_risks_by_category(categ_id) if number_risks > 0: risk = { "value": total_value, @@ -358,20 +357,14 @@ def add_reinsurance(self, category, excess_fraction, deductible_fraction, contra ) self.category_reinsurance[category] = contract - def delete_reinsurance( - self, category, excess_fraction, deductible_fraction, contract - ): + def delete_reinsurance(self, category, contract): """Method called by reinsurancecontract to delete the reinsurance contract from the firms counter for the given category, used so that another reinsurance contract can be issued for that category if needed. Accepts: category: Type Integer. - excess_fraction: Type Decimal. Value of excess. - deductible_fraction: Type Decimal. Value of deductible. contract: Type Class. Reinsurance contract issued to firm. No return values.""" - self.riskmodel.delete_reinsurance( - category, excess_fraction, deductible_fraction, contract - ) + self.riskmodel.delete_reinsurance(category, contract) self.category_reinsurance[category] = None def issue_cat_bond(self, time, categ_id, per_value_per_period_premium=0): @@ -384,9 +377,12 @@ def issue_cat_bond(self, time, categ_id, per_value_per_period_premium=0): Method is only called by increase_capacity_by_category method when CatBond prices are lower than reinsurance. It then creates the CatBond as a quasi-reinsurance contract that is paid for immediately (by simulation) with no premium payments.""" - total_value, avg_risk_factor, number_risks, periodized_total_premium = self.characterize_underwritten_risks_by_category( - time, categ_id - ) + [ + total_value, + avg_risk_factor, + number_risks, + _, + ] = self.characterize_underwritten_risks_by_category(categ_id) if number_risks > 0: risk = { "value": total_value, @@ -411,9 +407,11 @@ def issue_cat_bond(self, time, categ_id, per_value_per_period_premium=0): ] ) # TODO: or is it range(1, risk["runtime"]+1)? # catbond = CatBond(self.simulation, per_period_premium) + # TODO: shift obtain_yield method to insurancesimulation, thereby making it unnecessary to drag + # parameters like self.interest_rate from instance to instance and from class to class new_catbond = catbond.CatBond( self.simulation, per_period_premium, self.interest_rate - ) # TODO: shift obtain_yield method to insurancesimulation, thereby making it unnecessary to drag parameters like self.interest_rate from instance to instance and from class to class + ) """add contract; contract is a quasi-reinsurance contract""" contract = ReinsuranceContract( @@ -427,7 +425,7 @@ def issue_cat_bond(self, time, categ_id, per_value_per_period_premium=0): initial_var=var_this_risk, insurancetype=risk["insurancetype"], ) - # per_value_reinsurance_premium = 0 because the insurance firm does not continue to make payments to the cat bond. Only once. + # per_value_reinsurance_premium = 0 because the insurance firm make only one payment to catbond new_catbond.set_contract(contract) """sell cat bond (to self.simulation)""" @@ -493,9 +491,12 @@ def get_excess_of_loss_reinsurance(self): def create_reinrisk(self, time, categ_id): """Proceed with creation of reinsurance risk only if category is not empty.""" - total_value, avg_risk_factor, number_risks, periodized_total_premium = self.characterize_underwritten_risks_by_category( - time, categ_id - ) + [ + total_value, + avg_risk_factor, + number_risks, + periodized_total_premium, + ] = self.characterize_underwritten_risks_by_category(categ_id) if number_risks > 0: # TODO: make runtime into a parameter risk = { diff --git a/insurancesimulation.py b/insurancesimulation.py index b695372..0ef360c 100644 --- a/insurancesimulation.py +++ b/insurancesimulation.py @@ -28,7 +28,7 @@ class InsuranceSimulation: def __init__( self, override_no_riskmodels, - replic_ID, + replic_id, simulation_parameters, rc_event_schedule, rc_event_damage, @@ -53,11 +53,11 @@ def __init__( self.number_riskmodels = simulation_parameters["no_riskmodels"] # save parameters - if (replic_ID is None) or isleconfig.force_foreground: + if (replic_id is None) or isleconfig.force_foreground: self.background_run = False else: self.background_run = True - self.replic_ID = replic_ID + self.replic_id = replic_id self.simulation_parameters = simulation_parameters "Unpacks parameters and sets distributions" @@ -171,8 +171,7 @@ def __init__( ) self.inaccuracy = self._get_all_riskmodel_combinations( - self.simulation_parameters["no_categories"], - self.simulation_parameters["riskmodel_inaccuracy_parameter"], + self.simulation_parameters["riskmodel_inaccuracy_parameter"] ) self.inaccuracy = random.sample( @@ -205,6 +204,9 @@ def __init__( # firms (why can't we just get that from the instances of InsuranceFirm) or a list of the *possible* parameter # values for insurance firms (in which case why does it have the length it does)? self.agent_parameters = {"insurancefirm": [], "reinsurancefirm": []} + self.insurer_id_counter = 0 + self.reinsurer_id_counter = 0 + self.initialize_agent_parameters( "insurancefirm", simulation_parameters, risk_model_configurations ) @@ -245,6 +247,7 @@ def __init__( self.simulation_parameters["no_categories"] ) self._time = None + self.RN = None def initialize_agent_parameters( self, firmtype, simulation_parameters, risk_model_configurations @@ -254,7 +257,6 @@ def initialize_agent_parameters( Creates the agent parameters of both firm types for the initial number specified in isleconfig.py Returns None""" if firmtype == "insurancefirm": - self.insurer_id_counter = 0 no_firms = simulation_parameters["no_insurancefirms"] initial_cash = "initial_agent_cash" reinsurance_level_lowerbound = simulation_parameters[ @@ -265,7 +267,6 @@ def initialize_agent_parameters( ] elif firmtype == "reinsurancefirm": - self.reinsurer_id_counter = 0 no_firms = simulation_parameters["no_reinsurancefirms"] initial_cash = "initial_reinagent_cash" reinsurance_level_lowerbound = simulation_parameters[ @@ -282,6 +283,8 @@ def initialize_agent_parameters( unique_id = self.get_unique_insurer_id() elif firmtype == "reinsurancefirm": unique_id = self.get_unique_reinsurer_id() + else: + raise ValueError(f"Firm type {firmtype} not recognised") if simulation_parameters["static_non-proportional_reinsurance_levels"]: reinsurance_level = simulation_parameters[ @@ -365,7 +368,7 @@ def add_agents(self, agent_class, agent_class_string, agents=None, n=1): ] # We've made the agents, add them to the simulation self.insurancefirms += agents - for agent in agents: + for _ in agents: self.logger.add_insurance_agent() elif agent_class_string == "reinsurancefirm": @@ -389,7 +392,7 @@ def add_agents(self, agent_class, agent_class_string, agents=None, n=1): for ap in agent_parameters ] self.reinsurancefirms += agents - for agent in agents: + for _ in agents: self.logger.add_reinsurance_agent() elif agent_class_string == "catbond": @@ -533,14 +536,12 @@ def save_data(self): Returns None.""" """ collect data """ - total_cash_no = sum( - [insurancefirm.cash for insurancefirm in self.insurancefirms] - ) + total_cash_no = sum([firm.cash for firm in self.insurancefirms]) total_excess_capital = sum( [firm.get_excess_capital() for firm in self.insurancefirms] ) total_profitslosses = sum( - [insurancefirm.get_profitslosses() for insurancefirm in self.insurancefirms] + [firm.get_profitslosses() for firm in self.insurancefirms] ) total_contracts_no = sum( [len(firm.underwritten_contracts) for firm in self.insurancefirms] @@ -681,7 +682,6 @@ def _pay(self, obligation): Returns None""" amount = obligation["amount"] recipient = obligation["recipient"] - purpose = obligation["purpose"] if not self.money_supply > amount: warnings.warn("Something wrong: economy out of money", RuntimeWarning) if recipient.get_operational(): @@ -709,9 +709,7 @@ def _reset_reinsurance_weights(self): self.not_accepted_reinrisks = [] operational_reinfirms = [ - reinsurancefirm - for reinsurancefirm in self.reinsurancefirms - if reinsurancefirm.operational + firm for firm in self.reinsurancefirms if firm.operational ] operational_no = len(operational_reinfirms) @@ -742,15 +740,9 @@ def _reset_insurance_weights(self): """Method for clearing and setting insurance weights dependant on how many insurance companies exist and how many insurance risks are offered. This determined which risks are sent to metainsuranceorg iteration.""" - operational_no = sum( - [insurancefirm.operational for insurancefirm in self.insurancefirms] - ) + operational_no = sum([firm.operational for firm in self.insurancefirms]) - operational_firms = [ - insurancefirm - for insurancefirm in self.insurancefirms - if insurancefirm.operational - ] + operational_firms = [firm for firm in self.insurancefirms if firm.operational] risks_no = len(self.risks) @@ -780,10 +772,10 @@ def _adjust_market_premium(self, capital): Accepts arguments capital: Type float. The total capital (cash) available in the insurance market (insurance only). No return value. - This method adjusts the premium charged by insurance firms for the risks covered. The premium reduces linearly - with the capital available in the insurance market and viceversa. The premium reduces until it reaches a minimum - below which no insurer is willing to reduce further the price. This method is only called in the self.iterate() - method of this class.""" + This method adjusts the premium charged by insurance firms for the risks covered. The premium reduces + linearly with the capital available in the insurance market and viceversa. The premium reduces until it + reaches a minimum below which no insurer is willing to reduce further the price. This method is only called + in the self.iterate() method of this class.""" self.market_premium = self.norm_premium * ( self.simulation_parameters["upper_price_limit"] - self.simulation_parameters["premium_sensitivity"] @@ -804,10 +796,10 @@ def _adjust_reinsurance_market_premium(self, capital): Accepts arguments capital: Type float. The total capital (cash) available in the reinsurance market (reinsurance only). No return value. - This method adjusts the premium charged by reinsurance firms for the risks covered. The premium reduces linearly - with the capital available in the reinsurance market and viceversa. The premium reduces until it reaches a minimum - below which no reinsurer is willing to reduce further the price. This method is only called in the self.iterate() - method of this class.""" + This method adjusts the premium charged by reinsurance firms for the risks covered. The premium reduces + linearly with the capital available in the reinsurance market and viceversa. The premium reduces until it + reaches a minimum below which no reinsurer is willing to reduce further the price. This method is only + called in the self.iterate() method of this class.""" self.reinsurance_market_premium = self.norm_premium * ( self.simulation_parameters["upper_price_limit"] - self.simulation_parameters["reinpremium_sensitivity"] @@ -856,8 +848,9 @@ def get_reinsurance_premium(self, np_reinsurance_deductible_fraction): ) def get_cat_bond_price(self, np_reinsurance_deductible_fraction): - """Method to calculate and return catbond price. If catbonds are not desired will return infinity so no catbonds - will be issued. Otherwise calculates based on reinsurance market premium, catbond premium, deductible fraction. + """Method to calculate and return catbond price. If catbonds are not desired will return infinity so no + catbonds will be issued. Otherwise calculates based on reinsurance market premium, catbond premium, + deductible fraction. Accepts: np_reinsurance_deductible_fraction: Type Integer Returns: @@ -891,7 +884,7 @@ def get_reinrisks(self): np.random.shuffle(self.reinrisks) return self.reinrisks - def solicit_insurance_requests(self, cash, insurer): + def solicit_insurance_requests(self, insurer): """Method for determining which risks are to be assessed by firms based on insurer weights Accepts: cash: Type Integer @@ -910,7 +903,7 @@ def solicit_insurance_requests(self, cash, insurer): return risks_to_be_sent - def solicit_reinsurance_requests(self, cash, reinsurer): + def solicit_reinsurance_requests(self, reinsurer): """Method for determining which reinsurance risks are to be assessed by firms based on reinsurer weights Accepts: id: Type integer @@ -947,7 +940,7 @@ def return_reinrisks(self, not_accepted_risks): Returns None""" self.not_accepted_reinrisks += not_accepted_risks - def _get_all_riskmodel_combinations(self, n, rm_factor): + def _get_all_riskmodel_combinations(self, rm_factor): """Method for calculating riskmodels for each category based on the risk model inaccuracy parameter, and is used purely to assign inaccuracy. Undervalues one risk category and overestimates all the rest. Accepts: @@ -999,8 +992,9 @@ def record_market_exit(self): """Record_market_exit Method. Accepts no arguments. No return value. - This method is used to record the firms that leave the market due to underperforming conditions. It is only called - from the method dissolve() from the class metainsuranceorg.py after the dissolution of the firm.""" + This method is used to record the firms that leave the market due to underperforming conditions. It is + only called from the method dissolve() from the class metainsuranceorg.py after the dissolution of the + firm.""" self.cumulative_market_exits += 1 def record_unrecovered_claims(self, loss): @@ -1092,6 +1086,7 @@ def reinsurance_entry_index(self): 0 : self.simulation_parameters["no_riskmodels"] ].argmin() + # noinspection PyMethodMayBeStatic def get_operational(self): """Method to return if simulation is operational. Always true. Used only in pay methods above and metainsuranceorg. @@ -1131,8 +1126,8 @@ def reset_pls(self): Accepts no arguments. No return value. This method reset all the profits and losses of all insurance firms, reinsurance firms and catbonds.""" - for insurancefirm in self.insurancefirms: - insurancefirm.reset_pl() + for firm in self.insurancefirms: + firm.reset_pl() for reininsurancefirm in self.reinsurancefirms: reininsurancefirm.reset_pl() diff --git a/isleconfig.py b/isleconfig.py index 4775043..e45855d 100644 --- a/isleconfig.py +++ b/isleconfig.py @@ -21,7 +21,7 @@ "margin_increase": 0, # "margin_increase" modifies the margin of safety depending on the number of risks models available in the market. # When it is 0 all risk models have the same margin of safety. - "value_at_risk_tail_probability": 0.005, + "value_at_risk_tail_probability": 0.02, # values <1, >0, usually close to 0; tail probability at which the value at risk is taken by the risk models "norm_profit_markup": 0.15, "rein_norm_profit_markup": 0.15, @@ -102,9 +102,10 @@ "lower_price_limit": 0.85, "no_risks": 20000, # Determines the maximum upscaling of premiums based on insurer size - set to 1 to disable scaled premiums. - # Values between 0 and 1 will make premiums decrease for bigger insurers - "max_scale_premiums": 2, + # High values will give bigger insurers more money + # Values between 0 and 1 will make premiums decrease for bigger insurers. + "max_scale_premiums": 1.2, # Determines the minimum fraction of inaccuracy that insurers can achieve - a value of 0 means the biggest insurers # can be perfectly accurate, a value of 1 disables changes in inaccuracy based on size - "scale_inaccuracy": 0.5, + "scale_inaccuracy": 0.3, } diff --git a/logger.py b/logger.py index d45d2ea..23ad149 100644 --- a/logger.py +++ b/logger.py @@ -1,7 +1,6 @@ """Logging class. Handles records of a single simulation run. Can save and reload. """ import numpy as np -import pdb import listify LOG_DEFAULT = ( diff --git a/metainsurancecontract.py b/metainsurancecontract.py index 627ed35..bbb256b 100644 --- a/metainsurancecontract.py +++ b/metainsurancecontract.py @@ -24,7 +24,7 @@ def __init__( payment_period: Type integer. expire_immediately: Type boolean. True if the contract expires with the first risk event. False if multiple risk events are covered. - initial_var: Type float. Initial value at risk. Used only to compute true and estimated value at risk. + initial_var: Type float. Initial value at risk. Used only to compute true and estimated VaR. optional: insurancetype: Type string. The type of this contract, especially "proportional" vs "excess_of_loss" deductible_fraction: Type float (or int) @@ -111,7 +111,8 @@ def check_payment_due(self, time): Accepts: time: Type integer No return values. - This method checks if a scheduled premium payment is due, pays it to the insurer, and removes from schedule.""" + This method checks if a scheduled premium payment is due, pays it to the insurer, + and removes from schedule.""" if len(self.payment_times) > 0 and time >= self.payment_times[-1]: # Create obligation for premium payment self.property_holder.receive_obligation( @@ -128,7 +129,8 @@ def get_and_reset_current_claim(self): Returns: self.category: Type integer. Which category the contracted risk is in. current_claim: Type decimal - self.insurancetype == proportional: Type Boolean. Returns True if insurance is proportional and vice versa. + self.insurancetype == "proportional": Type Boolean. Returns True if insurance is + proportional and vice versa. This method retuns the current claim, then resets it, and also indicates the type of insurance.""" current_claim = self.current_claim self.current_claim = 0 diff --git a/metainsuranceorg.py b/metainsuranceorg.py index 833cadb..f9aa628 100644 --- a/metainsuranceorg.py +++ b/metainsuranceorg.py @@ -10,6 +10,14 @@ def get_mean(x): + """ + Returns the mean of a list + Args: + x: an iterable of numerics + + Returns: + the mean of x + """ return sum(x) / len(x) @@ -30,8 +38,8 @@ def __init__(self, simulation_parameters, agent_parameters): Accepts: Simulation_parameters: Type DataDict agent_parameters: Type DataDict - Constructor creates general instance of an insurance company which is inherited by the reinsurance and - insurance firm classes. Initialises all necessary values provided by config file.""" + Constructor creates general instance of an insurance company which is inherited by the reinsurance + and insurance firm classes. Initialises all necessary values provided by config file.""" self.simulation = simulation_parameters["simulation"] self.simulation_parameters = simulation_parameters self.contract_runtime_dist = scipy.stats.randint( @@ -115,7 +123,7 @@ def __init__(self, simulation_parameters, agent_parameters): ) self.category_reinsurance = [ - None for i in range(self.simulation_no_risk_categories) + None for _ in range(self.simulation_no_risk_categories) ] if self.simulation_reinsurance_type == "non-proportional": if agent_parameters["non-proportional_reinsurance_level"] is not None: @@ -160,7 +168,7 @@ def __init__(self, simulation_parameters, agent_parameters): self.simulation_parameters["no_categories"] ) self.market_permanency_counter = 0 - # The share of all risks that this firm holds + # The share of all risks that this firm holds. Gets updated every timestep self.risk_share = 0 def iterate(self, time): @@ -168,9 +176,9 @@ def iterate(self, time): Accepts: Time: Type Integer No return value - For each time step this method obtains every firms interest payments, pays obligations, claim reinsurance, - matures necessary contracts. Check condition for operational firms (as never removed) so only operational - firms receive new risks to evaluate, pay dividends, adjust capacity.""" + For each time step this method obtains every firms interest payments, pays obligations, claim + reinsurance, matures necessary contracts. Check condition for operational firms (as never removed) + so only operational firms receive new risks to evaluate, pay dividends, adjust capacity.""" """obtain investments yield""" self.obtain_yield(time) @@ -269,10 +277,16 @@ def collect_process_evaluate_risks(self, time, contracts_dissolved): """obtain risk model evaluation (VaR) for underwriting decisions and for capacity specific decisions""" # TODO: Enable reinsurance shares other than 0.0 and 1.0 - expected_profit, acceptable_by_category, cash_left_by_categ, var_per_risk_per_categ, self.excess_capital = self.riskmodel.evaluate( - underwritten_risks, self.cash - ) - # TODO: resolve insurance reinsurance inconsistency (insurer underwrite after capacity decisions, reinsurers before). + [ + _, + acceptable_by_category, + cash_left_by_categ, + var_per_risk_per_categ, + self.excess_capital, + ] = self.riskmodel.evaluate(underwritten_risks, self.cash) + # TODO: resolve insurance reinsurance inconsistency (insurer underwrite after capacity decisions, + # reinsurers before). + # This is currently so because it minimizes the number of times we need to run self.riskmodel.evaluate(). # It would also be more consistent if excess capital would be updated at the end of the iteration. """handle adjusting capacity target and capacity""" @@ -305,7 +319,8 @@ def collect_process_evaluate_risks(self, time, contracts_dissolved): ) for repetition in range(self.recursion_limit): - # TODO: find an efficient way to stop the recursion if there are no more risks to accept or if it is not accepting any more over several iterations. + # TODO: find an efficient way to stop the recursion if there are no more risks to accept or if it is + # not accepting any more over several iterations. Done, maybe? former_risks_per_categ = copy.copy(risks_per_categ) # Here we process all the new risks in order to keep the portfolio as balanced as possible. risks_per_categ, not_accepted_risks = self.process_newrisks_insurer( @@ -330,9 +345,9 @@ def enter_illiquidity(self, time): Accepts arguments time: Type integer. The current time. No return value. - This method is called when a firm does not have enough cash to pay all its obligations. It is only called from - the method self._effect_payments() which is called at the beginning of the self.iterate() method of this class. - This method formalizes the bankruptcy through the method self.enter_bankruptcy().""" + This method is called when a firm does not have enough cash to pay all its obligations. It is only called + from the method self._effect_payments() which is called at the beginning of the self.iterate() method of + this class. This method formalizes the bankruptcy through the method self.enter_bankruptcy().""" self.enter_bankruptcy(time) def enter_bankruptcy(self, time): @@ -350,11 +365,11 @@ def market_exit(self, time): Accepts arguments time: Type integer. The current time. No return value. - This method is called when a firms wants to leave the market because it feels that it has been underperforming - for too many periods. It is only called from the method self.market_permanency() that it is run in the main iterate - method of this class. It needs to be different from the method self.enter_bankruptcy() because in this case - all the obligations can be paid. After paying all the obligations this method dissolves the firm through the - method self.dissolve().""" + This method is called when a firms wants to leave the market because it feels that it has been + underperforming for too many periods. It is only called from the method self.market_permanency() that it is + run in the main iterate method of this class. It needs to be different from the method + self.enter_bankruptcy() because in this case all the obligations can be paid. After paying all the + obligations this method dissolves the firm through the method self.dissolve().""" due = [item for item in self.obligations] for obligation in due: self.pay(obligation) @@ -369,11 +384,12 @@ def dissolve(self, time, record): the dissolution of the firm.So far it can be either 'record_bankruptcy' or 'record_market_exit'. No return value. This method dissolves the firm. It is called from the methods self.enter_bankruptcy() and self.market_exit() - of this class (metainsuranceorg.py). First of all it dissolves all the contracts currently held (those in self.underwritten_contracts). + of this class (metainsuranceorg.py). First of all it dissolves all the contracts currently held (those in + self.underwritten_contracts). Next all the cash currently available is transferred to insurancesimulation.py through an obligation in the next iteration. Finally the type of dissolution is recorded and the operational state is set to false. - Different class variables are reset during the process: self.risks_kept, self.reinrisks_kept, self.excess_capital - and self.profits_losses.""" + Different class variables are reset during the process: self.risks_kept, self.reinrisks_kept, + self.excess_capital and self.profits_losses.""" for contract in self.underwritten_contracts: contract.dissolve(time) # removing (dissolving) all risks immediately after bankruptcy (may not be realistic, @@ -478,7 +494,8 @@ def pay_dividends(self, time): Accepts: time: Type integer No return value - If firm has positive profits will pay percentage of them as dividends. Currently pays to simulation.""" + If firm has positive profits will pay percentage of them as dividends. Currently pays to simulation. + """ self.receive_obligation(self.per_period_dividend, self.owner, time, "dividend") @@ -587,9 +604,9 @@ def get_newrisks_by_type(self): new_risks: Type list of DataDicts.""" new_risks = [] if self.is_insurer: - new_risks += self.simulation.solicit_insurance_requests(self.cash, self) + new_risks += self.simulation.solicit_insurance_requests(self) if self.is_reinsurer: - new_risks += self.simulation.solicit_reinsurance_requests(self.cash, self) + new_risks += self.simulation.solicit_reinsurance_requests(self) new_nonproportional_risks = [ risk @@ -719,7 +736,7 @@ def process_newrisks_reinsurer( """Method to decide if new risks are underwritten for the reinsurance firm. Accepts: reinrisks_per_categ: Type List of lists containing new reinsurance risks. - number_reinrisks_per_categ: Type List of integers, contains number of new reinsurance risks per category. + number_reinrisks_per_categ: Type List of integers, contains number of new reinsurance risks by category. time: Type integer No return values. This method processes one by one the reinrisks contained in reinrisks_per_categ in order to decide whether diff --git a/plotter.py b/plotter.py index 7ccba97..562d44b 100755 --- a/plotter.py +++ b/plotter.py @@ -1,4 +1,5 @@ import matplotlib.pyplot as plt +from numpy import array rfile = open("data/history_logs.dat", "r") diff --git a/reinsurancecontract.py b/reinsurancecontract.py index 9e8164d..f09047f 100644 --- a/reinsurancecontract.py +++ b/reinsurancecontract.py @@ -102,10 +102,7 @@ def mature(self, time): if self.insurancetype == "excess-of-loss": self.property_holder.delete_reinsurance( - category=self.category, - excess_fraction=self.excess_fraction, - deductible_fraction=self.deductible_fraction, - contract=self, + category=self.category, contract=self ) else: # TODO: ? Instead: if self.insurancetype == "proportional": self.contract.unreinsure() diff --git a/riskmodel.py b/riskmodel.py index 7993542..2341558 100644 --- a/riskmodel.py +++ b/riskmodel.py @@ -24,7 +24,7 @@ def __init__( self.cat_separation_distribution = cat_separation_distribution self.norm_premium = norm_premium # QUERY: Whis is this passed as an argument and then ignored? - self.var_tail_prob = 0.02 + self.var_tail_prob = var_tail_prob self.expire_immediately = expire_immediately self.category_number = category_number self.init_average_exposure = init_average_exposure @@ -86,12 +86,35 @@ def compute_expectation(self, categ_risks, categ_id): # TODO: more intuitive na if self.expire_immediately: incr_expected_profits = -1 # TODO: fix the norm_premium estimation - # incr_expected_profits = (self.norm_premium - (1 - scipy.stats.poisson(1 / self.cat_separation_distribution.mean() * \ - # mean_runtime).pmf(0)) * self.damage_distribution[categ_id].mean() * average_risk_factor) * average_exposure * len(categ_risks) + # incr_expected_profits = ( + # ( + # self.norm_premium + # - ( + # 1 + # - scipy.stats.poisson( + # 1 / self.cat_separation_distribution.mean() * mean_runtime + # ).pmf(0) + # ) + # * self.damage_distribution[categ_id].mean() + # * average_risk_factor + # ) + # * average_exposure + # * len(categ_risks) + # ) else: incr_expected_profits = -1 # TODO: expected profits should only be returned once the expire_immediately == False case is fixed - # incr_expected_profits = (self.norm_premium - mean_runtime / self.cat_separation_distribution[categ_id].mean() * self.damage_distribution.mean() * average_risk_factor) * average_exposure * len(categ_risks) + # incr_expected_profits = ( + # ( + # self.norm_premium + # - mean_runtime + # / self.cat_separation_distribution[categ_id].mean() + # * self.damage_distribution.mean() + # * average_risk_factor + # ) + # * average_exposure + # * len(categ_risks) + # ) return average_risk_factor, average_exposure, incr_expected_profits @@ -102,8 +125,10 @@ def evaluate_proportional(self, risks, cash): cash: Type List. Gives cash available for each category. Returns: expected_profits: Type Decimal (Currently returns None) - remaining_acceptable_by_category: Type List of Integers. Number of risks that would not be covered by firms cash. - cash_left_by_category: Type List of Integers. Firms expected cash left if underwriting the risks from that category. + remaining_acceptable_by_category: Type List of Integers. Number of risks that would not be covered by + firms cash. + cash_left_by_category: Type List of Integers. Firms expected cash left if underwriting the risks from + that category. var_per_risk_per_categ: List of Integers. Average VaR per category. This method iterates through the risks in each category and calculates the average VaR, how many could be underwritten according to their average VaR, how much cash would be left per category if all risks were @@ -185,7 +210,7 @@ def evaluate_proportional(self, risks, cash): max_cash_by_categ = max(cash_left_by_category) floored_cash_by_categ = cash_left_by_category.copy() floored_cash_by_categ[floored_cash_by_categ < 0] = 0 - remaining_acceptable_by_category_old = remaining_acceptable_by_category.copy() + # remaining_acceptable_by_category_old = remaining_acceptable_by_category.copy() for categ_id in range(self.category_number): # QUERY: Where does this come from? remaining_acceptable_by_category[categ_id] = math.floor( @@ -292,16 +317,19 @@ def evaluate(self, risks, cash, offered_risk=None): offered_risk: Type DataDict or defaults to None. (offered_risk = None) Returns: expected_profits_proportional: Type Decimal (Currently returns None) - remaining_acceptable_by_categ: Type List of Integers. Number of risks that would not be covered by firms cash. - cash_left_by_categ: Type List of Integers. Firms expected cash left if underwriting the risks from that category. + remaining_acceptable_by_categ: Type List of Integers. Number of risks that would not be covered by + firms cash. + cash_left_by_categ: Type List of Integers. Firms expected cash left if underwriting the risks from that + category. var_per_risk_per_categ: List of Integers. Average VaR per category min(cash_left_by_categ): Type Decimal. Minimum (offered_risk != None) Returns: - (cash_left_by_categ - additional_required > 0).all(): Type Boolean. Returns True only if all categories have - enough to cover the additional capital to insure risk. + (cash_left_by_categ - additional_required > 0).all(): Type Boolean. Returns True only if all categories + have enough to cover the additional capital to insure risk. cash_left_by_categ: Type List of Decimals. Cash left per category if all risks claimed. var_this_risk: Type Decimal. Expected claim of offered risk. - min(cash_left_by_categ): Type Decimal. Minimum value of cash left in a category after covering all expected claims. + min(cash_left_by_categ): Type Decimal. Minimum value of cash left in a category after covering all + expected claims. This method organises all risks by insurance type then delegates then to respective methods (evaluate_prop/evaluate_excess_of_loss). Excess of loss risks are processed one at a time and are admitted using the offered_risk argument, whereas proportional risks are processed all at once leaving offered_risk = 0. This @@ -328,9 +356,12 @@ def evaluate(self, risks, cash, offered_risk=None): el_risks, cash_left_by_categ, offered_risk ) if (offered_risk is None) or (len(risks) > 0): - expected_profits_proportional, remaining_acceptable_by_categ, cash_left_by_categ, var_per_risk_per_categ = self.evaluate_proportional( - risks, cash_left_by_categ - ) + [ + expected_profits_proportional, + remaining_acceptable_by_categ, + cash_left_by_categ, + var_per_risk_per_categ, + ] = self.evaluate_proportional(risks, cash_left_by_categ) if offered_risk is None: # return numbers of remaining acceptable risks by category return ( @@ -378,16 +409,12 @@ def add_reinsurance(self, categ_id, excess_fraction, deductible_fraction, contra dist=self.damage_distribution[categ_id], ) - def delete_reinsurance( - self, categ_id, excess_fraction, deductible_fraction, contract - ): + def delete_reinsurance(self, categ_id, contract): """Method to remove any instance of reinsurance to risk models list of reinsurance contracts, and remove its damage distribution from the stack of damage distributions per category. Only used in the delete_reinsurance method of insurancefirm. Accepts: categ_id: Type Integer. - excess_fraction: Type Decimal. - deductible_fraction: Type Decimal. contract: Type DataDict. No return values.""" assert self.reinsurance_contract_stack[categ_id][-1] == contract From e093301ed0741c4081d8800e584240af9e6b50d2 Mon Sep 17 00:00:00 2001 From: Chris Hughes Date: Tue, 16 Jul 2019 16:45:30 +0100 Subject: [PATCH 046/125] Slightly updated the readme --- README.md | 20 ++++++++++++++++---- start.py | 54 ++++++++++++++++++++++++------------------------------ 2 files changed, 40 insertions(+), 34 deletions(-) diff --git a/README.md b/README.md index 385044d..50b48ef 100644 --- a/README.md +++ b/README.md @@ -12,6 +12,9 @@ $ pip install -r requirements.txt # Usage +Isle requires that `./data` does not exist as a file, and may overwrite +particular file names in `./data/`. + ## Simulation Execute a single simulation run with the command: @@ -25,15 +28,16 @@ $ python3 start.py The ```start.py``` script accepts a number of options. ``` -usage: start.py [--oneriskmodel] [--riskmodels {1,2,3,4}] [--replicid REPLICID] - [--replicating] [--randomseed RANDOMSEED] [--foreground] - [--shownetwork] [-p] [-v] [--save_iterations] +usage: start.py [-h] [-f FILE] [-r] [-o] [-p] [-v] [--resume] [--oneriskmodel] + [--riskmodels {1,2,3,4}] [--randomseed RANDOMSEED] + [--foreground] [--shownetwork] + [--save_iterations SAVE_ITERATIONS] ``` See the help for more details ``` -python3 start.py --help +$ python3 start.py --help ``` ## Ensemble simulations @@ -57,3 +61,11 @@ Use the script ```plotter.py``` to plot insurer and reinsurer data, or run Use ```metaplotter_pl_timescale.py```, ```metaplotter_pl_timescale_additional_measures.py```, or ```visualisation.py [--comparison]``` to visualize ensemble runs. +# Contributing + +## Code style + +[PEP 8](https://www.python.org/dev/peps/pep-0008/) styling should be used where possible. +The Python code formatter [black](https://github.com/python/black) is a good way +to automatically fix style problems - install it with `$ pip install black` and +then run it with, say, `black *.py`. diff --git a/start.py b/start.py index f534ca2..01ba166 100644 --- a/start.py +++ b/start.py @@ -139,28 +139,6 @@ def load_simulation(): """ use argparse to handle command line arguments""" parser = argparse.ArgumentParser(description="Model the Insurance sector") - parser.add_argument( - "--abce", action="store_true", help="[REMOVED] ABCE no longer supported" - ) - parser.add_argument( - "--resume", - action="store_true", - help="Resume the simulation from a previous save in ./data/simulation_save.pkl. " - "All other arguments will be ignored", - ) - parser.add_argument( - "--oneriskmodel", - action="store_true", - help="allow overriding the number of riskmodels from the standard config (with 1)", - ) - parser.add_argument( - "--riskmodels", - type=int, - choices=[1, 2, 3, 4], - help="allow overriding the number of riskmodels from standard config (with 1 or other numbers)." - " Overrides --oneriskmodel", - ) - parser.add_argument("--replicid", type=int, help="[REMOVED], use -f (--file)") parser.add_argument( "-f", "--file", @@ -182,6 +160,30 @@ def load_simulation(): action="store_true", help="allows overwriting of the file specified by -f", ) + parser.add_argument( + "-p", "--showprogress", action="store_true", help="show timesteps" + ) + parser.add_argument( + "-v", "--verbose", action="store_true", help="more detailed output" + ) + parser.add_argument( + "--resume", + action="store_true", + help="Resume the simulation from a previous save in ./data/simulation_save.pkl. " + "All other arguments will be ignored", + ) + parser.add_argument( + "--oneriskmodel", + action="store_true", + help="allow overriding the number of riskmodels from the standard config (with 1)", + ) + parser.add_argument( + "--riskmodels", + type=int, + choices=[1, 2, 3, 4], + help="allow overriding the number of riskmodels from standard config (with 1 or other numbers)." + " Overrides --oneriskmodel", + ) parser.add_argument( "--randomseed", type=float, help="allow setting of numpy random seed" ) @@ -195,12 +197,6 @@ def load_simulation(): action="store_true", help="show reinsurance relations as network", ) - parser.add_argument( - "-p", "--showprogress", action="store_true", help="show timesteps" - ) - parser.add_argument( - "-v", "--verbose", action="store_true", help="more detailed output" - ) parser.add_argument( "--save_iterations", type=int, @@ -208,8 +204,6 @@ def load_simulation(): ) args = parser.parse_args() - if args.abce: - raise Exception("ABCE is not and will not be supported") if args.oneriskmodel: isleconfig.oneriskmodel = True override_no_riskmodels = 1 From f7d3bfd080bba362ccc3f43d6a0661fe0037a9c3 Mon Sep 17 00:00:00 2001 From: KloskaT Date: Wed, 17 Jul 2019 13:04:02 +0100 Subject: [PATCH 047/125] Can now call either pie plots or timeseries from command line when running file. Added events to pie chart. --- visualisation.py | 35 +++++++++++++++++++++-------------- 1 file changed, 21 insertions(+), 14 deletions(-) diff --git a/visualisation.py b/visualisation.py index 5cecccd..ac57620 100644 --- a/visualisation.py +++ b/visualisation.py @@ -24,7 +24,8 @@ def __init__(self, series_list, event_schedule, damage_schedule, title="",xlabel self.fig, self.axlst = plt.subplots(self.size,sharex=True) def plot(self, schedule=False): - event_categ_colours = ['r', 'b', 'g', 'fuchsia'] + multi_categ_colours = ['r', 'b', 'g', 'fuchsia'] + single_categ_colours = ['b', 'b', 'b', 'b'] for i, (series, series_label, fill_lower, fill_upper) in enumerate(self.series_list): self.axlst[i].plot(self.timesteps, series,color=self.colour) self.axlst[i].set_ylabel(series_label) @@ -37,7 +38,7 @@ def plot(self, schedule=False): for event_time in self.events_schedule[categ]: index = self.events_schedule[categ].index(event_time) if self.damage_schedule[categ][index] > 0.5: # Only plots line if event is significant - self.axlst[i].axvline(event_time, color=event_categ_colours[categ], alpha=self.damage_schedule[categ][index]) + self.axlst[i].axvline(event_time, color=single_categ_colours[categ], alpha=self.damage_schedule[categ][index]) self.axlst[self.size-1].set_xlabel(self.xlabel) self.fig.suptitle(self.title) @@ -60,7 +61,7 @@ class InsuranceFirmAnimation(object): No return values. This class takes the cash and contract data of each firm over all time and produces an animation showing how the proportion of each for all operational firms changes with time. Allows it to be saved as an MP4.""" - def __init__(self, cash_data, insure_contracts, event_schedule, type, save=False, perils=False): + def __init__(self, cash_data, insure_contracts, event_schedule, type, save=False, perils=True): # Converts list of events by category into list of all events. self.perils_condition = perils self.all_event_times = [] @@ -78,6 +79,11 @@ def __init__(self, cash_data, insure_contracts, event_schedule, type, save=False self.type = type def animate(self): + """Method to call animation of pie charts. + No accepted values. + No returned values. + This method is called after the simulation class is initialised to start the animation of pie charts, and will + save it as an mp4 if applicable.""" self.pies = [0, 0] self.fig, (self.ax1, self.ax2) = plt.subplots(1, 2) self.stream = self.data_stream() @@ -127,10 +133,8 @@ def update(self, i): self.fig.suptitle("%s Timestep : %i" % (self.type, i)) if self.perils_condition: if i == self.all_event_times[0]: - self.fig.set_facecolor('r') + self.fig.suptitle('EVENT AT TIME %i!' % i) self.all_event_times = self.all_event_times[1:] - else: - self.fig.set_facecolor('w') return self.pies def save(self): @@ -230,7 +234,7 @@ def reinsurer_time_series(self, runs=None, axlst=None, fig=None, title="Reinsure (reincash, 'Cash', np.percentile(reincash_agg,percentiles[0], axis=0), np.percentile(reincash_agg, percentiles[1], axis=0)), (catbonds_number, "Activate Cat Bonds", np.percentile(catbonds_number_agg,percentiles[0], axis=0), np.percentile(catbonds_number_agg, percentiles[1], axis=0)), ], events, damages, title=title, xlabel="Time", axlst=axlst, fig=fig, colour=colour) - self.reins_time_series.plot() + self.reins_time_series.plot(schedule=True) return self.reins_time_series def metaplotter_timescale(self): @@ -281,11 +285,12 @@ def save(self): if __name__ == "__main__": # use argparse to handle command line arguments parser = argparse.ArgumentParser(description='Model the Insurance sector') - parser.add_argument("--single", action="store_true", help="plot time series of a single run of the insurance model") + parser.add_argument("--single", action="store_true", help="plot a single run of the insurance model") parser.add_argument("--comparison", action="store_true", help="plot the result of an ensemble of replicatons of the insurance model") - + parser.add_argument("--pie", action="store_true", help="plot animated pie charts of contract and cash data") + parser.add_argument("--timeseries", action="store_true", help="plot time series of firm data") args = parser.parse_args() - args.single = True + args.single = args.pie = True if args.single: # load in data from the history_logs dictionarywith open("data/history_logs.dat","r") as rfile: @@ -294,10 +299,12 @@ def save(self): # first create visualisation object, then create graph/animation objects as necessary vis = visualisation(history_logs_list) - vis.insurer_pie_animation() - vis.reinsurer_pie_animation() - # vis.insurer_time_series() - # vis.reinsurer_time_series() + if args.pie: + vis.insurer_pie_animation() + vis.reinsurer_pie_animation() + if args.timeseries: + vis.insurer_time_series() + vis.reinsurer_time_series() vis.show() N = len(history_logs_list) From 5cc359f940b7e690ddc552d2d40ea73388d2414f Mon Sep 17 00:00:00 2001 From: KloskaT Date: Wed, 17 Jul 2019 13:08:49 +0100 Subject: [PATCH 048/125] Necessary network data is now saved. Added class that animates (and saves) saved network data. Can be called fun from command line. Also added unique firm IDs to nodes. Added event timings to network. --- insurancesimulation.py | 16 +++-- visualization_network.py | 122 ++++++++++++++++++++++++++++++++++++++- 2 files changed, 130 insertions(+), 8 deletions(-) diff --git a/insurancesimulation.py b/insurancesimulation.py index eec5803..c12a0ce 100644 --- a/insurancesimulation.py +++ b/insurancesimulation.py @@ -11,7 +11,7 @@ import copy import logger -if isleconfig.show_network: +if isleconfig.show_network or isleconfig.save_network: import visualization_network @@ -343,11 +343,17 @@ def iterate(self, t): self.reinsurance_models_counter[i] += 1 network_division = 1 # How often network is updated. - if isleconfig.show_network and t % network_division == 0 and t > 0: - if t == network_division: - self.RN = visualization_network.ReinsuranceNetwork() # Only creates once instance so only one figure. + if (isleconfig.show_network or isleconfig.save_network) and t % network_division == 0 and t > 0: + if t == network_division: # Only creates once instance so only one figure. + self.RN = visualization_network.ReinsuranceNetwork(self.rc_event_schedule_initial) + self.RN.update(self.insurancefirms, self.reinsurancefirms, self.catbonds) - self.RN.visualize() + + if isleconfig.show_network: + self.RN.visualize() + if isleconfig.save_network and t == (self.simulation_parameters['max_time']-800): + self.RN.save_network_data() + print("Network data has been saved to data/network_data.dat") def save_data(self): """Method to collect statistics about the current state of the simulation. Will pass these to the diff --git a/visualization_network.py b/visualization_network.py index b0497d1..80198e3 100644 --- a/visualization_network.py +++ b/visualization_network.py @@ -1,14 +1,19 @@ import networkx as nx import matplotlib.pyplot as plt +import matplotlib.animation as animation import numpy as np +import argparse -class ReinsuranceNetwork(): - def __init__(self): + +class ReinsuranceNetwork: + def __init__(self, event_schedule=None): """Initialising method for ReinsuranceNetwork. No accepted values. This created the figure that the network will be displayed on so only called once, and only if show_network is True.""" self.figure = plt.figure(num=None, figsize=(10, 8), dpi=100, facecolor='w', edgecolor='k') + self.save_data = {'unweighted_network': [], 'weighted_network': [], 'network_edgelabels': [], 'network_node_labels': [], 'number_of_agents': []} + self.event_schedule = event_schedule def compute_measures(self): """Method to obtain the network distribution and print it. @@ -55,10 +60,11 @@ def update(self, insurancefirms, reinsurancefirms, catbonds): """Create weighted adjacency matrix and category edge labels""" weights_matrix = np.zeros(self.network_size ** 2).reshape(self.network_size, self.network_size) self.edge_labels = {} + self.node_labels = {} for idx_to, firm in enumerate(op_entities["insurers"] + op_entities["reinsurers"]): + self.node_labels[idx_to] = firm.id eolrs = firm.get_excess_of_loss_reinsurance() for eolr in eolrs: - # pdb.set_trace() try: idx_from = self.num_entities["insurers"] + (op_entities["reinsurers"] + op_entities["catbonds"]).index(eolr["reinsurer"]) weights_matrix[idx_from][idx_to] = eolr["value"] @@ -73,6 +79,13 @@ def update(self, insurancefirms, reinsurancefirms, catbonds): self.network = nx.from_numpy_array(weights_matrix, create_using=nx.DiGraph()) # weighted self.network_unweighted = nx.from_numpy_array(adj_matrix, create_using=nx.DiGraph()) # unweighted + """Add this iteration of network data to be saved""" + self.save_data['unweighted_network'].append(adj_matrix.tolist()) + self.save_data['weighted_network'].append(weights_matrix.tolist()) + self.save_data['network_edgelabels'].append(self.edge_labels) + self.save_data['network_node_labels'].append(self.node_labels) + self.save_data['number_of_agents'].append(self.num_entities) + def visualize(self): """Method to add the network to the figure initialised in __init__. No accepted values. @@ -103,6 +116,7 @@ def visualize(self): node_color='g', node_size=50, alpha=0.9, label='CatBond') nx.draw_networkx_edges(self.network_unweighted, pos, width=1.0, alpha=0.5, node_size=50) nx.draw_networkx_edge_labels(self.network_unweighted, pos, self.edge_labels, font_size=5) + nx.draw_networkx_labels(self.network_unweighted, pos, self.node_labels, font_size=20) plt.legend(scatterpoints=1, loc='upper right') plt.axis('off') plt.show() @@ -110,3 +124,105 @@ def visualize(self): """Update figure""" self.figure.canvas.flush_events() self.figure.clear() + + def save_network_data(self): + with open("data/network_data.dat", "w") as wfile: + wfile.write(str(self.save_data) + "\n") + wfile.write(str(self.event_schedule) + "\n") + + +class LoadNetwork: + def __init__(self, network_data, num_iter): + """Initialises LoadNetwork class. + Accepts: + network_data: Type List. Contains a DataDict of the network data, and a list of events. + num_iter: Type Integer. Used to tell animation how many frames it should have. + No return values. + This class is given the loaded network data and then uses it to create an animated network.""" + self.figure = plt.figure(num=None, figsize=(10, 8), dpi=100, facecolor='w', edgecolor='k') + self.unweighted_network_data = network_data[0]["unweighted_network"] + # self.weighted_network_data = network_data[0]["weighted_network"] # Unused for now + self.network_edge_labels = network_data[0]["network_edgelabels"] + self.network_node_labels = network_data[0]["network_node_labels"] + self.number_agent_type = network_data[0]["number_of_agents"] + self.event_schedule = network_data[1] + self.num_iter = num_iter + + self.all_events = [] + for categ in self.event_schedule: + self.all_events += categ + self.all_events.sort() + + def update(self, i): + """Method to update network animation. + Accepts: + i: Type Integer, iterator. + No return values. + This method is called from matplotlib.animate.FuncAnimation to update the plot to the next time iteration.""" + self.figure.clear() + plt.suptitle('Network Timestep %i' % i) + unweighted_nx_network = nx.from_numpy_array(np.array(self.unweighted_network_data[i])) + pos = nx.shell_layout(unweighted_nx_network) + + nx.draw_networkx_nodes(unweighted_nx_network, pos, list(range(self.number_agent_type[i]["insurers"])), + node_color='b', node_size=50, alpha=0.9, label='Insurer') + nx.draw_networkx_nodes(unweighted_nx_network, pos, list( + range(self.number_agent_type[i]["insurers"], + self.number_agent_type[i]["insurers"] + self.number_agent_type[i]["reinsurers"])), + node_color='r', node_size=50, alpha=0.9, label='Reinsurer') + nx.draw_networkx_nodes(unweighted_nx_network, pos, list( + range(self.number_agent_type[i]["insurers"] + self.number_agent_type[i]["reinsurers"], + self.number_agent_type[i]["insurers"] + self.number_agent_type[i]["reinsurers"] + + self.number_agent_type[i]['catbonds'])), + node_color='g', node_size=50, alpha=0.9, label='CatBond') + nx.draw_networkx_edges(unweighted_nx_network, pos, width=1.0, alpha=0.5, node_size=50) + + nx.draw_networkx_edge_labels(self.unweighted_network_data[i], pos, self.network_edge_labels[i], font_size=5) + nx.draw_networkx_labels(self.unweighted_network_data[i], pos, self.network_node_labels[i], font_size=10) + + while self.all_events[0] == i: + plt.title('EVENT!') + self.all_events = self.all_events[1:] + + plt.legend() + plt.axis('off') + + def animate(self): + """Method to create animation. + No accepted values. + No return values.""" + self.network_ani = animation.FuncAnimation(self.figure, self.update, frames=self.num_iter, repeat=False, + interval=20, save_count=self.num_iter) + + def save_network_animation(self): + """Method to save animation as MP4. + No accepted values. + No return values.""" + self.network_ani.save("data/animated_network.mp4", writer="ffmpeg", dpi=200, fps=5) + + +if __name__ == "__main__": + # Use argparse to handle command line arguments + parser = argparse.ArgumentParser(description='Plot the network of the insurance sector') + parser.add_argument("--save", action="store_true", help="Save the network as an mp4") + parser.add_argument("--number_iterations", type=int, help="number of frames for animation") + args = parser.parse_args() + + if args.number_iterations: + num_iter = args.number_iterations + else: + num_iter = 100 + + # Access stored network data + with open("data/network_data.dat", "r") as rfile: + network_data_dict = [eval(k) for k in rfile] + + # Load network data and create animation data for given number of iterations + loaded_network = LoadNetwork(network_data_dict, num_iter=num_iter) + loaded_network.animate() + + # Either display or save network, dependant on args + if args.save: + loaded_network.save_network_animation() + else: + plt.show() From 681c2871d4d05f3f80ab3af8e883f77d02b9135b Mon Sep 17 00:00:00 2001 From: KloskaT Date: Wed, 17 Jul 2019 13:09:27 +0100 Subject: [PATCH 049/125] Added condition for saving network data, set true by default. --- isleconfig.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/isleconfig.py b/isleconfig.py index 8882c5d..5f46e01 100644 --- a/isleconfig.py +++ b/isleconfig.py @@ -3,7 +3,8 @@ force_foreground = False verbose = False showprogress = True -show_network = True # Should network be visualized? This should be False by default, to be overridden by commandline arguments +show_network = False # Should network be visualized? This should be False by default, to be overridden by commandline arguments +save_network = True slim_log = False # Should logs be small in ensemble runs (only aggregated level data)? simulation_parameters = {"no_categories": 4, @@ -75,4 +76,3 @@ "lower_price_limit": 0.85, "no_risks": 20000} - From eccc94b64fdcbf938a3e20fd758b6af09fc2df8d Mon Sep 17 00:00:00 2001 From: KloskaT Date: Wed, 17 Jul 2019 14:18:55 +0100 Subject: [PATCH 050/125] Updated README for visualisation. --- README.md | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 385044d..a28e631 100644 --- a/README.md +++ b/README.md @@ -47,11 +47,15 @@ bash starter_four.sh bash starter_three.sh ``` -## Plotting +## Visualisation #### Single runs -Use the script ```plotter.py``` to plot insurer and reinsurer data, or run -```visualisation.py [--single]``` from the command line to plot this and visualise the run. +Use the script ```visualisation.py [--single]``` from the command line to plot data from a single run. It also takes the +arguments ```[--pie] [--timeseries]``` for which data representation is wanted. + +If the necessary data has been saved a network animation can also be created by running ```visualization_network.py``` +which takes the arguments ```[--save] [--number_iterations]``` if you want the animation to be saved as an mp4, and how +time iterations you want in the animation. #### Ensemble runs Use ```metaplotter_pl_timescale.py```, ```metaplotter_pl_timescale_additional_measures.py```, From 7d56c23d7d00d182bba6d8715b7d0b8fc5f0ded0 Mon Sep 17 00:00:00 2001 From: Chris Hughes Date: Wed, 17 Jul 2019 16:23:28 +0100 Subject: [PATCH 051/125] Added some classes and lots of type hints --- catbond.py | 7 +- genericclasses.py | 104 +++++ insurancecontract.py | 40 +- insurancefirm.py => insurancefirms.py | 194 +++++---- insurancesimulation.py | 342 ++++++++-------- metainsurancecontract.py | 56 +-- metainsuranceorg.py | 550 ++++++++++++-------------- reinsurancecontract.py | 10 +- reinsurancefirm.py | 16 - riskmodel.py | 118 +++--- start.py | 17 +- 11 files changed, 773 insertions(+), 681 deletions(-) create mode 100644 genericclasses.py rename insurancefirm.py => insurancefirms.py (81%) delete mode 100644 reinsurancefirm.py diff --git a/catbond.py b/catbond.py index adf8c0b..7d17fad 100644 --- a/catbond.py +++ b/catbond.py @@ -38,8 +38,7 @@ def iterate(self, time): time: Type Integer No return values For each time iteration this is called from insurancesimulation to perform duties: interest payments, - pay obligations, mature the contract if ended, make payments.""" - # QUERY: Shouldn't the interest on the cat bond be paid by the issuer, not the bank/market? + _pay obligations, mature the contract if ended, make payments.""" self.obtain_yield(time) self.effect_payments(time) if isleconfig.verbose: @@ -109,8 +108,8 @@ def mature_bond(self): "due_time": 1, "purpose": "mature", } - self.pay(obligation) - self.simulation.delete_agents("catbond", [self]) + self._pay(obligation) + self.simulation.delete_agents([self]) self.operational = False else: print("CatBond is not operational so cannot mature") diff --git a/genericclasses.py b/genericclasses.py new file mode 100644 index 0000000..760e444 --- /dev/null +++ b/genericclasses.py @@ -0,0 +1,104 @@ +from __future__ import annotations +import dataclasses +from typing import Mapping +import metainsurancecontract + + +class GenericAgent: + def __init__(self): + self.cash = 0 + self.obligations = [] + self.operational = True + self.profits_losses = 0 + + def _pay(self, obligation): + """Method to _pay other class instances. + Accepts: + Obligation: Type DataDict + No return value + Method removes value payed from the agents cash and adds it to recipient agents cash.""" + amount = obligation["amount"] + recipient = obligation["recipient"] + purpose = obligation["purpose"] + if self.get_operational() and recipient.get_operational(): + self.cash -= amount + if purpose is not "dividend": + self.profits_losses -= amount + recipient.receive(amount) + + def get_operational(self): + """Method to return boolean of if agent is operational. Only used as check for payments. + No accepted values + Returns Boolean""" + return self.operational + + def iterate(self, time): + raise NotImplementedError( + "Iterate is not implemented in GenericAgent, should have be overridden" + ) + + def receive_obligation(self, amount, recipient, due_time, purpose): + """Method for receiving obligations that the firm will have to _pay. + Accepts: + amount: Type integer, how much will be payed + recipient: Type Class instance, who will be payed + due_time: Type Integer, what time value they will be payed + purpose: Type string, why they are being payed + No return value + Adds obligation (Type DataDict) to list of obligations owed by the firm.""" + + obligation = { + "amount": amount, + "recipient": recipient, + "due_time": due_time, + "purpose": purpose, + } + self.obligations.append(obligation) + + def receive(self, amount: float): + """Method to accept cash payments.""" + self.cash += amount + self.profits_losses += amount + + +@dataclasses.dataclass +class RiskProperties: + """Class for holding the properties of an insured risk""" + + risk_factor: float + value: float + category: int + owner: GenericAgent + + number_risks: int = 1 + contract: metainsurancecontract.MetaInsuranceContract = None + insurancetype: str = None + deductible: float = None + runtime: int = None + expiration: int = None + excess_fraction: float = None + deductible_fraction: float = None + reinsurance_share: float = None + periodized_total_premium: float = None + excess: float = None + runtime_left: int = None + + +@dataclasses.dataclass +class AgentProperties: + """Class for holding the properties of an agent""" + + id: int + initial_cash: float + riskmodel_config: Mapping + norm_premium: float + profit_target: float + initial_acceptance_threshold: float + acceptance_threshold_friction: float + reinsurance_limit: float + non_proportional_reinsurance_level: float + capacity_target_decrement_threshold: float + capacity_target_increment_threshold: float + capacity_target_decrement_factor: float + capacity_target_increment_factor: float + interest_rate: float diff --git a/insurancecontract.py b/insurancecontract.py index eb7035e..1c32545 100644 --- a/insurancecontract.py +++ b/insurancecontract.py @@ -1,9 +1,13 @@ -from metainsurancecontract import MetaInsuranceContract +from __future__ import annotations +import metainsurancecontract +import genericclasses +import metainsuranceorg -class InsuranceContract(MetaInsuranceContract): + +class InsuranceContract(metainsurancecontract.MetaInsuranceContract): """ReinsuranceContract class. - Inherits from InsuranceContract. + Inherits from MetaInsuranceContract. Constructor is not currently required but may be used in the future to distinguish InsuranceContract and ReinsuranceContract objects. The signature of this class' constructor is the same as that of the InsuranceContract constructor. @@ -11,22 +15,22 @@ class InsuranceContract(MetaInsuranceContract): def __init__( self, - insurer, - properties, - time, - premium, - runtime, - payment_period, - expire_immediately, - initial_var=0.0, - insurancetype="proportional", - deductible_fraction=None, - excess_fraction=None, - reinsurance=0, + insurer: metainsuranceorg.MetaInsuranceOrg, + risk: genericclasses.RiskProperties, + time: int, + premium: float, + runtime: int, + payment_period: int, + expire_immediately: bool, + initial_var: float = 0.0, + insurancetype: str = "proportional", + deductible_fraction: float = None, + excess_fraction: float = None, + reinsurance: float = 0, ): super().__init__( insurer, - properties, + risk, time, premium, runtime, @@ -39,8 +43,6 @@ def __init__( reinsurance, ) - self.risk_data = properties - def explode(self, time, uniform_value, damage_extent): """Explode method. Accepts arguments @@ -80,4 +82,4 @@ def mature(self, time): self.terminate_reinsurance(time) if not self.roll_over_flag: - self.property_holder.return_risks([self.risk_data]) + self.property_holder.return_risks([self.risk]) diff --git a/insurancefirm.py b/insurancefirms.py similarity index 81% rename from insurancefirm.py rename to insurancefirms.py index 3bf46a0..33a3df5 100644 --- a/insurancefirm.py +++ b/insurancefirms.py @@ -1,8 +1,13 @@ +from __future__ import annotations +from typing import Optional, Tuple + +import numpy as np + from metainsuranceorg import MetaInsuranceOrg import catbond -import numpy as np from reinsurancecontract import ReinsuranceContract import isleconfig +import genericclasses class InsuranceFirm(MetaInsuranceOrg): @@ -19,7 +24,7 @@ def __init__(self, simulation_parameters, agent_parameters): self.is_insurer = True self.is_reinsurer = False - def adjust_dividends(self, time, actual_capacity): + def adjust_dividends(self, time: int, actual_capacity: float): """Method to adjust dividends firm pays to investors. Accepts: time: Type Integer. Not used. @@ -35,7 +40,7 @@ def adjust_dividends(self, time, actual_capacity): # no dividends if firm misses capital target self.per_period_dividend = 0 - def get_reinsurance_var_estimate(self, max_var): + def get_reinsurance_var_estimate(self, max_var: float) -> float: """Method to estimate the VaR if another reinsurance contract were to be taken. Accepts: max_var: Type Decimal. Max value at risk @@ -57,7 +62,7 @@ def get_reinsurance_var_estimate(self, max_var): reinsurance_var_estimate = max_var * (1.0 + reinsurance_factor_estimate) return reinsurance_var_estimate - def adjust_capacity_target(self, max_var): + def adjust_capacity_target(self, max_var: float): """Method to adjust capacity target. Accepts: max_var: Type Decimal. @@ -84,9 +89,8 @@ def adjust_capacity_target(self, max_var): < self.capacity_target_decrement_threshold ): self.capacity_target *= self.capacity_target_decrement_factor - return - def get_capacity(self, max_var): + def get_capacity(self, max_var: float) -> float: """Method to get capacity of firm. Accepts: max_var: Type Decimal. @@ -103,7 +107,7 @@ def get_capacity(self, max_var): # Ensure insurer recovers complete coverage.) return self.cash - def increase_capacity(self, time, max_var): + def increase_capacity(self, time: int, max_var: float) -> float: """Method to increase the capacity of the firm. Accepts: time: Type Integer. @@ -161,8 +165,13 @@ def increase_capacity(self, time, max_var): return capacity def increase_capacity_by_category( - self, time, categ_id, reinsurance_price, cat_bond_price, force=False - ): + self, + time: int, + categ_id: int, + reinsurance_price: float, + cat_bond_price: float, + force: bool = False, + ) -> bool: """Method to increase capacity. Only called by increase_capacity. Accepts: time: Type Integer @@ -176,7 +185,8 @@ def increase_capacity_by_category( only issue if market premium is greater than firms average premium.""" if isleconfig.verbose: print( - f"IF {self.id:d} increasing capacity in period {time:d}, cat bond price: {cat_bond_price:f}, reinsurance premium {reinsurance_price:f}" + f"IF {self.id:d} increasing capacity in period {time:d}, cat bond price: {cat_bond_price:f}," + f" reinsurance premium {reinsurance_price:f}" ) if not force: actual_premium = self.get_average_premium(categ_id) @@ -194,7 +204,7 @@ def increase_capacity_by_category( self.ask_reinsurance_non_proportional_by_category(time, categ_id) return True - def get_average_premium(self, categ_id): + def get_average_premium(self, categ_id: int) -> float: """Method to calculate and return the firms average premium for all currently underwritten contracts. Accepts: categ_id: Type Integer. @@ -211,7 +221,7 @@ def get_average_premium(self, categ_id): return 0 # will prevent any attempt to reinsure empty categories return weighted_premium_sum * 1.0 / total_weight - def ask_reinsurance(self, time): + def ask_reinsurance(self, time: int): """Method called specifically to call relevant reinsurance function for simulations reinsurance type. Only non-proportional type is used as this is the one mainly used in reality. Accepts: @@ -222,9 +232,11 @@ def ask_reinsurance(self, time): elif self.simulation_reinsurance_type == "non-proportional": self.ask_reinsurance_non_proportional(time) else: - assert False, "Undefined reinsurance type" + raise ValueError( + f"Undefined reinsurance type {self.simulation_reinsurance_type}" + ) - def ask_reinsurance_non_proportional(self, time): + def ask_reinsurance_non_proportional(self, time: int): """ Method for requesting excess of loss reinsurance for all underwritten contracts by category. The method calculates the combined value at risk. With a probability it then creates a combined reinsurance risk that may then be underwritten by a reinsurance firm. @@ -239,11 +251,12 @@ def ask_reinsurance_non_proportional(self, time): if self.category_reinsurance[categ_id] is None: self.ask_reinsurance_non_proportional_by_category(time, categ_id) - def characterize_underwritten_risks_by_category(self, categ_id): + def characterize_underwritten_risks_by_category( + self, categ_id: int + ) -> Tuple[float, float, int, float]: """Method to characterise associated risks in a given category in terms of value, number, avg risk factor, and total premium per iteration. Accepts: - time: Type Integer. Not used.. categ_id: Type Integer. The given category for characterising risks. Returns: total_value: Type Decimal. Total value of all contracts in the category. @@ -265,8 +278,8 @@ def characterize_underwritten_risks_by_category(self, categ_id): return total_value, avg_risk_factor, number_risks, periodized_total_premium def ask_reinsurance_non_proportional_by_category( - self, time, categ_id, purpose="newrisk" - ): + self, time: int, categ_id: int, purpose: str = "newrisk" + ) -> Optional[genericclasses.RiskProperties]: """Method to create a reinsurance risk for a given category for firm that calls it. Called from increase_ capacity_by_category, ask_reinsurance_non_proportional, and roll_over in metainsuranceorg. Accepts: @@ -286,19 +299,19 @@ def ask_reinsurance_non_proportional_by_category( periodized_total_premium, ] = self.characterize_underwritten_risks_by_category(categ_id) if number_risks > 0: - risk = { - "value": total_value, - "category": categ_id, - "owner": self, - "insurancetype": "excess-of-loss", - "number_risks": number_risks, - "deductible_fraction": self.np_reinsurance_deductible_fraction, - "excess_fraction": self.np_reinsurance_excess_fraction, - "periodized_total_premium": periodized_total_premium, - "runtime": 12, - "expiration": time + 12, - "risk_factor": avg_risk_factor, - } # TODO: make runtime into a parameter + risk = genericclasses.RiskProperties( + value=total_value, + category=categ_id, + owner=self, + insurancetype="excess-of-loss", + number_risks=number_risks, + deductible_fraction=self.np_reinsurance_deductible_fraction, + excess_fraction=self.np_reinsurance_excess_fraction, + periodized_total_premium=periodized_total_premium, + runtime=12, + expiration=time + 12, + risk_factor=avg_risk_factor, + ) # TODO: make runtime into a parameter if purpose == "newrisk": self.simulation.append_reinrisks(risk) elif purpose == "rollover": @@ -327,23 +340,28 @@ def ask_reinsurance_proportional(self): ) for contract in nonreinsured: if counter < limitrein: - risk = { - "value": contract.value, - "category": contract.category, - "owner": self, - # "identifier": uuid.uuid1(), - "reinsurance_share": 1.0, - "expiration": contract.expiration, - "contract": contract, - "risk_factor": contract.risk_factor, - } + risk = genericclasses.RiskProperties( + value=contract.value, + category=contract.category, + owner=self, + reinsurance_share=1.0, + expiration=contract.expiration, + contract=contract, + risk_factor=contract.risk_factor, + ) self.simulation.append_reinrisks(risk) counter += 1 else: break - def add_reinsurance(self, category, excess_fraction, deductible_fraction, contract): + def add_reinsurance( + self, + category: int, + excess_fraction: float, + deductible_fraction: float, + contract: ReinsuranceContract, + ): """Method called by reinsurancecontract to add the reinsurance contract to the firms counter for the given category, normally used so only one reinsurance contract is issued per category at a time. Accepts: @@ -357,7 +375,7 @@ def add_reinsurance(self, category, excess_fraction, deductible_fraction, contra ) self.category_reinsurance[category] = contract - def delete_reinsurance(self, category, contract): + def delete_reinsurance(self, category: int, contract: ReinsuranceContract): """Method called by reinsurancecontract to delete the reinsurance contract from the firms counter for the given category, used so that another reinsurance contract can be issued for that category if needed. Accepts: @@ -367,7 +385,9 @@ def delete_reinsurance(self, category, contract): self.riskmodel.delete_reinsurance(category, contract) self.category_reinsurance[category] = None - def issue_cat_bond(self, time, categ_id, per_value_per_period_premium=0): + def issue_cat_bond( + self, time: int, categ_id: int, per_value_per_period_premium: int = 0 + ): """Method to issue cat bond to given firm for given category. Accepts: time: Type Integer. @@ -384,26 +404,27 @@ def issue_cat_bond(self, time, categ_id, per_value_per_period_premium=0): _, ] = self.characterize_underwritten_risks_by_category(categ_id) if number_risks > 0: - risk = { - "value": total_value, - "category": categ_id, - "owner": self, - # "identifier": uuid.uuid1(), - "insurancetype": "excess-of-loss", - "number_risks": number_risks, - "deductible_fraction": self.np_reinsurance_deductible_fraction, - "excess_fraction": self.np_reinsurance_excess_fraction, - "periodized_total_premium": 0, - "runtime": 12, - "expiration": time + 12, - "risk_factor": avg_risk_factor, - } # TODO: make runtime into a parameter + # TODO: make runtime into a parameter + risk = genericclasses.RiskProperties( + value=total_value, + category=categ_id, + owner=self, + insurancetype="excess-of-loss", + number_risks=number_risks, + deductible_fraction=self.np_reinsurance_deductible_fraction, + excess_fraction=self.np_reinsurance_excess_fraction, + periodized_total_premium=0, + runtime=12, + expiration=time + 12, + risk_factor=avg_risk_factor, + ) + _, _, var_this_risk, _ = self.riskmodel.evaluate([], self.cash, risk) - per_period_premium = per_value_per_period_premium * risk["value"] + per_period_premium = per_value_per_period_premium * risk.value total_premium = sum( [ per_period_premium * ((1 / (1 + self.interest_rate)) ** i) - for i in range(risk["runtime"]) + for i in range(risk.runtime) ] ) # TODO: or is it range(1, risk["runtime"]+1)? # catbond = CatBond(self.simulation, per_period_premium) @@ -419,11 +440,11 @@ def issue_cat_bond(self, time, categ_id, per_value_per_period_premium=0): risk, time, 0, - risk["runtime"], + risk.runtime, self.default_contract_payment_period, expire_immediately=self.simulation_parameters["expire_immediately"], initial_var=var_this_risk, - insurancetype=risk["insurancetype"], + insurancetype=risk.insurancetype, ) # per_value_reinsurance_premium = 0 because the insurance firm make only one payment to catbond @@ -438,11 +459,11 @@ def issue_cat_bond(self, time, categ_id, per_value_per_period_premium=0): "due_time": time, "purpose": "bond", } - self.pay(obligation) # TODO: is var_this_risk the correct amount? + self._pay(obligation) # TODO: is var_this_risk the correct amount? """register catbond""" self.simulation.add_agents(catbond.CatBond, "catbond", [new_catbond]) - def make_reinsurance_claims(self, time): + def make_reinsurance_claims(self, time: int): """Method to make reinsurance claims. Accepts: time: Type Integer. @@ -489,7 +510,9 @@ def get_excess_of_loss_reinsurance(self): reinsurance.append(reinsurance_contract) return reinsurance - def create_reinrisk(self, time, categ_id): + def create_reinrisk( + self, time: int, categ_id: int + ) -> Optional[genericclasses.RiskProperties]: """Proceed with creation of reinsurance risk only if category is not empty.""" [ total_value, @@ -499,19 +522,34 @@ def create_reinrisk(self, time, categ_id): ] = self.characterize_underwritten_risks_by_category(categ_id) if number_risks > 0: # TODO: make runtime into a parameter - risk = { - "value": total_value, - "category": categ_id, - "owner": self, - "insurancetype": "excess-of-loss", - "number_risks": number_risks, - "deductible_fraction": self.np_reinsurance_deductible_fraction, - "excess_fraction": self.np_reinsurance_excess_fraction, - "periodized_total_premium": periodized_total_premium, - "runtime": 12, - "expiration": time + 12, - "risk_factor": avg_risk_factor, - } + risk = genericclasses.RiskProperties( + value=total_value, + category=categ_id, + owner=self, + insurancetype="excess-of-loss", + number_risks=number_risks, + deductible_fraction=self.np_reinsurance_deductible_fraction, + excess_fraction=self.np_reinsurance_excess_fraction, + periodized_total_premium=periodized_total_premium, + runtime=12, + expiration=time + 12, + risk_factor=avg_risk_factor, + ) return risk else: return None + + +class ReinsuranceFirm(InsuranceFirm): + """ReinsuranceFirm class. + Inherits from InsuranceFirm.""" + + def __init__(self, simulation_parameters, agent_parameters): + """Constructor method. + Accepts arguments + Signature is identical to constructor method of parent class. + Constructor calls parent constructor and only overwrites boolean indicators of insurer and reinsurer role of + the object.""" + super().__init__(simulation_parameters, agent_parameters) + self.is_insurer = False + self.is_reinsurer = True diff --git a/insurancesimulation.py b/insurancesimulation.py index 0ef360c..543f7c5 100644 --- a/insurancesimulation.py +++ b/insurancesimulation.py @@ -1,19 +1,25 @@ -from distributiontruncated import TruncatedDistWrapper -import visualization_network -import insurancefirm -import reinsurancefirm -import numpy as np -import scipy.stats +from __future__ import annotations import math -import isleconfig import random import copy import logger import warnings +from typing import Mapping, MutableMapping, MutableSequence, Sequence, Any, Optional + +import scipy.stats +import numpy as np + +from distributiontruncated import TruncatedDistWrapper +import visualization_network +import insurancefirms +import isleconfig import utils +from genericclasses import GenericAgent, RiskProperties, AgentProperties +from metainsuranceorg import MetaInsuranceOrg +import catbond -class InsuranceSimulation: +class InsuranceSimulation(GenericAgent): """ Simulation object that is responsible for handling all aspects of the world. Tracks all agents (firms, catbonds) as well as acting as the insurance market. Iterates other objects, @@ -45,12 +51,12 @@ def __init__( simulation parameters: DataDict from isleconfig rc_event_schedule: List of when event will occur, allows for replication re_event_damage: List of severity of each event, allows for replication""" - + super().__init__() "Override one-riskmodel case (this is to ensure all other parameters are truly identical for comparison runs)" if override_no_riskmodels: simulation_parameters["no_riskmodels"] = override_no_riskmodels # QUERY: why do we keep duplicates of so many simulation parameters (and then not use many of them)? - self.number_riskmodels = simulation_parameters["no_riskmodels"] + self.number_riskmodels: int = simulation_parameters["no_riskmodels"] # save parameters if (replic_id is None) or isleconfig.force_foreground: @@ -58,13 +64,14 @@ def __init__( else: self.background_run = True self.replic_id = replic_id - self.simulation_parameters = simulation_parameters + self.simulation_parameters: MutableMapping = simulation_parameters + self.simulation_parameters["simulation"] = self "Unpacks parameters and sets distributions" self.damage_distribution = damage_distribution - self.catbonds_off = simulation_parameters["catbonds_off"] - self.reinsurance_off = simulation_parameters["reinsurance_off"] + self.catbonds_off: bool = simulation_parameters["catbonds_off"] + self.reinsurance_off: bool = simulation_parameters["reinsurance_off"] # TODO: research whether this is accurate, is it different for different types of catastrophy? self.cat_separation_distribution = scipy.stats.expon( 0, simulation_parameters["event_time_mean_separation"] @@ -72,8 +79,10 @@ def __init__( # Risk factors represent, for example, the earthquake risk for a particular house (compare to the value) # TODO: Implement! Think about insureres rejecting risks under certain situations (high risk factor) - self.risk_factor_lower_bound = simulation_parameters["risk_factor_lower_bound"] - self.risk_factor_spread = ( + self.risk_factor_lower_bound: float = simulation_parameters[ + "risk_factor_lower_bound" + ] + self.risk_factor_spread: float = ( simulation_parameters["risk_factor_upper_bound"] - self.risk_factor_lower_bound ) @@ -100,33 +109,35 @@ def __init__( self.simulation_parameters["mean_contract_runtime"] / self.cat_separation_distribution.mean() ) - self.norm_premium = ( + self.norm_premium: float = ( expected_damage_frequency * self.damage_distribution.mean() * risk_factor_mean * (1 + self.simulation_parameters["norm_profit_markup"]) ) - self.market_premium = self.norm_premium - self.reinsurance_market_premium = self.market_premium + self.market_premium: float = self.norm_premium + self.reinsurance_market_premium: float = self.market_premium # TODO: is this problematic as initial value? (later it is recomputed in every iteration) - self.total_no_risks = simulation_parameters["no_risks"] + self.total_no_risks: int = simulation_parameters["no_risks"] "Set up monetary system (should instead be with the customers, if customers are modeled explicitly)" - self.money_supply = self.simulation_parameters["money_supply"] - self.obligations = [] + self.cash: float = self.simulation_parameters["money_supply"] # QUERY Why is this a property of the simulation rather than of the obligated parties? "set up risk categories" # QUERY What do risk categories represent? Different types of catastrophes? - self.riskcategories = list(range(self.simulation_parameters["no_categories"])) - self.rc_event_schedule = [] - self.rc_event_damage = [] - self.rc_event_schedule_initial = [] + self.riskcategories: Sequence[int] = list( + range(self.simulation_parameters["no_categories"]) + ) + self.rc_event_schedule: MutableSequence[int] = [] + self.rc_event_damage: MutableSequence[float] = [] + # For debugging (cloud debugging) purposes is good to store the initial schedule of catastrophes # and damages that will be use in a single run of the model. - self.rc_event_damage_initial = [] + self.rc_event_schedule_initial: Sequence[float] = [] + self.rc_event_damage_initial: Sequence[float] = [] if ( rc_event_schedule is not None and rc_event_damage is not None ): # If we have schedules pass as arguments we used them. @@ -153,24 +164,18 @@ def __init__( self.simulation_parameters["no_categories"], size=self.simulation_parameters["no_risks"], ) - self.risks = [ - { - "risk_factor": rrisk_factors[i], - "value": rvalues[i], - "category": rcategories[i], - "owner": self, - } + + self.risks: MutableSequence[RiskProperties] = [ + RiskProperties(rrisk_factors[i], rvalues[i], rcategories[i], self) for i in range(self.simulation_parameters["no_risks"]) ] - self.risks_counter = [0, 0, 0, 0] + self.risks_counter: MutableSequence[int] = [0, 0, 0, 0] for item in self.risks: - self.risks_counter[item["category"]] = ( - self.risks_counter[item["category"]] + 1 - ) + self.risks_counter[item.category] = self.risks_counter[item.category] + 1 - self.inaccuracy = self._get_all_riskmodel_combinations( + self.inaccuracy: Sequence[Sequence[int]] = self._get_all_riskmodel_combinations( self.simulation_parameters["riskmodel_inaccuracy_parameter"] ) @@ -203,9 +208,12 @@ def __init__( # QUERY: What is agent_parameters["insurancefirm"] meant to be? Is it a list of the parameters for the existing # firms (why can't we just get that from the instances of InsuranceFirm) or a list of the *possible* parameter # values for insurance firms (in which case why does it have the length it does)? - self.agent_parameters = {"insurancefirm": [], "reinsurancefirm": []} - self.insurer_id_counter = 0 - self.reinsurer_id_counter = 0 + self.agent_parameters: Mapping[str, MutableSequence[AgentProperties]] = { + "insurancefirm": [], + "reinsurancefirm": [], + } + self.insurer_id_counter: int = 0 + self.reinsurer_id_counter: int = 0 self.initialize_agent_parameters( "insurancefirm", simulation_parameters, risk_model_configurations @@ -215,42 +223,57 @@ def __init__( ) "Agent lists" - self.reinsurancefirms = [] - self.insurancefirms = [] - self.catbonds = [] + self.reinsurancefirms: MutableSequence = [] + self.insurancefirms: MutableSequence = [] + self.catbonds: MutableSequence = [] "Lists of agent weights" - self.insurers_weights = {} - self.reinsurers_weights = {} + self.insurers_weights: MutableMapping[int, float] = {} + self.reinsurers_weights: MutableMapping[int, float] = {} "List of reinsurance risks offered for underwriting" - self.reinrisks = [] - self.not_accepted_reinrisks = [] + self.reinrisks: MutableSequence[RiskProperties] = [] + self.not_accepted_reinrisks: MutableSequence[RiskProperties] = [] "Cumulative variables for history and logging" - self.cumulative_bankruptcies = 0 - self.cumulative_market_exits = 0 - self.cumulative_unrecovered_claims = 0.0 - self.cumulative_claims = 0.0 + self.cumulative_bankruptcies: int = 0 + self.cumulative_market_exits: int = 0 + self.cumulative_unrecovered_claims: float = 0.0 + self.cumulative_claims: float = 0.0 "Lists for logging history" - self.logger = logger.Logger( + self.logger: logger.Logger = logger.Logger( no_riskmodels=simulation_parameters["no_riskmodels"], rc_event_schedule_initial=self.rc_event_schedule_initial, rc_event_damage_initial=self.rc_event_damage_initial, ) - self.insurance_models_counter = np.zeros( + self.insurance_models_counter: np.ndarray = np.zeros( self.simulation_parameters["no_categories"] ) - self.reinsurance_models_counter = np.zeros( + self.reinsurance_models_counter: np.ndarray = np.zeros( self.simulation_parameters["no_categories"] ) - self._time = None - self.RN = None + "Add initial set of agents" + self.add_agents( + insurancefirms.InsuranceFirm, + "insurancefirm", + n=self.simulation_parameters["no_insurancefirms"], + ) + self.add_agents( + insurancefirms.ReinsuranceFirm, + "reinsurancefirm", + n=self.simulation_parameters["no_reinsurancefirms"], + ) + + self._time: Optional[int] = None + self.RN: Optional[visualization_network.ReinsuranceNetwork] = None def initialize_agent_parameters( - self, firmtype, simulation_parameters, risk_model_configurations + self, + firmtype: str, + simulation_parameters: Mapping[str, Any], + risk_model_configurations: Sequence[Mapping], ): """General function for initialising the agent parameters Takes the firm type as argument, also needing sim params and risk configs @@ -299,37 +322,43 @@ def initialize_agent_parameters( i % len(risk_model_configurations) ] self.agent_parameters[firmtype].append( - { - "id": unique_id, - "initial_cash": simulation_parameters[initial_cash], - "riskmodel_config": riskmodel_config, - "norm_premium": self.norm_premium, - "profit_target": simulation_parameters["norm_profit_markup"], - "initial_acceptance_threshold": simulation_parameters[ + AgentProperties( + id=unique_id, + initial_cash=simulation_parameters[initial_cash], + riskmodel_config=riskmodel_config, + norm_premium=self.norm_premium, + profit_target=simulation_parameters["norm_profit_markup"], + initial_acceptance_threshold=simulation_parameters[ "initial_acceptance_threshold" ], - "acceptance_threshold_friction": simulation_parameters[ + acceptance_threshold_friction=simulation_parameters[ "acceptance_threshold_friction" ], - "reinsurance_limit": simulation_parameters["reinsurance_limit"], - "non-proportional_reinsurance_level": reinsurance_level, - "capacity_target_decrement_threshold": simulation_parameters[ + reinsurance_limit=simulation_parameters["reinsurance_limit"], + non_proportional_reinsurance_level=reinsurance_level, + capacity_target_decrement_threshold=simulation_parameters[ "capacity_target_decrement_threshold" ], - "capacity_target_increment_threshold": simulation_parameters[ + capacity_target_increment_threshold=simulation_parameters[ "capacity_target_increment_threshold" ], - "capacity_target_decrement_factor": simulation_parameters[ + capacity_target_decrement_factor=simulation_parameters[ "capacity_target_decrement_factor" ], - "capacity_target_increment_factor": simulation_parameters[ + capacity_target_increment_factor=simulation_parameters[ "capacity_target_increment_factor" ], - "interest_rate": simulation_parameters["interest_rate"], - } + interest_rate=simulation_parameters["interest_rate"], + ) ) - def add_agents(self, agent_class, agent_class_string, agents=None, n=1): + def add_agents( + self, + agent_class: type, + agent_class_string: str, + agents: Sequence[GenericAgent] = None, + n: int = 1, + ): """Method for building agents and adding them to the simulation. Can also add pre-made catbond agents directly Accepts: agent_class: class of the agent, InsuranceFirm, ReinsuranceFirm or CatBond @@ -361,7 +390,7 @@ def add_agents(self, agent_class, agent_class_string, agents=None, n=1): for _ in range(n) ] for ap in agent_parameters: - ap["id"] = self.get_unique_insurer_id() + ap.id = self.get_unique_insurer_id() agents = [ agent_class(self.simulation_parameters, ap) for ap in agent_parameters @@ -384,9 +413,9 @@ def add_agents(self, agent_class, agent_class_string, agents=None, n=1): for _ in range(n) ] for ap in agent_parameters: - ap["id"] = self.get_unique_reinsurer_id() + ap.id = self.get_unique_reinsurer_id() # QUERY: This was written but not actually used in the original implementation - should it be? - # ap["initial_cash"] = self.reinsurance_capital_entry() + # ap.initial_cash = self.reinsurance_capital_entry() agents = [ agent_class(self.simulation_parameters, ap) for ap in agent_parameters @@ -404,22 +433,22 @@ def add_agents(self, agent_class, agent_class_string, agents=None, n=1): total_new_agent_cash = sum([agent.cash for agent in agents]) self._reduce_money_supply(total_new_agent_cash) - def delete_agents(self, agent_class_string, agents): + def delete_agents(self, agents: Sequence[catbond.CatBond]): """Method for deleting catbonds as it is only agent that is allowed to be removed alters lists of catbonds Returns none""" - if agent_class_string == "catbond": - for agent in agents: + for agent in agents: + if isinstance(agent, catbond.CatBond): self.catbonds.remove(agent) - else: - raise ValueError( - f"Trying to remove unremovable agent, type: {agent_class_string}" - ) + else: + raise ValueError( + f"Trying to remove unremovable agent, type: {type(agent)}" + ) - def iterate(self, t): + def iterate(self, t: int): """Function that is called from start.py for each iteration that settles obligations, capital then reselects risks for the insurance and reinsurance companies to evaluate. Firms are then iterated through to accept - new risks, pay obligations, increase capacity etc. + new risks, _pay obligations, increase capacity etc. Accepts: t: Integer, current time step Returns None""" @@ -432,10 +461,10 @@ def iterate(self, t): print(f"\rTime: {t}", end="") if self.firm_enters_market(agent_type="InsuranceFirm"): - self.add_agents(insurancefirm.InsuranceFirm, "insurancefirm", n=1) + self.add_agents(insurancefirms.InsuranceFirm, "insurancefirm", n=1) if self.firm_enters_market(agent_type="ReinsuranceFirm"): - self.add_agents(reinsurancefirm.ReinsuranceFirm, "reinsurancefirm", n=1) + self.add_agents(insurancefirms.ReinsuranceFirm, "reinsurancefirm", n=1) self.reset_pls() @@ -609,7 +638,7 @@ def save_data(self): # This function allows to return in a list all the data generated by the model. There is no other way to transfer # it back from the cloud. - def obtain_log(self, requested_logs=None): + def obtain_log(self, requested_logs: Mapping = None): return self.logger.obtain_log(requested_logs) def finalize(self, *args): @@ -621,7 +650,7 @@ def finalize(self, *args): """ pass - def _inflict_peril(self, categ_id, damage, t): + def _inflict_peril(self, categ_id: int, damage: float, t: int): """Method that calculates percentage damage done to each underwritten risk that is affected in the category that event happened in. Passes values to allow calculation contracts to be resolved. Arguments: @@ -644,24 +673,7 @@ def _inflict_peril(self, categ_id, damage, t): for i, contract in enumerate(affected_contracts): contract.explode(t, uniformvalues[i], damagevalues[i]) - def receive_obligation(self, amount, recipient, due_time, purpose): - """Method for adding obligation to list that is resolved at the start if each iteration of simulation. Only - called by metainsuranceorg for adding interest to cash. - Arguments - Amount: obligation value - Recipient: Who obligation is owed to - Due Time - Purpose: Reason for obligation (Interest due) - Returns None""" - obligation = { - "amount": amount, - "recipient": recipient, - "due_time": due_time, - "purpose": purpose, - } - self.obligations.append(obligation) - - def _effect_payments(self, time): + def _effect_payments(self, time: int): """Method for checking and paying obligation if due. Arguments Current time to allow check if due @@ -673,35 +685,19 @@ def _effect_payments(self, time): ] # sum_due = sum([item["amount"] for item in due]) for obligation in due: + if self.cash < obligation["amount"]: + warnings.warn( + "Something wrong: economy out of money", RuntimeWarning + ) self._pay(obligation) - def _pay(self, obligation): - """Method for paying obligations called from effect_payments - Accepts: - Obligation: Type DataDict with categories amount, recipient, due time, purpose. - Returns None""" - amount = obligation["amount"] - recipient = obligation["recipient"] - if not self.money_supply > amount: - warnings.warn("Something wrong: economy out of money", RuntimeWarning) - if recipient.get_operational(): - self.money_supply -= amount - recipient.receive(amount) - - def receive(self, amount): - """Method to accept cash payments. As insurance simulation cash is economy, adds money to total economy. - Accepts: - Amount due: Type Integer - Returns None""" - self.money_supply += amount - - def _reduce_money_supply(self, amount): + def _reduce_money_supply(self, amount: float): """Method to reduce money supply immediately and without payment recipient (used to adjust money supply to compensate for agent endowment). Accepts: amount: Type Integer""" - self.money_supply -= amount - assert self.money_supply >= 0 + self.cash -= amount + assert self.cash >= 0 def _reset_reinsurance_weights(self): """Method for clearing and setting reinsurance weights dependant on how many reinsurance companies exist and @@ -767,7 +763,7 @@ def _shuffle_risks(self): np.random.shuffle(self.reinrisks) np.random.shuffle(self.risks) - def _adjust_market_premium(self, capital): + def _adjust_market_premium(self, capital: float): """Adjust_market_premium Method. Accepts arguments capital: Type float. The total capital (cash) available in the insurance market (insurance only). @@ -791,7 +787,7 @@ def _adjust_market_premium(self, capital): self.norm_premium * self.simulation_parameters["lower_price_limit"], ) - def _adjust_reinsurance_market_premium(self, capital): + def _adjust_reinsurance_market_premium(self, capital: float): """Adjust_market_premium Method. Accepts arguments capital: Type float. The total capital (cash) available in the reinsurance market (reinsurance only). @@ -815,7 +811,7 @@ def _adjust_reinsurance_market_premium(self, capital): self.norm_premium * self.simulation_parameters["lower_price_limit"], ) - def get_market_premium(self): + def get_market_premium(self) -> float: """Get_market_premium Method. Accepts no arguments. Returns: @@ -823,8 +819,8 @@ def get_market_premium(self): This method returns the current insurance market premium.""" return self.market_premium - def get_market_reinpremium(self): - # QUERY: What's the difference between this and get_reinsurance_premium? + def get_market_reinpremium(self) -> float: + # QUERY: What's the difference between this and get_reinsurance_premium below? """Get_market_reinpremium Method. Accepts no arguments. Returns: @@ -832,7 +828,9 @@ def get_market_reinpremium(self): This method returns the current reinsurance market premium.""" return self.reinsurance_market_premium - def get_reinsurance_premium(self, np_reinsurance_deductible_fraction): + def get_reinsurance_premium( + self, np_reinsurance_deductible_fraction: float + ) -> float: """Method to determine reinsurance premium based on deductible fraction Accepts: np_reinsurance_deductible_fraction: Type Integer @@ -841,13 +839,13 @@ def get_reinsurance_premium(self, np_reinsurance_deductible_fraction): # TODO: make max_reduction into simulation_parameter ? if self.reinsurance_off: return float("inf") - max_reduction = 0.1 - # QUERY: why is this this way? Why not, say, 1.0 - min(max_reduction * np_reinsurance_deductible_fraction)? - return self.reinsurance_market_premium * ( - 1.0 - max_reduction * np_reinsurance_deductible_fraction - ) + else: + max_reduction = 0.1 + return self.reinsurance_market_premium * ( + 1.0 - max_reduction * np_reinsurance_deductible_fraction + ) - def get_cat_bond_price(self, np_reinsurance_deductible_fraction): + def get_cat_bond_price(self, np_reinsurance_deductible_fraction: float) -> float: """Method to calculate and return catbond price. If catbonds are not desired will return infinity so no catbonds will be issued. Otherwise calculates based on reinsurance market premium, catbond premium, deductible fraction. @@ -868,26 +866,27 @@ def get_cat_bond_price(self, np_reinsurance_deductible_fraction): - max_reduction * np_reinsurance_deductible_fraction ) - def append_reinrisks(self, item): + def append_reinrisks(self, item: RiskProperties): """Method for appending reinrisks to simulation instance. Called from insurancefirm Accepts: item (Type: List)""" if item: self.reinrisks.append(item) - def remove_reinrisks(self, risko): + def remove_reinrisks(self, risko: RiskProperties): if risko is not None: self.reinrisks.remove(risko) - def get_reinrisks(self): + def get_reinrisks(self) -> Sequence[RiskProperties]: """Method for shuffling reinsurance risks Returns: reinsurance risks""" np.random.shuffle(self.reinrisks) return self.reinrisks - def solicit_insurance_requests(self, insurer): + def solicit_insurance_requests( + self, insurer: MetaInsuranceOrg + ) -> Sequence[RiskProperties]: """Method for determining which risks are to be assessed by firms based on insurer weights Accepts: - cash: Type Integer insurer: Type firm metainsuranceorg instance Returns: risks_to_be_sent: Type List""" @@ -903,7 +902,9 @@ def solicit_insurance_requests(self, insurer): return risks_to_be_sent - def solicit_reinsurance_requests(self, reinsurer): + def solicit_reinsurance_requests( + self, reinsurer: MetaInsuranceOrg + ) -> Sequence[RiskProperties]: """Method for determining which reinsurance risks are to be assessed by firms based on reinsurer weights Accepts: id: Type integer @@ -925,14 +926,14 @@ def solicit_reinsurance_requests(self, reinsurer): return reinrisks_to_be_sent - def return_risks(self, not_accepted_risks): + def return_risks(self, not_accepted_risks: Sequence[RiskProperties]): """Method for adding risks that were not deemed acceptable to underwrite back to list of uninsured risks Accepts: not_accepted_risks: Type List No return value""" self.risks += not_accepted_risks - def return_reinrisks(self, not_accepted_risks): + def return_reinrisks(self, not_accepted_risks: Sequence[RiskProperties]): """Method for adding reinsuracne risks that were not deemed acceptable to list of unaccepted reinsurance risks Cleared every round and is never called so redundant? Accepts: @@ -940,7 +941,9 @@ def return_reinrisks(self, not_accepted_risks): Returns None""" self.not_accepted_reinrisks += not_accepted_risks - def _get_all_riskmodel_combinations(self, rm_factor): + def _get_all_riskmodel_combinations( + self, rm_factor: float + ) -> Sequence[Sequence[float]]: """Method for calculating riskmodels for each category based on the risk model inaccuracy parameter, and is used purely to assign inaccuracy. Undervalues one risk category and overestimates all the rest. Accepts: @@ -956,7 +959,9 @@ def _get_all_riskmodel_combinations(self, rm_factor): riskmodels.append(riskmodel_combination) return riskmodels - def firm_enters_market(self, prob=-1, agent_type="InsuranceFirm"): + def firm_enters_market( + self, prob: float = -1, agent_type: str = "InsuranceFirm" + ) -> bool: """Method to determine if re/insurance firm enters the market based on set entry probabilities and a random integer generated between 0, 1. Accepts: @@ -997,15 +1002,15 @@ def record_market_exit(self): firm.""" self.cumulative_market_exits += 1 - def record_unrecovered_claims(self, loss): - """Method for recording unrecovered claims. If firm runs out of money it cannot pay more claims and so that + def record_unrecovered_claims(self, loss: float): + """Method for recording unrecovered claims. If firm runs out of money it cannot _pay more claims and so that money is lost and recorded using this method. Accepts: loss: Type integer, value of lost claim No return value""" self.cumulative_unrecovered_claims += loss - def record_claims(self, claims): + def record_claims(self, claims: float): """This method records every claim made to insurers and reinsurers. It is called from both insurers and reinsurers (metainsuranceorg.py).""" self.cumulative_claims += claims @@ -1019,7 +1024,7 @@ def log(self): or not.""" self.logger.save_log(self.background_run) - def compute_market_diffvar(self): + def compute_market_diffvar(self) -> float: """Method for calculating difference between number of all firms and the total value at risk. Used only in save data when adding to the logger data dict.""" totalina = sum( @@ -1050,7 +1055,7 @@ def compute_market_diffvar(self): return totaldiff # self.history_logs['market_diffvar'].append(totaldiff) - def get_unique_insurer_id(self): + def get_unique_insurer_id(self) -> int: """Method for getting unique id for insurer. Used in initialising agents in start.py and insurancesimulation. Iterates after each call so id is unique to each firm. Returns: @@ -1059,7 +1064,7 @@ def get_unique_insurer_id(self): self.insurer_id_counter += 1 return current_id - def get_unique_reinsurer_id(self): + def get_unique_reinsurer_id(self) -> int: """Method for getting unique id for insurer. Used in initialising agents in start.py and insurancesimulation. Iterates after each call so id is unique to each firm. Returns: @@ -1068,7 +1073,7 @@ def get_unique_reinsurer_id(self): self.reinsurer_id_counter += 1 return current_id - def insurance_entry_index(self): + def insurance_entry_index(self) -> int: """Method that returns the entry index for insurance firms, i.e. the index for the initial agent parameters that is taken from the list of already created parameters. Returns: @@ -1077,7 +1082,7 @@ def insurance_entry_index(self): 0 : self.simulation_parameters["no_riskmodels"] ].argmin() - def reinsurance_entry_index(self): + def reinsurance_entry_index(self) -> int: """Method that returns the entry index for reinsurance firms, i.e. the index for the initial agent parameters that is taken from the list of already created parameters. Returns: @@ -1087,19 +1092,16 @@ def reinsurance_entry_index(self): ].argmin() # noinspection PyMethodMayBeStatic - def get_operational(self): - """Method to return if simulation is operational. Always true. Used only in pay methods above and - metainsuranceorg. - Accepts no arguments - Returns True""" + def get_operational(self) -> bool: + """Override get_operational to always return True, as the market will never die""" return True - def reinsurance_capital_entry(self): + def reinsurance_capital_entry(self) -> float: # This method determines the capital market entry (initial cash) of reinsurers. It is only run in start.py. capital_per_non_re_cat = [] for reinrisk in self.not_accepted_reinrisks: - capital_per_non_re_cat.append(reinrisk["value"]) + capital_per_non_re_cat.append(reinrisk.value) # It takes all the values of the reinsurance risks NOT REINSURED. # If there are any non-reinsured risks going, take a sample of them and have starting capital equal to twice @@ -1135,7 +1137,7 @@ def reset_pls(self): for catbond in self.catbonds: catbond.reset_pl() - def get_risk_share(self, firm): + def get_risk_share(self, firm: MetaInsuranceOrg) -> float: """Method to determine the total percentage of risks in the market that are held by a particular firm. For insurers uses insurance risks, for reinsurers uses reinsurance risks diff --git a/metainsurancecontract.py b/metainsurancecontract.py index bbb256b..712b709 100644 --- a/metainsurancecontract.py +++ b/metainsurancecontract.py @@ -1,23 +1,26 @@ +from genericclasses import GenericAgent, RiskProperties + + class MetaInsuranceContract: def __init__( self, - insurer, - properties, - time, - premium, - runtime, - payment_period, - expire_immediately, - initial_var=0.0, - insurancetype="proportional", - deductible_fraction=None, - excess_fraction=None, - reinsurance=0, + insurer: GenericAgent, + risk: RiskProperties, + time: int, + premium: float, + runtime: int, + payment_period: int, + expire_immediately: bool, + initial_var: float = 0.0, + insurancetype: str = "proportional", + deductible_fraction: float = None, + excess_fraction: float = None, + reinsurance: float = 0, ): """Constructor method. Accepts arguments insurer: Type InsuranceFirm. - properties: Type dict. + risk: Type RiskProperties. time: Type integer. The current time. premium: Type float. runtime: Type integer. @@ -37,15 +40,14 @@ def __init__( # Save parameters self.insurer = insurer - self.risk_factor = properties["risk_factor"] - self.category = properties["category"] - self.property_holder = properties["owner"] - self.value = properties["value"] - self.contract = properties.get( - "contract" - ) # will assign None if key does not exist + self.risk_factor = risk.risk_factor + self.category = risk.category + self.property_holder = risk.owner + self.value = risk.value + self.contract = risk.contract # May be None + self.risk = risk self.insurancetype = ( - properties.get("insurancetype") if insurancetype is None else insurancetype + risk.insurancetype if insurancetype is None else insurancetype ) self.runtime = runtime self.starttime = time @@ -59,7 +61,7 @@ def __init__( self.deductible_fraction = ( deductible_fraction if deductible_fraction is not None - else properties.get("deductible_fraction", default_deductible_fraction) + else risk.deductible_fraction or default_deductible_fraction ) self.deductible = self.deductible_fraction * self.value @@ -69,7 +71,7 @@ def __init__( self.excess_fraction = ( excess_fraction if excess_fraction is not None - else properties.get("excess_fraction", default_excess_fraction) + else risk.excess_fraction or default_excess_fraction ) self.excess = self.excess_fraction * self.value @@ -99,14 +101,14 @@ def __init__( if self.contract: self.contract.reinsure( reinsurer=self.insurer, - reinsurance_share=properties["reinsurance_share"], + reinsurance_share=risk.reinsurance_share, reincontract=self, ) # This flag is set to 1, when the contract is about to expire and there is an attempt to roll it over. self.roll_over_flag = 0 - def check_payment_due(self, time): + def check_payment_due(self, time: int): """Method to check if a contract payment is due. Accepts: time: Type integer @@ -136,7 +138,7 @@ def get_and_reset_current_claim(self): self.current_claim = 0 return self.category, current_claim, (self.insurancetype == "proportional") - def terminate_reinsurance(self, time): + def terminate_reinsurance(self, time: int): """Terminate reinsurance method. Accepts arguments time: Type integer. The current time. @@ -145,7 +147,7 @@ def terminate_reinsurance(self, time): if self.reincontract is not None: self.reincontract.dissolve(time) - def dissolve(self, time): + def dissolve(self, time: int): """Dissolve method. Accepts arguments time: Type integer. The current time. diff --git a/metainsuranceorg.py b/metainsuranceorg.py index f9aa628..d0b0b09 100644 --- a/metainsuranceorg.py +++ b/metainsuranceorg.py @@ -1,15 +1,23 @@ -import isleconfig -import numpy as np -import scipy.stats +from __future__ import annotations import copy import math +import functools +from typing import Optional, Tuple, Sequence, Mapping, MutableSequence +from itertools import zip_longest, chain + +import numpy as np +import scipy.stats + +import isleconfig from insurancecontract import InsuranceContract +import insurancesimulation from reinsurancecontract import ReinsuranceContract +import metainsurancecontract from riskmodel import RiskModel -import functools +from genericclasses import GenericAgent, RiskProperties, AgentProperties -def get_mean(x): +def get_mean(x: Sequence[float]) -> float: """ Returns the mean of a list Args: @@ -23,7 +31,7 @@ def get_mean(x): # A quick check tells me that we don't need a very large cache for this, as it only tends to repeat a couple of times. @functools.lru_cache(maxsize=16) -def get_mean_std(x): +def get_mean_std(x: Tuple[float, ...]) -> Tuple[float, float]: # At the moment this is always called with a no_category length array # I have tested the numpy versions of this, they are slower for small arrays but much, much faster for large ones # If we ever let no_category be much larger, might want to use np for this bit @@ -32,16 +40,21 @@ def get_mean_std(x): return m, std -class MetaInsuranceOrg: - def __init__(self, simulation_parameters, agent_parameters): +class MetaInsuranceOrg(GenericAgent): + def __init__( + self, simulation_parameters: Mapping, agent_parameters: AgentProperties + ): """Constructor method. Accepts: Simulation_parameters: Type DataDict agent_parameters: Type DataDict Constructor creates general instance of an insurance company which is inherited by the reinsurance and insurance firm classes. Initialises all necessary values provided by config file.""" - self.simulation = simulation_parameters["simulation"] - self.simulation_parameters = simulation_parameters + super().__init__() + self.simulation: insurancesimulation.InsuranceSimulation = simulation_parameters[ + "simulation" + ] + self.simulation_parameters: Mapping = simulation_parameters self.contract_runtime_dist = scipy.stats.randint( simulation_parameters["mean_contract_runtime"] - simulation_parameters["contract_runtime_halfspread"], @@ -49,35 +62,33 @@ def __init__(self, simulation_parameters, agent_parameters): + simulation_parameters["contract_runtime_halfspread"] + 1, ) - self.default_contract_payment_period = simulation_parameters[ + self.default_contract_payment_period: int = simulation_parameters[ "default_contract_payment_period" ] - self.id = agent_parameters["id"] - self.cash = agent_parameters["initial_cash"] + self.id = agent_parameters.id + self.cash = agent_parameters.initial_cash self.capacity_target = self.cash * 0.9 - self.capacity_target_decrement_threshold = agent_parameters[ - "capacity_target_decrement_threshold" - ] - self.capacity_target_increment_threshold = agent_parameters[ - "capacity_target_increment_threshold" - ] - self.capacity_target_decrement_factor = agent_parameters[ - "capacity_target_decrement_factor" - ] - self.capacity_target_increment_factor = agent_parameters[ - "capacity_target_increment_factor" - ] + self.capacity_target_decrement_threshold = ( + agent_parameters.capacity_target_decrement_threshold + ) + self.capacity_target_increment_threshold = ( + agent_parameters.capacity_target_increment_threshold + ) + self.capacity_target_decrement_factor = ( + agent_parameters.capacity_target_decrement_factor + ) + self.capacity_target_increment_factor = ( + agent_parameters.capacity_target_increment_factor + ) self.excess_capital = self.cash - self.premium = agent_parameters["norm_premium"] - self.profit_target = agent_parameters["profit_target"] - self.acceptance_threshold = agent_parameters[ - "initial_acceptance_threshold" - ] # 0.5 - self.acceptance_threshold_friction = agent_parameters[ - "acceptance_threshold_friction" - ] # 0.9 #1.0 to switch off - self.interest_rate = agent_parameters["interest_rate"] - self.reinsurance_limit = agent_parameters["reinsurance_limit"] + self.premium = agent_parameters.norm_premium + self.profit_target = agent_parameters.profit_target + self.acceptance_threshold = agent_parameters.initial_acceptance_threshold # 0.5 + self.acceptance_threshold_friction = ( + agent_parameters.acceptance_threshold_friction + ) # 0.9 #1.0 to switch off + self.interest_rate = agent_parameters.interest_rate + self.reinsurance_limit = agent_parameters.reinsurance_limit self.simulation_no_risk_categories = simulation_parameters["no_categories"] self.simulation_reinsurance_type = simulation_parameters[ "simulation_reinsurance_type" @@ -90,7 +101,7 @@ def __init__(self, simulation_parameters, agent_parameters): self.per_period_dividend = 0 self.cash_last_periods = list(np.zeros(4, dtype=int) * self.cash) - rm_config = agent_parameters["riskmodel_config"] + rm_config = agent_parameters.riskmodel_config """Here we modify the margin of safety depending on the number of risks models available in the market. When is 0 all risk models have the same margin of safety. The reason for doing this is that with more risk @@ -126,10 +137,10 @@ def __init__(self, simulation_parameters, agent_parameters): None for _ in range(self.simulation_no_risk_categories) ] if self.simulation_reinsurance_type == "non-proportional": - if agent_parameters["non-proportional_reinsurance_level"] is not None: - self.np_reinsurance_deductible_fraction = agent_parameters[ - "non-proportional_reinsurance_level" - ] + if agent_parameters.non_proportional_reinsurance_level is not None: + self.np_reinsurance_deductible_fraction = ( + agent_parameters.non_proportional_reinsurance_level + ) else: self.np_reinsurance_deductible_fraction = simulation_parameters[ "default_non-proportional_reinsurance_deductible" @@ -140,11 +151,8 @@ def __init__(self, simulation_parameters, agent_parameters): self.np_reinsurance_premium_share = simulation_parameters[ "default_non-proportional_reinsurance_premium_share" ] - self.obligations = [] - self.underwritten_contracts = [] - self.profits_losses = 0 + self.underwritten_contracts: MutableSequence[metainsurancecontract.MetaInsuranceContract] = [] # self.reinsurance_contracts = [] - self.operational = True self.is_insurer = True self.is_reinsurer = False @@ -171,14 +179,14 @@ def __init__(self, simulation_parameters, agent_parameters): # The share of all risks that this firm holds. Gets updated every timestep self.risk_share = 0 - def iterate(self, time): + def iterate(self, time: int): """Method that iterates each firm by one time step. Accepts: Time: Type Integer No return value For each time step this method obtains every firms interest payments, pays obligations, claim reinsurance, matures necessary contracts. Check condition for operational firms (as never removed) - so only operational firms receive new risks to evaluate, pay dividends, adjust capacity.""" + so only operational firms receive new risks to evaluate, _pay dividends, adjust capacity.""" """obtain investments yield""" self.obtain_yield(time) @@ -219,7 +227,9 @@ def iterate(self, time): self.estimate_var() - def collect_process_evaluate_risks(self, time, contracts_dissolved): + def collect_process_evaluate_risks( + self, time: int, contracts_dissolved: int + ) -> None: if self.operational: """request risks to be considered for underwriting in the next period and collect those for this period""" @@ -248,9 +258,7 @@ def collect_process_evaluate_risks(self, time, contracts_dissolved): [ reinrisks_per_categ, not_accepted_reinrisks, - ] = self.process_newrisks_reinsurer( - reinrisks_per_categ, number_reinrisks_categ, time - ) + ] = self.process_newrisks_reinsurer(reinrisks_per_categ, time) # QUERY: I moved this into the loop - was this correct? # The loop only runs once in my tests, what needs tweaking to have firms not accept risks? @@ -262,15 +270,16 @@ def collect_process_evaluate_risks(self, time, contracts_dissolved): # TODO: This takes up about 8% of processing time. Can we update the list instead of rebuilding it? underwritten_risks = [ - { - "value": contract.value, - "category": contract.category, - "risk_factor": contract.risk_factor, - "deductible": contract.deductible, - "excess": contract.excess, - "insurancetype": contract.insurancetype, - "runtime": contract.runtime, - } + RiskProperties( + owner=self, + value=contract.value, + category=contract.category, + risk_factor=contract.risk_factor, + deductible=contract.deductible, + excess=contract.excess, + insurancetype=contract.insurancetype, + runtime=contract.runtime, + ) for contract in self.underwritten_contracts if contract.reinsurance_share != 1.0 ] @@ -325,7 +334,6 @@ def collect_process_evaluate_risks(self, time, contracts_dissolved): # Here we process all the new risks in order to keep the portfolio as balanced as possible. risks_per_categ, not_accepted_risks = self.process_newrisks_insurer( risks_per_categ, - number_risks_categ, acceptable_by_category, var_per_risk_per_categ, cash_left_by_categ, @@ -340,27 +348,27 @@ def collect_process_evaluate_risks(self, time, contracts_dissolved): # print(self.id, " now has ", len(self.underwritten_contracts), " & returns ", len(not_accepted_risks)) - def enter_illiquidity(self, time): + def enter_illiquidity(self, time: int): """Enter_illiquidity Method. Accepts arguments time: Type integer. The current time. No return value. - This method is called when a firm does not have enough cash to pay all its obligations. It is only called + This method is called when a firm does not have enough cash to _pay all its obligations. It is only called from the method self._effect_payments() which is called at the beginning of the self.iterate() method of this class. This method formalizes the bankruptcy through the method self.enter_bankruptcy().""" self.enter_bankruptcy(time) - def enter_bankruptcy(self, time): + def enter_bankruptcy(self, time: int): """Enter_bankruptcy Method. Accepts arguments time: Type integer. The current time. No return value. - This method is used when a firm does not have enough cash to pay all its obligations. It is only called from + This method is used when a firm does not have enough cash to _pay all its obligations. It is only called from the method self.enter_illiquidity() which is only called from the method self._effect_payments(). This method dissolves the firm through the method self.dissolve().""" self.dissolve(time, "record_bankruptcy") - def market_exit(self, time): + def market_exit(self, time: int): """Market_exit Method. Accepts arguments time: Type integer. The current time. @@ -372,11 +380,11 @@ def market_exit(self, time): obligations this method dissolves the firm through the method self.dissolve().""" due = [item for item in self.obligations] for obligation in due: - self.pay(obligation) + self._pay(obligation) self.obligations = [] self.dissolve(time, "record_market_exit") - def dissolve(self, time, record): + def dissolve(self, time: int, record: str): """Dissolve Method. Accepts arguments time: Type integer. The current time. @@ -404,7 +412,7 @@ def dissolve(self, time, record): "due_time": time, "purpose": "Dissolution", } - self.pay( + self._pay( obligation ) # This MUST be the last obligation before the dissolution of the firm. self.excess_capital = ( @@ -421,25 +429,7 @@ def dissolve(self, time, record): category_reinsurance.dissolve(time) self.operational = False - def receive_obligation(self, amount, recipient, due_time, purpose): - """Method for receiving obligations that the firm will have to pay. - Accepts: - amount: Type integer, how much will be payed - recipient: Type Class instance, who will be payed - due_time: Type Integer, what time value they will be payed - purpose: Type string, why they are being payed - No return value - Adds obligation (Type DataDict) to list of obligations owed by the firm.""" - - obligation = { - "amount": amount, - "recipient": recipient, - "due_time": due_time, - "purpose": purpose, - } - self.obligations.append(obligation) - - def effect_payments(self, time): + def effect_payments(self, time: int): """Method for checking if any payments are due. Accepts: time: Type Integer @@ -464,42 +454,20 @@ def effect_payments(self, time): # TODO: effect partial payment else: for obligation in due: - self.pay(obligation) + self._pay(obligation) - def pay(self, obligation): - """Method to pay other class instances. - Accepts: - Obligation: Type DataDict - No return value - Method removes value payed from the agents cash and adds it to recipient agents cash.""" - amount = obligation["amount"] - recipient = obligation["recipient"] - purpose = obligation["purpose"] - if self.get_operational() and recipient.get_operational(): - self.cash -= amount - if purpose is not "dividend": - self.profits_losses -= amount - recipient.receive(amount) - - def receive(self, amount): - """Method to accept cash payments. - Accepts: - amount: Type Integer - No return value""" - self.cash += amount - self.profits_losses += amount - - def pay_dividends(self, time): + def pay_dividends(self, time: int): """Method to receive dividend obligation. Accepts: time: Type integer No return value - If firm has positive profits will pay percentage of them as dividends. Currently pays to simulation. + If firm has positive profits will _pay percentage of them as dividends. + Currently pays to simulation. """ self.receive_obligation(self.per_period_dividend, self.owner, time, "dividend") - def obtain_yield(self, time): + def obtain_yield(self, time: int): """Method to obtain intereset on cash reserves Accepts: time: Type integer @@ -509,7 +477,7 @@ def obtain_yield(self, time): # This interest rate should be taken from self.simulation with a getter method self.simulation.receive_obligation(amount, self, time, "yields") - def mature_contracts(self, time): + def mature_contracts(self, time: int) -> int: """Method to mature contracts that have expired Accepts: time: Type integer @@ -527,42 +495,34 @@ def mature_contracts(self, time): contract.mature(time) return len(maturing) - def get_cash(self): + def get_cash(self) -> float: """Method to return agents cash. Only used to calculate total sum of capital to recalculate market premium each iteration. No accepted values. No return values.""" return self.cash - def get_excess_capital(self): + def get_excess_capital(self) -> float: """Method to get agents excess capital. Only used for saving data. Called by simulation. No Accepted values. Returns agents excess capital""" return self.excess_capital - def number_underwritten_contracts(self): + def number_underwritten_contracts(self) -> int: return len(self.underwritten_contracts) - def get_underwritten_contracts(self): + def get_underwritten_contracts( + self + ) -> Sequence[metainsurancecontract.MetaInsuranceContract]: return self.underwritten_contracts - def get_profitslosses(self): + def get_profitslosses(self) -> float: """Method to get agents profit or loss. Only used for saving data. Called by simulation. No Accepted values. Returns agents profits/losses""" return self.profits_losses - def get_operational(self): - """Method to return boolean of if agent is operational. Only used as check for payments. - No accepted values - Returns Boolean""" - return self.operational - - def get_pointer(self): - """Method to get pointer. Returns self so renduant? Called only by resume.py""" - return self - - def estimate_var(self): + def estimate_var(self) -> None: """Method to estimate Value at Risk. No Accepted arguments. No return values @@ -595,7 +555,9 @@ def estimate_var(self): else: self.var_counter_per_risk = 0 - def get_newrisks_by_type(self): + def get_newrisks_by_type( + self + ) -> Tuple[Sequence[RiskProperties], Sequence[RiskProperties]]: """Method for soliciting new risks from insurance simulation then organising them based if non-proportional or not. No accepted Values. @@ -611,14 +573,12 @@ def get_newrisks_by_type(self): new_nonproportional_risks = [ risk for risk in new_risks - if risk.get("insurancetype") == "excess-of-loss" - and risk["owner"] is not self + if risk.insurancetype == "excess-of-loss" and risk.owner is not self ] new_risks = [ risk for risk in new_risks - if risk.get("insurancetype") in ["proportional", None] - and risk["owner"] is not self + if risk.insurancetype in ["proportional", None] and risk.owner is not self ] return new_nonproportional_risks, new_risks @@ -637,7 +597,9 @@ def adjust_capacity_target(self, time): "Method not implemented. adjust_capacity_target method should be implemented in inheriting classes" ) - def risks_reinrisks_organizer(self, new_risks): + def risks_reinrisks_organizer( + self, new_risks: Sequence[RiskProperties] + ) -> Tuple[Sequence[Sequence[RiskProperties]], Sequence[int]]: """This method organizes the new risks received by the insurer (or reinsurer) by category. Accepts: new_risks: Type list of DataDicts @@ -653,14 +615,19 @@ def risks_reinrisks_organizer(self, new_risks): for categ_id in range(self.simulation_parameters["no_categories"]): risks_by_category[categ_id] = [ - risk for risk in new_risks if risk["category"] == categ_id + risk for risk in new_risks if risk.category == categ_id ] number_risks_categ[categ_id] = len(risks_by_category[categ_id]) # The method returns both risks_by_category and number_risks_categ. return risks_by_category, number_risks_categ - def balanced_portfolio(self, risk, cash_left_by_categ, var_per_risk): + def balanced_portfolio( + self, + risk: RiskProperties, + cash_left_by_categ: np.ndarray, + var_per_risk: Optional[Sequence[float]], + ) -> Tuple[bool, np.ndarray]: """This method decides whether the portfolio is balanced enough to accept a new risk or not. If it is balanced enough return True otherwise False. This method also returns the cash available per category independently the risk is accepted or not. @@ -681,33 +648,31 @@ def balanced_portfolio(self, risk, cash_left_by_categ, var_per_risk): # cash_reserved_by_categ_store = np.copy(cash_reserved_by_categ) cash_reserved_by_categ_store = np.array(cash_reserved_by_categ) - if risk.get("insurancetype") == "excess-of-loss": + if risk.insurancetype == "excess-of-loss": percentage_value_at_risk = self.riskmodel.get_ppf( - categ_id=risk["category"], tail_size=self.riskmodel.var_tail_prob + categ_id=risk.category, tail_size=self.riskmodel.var_tail_prob ) expected_damage = ( percentage_value_at_risk - * risk["value"] - * risk["risk_factor"] - * self.riskmodel.inaccuracy[risk["category"]] + * risk.value + * risk.risk_factor + * self.riskmodel.inaccuracy[risk.category] ) expected_claim = ( - min(expected_damage, risk["value"] * risk["excess_fraction"]) - - risk["value"] * risk["deductible_fraction"] + min(expected_damage, risk.value * risk.excess_fraction) + - risk.value * risk.deductible_fraction ) # record liquidity requirement and apply margin of safety for liquidity requirement # Compute how the cash reserved by category would change if the new reinsurance risk was accepted - cash_reserved_by_categ_store[risk["category"]] += ( + cash_reserved_by_categ_store[risk.category] += ( expected_claim * self.riskmodel.margin_of_safety ) else: # Compute how the cash reserved by category would change if the new insurance risk was accepted - cash_reserved_by_categ_store[risk["category"]] += var_per_risk[ - risk["category"] - ] + cash_reserved_by_categ_store[risk.category] += var_per_risk[risk.category] # Compute the mean, std of the cash reserved by category after the new risk of reinrisk is accepted mean, std_post = get_mean_std(tuple(cash_reserved_by_categ_store)) @@ -731,104 +696,93 @@ def balanced_portfolio(self, risk, cash_left_by_categ, var_per_risk): return False, cash_left_by_categ def process_newrisks_reinsurer( - self, reinrisks_per_categ, number_reinrisks_categ, time + self, reinrisks_per_categ: Sequence[Sequence[RiskProperties]], time: int ): """Method to decide if new risks are underwritten for the reinsurance firm. Accepts: reinrisks_per_categ: Type List of lists containing new reinsurance risks. - number_reinrisks_per_categ: Type List of integers, contains number of new reinsurance risks by category. time: Type integer No return values. This method processes one by one the reinrisks contained in reinrisks_per_categ in order to decide whether they should be underwritten or not. It is done in this way to maintain the portfolio as balanced as possible. For that reason we process risk[C1], risk[C2], risk[C3], risk[C4], risk[C1], risk[C2], ... and so forth. If risks are accepted then a contract is written.""" - - for iterion in range(max(number_reinrisks_categ)): - for categ_id in range(self.simulation_parameters["no_categories"]): - # Here we take only one risk per category at a time to achieve risk[C1], risk[C2], risk[C3], - # risk[C4], risk[C1], risk[C2], ... if possible. - if ( - iterion < number_reinrisks_categ[categ_id] - and reinrisks_per_categ[categ_id][iterion] is not None - ): - risk_to_insure = reinrisks_per_categ[categ_id][iterion] - underwritten_risks = [ - { - "value": contract.value, - "category": contract.category, - "risk_factor": contract.risk_factor, - "deductible": contract.deductible, - "excess": contract.excess, - "insurancetype": contract.insurancetype, - "runtime_left": (contract.expiration - time), - } - for contract in self.underwritten_contracts - if contract.insurancetype == "excess-of-loss" - ] - accept, cash_left_by_categ, var_this_risk, self.excess_capital = self.riskmodel.evaluate( - underwritten_risks, self.cash, risk_to_insure + not_accepted_reinrisks = [] + for risk in chain.from_iterable(zip_longest(*reinrisks_per_categ)): + # Here we take only one risk per category at a time to achieve risk[C1], risk[C2], risk[C3], + # risk[C4], risk[C1], risk[C2], ... if possible. + if risk: + # TODO: Move this out of the loop (maybe somewhere else entirely) and update it when needed + underwritten_risks = [ + RiskProperties( + owner=self, + value=contract.value, + category=contract.category, + risk_factor=contract.risk_factor, + deductible=contract.deductible, + excess=contract.excess, + insurancetype=contract.insurancetype, + runtime_left=(contract.expiration - time), ) - # TODO: change riskmodel.evaluate() to accept new risk to be evaluated and - # to account for existing non-proportional risks correctly -> DONE. - if accept: - # TODO: rename this to per_value_premium in insurancecontract.py to avoid confusion - per_value_reinsurance_premium = ( - self.np_reinsurance_premium_share - * risk_to_insure["periodized_total_premium"] - * risk_to_insure["runtime"] - * ( - self.simulation.get_market_reinpremium() - / self.simulation.get_market_premium() - ) - / risk_to_insure["value"] - ) - - # Here it is check whether the portfolio is balanced or not if the reinrisk - # (risk_to_insure) is underwritten. Return True if it is balanced. False otherwise. - condition, cash_left_by_categ = self.balanced_portfolio( - risk_to_insure, cash_left_by_categ, None + for contract in self.underwritten_contracts + if contract.insurancetype == "excess-of-loss" + ] + accept, cash_left_by_categ, var_this_risk, self.excess_capital = self.riskmodel.evaluate( + underwritten_risks, self.cash, risk + ) + # TODO: change riskmodel.evaluate() to accept new risk to be evaluated and + # to account for existing non-proportional risks correctly -> DONE. + if accept: + # TODO: rename this to per_value_premium in insurancecontract.py to avoid confusion + per_value_reinsurance_premium = ( + self.np_reinsurance_premium_share + * risk.periodized_total_premium + * risk.runtime + * ( + self.simulation.get_market_reinpremium() + / self.simulation.get_market_premium() ) + / risk.value + ) - if condition: - contract = ReinsuranceContract( - self, - risk_to_insure, - time, - per_value_reinsurance_premium, - risk_to_insure["runtime"], - self.default_contract_payment_period, - expire_immediately=self.simulation_parameters[ - "expire_immediately" - ], - initial_var=var_this_risk, - insurancetype=risk_to_insure["insurancetype"], - ) # TODO: implement excess of loss for reinsurance contracts - self.underwritten_contracts.append(contract) - self.cash_left_by_categ = cash_left_by_categ - reinrisks_per_categ[categ_id][iterion] = None + # Here it is check whether the portfolio is balanced or not if the reinrisk + # (risk_to_insure) is underwritten. Return True if it is balanced. False otherwise. + condition, cash_left_by_categ = self.balanced_portfolio( + risk, cash_left_by_categ, None + ) - not_accepted_reinrisks = [] - for categ_id in range(self.simulation_parameters["no_categories"]): - for reinrisk in reinrisks_per_categ[categ_id]: - if reinrisk is not None: - not_accepted_reinrisks.append(reinrisk) + if condition: + contract = ReinsuranceContract( + self, + risk, + time, + per_value_reinsurance_premium, + risk.runtime, + self.default_contract_payment_period, + expire_immediately=self.simulation_parameters[ + "expire_immediately" + ], + initial_var=var_this_risk, + insurancetype=risk.insurancetype, + ) # TODO: implement excess of loss for reinsurance contracts + self.underwritten_contracts.append(contract) + self.cash_left_by_categ = cash_left_by_categ + else: + not_accepted_reinrisks.append(risk) return reinrisks_per_categ, not_accepted_reinrisks def process_newrisks_insurer( self, - risks_per_categ, - number_risks_categ, - acceptable_by_category, - var_per_risk_per_categ, - cash_left_by_categ, - time, - ): + risks_per_categ: Sequence[Sequence[RiskProperties]], + acceptable_by_category: Sequence[int], + var_per_risk_per_categ: Sequence[float], + cash_left_by_categ: Sequence[float], + time: int, + ) -> Tuple[Sequence[Sequence[RiskProperties]], Sequence[RiskProperties]]: """Method to decide if new risks are underwritten for the insurance firm. Accepts: risks_per_categ: Type List of lists containing new risks. - number_risks_categ: Type List of integers, contains number of new risks per category. acceptable_per_category: var_per_risk_per_categ: Type list of integers contains VaR for each category defined in getPPF. cash_left_by_categ: Type List, contains list of available cash per category @@ -836,83 +790,73 @@ def process_newrisks_insurer( Returns: risks_per_categ: Type list of list, same as above however with None where contracts were accepted. not_accepted_risks: Type List of DataDicts - This method processes one by one the reinrisks contained in reinrisks_per_categ in order to decide whether + This method processes one by one the risks contained in risks_per_categ in order to decide whether they should be underwritten or not. It is done in this way to maintain the portfolio as balanced as possible. For that reason we process risk[C1], risk[C2], risk[C3], risk[C4], risk[C1], risk[C2], ... and so forth. If risks are accepted then a contract is written.""" _cached_rvs = self.contract_runtime_dist.rvs() - for risk_index in range(max(number_risks_categ)): - for categ_id in range(len(acceptable_by_category)): - if ( - risk_index < number_risks_categ[categ_id] - and acceptable_by_category[categ_id] > 0 - and risks_per_categ[categ_id][risk_index] is not None - ): - risk_to_insure = risks_per_categ[categ_id][risk_index] - if ( - "contract" in risk_to_insure - and risk_to_insure["contract"].expiration > time - ): - # In this case the risk being inspected already has a contract, so we are deciding whether to - # give reinsurance for it # QUERY: is this correct? - [condition, cash_left_by_categ] = self.balanced_portfolio( - risk_to_insure, cash_left_by_categ, None + not_accepted_risks = [] + for risk in chain.from_iterable(zip_longest(*risks_per_categ)): + if risk and acceptable_by_category[risk.category] > 0: + categ_id = risk.category + if risk.contract and risk.contract.expiration > time: + # In this case the risk being inspected already has a contract, so we are deciding whether to + # give reinsurance for it # QUERY: is this correct? + [condition, cash_left_by_categ] = self.balanced_portfolio( + risk, cash_left_by_categ, None + ) + # Here we check whether the portfolio is balanced or not if the reinrisk (risk_to_insure) is + # underwritten. Return True if it is balanced. False otherwise. + if condition: + contract = ReinsuranceContract( + self, + risk, + time, + self.insurance_premium(), + risk.expiration - time, + self.default_contract_payment_period, + expire_immediately=self.simulation_parameters[ + "expire_immediately" + ], ) - # Here we check whether the portfolio is balanced or not if the reinrisk (risk_to_insure) is - # underwritten. Return True if it is balanced. False otherwise. - if condition: - contract = ReinsuranceContract( - self, - risk_to_insure, - time, - self.insurance_premium(), - risk_to_insure["expiration"] - time, - self.default_contract_payment_period, - expire_immediately=self.simulation_parameters[ - "expire_immediately" - ], - ) - self.underwritten_contracts.append(contract) - self.cash_left_by_categ = cash_left_by_categ - risks_per_categ[categ_id][risk_index] = None - + self.underwritten_contracts.append(contract) + self.cash_left_by_categ = cash_left_by_categ else: - [condition, cash_left_by_categ] = self.balanced_portfolio( - risk_to_insure, cash_left_by_categ, var_per_risk_per_categ - ) - # In this case there is no contact currently associated with the risk, so we decide whether - # to insure it - # Here it is check whether the portfolio is balanced or not if the risk (risk_to_insure) is - # underwritten. Return True if it is balanced. False otherwise. - if condition: - contract = InsuranceContract( - self, - risk_to_insure, - time, - self.simulation.get_market_premium(), - _cached_rvs, - self.default_contract_payment_period, - expire_immediately=self.simulation_parameters[ - "expire_immediately" - ], - initial_var=var_per_risk_per_categ[categ_id], - ) - self.underwritten_contracts.append(contract) - self.cash_left_by_categ = cash_left_by_categ - risks_per_categ[categ_id][risk_index] = None - acceptable_by_category[categ_id] -= 1 - # TODO: allow different values per risk (i.e. sum over value (and reinsurance_share) or - # exposure instead of counting) + not_accepted_risks.append(risk) - not_accepted_risks = [] - for categ_id in range(len(acceptable_by_category)): - for risk in risks_per_categ[categ_id]: - if risk is not None: - not_accepted_risks.append(risk) + else: + [condition, cash_left_by_categ] = self.balanced_portfolio( + risk, cash_left_by_categ, var_per_risk_per_categ + ) + # In this case there is no contact currently associated with the risk, so we decide whether + # to insure it + # Here it is check whether the portfolio is balanced or not if the risk (risk_to_insure) is + # underwritten. Return True if it is balanced. False otherwise. + if condition: + contract = InsuranceContract( + self, + risk, + time, + self.simulation.get_market_premium(), + _cached_rvs, + self.default_contract_payment_period, + expire_immediately=self.simulation_parameters[ + "expire_immediately" + ], + initial_var=var_per_risk_per_categ[categ_id], + ) + self.underwritten_contracts.append(contract) + self.cash_left_by_categ = cash_left_by_categ + else: + not_accepted_risks.append(risk) + # QUERY: should we only decrease this if the risk is accepted? + acceptable_by_category[categ_id] -= 1 + # TODO: allow different values per risk (i.e. sum over value (and reinsurance_share) or + # exposure instead of counting) return risks_per_categ, not_accepted_risks - def market_permanency(self, time): + def market_permanency(self, time: int): """Method determining if firm stays in market. Accepts: Time: Type Integer @@ -984,7 +928,7 @@ def market_permanency(self, time): # Here we determine how much is too long. self.market_exit(time) - def register_claim(self, claim): + def register_claim(self, claim: float): """Method to register claims. Accepts: claim: Type Integer, value of claim. @@ -1001,7 +945,7 @@ def reset_pl(self): insurancesimulation.py at the beginning of the iterate method""" self.profits_losses = 0 - def roll_over(self, time): + def roll_over(self, time: int): """Roll_over Method. Accepts arguments time: Type integer. The current time. No return value. @@ -1031,10 +975,10 @@ def roll_over(self, time): > self.simulation_parameters["insurance_retention"] ): self.simulation.return_risks( - [contract.risk_data] + [contract.risk] ) # TODO: This is not a retention, so the roll_over_flag might be confusing in this case else: - self.risks_kept.append(contract.risk_data) + self.risks_kept.append(contract.risk) if self.is_reinsurer: for reincontract in maturing_next: @@ -1050,28 +994,30 @@ def roll_over(self, time): if reinrisk is not None: self.reinrisks_kept.append(reinrisk) - def make_reinsurance_claims(self, time): + def make_reinsurance_claims(self, time: int): raise NotImplementedError( "MetaInsuranceOrg does not implement make_reinsurance_claims, " "it should have been overridden" ) def update_risk_share(self): - """Updates own value for share of all risks held by this firm""" + """Updates own value for share of all risks held by this firm. Has neither arguments nor a return value""" self.risk_share = self.simulation.get_risk_share(self) - def insurance_premium(self): + def insurance_premium(self) -> float: """Returns the premium this firm will charge for insurance. Returns the market premium multiplied by a factor that scales linearly with self.risk_share between 1 and the max permissble adjustment""" max_adjustment = isleconfig.simulation_parameters["max_scale_premiums"] - return self.simulation.get_market_premium() * ( + premium = self.simulation.get_market_premium() * ( 1 * (1 - self.risk_share) + max_adjustment * self.risk_share ) + return premium def adjust_riskmodel(self): - """Adjusts the inaccuracy parameter in the risk model under use depending on the share of risks held + """Adjusts the inaccuracy parameter in the risk model under use depending on the share of risks held. + Accepts no parameters and has no return Shrinks the risk model towards the best available risk model (as determined by "scale_inaccuracy" in isleconfig) by the share of risk this firm holds. diff --git a/reinsurancecontract.py b/reinsurancecontract.py index f09047f..88905d1 100644 --- a/reinsurancecontract.py +++ b/reinsurancecontract.py @@ -12,7 +12,7 @@ class ReinsuranceContract(MetaInsuranceContract): def __init__( self, insurer, - properties, + risk, time, premium, runtime, @@ -26,7 +26,7 @@ def __init__( ): super().__init__( insurer, - properties, + risk, time, premium, runtime, @@ -79,9 +79,9 @@ def explode(self, time, damage_extent=None): else: raise ValueError(f"Unexpected insurance type {self.insurancetype}") # Reinsurer pays as soon as possible. - self.insurer.register_claim( - claim - ) # Every reinsurance claim made is immediately registered. + # Every reinsurance claim made is immediately registered. + self.insurer.register_claim(claim) + if self.expire_immediately: self.current_claim += self.contract.claim # TODO: should proportional reinsurance claims be subject to excess_of_loss retrocession? diff --git a/reinsurancefirm.py b/reinsurancefirm.py deleted file mode 100644 index 5e1043c..0000000 --- a/reinsurancefirm.py +++ /dev/null @@ -1,16 +0,0 @@ -from insurancefirm import InsuranceFirm - - -class ReinsuranceFirm(InsuranceFirm): - """ReinsuranceFirm class. - Inherits from InsuranceFirm.""" - - def __init__(self, simulation_parameters, agent_parameters): - """Constructor method. - Accepts arguments - Signature is identical to constructor method of parent class. - Constructor calls parent constructor and only overwrites boolean indicators of insurer and reinsurer role of - the object.""" - super().__init__(simulation_parameters, agent_parameters) - self.is_insurer = False - self.is_reinsurer = True diff --git a/riskmodel.py b/riskmodel.py index 2341558..357e586 100644 --- a/riskmodel.py +++ b/riskmodel.py @@ -1,26 +1,29 @@ import math +from typing import Sequence, Tuple, Union, Optional, List import numpy as np import isleconfig from distributionreinsurance import ReinsuranceDistWrapper +from genericclasses import RiskProperties +from metainsurancecontract import MetaInsuranceContract class RiskModel: def __init__( self, damage_distribution, - expire_immediately, + expire_immediately: bool, cat_separation_distribution, - norm_premium, - category_number, - init_average_exposure, - init_average_risk_factor, - init_profit_estimate, - margin_of_safety, - var_tail_prob, - inaccuracy, - ): + norm_premium: float, + category_number: int, + init_average_exposure: float, + init_average_risk_factor: float, + init_profit_estimate: float, + margin_of_safety: float, + var_tail_prob: float, + inaccuracy: Sequence[float], + ) -> None: self.cat_separation_distribution = cat_separation_distribution self.norm_premium = norm_premium # QUERY: Whis is this passed as an argument and then ignored? @@ -33,15 +36,19 @@ def __init__( self.margin_of_safety = margin_of_safety """damage_distribution is some scipy frozen rv distribution which is bound between 0 and 1 and indicates the share of risks suffering damage as part of any single catastrophic peril""" - self.damage_distribution = [ + self.damage_distribution: List = [ damage_distribution for _ in range(self.category_number) ] # TODO: separate that category wise? -> DONE. - self.damage_distribution_stack = [[] for _ in range(self.category_number)] - self.reinsurance_contract_stack = [[] for _ in range(self.category_number)] + self.damage_distribution_stack: Sequence[List] = [ + [] for _ in range(self.category_number) + ] + self.reinsurance_contract_stack: Sequence[List[MetaInsuranceContract]] = [ + [] for _ in range(self.category_number) + ] # self.inaccuracy = np.random.uniform(9/10., 10/9., size=self.category_number) - self.inaccuracy = inaccuracy + self.inaccuracy: Sequence[float] = inaccuracy - def get_ppf(self, categ_id, tail_size): + def get_ppf(self, categ_id: int, tail_size: float) -> float: """Method for getting quantile function of the damage distribution (value at risk) by category. Positional arguments: categ_id integer: category @@ -49,7 +56,9 @@ def get_ppf(self, categ_id, tail_size): Returns value-at-risk.""" return self.damage_distribution[categ_id].ppf(1 - tail_size) - def get_risks_by_categ(self, risks): + def get_risks_by_categ( + self, risks: Sequence[RiskProperties] + ) -> Sequence[Sequence[RiskProperties]]: """Method splits list of risks by category Accepts: risks: Type List of DataDicts @@ -57,10 +66,13 @@ def get_risks_by_categ(self, risks): categ_risks: Type List of DataDicts.""" risks_by_categ = [[] for _ in range(self.category_number)] for risk in risks: - risks_by_categ[risk["category"]].append(risk) + risks_by_categ[risk.category].append(risk) return risks_by_categ - def compute_expectation(self, categ_risks, categ_id): # TODO: more intuitive name? + def compute_expectation( + self, categ_risks: Sequence[RiskProperties], categ_id: int + ) -> Tuple[float, float, float]: + # TODO: more intuitive name? """Method to compute the average exposure and risk factor as well as the increase in expected profits for the risks in a given category. Accepts: @@ -75,10 +87,10 @@ def compute_expectation(self, categ_risks, categ_id): # TODO: more intuitive na runtimes = np.zeros(len(categ_risks)) for i, risk in enumerate(categ_risks): # TODO: factor in excess instead of value? - exposures[i] = risk["value"] - risk["deductible"] - risk_factors[i] = risk["risk_factor"] - runtimes[i] = risk["runtime"] - average_exposure = np.mean(exposures) + exposures[i] = risk.value - risk.deductible + risk_factors[i] = risk.risk_factor + runtimes[i] = risk.runtime + average_exposure: float = np.mean(exposures) average_risk_factor = self.inaccuracy[categ_id] * np.mean(risk_factors) # mean_runtime = np.mean(runtimes) @@ -118,7 +130,9 @@ def compute_expectation(self, categ_risks, categ_id): # TODO: more intuitive na return average_risk_factor, average_exposure, incr_expected_profits - def evaluate_proportional(self, risks, cash): + def evaluate_proportional( + self, risks: Sequence[RiskProperties], cash: Sequence[float] + ) -> Tuple[float, Sequence[int], Sequence[int], Sequence[float]]: """Method to evaluate proportional type risks. Accepts: risks: Type List of DataDicts. @@ -230,7 +244,12 @@ def evaluate_proportional(self, risks, cash): var_per_risk_per_categ, ) - def evaluate_excess_of_loss(self, risks, cash, offered_risk=None): + def evaluate_excess_of_loss( + self, + risks: Sequence[RiskProperties], + cash: Sequence[float], + offered_risk: Optional[RiskProperties] = None, + ) -> Tuple[Sequence[float], Sequence[float], float]: """Method to evaluate excess-of-loss type risks. Accepts: risks: Type List of DataDicts. @@ -266,34 +285,30 @@ def evaluate_excess_of_loss(self, risks, cash, offered_risk=None): for risk in categ_risks: expected_damage = ( percentage_value_at_risk - * risk["value"] - * risk["risk_factor"] + * risk.value + * risk.risk_factor * self.inaccuracy[categ_id] ) # QUERY: This doesn't look accurate to me - E(f(X)) != f(E(X)) in general # QUERY: Isn't this wrong? - expected_claim = ( - min(expected_damage, risk["excess"]) - risk["deductible"] - ) + expected_claim = min(expected_damage, risk.excess) - risk.deductible # record liquidity requirement and apply margin of safety for liquidity requirement cash_left_by_categ[categ_id] -= expected_claim * self.margin_of_safety # compute additional liquidity requirements from newly offered contract - if (offered_risk is not None) and ( - offered_risk.get("category") == categ_id - ): + if (offered_risk is not None) and (offered_risk.category == categ_id): expected_damage_fraction = ( percentage_value_at_risk - * offered_risk["risk_factor"] + * offered_risk.risk_factor * self.inaccuracy[categ_id] ) expected_claim_fraction = ( - min(expected_damage_fraction, offered_risk["excess_fraction"]) - - offered_risk["deductible_fraction"] + min(expected_damage_fraction, offered_risk.excess_fraction) + - offered_risk.deductible_fraction ) - expected_claim_total = expected_claim_fraction * offered_risk["value"] + expected_claim_total = expected_claim_fraction * offered_risk.value # record liquidity requirement and apply margin of safety for liquidity requirement additional_required[categ_id] += ( @@ -308,14 +323,23 @@ def evaluate_excess_of_loss(self, risks, cash, offered_risk=None): return cash_left_by_categ, additional_required, var_this_risk # noinspection PyUnboundLocalVariable - def evaluate(self, risks, cash, offered_risk=None): + def evaluate( + self, + risks: Sequence[RiskProperties], + cash: Union[float, Sequence[float]], + offered_risk: Optional[RiskProperties] = None, + ) -> Union[ + Tuple[float, Sequence[int], Sequence[float], Sequence[float], float], + Tuple[bool, Sequence[float], float, float], + ]: """Method to evaluate given risks and the offered risk. Accepts: risks: List of DataDicts. cash: Type Decimal. Optional: offered_risk: Type DataDict or defaults to None. - (offered_risk = None) Returns: + (offered_risk = None) + Returns: expected_profits_proportional: Type Decimal (Currently returns None) remaining_acceptable_by_categ: Type List of Integers. Number of risks that would not be covered by firms cash. @@ -336,9 +360,7 @@ def evaluate(self, risks, cash, offered_risk=None): results in two sets of return values being used. These return values are what is used to determine if risks are underwritten or not.""" # ensure that any risk to be considered supplied directly as argument is non-proportional/excess-of-loss - assert (offered_risk is None) or offered_risk.get( - "insurancetype" - ) == "excess-of-loss" + assert (offered_risk is None) or offered_risk.insurancetype == "excess-of-loss" # construct cash_left_by_categ as a sequence, defining remaining liquidity by category if not isinstance(cash, (np.ndarray, list)): @@ -348,8 +370,8 @@ def evaluate(self, risks, cash, offered_risk=None): assert len(cash_left_by_categ) == self.category_number # sort current contracts - el_risks = [risk for risk in risks if risk["insurancetype"] == "excess-of-loss"] - risks = [risk for risk in risks if risk["insurancetype"] == "proportional"] + el_risks = [risk for risk in risks if risk.insurancetype == "excess-of-loss"] + risks = [risk for risk in risks if risk.insurancetype == "proportional"] # compute liquidity requirements and acceptable risks from existing contract if (offered_risk is not None) or (len(el_risks) > 0): cash_left_by_categ, additional_required, var_this_risk = self.evaluate_excess_of_loss( @@ -389,7 +411,13 @@ def evaluate(self, risks, cash, offered_risk=None): min(cash_left_by_categ), ) - def add_reinsurance(self, categ_id, excess_fraction, deductible_fraction, contract): + def add_reinsurance( + self, + categ_id: int, + excess_fraction: float, + deductible_fraction: float, + contract: MetaInsuranceContract, + ): """Method to add any instance of reinsurance to risk models list of reinsurance contracts, and add damage distribution to stack of damage distributions per category, then replace with a new distribution. Only used in thad add_reinsurance method of insurancefirm. @@ -409,7 +437,7 @@ def add_reinsurance(self, categ_id, excess_fraction, deductible_fraction, contra dist=self.damage_distribution[categ_id], ) - def delete_reinsurance(self, categ_id, contract): + def delete_reinsurance(self, categ_id: int, contract: MetaInsuranceContract): """Method to remove any instance of reinsurance to risk models list of reinsurance contracts, and remove its damage distribution from the stack of damage distributions per category. Only used in the delete_reinsurance method of insurancefirm. diff --git a/start.py b/start.py index 01ba166..14c9338 100644 --- a/start.py +++ b/start.py @@ -8,13 +8,11 @@ import copy import calibrationscore -import insurancefirm import insurancesimulation # import config file and apply configuration import isleconfig import logger -import reinsurancefirm simulation_parameters = isleconfig.simulation_parameters filepath = None @@ -55,17 +53,6 @@ def main( rc_event_schedule, rc_event_damage, ) - - simulation.add_agents( - insurancefirm.InsuranceFirm, - "insurancefirm", - n=simulation_parameters["no_insurancefirms"], - ) - simulation.add_agents( - reinsurancefirm.ReinsuranceFirm, - "reinsurancefirm", - n=simulation_parameters["no_reinsurancefirms"], - ) time = 0 else: d = load_simulation() @@ -209,8 +196,8 @@ def load_simulation(): override_no_riskmodels = 1 if args.riskmodels: override_no_riskmodels = args.riskmodels - if args.replicid: # TODO: track down all uses of replicid - raise ValueError("--replicid is no longer supported, use --file") + # if args.replicid: # TODO: track down all uses of replicid + # raise ValueError("--replicid is no longer supported, use --file") if args.file: filepath = args.file if args.overwrite: From 94f5e5f49b179b86607bedf32cb3b09c40c4734b Mon Sep 17 00:00:00 2001 From: KloskaT Date: Wed, 17 Jul 2019 17:36:49 +0100 Subject: [PATCH 052/125] Added visualisation_paper3.py stuff to file, inluding histogram and CDF, CDF doesnt work for data set. --- visualisation.py | 490 +++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 476 insertions(+), 14 deletions(-) diff --git a/visualisation.py b/visualisation.py index ac57620..097d262 100644 --- a/visualisation.py +++ b/visualisation.py @@ -3,7 +3,11 @@ import argparse import matplotlib.pyplot as plt import matplotlib.animation as animation - +import isleconfig +import pickle +import scipy +import scipy.stats +from matplotlib.offsetbox import AnchoredText class TimeSeries(object): def __init__(self, series_list, event_schedule, damage_schedule, title="",xlabel="Time", colour='k', axlst=None, fig=None, percentiles=None, alpha=0.7): @@ -61,7 +65,7 @@ class InsuranceFirmAnimation(object): No return values. This class takes the cash and contract data of each firm over all time and produces an animation showing how the proportion of each for all operational firms changes with time. Allows it to be saved as an MP4.""" - def __init__(self, cash_data, insure_contracts, event_schedule, type, save=False, perils=True): + def __init__(self, cash_data, insure_contracts, event_schedule, type, save=True, perils=True): # Converts list of events by category into list of all events. self.perils_condition = perils self.all_event_times = [] @@ -87,7 +91,7 @@ def animate(self): self.pies = [0, 0] self.fig, (self.ax1, self.ax2) = plt.subplots(1, 2) self.stream = self.data_stream() - self.animate = animation.FuncAnimation(self.fig, self.update, repeat=False, interval=20, save_count=98) + self.animate = animation.FuncAnimation(self.fig, self.update, repeat=False, interval=20, save_count=998) if self.save_condition: self.save() @@ -251,6 +255,92 @@ def metaplotter_timescale(self): catbonds_number = np.median([history_logs['total_catbondsoperational'] for history_logs in self.history_logs_list],axis=0) return + def aux_clustered_exit_records(self, exits): + """Auxiliary method for creation of data series on clustered events such as firm market exits. + Will take an unclustered series and aggregate every series of non-zero elements into + the first element of that series. + Arguments: + exits: numpy ndarray or list - unclustered series + Returns: + numpy ndarray of the same length as argument "exits": the clustered series.""" + exits2 = [] + ci = False + cidx = 0 + for ee in exits: + if ci: + exits2.append(0) + if ee > 0: + exits2[cidx] += ee + else: + ci = False + else: + exits2.append(ee) + if ee > 0: + ci = True + cidx = len(exits2) - 1 + + return np.asarray(exits2, dtype=np.float64) + + def populate_scatter_data(self): + """Method to generate data samples that do not have a time component (e.g. the size of bankruptcy events, i.e. + how many firms exited each time. + The method saves these in the instance variable self.scatter_data. This variable is of type dict. + Arguments: None. + Returns: None.""" + + """Record data on sizes of unrecovered_claims""" + self.scatter_data["unrecovered_claims"] = [] + for hlog in self.history_logs_list: # for each replication + urc = np.diff(np.asarray(hlog["cumulative_unrecovered_claims"])) + self.scatter_data["unrecovered_claims"] = np.hstack( + [self.scatter_data["unrecovered_claims"], np.extract(urc > 0, urc)]) + + """Record data on sizes of unrecovered_claims""" + self.scatter_data["relative_unrecovered_claims"] = [] + for hlog in self.history_logs_list: # for each replication + urc = np.diff(np.asarray(hlog["cumulative_unrecovered_claims"])) + tcl = np.diff(np.asarray(hlog["cumulative_claims"])) + rurc = urc / tcl + self.scatter_data["relative_unrecovered_claims"] = np.hstack( + [self.scatter_data["unrecovered_claims"], np.extract(rurc > 0, rurc)]) + try: + assert np.isinf(self.scatter_data["relative_unrecovered_claims"]).any() == False + except: + pass + # pdb.set_trace() + + """Record data on sizes of bankruptcy_events""" + self.scatter_data["bankruptcy_events"] = [] + self.scatter_data["bankruptcy_events_relative"] = [] + self.scatter_data["bankruptcy_events_clustered"] = [] + self.scatter_data["bankruptcy_events_relative_clustered"] = [] + for hlog in self.history_logs_list: # for each replication + """Obtain numbers of operational firms. This is for computing the relative share of exiting firms.""" + in_op = np.asarray(hlog["total_operational"])[:-1] + rein_op = np.asarray(hlog["total_reinoperational"])[:-1] + op = in_op + rein_op + exits = np.diff(np.asarray(hlog["cumulative_market_exits"], dtype=np.float64)) + assert (exits <= op).all() + op[op == 0] = 1 + + """Obtain exits and relative exits""" + # exits = np.diff(np.asarray(hlog["cumulative_market_exits"], dtype=np.float64)) # used above already + rel_exits = exits / op + + """Obtain clustered exits (absolute and relative)""" + exits2 = self.aux_clustered_exit_records(exits) + rel_exits2 = exits2 / op + + """Record data""" + self.scatter_data["bankruptcy_events"] = np.hstack( + [self.scatter_data["bankruptcy_events"], np.extract(exits > 0, exits)]) + self.scatter_data["bankruptcy_events_relative"] = np.hstack( + [self.scatter_data["bankruptcy_events_relative"], np.extract(rel_exits > 0, rel_exits)]) + self.scatter_data["bankruptcy_events_clustered"] = np.hstack( + [self.scatter_data["bankruptcy_events_clustered"], np.extract(exits2 > 0, exits2)]) + self.scatter_data["bankruptcy_events_relative_clustered"] = np.hstack( + [self.scatter_data["bankruptcy_events_relative_clustered"], np.extract(rel_exits2 > 0, rel_exits2)]) + def show(self): plt.show() return @@ -280,17 +370,353 @@ def show(self): def save(self): # logic to save plots pass - + + +class CDF_distribution_plot(): + """Class for CDF/cCDF distribution plots using auxiliary class from visualisation_distribution_plots.py. + This class arranges as many such plots stacked in one diagram as there are series in the history + logs they are created from, i.e. len(vis_list).""" + def __init__(self, vis_list, colour_list, quantiles=[.25, .75], variable="reinsurance_firms_cash", timestep=-1, + plot_cCDF=True): + """Constructor. + Arguments: + vis_list: list of visualisation objects - objects hilding the data + colour list: list of str - colors to be used for each plot + quantiles: list of float of length 2 - lower and upper quantile for inter quantile range in plot + variable: string (must be a valid dict key in vis_list[i].history_logs_list + - the history log variable for which the distribution is plotted + (will be either "insurance_firms_cash" or "reinsurance_firms_cash") + timestep: int - timestep at which the distribution to be plotted is taken + plot_cCDF: bool - plot survival function (cCDF) instead of CDF + Returns class instance.""" + self.vis_list = vis_list + self.colour_list = colour_list + self.lower_quantile, self.upper_quantile = quantiles + self.variable = variable + self.timestep = timestep + + def generate_plot(self, xlabel=None, filename=None): + """Method to generate and save the plot. + Arguments: + xlabel: str or None - the x axis label + filename: str or None - the filename without ending + Returns None.""" + + """Set x axis label and filename to default if not provided""" + xlabel = xlabel if xlabel is not None else self.variable + filename = filename if filename is not None else "CDF_plot_" + self.variable + + """Create figure with correct number of subplots""" + self.fig, self.ax = plt.subplots(nrows=len(self.vis_list)) + + """find max and min values""" + """combine all data sets""" + all_data = np.asarray([]) + for i in range(len(self.vis_list)): + """Extract firm records from history logs""" + series_x = [replication[self.variable][self.timestep] for replication in self.vis_list[i].history_logs_list] + """Extract the capital holdings from the tuple""" + for j in range(len(series_x)): + series_x[j] = [firm[0] for firm in series_x[j] if firm[2]] + series_x = np.hstack(series_x) + all_data = np.hstack([all_data, series_x]) + """Catch empty data sets""" + if len(all_data) == 0: + return + minmax = (np.min(all_data), np.max(all_data) / 2.) + + """Loop through simulation record series, populate subplot by subplot""" + for i in range(len(self.vis_list)): + """Extract firm records from history logs""" + series_x = [replication[self.variable][self.timestep] for replication in self.vis_list[i].history_logs_list] + """Extract the capital holdings from the tuple""" + for j in range(len(series_x)): + series_x[j] = [firm[0] for firm in series_x[j] if firm[2]] + """Create CDFDistribution object and populate the subfigure using it""" + VDP = CDFDistribution(series_x) + # VDP.make_figure(upper_quantile=self.upper_quantile, lower_quantile=self.lower_quantile) + c_xlabel = "" if i < len(self.vis_list) - 1 else xlabel + VDP.plot(ax=self.ax[i], ylabel="cCDF " + str(i + 1) + "RM", xlabel=c_xlabel, + upper_quantile=self.upper_quantile, lower_quantile=self.lower_quantile, color=self.colour_list[i], + plot_cCDF=True, xlims=minmax) + + """Finish and save figure""" + self.fig.tight_layout() + self.fig.savefig(filename + ".pdf") + self.fig.savefig(filename + ".png", density=300) + + +class Histogram_plot(): + """Class for CDF/cCDF distribution plots using auxiliary class from visualisation_distribution_plots.py. + This class arranges as many such plots stacked in one diagram as there are series in the history + logs they are created from, i.e. len(vis_list).""" + def __init__(self, vis_list, colour_list, variable="bankruptcy_events"): + """Constructor. + Arguments: + vis_list: list of visualisation objects - objects hilding the data + colour list: list of str - colors to be used for each plot + variable: string (must be a valid dict key in vis_list[i].scatter_data + - the history log variable for which the distribution is plotted + Returns class instance.""" + self.vis_list = vis_list + self.colour_list = colour_list + self.variable = variable + + def generate_plot(self, xlabel=None, filename=None, logscale=False, minmax=None, VaR005guess=0.3): + """Method to generate and save the plot. + Arguments: + xlabel: str or None - the x axis label + filename: str or None - the filename without ending + Returns None.""" + + """Set x axis label and filename to default if not provided""" + xlabel = xlabel if xlabel is not None else self.variable + filename = filename if filename is not None else "Histogram_plot_" + self.variable + + """Create figure with correct number of subplots""" + self.fig, self.ax = plt.subplots(nrows=len(self.vis_list)) + + """find max and min values""" + """combine all data sets""" + all_data = [np.asarray(vl.scatter_data[self.variable]) for vl in self.vis_list] + with open("scatter_data.pkl", "wb") as wfile: + pickle.dump(all_data, wfile) + all_data = np.hstack(all_data) + """Catch empty data sets""" + if len(all_data) == 0: + return + if minmax is None: + minmax = (np.min(all_data), np.max(all_data)) + num_bins = min(25, len(np.unique(all_data))) + + """Loop through simulation record series, populate subplot by subplot""" + for i in range(len(self.vis_list)): + """Extract records from history logs""" + scatter_data = self.vis_list[i].scatter_data[self.variable] + """Create Histogram object and populate the subfigure using it""" + H = Histogram(scatter_data) + c_xlabel = "" if i < len(self.vis_list) - 1 else xlabel + c_xtralabel = str(i + 1) + " risk models" if i > 0 else str(i + 1) + " risk model" + c_ylabel = "Frequency" if i == 2 else "" + H.plot(ax=self.ax[i], ylabel=c_ylabel, xtralabel=c_xtralabel, xlabel=c_xlabel, color=self.colour_list[i], + num_bins=num_bins, logscale=logscale, xlims=minmax) + VaR005 = sorted(scatter_data, reverse=True)[int(round(len(scatter_data) * 200. / 4000.))] + realized_events_beyond = len(np.extract(scatter_data > VaR005guess, scatter_data)) + realized_expected_shortfall = np.mean(np.extract(scatter_data > VaR005guess, scatter_data)) - VaR005guess + print(self.variable, c_xtralabel, "Slope: ", 1 / scipy.stats.expon.fit(scatter_data)[0], + "1/200 threshold: ", VaR005, " #Events beyond: ", realized_events_beyond, "Relative: ", + realized_events_beyond * 1.0 / len(scatter_data), " Expected shortfall: ", + realized_expected_shortfall) + + """Finish and save figure""" + self.fig.tight_layout(pad=.1, w_pad=.1, h_pad=.1) + self.fig.savefig(filename + ".pdf") + self.fig.savefig(filename + ".png", density=300) + + +class CDFDistribution(): + def __init__(self, samples_x): + """Constructor. + Arguments: + samples_x: list of list or ndarray of int or float - list of samples to be visualized. + Returns: + Class instance""" + self.samples_x = [] + self.samples_y = [] + for x in samples_x: + if len(x) > 0: + x = np.sort(np.asarray(x, dtype=np.float64)) + y = (np.arange(len(x), dtype=np.float64) + 1) / len(x) + self.samples_x.append(x) + self.samples_y.append(y) + self.series_y = None + self.median_x = None + self.mean_x = None + self.quantile_series_x = None + self.quantile_series_y_lower = None + self.quantile_series_y_upper = None + + def make_figure(self, upper_quantile=.25, lower_quantile=.75): + # pdb.set_trace() + """Method to do the necessary computations to create the CDF plot (incl. mean, median, quantiles. + This method populates the variables that are plotted. + Arguments: + upper_quantile: float \in [0,1] - upper quantile threshold + lower_quantile: float \in [0,1] - lower quantile threshold + Returns None.""" + + """Obtain ordered set of all y values""" + self.series_y = np.unique(np.sort(np.hstack(self.samples_y))) + + """Obtain x coordinates corresponding to the full ordered set of all y values (self.series_y) for each series""" + set_of_series_x = [] + for i in range(len(self.samples_x)): + x = [self.samples_x[i][np.argmax(self.samples_y[i] >= y)] if self.samples_y[i][0] <= y else 0 for y in + self.series_y] + set_of_series_x.append(x) + + """Join x coordinates to matrix of size m x n (n: number of series, m: length of ordered set of y values (self.series_y))""" + series_matrix_x = np.vstack(set_of_series_x) + + """Compute x quantiles, median, mean across all series""" + quantile_lower_x = np.quantile(series_matrix_x, .25, axis=0) + quantile_upper_x = np.quantile(series_matrix_x, .75, axis=0) + self.median_x = np.quantile(series_matrix_x, .50, axis=0) + self.mean_x = series_matrix_x.mean(axis=0) + + """Obtain x coordinates for quantile plots. This is the ordered set of all x coordinates in lower and upper quantile series.""" + self.quantile_series_x = np.unique(np.sort(np.hstack([quantile_lower_x, quantile_upper_x]))) + + """Obtain y coordinates for quantile plots. This is one y value for each x coordinate.""" + # self.quantile_series_y_lower = [self.series_y[np.argmax(quantile_lower_x>=x)] if quantile_lower_x[0]<=x else 0 for x in self.quantile_series_x] + self.quantile_series_y_lower = np.asarray([self.series_y[np.argmax(quantile_lower_x >= x)] if np.sum( + np.argmax(quantile_lower_x >= x)) > 0 else np.max(self.series_y) for x in self.quantile_series_x]) + self.quantile_series_y_upper = np.asarray( + [self.series_y[np.argmax(quantile_upper_x >= x)] if quantile_upper_x[0] <= x else 0 for x in + self.quantile_series_x]) + + """The first value of lower must be zero""" + self.quantile_series_y_lower[0] = 0.0 + + print(list(self.median_x), "\n\n", list(self.series_y), "\n\n\n\n") + + def reverse_CDF(self): + """Method to reverse the CDFs and obtain the complementary CDFs (survival functions) instead. + The method overwrites the attributes used for plotting. + Arguments: None. + Returns: None.""" + self.series_y = 1. - self.series_y + self.quantile_series_y_lower = 1. - self.quantile_series_y_lower + self.quantile_series_y_upper = 1. - self.quantile_series_y_upper + + def plot(self, ax=None, ylabel="CDF(x)", xlabel="y", upper_quantile=.25, lower_quantile=.75, + force_recomputation=False, show=False, outputname=None, color="C2", plot_cCDF=False, xlims=None): + """Method to compile the plot. The plot is added to a provided matplotlib axes object or a new one is created. + Arguments: + ax: matplitlib axes - the system of coordinates into which to plot + ylabel: str - y axis label + xlabel: str - x axis label + upper_quantile: float \in [0,1] - upper quantile threshold + lower_quantile: float \in [0,1] - lower quantile threshold + force_recomputation: bool - force re-computation of plots + show: bool - show plot + outputname: str - output file name without ending + color: str or other admissible matplotlib color label - color to use for the plot + plot_cCDF: bool - plot survival function (cCDF) instead of CDF + Returns: None.""" + + """If data set is empty, return without plotting""" + if self.samples_x == []: + return + + """Create figure if none was provided""" + if ax is None: + fig = plt.figure() + ax = fig.add_subplot(111) + + """Compute plots if not already done or if recomputation was requested""" + if (self.series_y is None) or force_recomputation: + self.make_figure(upper_quantile, lower_quantile) + + """Switch to cCDF if requested""" + if plot_cCDF: + self.reverse_CDF() + + """Plot""" + ax.fill_between(self.quantile_series_x, self.quantile_series_y_lower, self.quantile_series_y_upper, + facecolor=color, alpha=0.25) + ax.plot(self.median_x, self.series_y, color=color) + ax.plot(self.mean_x, self.series_y, dashes=[3, 3], color=color) + + """Set plot attributes""" + ax.set_ylabel(ylabel) + ax.set_xlabel(xlabel) + + """Set xlim if requested""" + if xlims is not None: + ax.set_xlim(xlims[0], xlims[1]) + + """Save if filename provided""" + if outputname is not None: + plt.savefig(outputname + ".pdf") + plt.savefig(outputname + ".png", density=300) + + """Show if requested""" + if show: + plt.show() + + +class Histogram(): + """Class for plots of ensembles of distributions as CDF (cumulative distribution function) or cCDF (complementary + cumulative distribution function) with mean, median, and quantiles""" + def __init__(self, sample_x): + self.sample_x = sample_x + + def plot(self, ax=None, ylabel="PDF(x)", xtralabel="", xlabel="x", num_bins=50, show=False, outputname=None, + color="C2", logscale=False, xlims=None): + """Method to compile the plot. The plot is added to a provided matplotlib axes object or a new one is created. + Arguments: + ax: matplitlib axes - the system of coordinates into which to plot + ylabel: str - y axis label + xlabel: str - x axis label + num_bins: int - number of bins + show: bool - show plot + outputname: str - output file name without ending + color: str or other admissible matplotlib color label - color to use for the plot + logscale: bool - y axis logscale + xlims: tuple, array of len 2, or none - x axis limits + Returns: None.""" + + """Create figure if none was provided""" + if ax is None: + fig = plt.figure() + ax = fig.add_subplot(111) + + """Plot""" + ax.hist(self.sample_x, bins=num_bins, color=color) + + """Set plot attributes""" + ax.set_ylabel(ylabel) + ax.set_xlabel(xlabel) + if xtralabel != "": + anchored_text = AnchoredText(xtralabel, loc=1) + ax.add_artist(anchored_text) + + """Set xlim if requested""" + if xlims is not None: + ax.set_xlim(xlims[0], xlims[1]) + + """Set yscale to log if requested""" + if logscale: + ax.set_yscale("log") + + """Save if filename provided""" + if outputname is not None: + plt.savefig(outputname + ".pdf") + plt.savefig(outputname + ".png", density=300) + + """Show if requested""" + if show: + plt.show() + if __name__ == "__main__": - # use argparse to handle command line arguments + # Use argparse to handle command line arguments parser = argparse.ArgumentParser(description='Model the Insurance sector') parser.add_argument("--single", action="store_true", help="plot a single run of the insurance model") - parser.add_argument("--comparison", action="store_true", help="plot the result of an ensemble of replicatons of the insurance model") parser.add_argument("--pie", action="store_true", help="plot animated pie charts of contract and cash data") parser.add_argument("--timeseries", action="store_true", help="plot time series of firm data") + parser.add_argument("--comparison", action="store_true", help="plot the result of an ensemble of replicatons of the insurance model") + parser.add_argument("--firmdistribution", action="store_true", + help="plot the CDFs of firm size distributions with quartiles indicating variation across " + "ensemble") + parser.add_argument("--bankruptcydistribution", action="store_true", + help="plot the histograms of bankruptcy events across ensemble") args = parser.parse_args() + + args.bankruptcydistribution = True args.single = args.pie = True + if args.single: # load in data from the history_logs dictionarywith open("data/history_logs.dat","r") as rfile: @@ -308,25 +734,61 @@ def save(self): vis.show() N = len(history_logs_list) - if args.comparison: + if args.comparison or args.firmdistribution or args.bankruptcydistribution: # for each run, generate an animation and time series for insurer and reinsurer # TODO: provide some way for these to be lined up nicely rather than having to manually arrange screen - #for i in range(N): + # for i in range(N): # vis.insurer_pie_animation(run=i) # vis.insurer_time_series(runs=[i]) # vis.reinsurer_pie_animation(run=i) # vis.reinsurer_time_series(runs=[i]) # vis.show() vis_list = [] - filenames = ["./data/"+x+"_history_logs.dat" for x in ["one","two","three","four"]] + filenames = ["./data/" + x + "_history_logs.dat" for x in ["one", "two", "three", "four"]] for filename in filenames: - with open(filename,'r') as rfile: - history_logs_list = [eval(k) for k in rfile] # one dict on each line + with open(filename, 'r') as rfile: + history_logs_list = [eval(k) for k in rfile] # one dict on each line vis_list.append(visualisation(history_logs_list)) - colour_list = ['blue', 'yellow', 'red', 'green'] + colour_list = ['red', 'blue', 'green', 'yellow'] + + if args.comparison: cmp_rsk = compare_riskmodels(vis_list, colour_list) - cmp_rsk.create_insurer_timeseries(percentiles=[10,90]) - cmp_rsk.create_reinsurer_timeseries(percentiles=[10,90]) + cmp_rsk.create_insurer_timeseries(percentiles=[10, 90]) + cmp_rsk.create_reinsurer_timeseries(percentiles=[10, 90]) cmp_rsk.show() + + if args.firmdistribution: + CP = CDF_distribution_plot(vis_list, colour_list, variable="insurance_firms_cash", timestep=-1, plot_cCDF=True) + CP.generate_plot(xlabel="Firm size (capital)") + if not isleconfig.simulation_parameters["reinsurance_off"]: + CP = CDF_distribution_plot(vis_list, colour_list, variable="reinsurance_firms_cash", timestep=-1, + plot_cCDF=True) + CP.generate_plot(xlabel="Firm size (capital)") + + if args.bankruptcydistribution: + for vis in vis_list: + vis.populate_scatter_data() + # HP = Histogram_plot(vis_list, colour_list, variable="bankruptcy_events") + # HP.generate_plot(logscale=True, xlabel="Number of bankruptcies") + # HP = Histogram_plot(vis_list, colour_list, variable="bankruptcy_events_relative") + # HP.generate_plot(logscale=True, xlabel="Share of bankrupt firms") + # HP = Histogram_plot(vis_list, colour_list, variable="bankruptcy_events_clustered") + # HP.generate_plot(logscale=True, xlabel="Number of bankruptcies") + + HP = Histogram_plot(vis_list, colour_list, variable="bankruptcy_events_relative_clustered") + HP.generate_plot(logscale=True, xlabel="Share of bankrupt firms", minmax=[0, 0.5], + VaR005guess=0.1) # =0.056338028169014086) # this is the VaR threshold for 4 risk models with reinsurance + # HP.generate_plot(logscale=True, xlabel="Share of bankrupt firms", minmax=[0, 0.5], VaR005guess=0.04580152671755725) # this is the VaR threshold for 4 risk models without reinsurance + + HP = Histogram_plot(vis_list, colour_list, variable="unrecovered_claims") + HP.generate_plot(logscale=True, xlabel="Damages not recovered", minmax=[0, 6450000], + VaR005guess=0.1) # =691186.8726311699) # this is the VaR threshold for 4 risk models with reinsurance + # HP.generate_plot(logscale=True, xlabel="Damages not recovered", minmax=[0, 6450000], VaR005guess=449707.1970911417) # this is the VaR threshold for 4 risk models without reinsurance + + # HP = Histogram_plot(vis_list, colour_list, variable="relative_unrecovered_claims") + # HP.generate_plot(logscale=True, xlabel="Damages not recovered")#, minmax=[0, 6450000]) + + +# ਲ਼ From c3c637695f50c717c1cb5ab7a01ae6386f76b992 Mon Sep 17 00:00:00 2001 From: Chris Hughes Date: Wed, 17 Jul 2019 16:24:48 +0100 Subject: [PATCH 053/125] Remove replicid argument --- start.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/start.py b/start.py index 01ba166..2e95e45 100644 --- a/start.py +++ b/start.py @@ -76,7 +76,6 @@ def main( simulation_parameters = d["simulation_parameters"] for key in d["isleconfig"]: isleconfig.__dict__[key] = d["isleconfig"][key] - simulation = copy.deepcopy(simulation) for t in range(time, simulation_parameters["max_time"]): # Main time iteration loop simulation.iterate(t) @@ -209,8 +208,8 @@ def load_simulation(): override_no_riskmodels = 1 if args.riskmodels: override_no_riskmodels = args.riskmodels - if args.replicid: # TODO: track down all uses of replicid - raise ValueError("--replicid is no longer supported, use --file") + # if args.replicid: # TODO: track down all uses of replicid + # raise ValueError("--replicid is no longer supported, use --file") if args.file: filepath = args.file if args.overwrite: From 64d6fe5d4b8204f7955cf26cce3320d9010d25d2 Mon Sep 17 00:00:00 2001 From: KloskaT Date: Thu, 18 Jul 2019 15:04:32 +0100 Subject: [PATCH 054/125] Commented. Now saves all to 'figures' folder. Updated visualisation README. --- README.md | 7 +- visualisation.py | 289 +++++++++++++++++++++++++++-------------------- 2 files changed, 172 insertions(+), 124 deletions(-) diff --git a/README.md b/README.md index a28e631..cdf0bfb 100644 --- a/README.md +++ b/README.md @@ -55,9 +55,10 @@ arguments ```[--pie] [--timeseries]``` for which data representation is wanted. If the necessary data has been saved a network animation can also be created by running ```visualization_network.py``` which takes the arguments ```[--save] [--number_iterations]``` if you want the animation to be saved as an mp4, and how -time iterations you want in the animation. +many time iterations you want in the animation. #### Ensemble runs -Use ```metaplotter_pl_timescale.py```, ```metaplotter_pl_timescale_additional_measures.py```, -or ```visualisation.py [--comparison]``` to visualize ensemble runs. +Ensemble runs can be plotted if the correct data is available using ``visualisation.py``. Takes the arguments +``[--comparison]`` for an averaged time series, ``[--firmdistribution]`` for a CDF of firm size using amount of cash, +and ``[--bankruptcydistribution]`` for histograms of bankruptcy events per number of risk models. diff --git a/visualisation.py b/visualisation.py index 097d262..fded500 100644 --- a/visualisation.py +++ b/visualisation.py @@ -9,8 +9,22 @@ import scipy.stats from matplotlib.offsetbox import AnchoredText + class TimeSeries(object): def __init__(self, series_list, event_schedule, damage_schedule, title="",xlabel="Time", colour='k', axlst=None, fig=None, percentiles=None, alpha=0.7): + """Intialisation method for creating timeseries. + Accepts: + series_list: Type List. Contains contract, cash, operational, premium, profitloss data. + event_schedule: Type List of Lists. Used to plot event times on timeseries if a single run. + damage_schedule: Type List of Lisst. Used for plotting event times on timeseries if single run. + title: Type string. + xlabel: Type string. + colour: Type string. + axlist: Type None or list of axes for subplots. + fig: Type None or figure. + percentiles: Type list. Has the percentiles within which data is plotted for. + alpha: Type Integer. Alpha of graph plots. + No return values""" self.series_list = series_list self.size = len(series_list) self.xlabel = xlabel @@ -27,8 +41,15 @@ def __init__(self, series_list, event_schedule, damage_schedule, title="",xlabel else: self.fig, self.axlst = plt.subplots(self.size,sharex=True) - def plot(self, schedule=False): - multi_categ_colours = ['r', 'b', 'g', 'fuchsia'] + def plot(self): + """Method to plot time series. + No accepted values. + Returns: + self.fig: Type figure. Used to for saving graph to a file. + self.axlst: Type axes list. + This method is called to plot a timeseries for five subplots of data for insurers/reinsurers. If called for a + single run event times are plotted as vertical lines, if an ensemble run then no events but the average data is + plotted with percentiles as deviations to the average.""" single_categ_colours = ['b', 'b', 'b', 'b'] for i, (series, series_label, fill_lower, fill_upper) in enumerate(self.series_list): self.axlst[i].plot(self.timesteps, series,color=self.colour) @@ -37,7 +58,7 @@ def plot(self, schedule=False): if fill_lower is not None and fill_upper is not None: self.axlst[i].fill_between(self.timesteps, fill_lower, fill_upper, color=self.colour, alpha=self.alpha) - if schedule: # Plots vertical lines for events if set. + if self.events_schedule is not None: # Plots vertical lines for events if set. for categ in range(len(self.events_schedule)): for event_time in self.events_schedule[categ]: index = self.events_schedule[categ].index(event_time) @@ -48,24 +69,20 @@ def plot(self, schedule=False): return self.fig, self.axlst - def save(self, filename): - self.fig.savefig("{filename}".format(filename=filename)) - return - class InsuranceFirmAnimation(object): - """Initialising method for the animation of insurance firm data. - Accepts: - cash_data: Type List of List of Lists: Contains the operational, ID and cash for each firm for each time. - insure_contracts: Type List of Lists. Contains number of underwritten contracts for each firm for each time. - event_schedule: Type List of Lists. Contains event times by category. - type: Type String. Used to specify which file to save to. - save: Type Boolean - perils: Type Boolean. For if screen should flash during peril time. - No return values. - This class takes the cash and contract data of each firm over all time and produces an animation showing how the - proportion of each for all operational firms changes with time. Allows it to be saved as an MP4.""" def __init__(self, cash_data, insure_contracts, event_schedule, type, save=True, perils=True): + """Initialising method for the animation of insurance firm data. + Accepts: + cash_data: Type List of List of Lists: Contains the operational, ID and cash for each firm for each time. + insure_contracts: Type List of Lists. Contains number of underwritten contracts for each firm for each time. + event_schedule: Type List of Lists. Contains event times by category. + type: Type String. Used to specify which file to save to. + save: Type Boolean + perils: Type Boolean. For if screen should flash during peril time. + No return values. + This class takes the cash and contract data of each firm over all time and produces an animation showing how the + proportion of each for all operational firms changes with time. Allows it to be saved as an MP4.""" # Converts list of events by category into list of all events. self.perils_condition = perils self.all_event_times = [] @@ -97,7 +114,7 @@ def animate(self): def data_stream(self): """Method to get the next set of firm data. - No accpeted values + No accepted values Yields: firm_cash_list: Type List. Contains the cash of each firm. firm_id_list: Type List. Contains the unique ID of each firm. @@ -146,42 +163,72 @@ def save(self): No accepted values. No return values.""" if self.type == "Insurance Firm": - self.animate.save("data/animated_insurfirm_pie.mp4", writer="ffmpeg", dpi=200, fps=10) + self.animate.save("figures/animated_insurfirm_pie.mp4", writer="ffmpeg", dpi=200, fps=10) elif self.type == "Reinsurance Firm": - self.animate.save("data/animated_reinsurefirm_pie.mp4", writer="ffmpeg", dpi=200, fps=10) + self.animate.save("figures/animated_reinsurefirm_pie.mp4", writer="ffmpeg", dpi=200, fps=10) else: print("Incorrect Type for Saving") class visualisation(object): def __init__(self, history_logs_list): + """Initialises visualisation class for all data. + Accepts: + history_logs_list: Type List of DataDicts. Each element is a different replication/run. Each DataDict + contains all info for that run. + No return values.""" self.history_logs_list = history_logs_list - return + self.scatter_data = {} def insurer_pie_animation(self, run=0): + """Method to created animated pie chart of cash and contract proportional per operational insurance firm. + Accepts: + run: Type Integer. Which replication/run is wanted. Allows loops or specific runs. + Returns: + self.ins_pie_anim: Type animation class instance. Not used outside this method but is saved.""" data = self.history_logs_list[run] insurance_cash = np.array(data['insurance_firms_cash']) - contract_data = self.history_logs_list[0]['individual_contracts'] - event_schedule = self.history_logs_list[0]["rc_event_schedule_initial"] + contract_data = data['individual_contracts'] + event_schedule = data["rc_event_schedule_initial"] self.ins_pie_anim = InsuranceFirmAnimation(insurance_cash, contract_data, event_schedule, 'Insurance Firm', save=True) self.ins_pie_anim.animate() return self.ins_pie_anim def reinsurer_pie_animation(self, run=0): + """Method to created animated pie chart of cash and contract proportional per operational reinsurance firm. + Accepts: + run: Type Integer. Which replication/run is wanted. Allows loops or specific runs. + Returns: + self.reins_pie_anim: Type animation class instance. Not used outside this method but is saved.""" data = self.history_logs_list[run] reinsurance_cash = np.array(data['reinsurance_firms_cash']) - contract_data = self.history_logs_list[0]['reinsurance_contracts'] - event_schedule = self.history_logs_list[0]["rc_event_schedule_initial"] + contract_data = data['reinsurance_contracts'] + event_schedule = data["rc_event_schedule_initial"] self.reins_pie_anim = InsuranceFirmAnimation(reinsurance_cash, contract_data, event_schedule, 'Reinsurance Firm', save=True) self.reins_pie_anim.animate() return self.reins_pie_anim - def insurer_time_series(self, runs=None, axlst=None, fig=None, title="Insurer", colour='black', percentiles=[25,75]): - # runs should be a list of the indexes you want included in the ensemble for consideration - if runs: - data = [self.history_logs_list[x] for x in runs] + def insurer_time_series(self, singlerun=True, axlst=None, fig=None, title="Insurer", colour='black', percentiles=[25,75]): + """Method to create a timeseries for insurance firms' data. + Accepts: + singlerun: Type Boolean. Sets event schedule if a single run. + axlst: Type axes list, normally None and created later. + fig: Type figure, normally None and created later. + title: Type String. + colour: Type String. + percentiles: Type List. For ensemble runs to plot outer limits of data. + Returns: + fig: Type figure. Used to save times series. + axlst: Type axes list. Not used. + This method is called to plot a timeseries of contract, cash, operational, profitloss, and premium data for + insurance firms from saved data. Also sets event schedule for single run data, to plots event times on + timeseries, as this is only helpful in this case.""" + if singlerun: + events = self.history_logs_list[0]["rc_event_schedule_initial"] + damages = self.history_logs_list[0]['rc_event_damage_initial'] else: - data = self.history_logs_list + events = None + damages = None # Take the element-wise means/medians of the ensemble set (axis=0) contracts_agg = [history_logs['total_contracts'] for history_logs in self.history_logs_list] @@ -196,24 +243,36 @@ def insurer_time_series(self, runs=None, axlst=None, fig=None, title="Insurer", cash = np.median(cash_agg, axis=0) premium = np.median(premium_agg, axis=0) - events = self.history_logs_list[0]["rc_event_schedule_initial"] - damages = self.history_logs_list[0]['rc_event_damage_initial'] - self.ins_time_series = TimeSeries([ (contracts, 'Contracts', np.percentile(contracts_agg,percentiles[0], axis=0), np.percentile(contracts_agg, percentiles[1], axis=0)), (profitslosses, 'Profitslosses', np.percentile(profitslosses_agg,percentiles[0], axis=0), np.percentile(profitslosses_agg, percentiles[1], axis=0)), (operational, 'Operational', np.percentile(operational_agg,percentiles[0], axis=0), np.percentile(operational_agg, percentiles[1], axis=0)), (cash, 'Cash', np.percentile(cash_agg,percentiles[0], axis=0), np.percentile(cash_agg, percentiles[1], axis=0)), (premium, "Premium", np.percentile(premium_agg,percentiles[0], axis=0), np.percentile(premium_agg, percentiles[1], axis=0))], events, damages, title=title, xlabel="Time", axlst=axlst, fig=fig, colour=colour) - self.ins_time_series.plot(schedule=True) - return self.ins_time_series + fig, axlst = self.ins_time_series.plot() + return fig, axlst - def reinsurer_time_series(self, runs=None, axlst=None, fig=None, title="Reinsurer", colour='black', percentiles=[25,75]): - # runs should be a list of the indexes you want included in the ensemble for consideration - if runs: - data = [self.history_logs_list[x] for x in runs] + def reinsurer_time_series(self, singlerun=True, axlst=None, fig=None, title="Reinsurer", colour='black', percentiles=[25,75]): + """Method to create a timeseries for reinsurance firms' data. + Accepts: + singlerun: Type Boolean. Sets event schedule if a single run. + axlst: Type axes list, normally None and created later. + fig: Type figure, normally None and created later. + title: Type String. + colour: Type String. + percentiles: Type List. For ensemble runs to plot outer limits of data. + Returns: + fig: Type figure. Used to save times series. + axlst: Type axes list. Not used. + This method is called to plot a timeseries of contract, cash, operational, profitloss, and catbond data for + reinsurance firms from saved data. Also sets event schedule for single run data, to plots event times on + timeseries, as this is only helpful in this case.""" + if singlerun: + events = self.history_logs_list[0]["rc_event_schedule_initial"] + damages = self.history_logs_list[0]['rc_event_damage_initial'] else: - data = self.history_logs_list + events = None + damages = None # Take the element-wise means/medians of the ensemble set (axis=0) reincontracts_agg = [history_logs['total_reincontracts'] for history_logs in self.history_logs_list] @@ -228,9 +287,6 @@ def reinsurer_time_series(self, runs=None, axlst=None, fig=None, title="Reinsure reincash = np.median(reincash_agg, axis=0) catbonds_number = np.median(catbonds_number_agg, axis=0) - events = self.history_logs_list[0]["rc_event_schedule_initial"] - damages = self.history_logs_list[0]['rc_event_damage_initial'] - self.reins_time_series = TimeSeries([ (reincontracts, 'Contracts', np.percentile(reincontracts_agg,percentiles[0], axis=0), np.percentile(reincontracts_agg, percentiles[1], axis=0)), (reinprofitslosses, 'Profitslosses', np.percentile(reinprofitslosses_agg,percentiles[0], axis=0), np.percentile(reinprofitslosses_agg, percentiles[1], axis=0)), @@ -238,29 +294,15 @@ def reinsurer_time_series(self, runs=None, axlst=None, fig=None, title="Reinsure (reincash, 'Cash', np.percentile(reincash_agg,percentiles[0], axis=0), np.percentile(reincash_agg, percentiles[1], axis=0)), (catbonds_number, "Activate Cat Bonds", np.percentile(catbonds_number_agg,percentiles[0], axis=0), np.percentile(catbonds_number_agg, percentiles[1], axis=0)), ], events, damages, title=title, xlabel="Time", axlst=axlst, fig=fig, colour=colour) - self.reins_time_series.plot(schedule=True) - return self.reins_time_series - - def metaplotter_timescale(self): - # Take the element-wise means/medians of the ensemble set (axis=0) - contracts = np.mean([history_logs['total_contracts'] for history_logs in self.history_logs_list],axis=0) - profitslosses = np.mean([history_logs['total_profitslosses'] for history_logs in self.history_logs_list],axis=0) - operational = np.median([history_logs['total_operational'] for history_logs in self.history_logs_list],axis=0) - cash = np.median([history_logs['total_cash'] for history_logs in self.history_logs_list],axis=0) - premium = np.median([history_logs['market_premium'] for history_logs in self.history_logs_list],axis=0) - reincontracts = np.mean([history_logs['total_reincontracts'] for history_logs in self.history_logs_list],axis=0) - reinprofitslosses = np.mean([history_logs['total_reinprofitslosses'] for history_logs in self.history_logs_list],axis=0) - reinoperational = np.median([history_logs['total_reinoperational'] for history_logs in self.history_logs_list],axis=0) - reincash = np.median([history_logs['total_reincash'] for history_logs in self.history_logs_list],axis=0) - catbonds_number = np.median([history_logs['total_catbondsoperational'] for history_logs in self.history_logs_list],axis=0) - return + fig, axlst = self.reins_time_series.plot() + return fig, axlst def aux_clustered_exit_records(self, exits): """Auxiliary method for creation of data series on clustered events such as firm market exits. - Will take an unclustered series and aggregate every series of non-zero elements into - the first element of that series. + Will take an unclustered series and aggregate every series of non-zero elements into + the first element of that series. Arguments: - exits: numpy ndarray or list - unclustered series + exits: numpy ndarray or list - unclustered series Returns: numpy ndarray of the same length as argument "exits": the clustered series.""" exits2 = [] @@ -348,34 +390,57 @@ def show(self): class compare_riskmodels(object): def __init__(self,vis_list, colour_list): - # take in list of visualisation objects and call their plot methods + """Initialises compare_riskmodels class. + Accepts: + vis_list: Type List of Visualisation class instances. Each instance is a different no. of risk models. + colour_list: Type List of string(colours). + Takes in list of visualisation objects and call their plot methods.""" self.vis_list = vis_list self.colour_list = colour_list + self.insurer_fig = self.insurer_axlst = None + self.reinsurer_fig = self.reinsurer_axlst = None def create_insurer_timeseries(self, fig=None, axlst=None, percentiles=[25,75]): - # create the time series for each object in turn and superpose them? - fig = axlst = None - for vis,colour in zip(self.vis_list, self.colour_list): - (fig, axlst) = vis.insurer_time_series(fig=fig, axlst=axlst, colour=colour, percentiles=percentiles) + """Method to create separate insurer time series for all numbers of risk models using visualisations' + insurer_time_series method. Loops through each separately and they are then saved automatically. Used for + ensemble runs. + Accepts: + fig: Type figure. + axlst: Type axes list. + percentiles: Type List. + No return values.""" + risk_model = 0 + for vis, colour in zip(self.vis_list, self.colour_list): + risk_model += 1 + (self.insurer_fig, self.insurer_axlst) = vis.insurer_time_series(singlerun=False, fig=fig, axlst=axlst, + colour=colour, percentiles=percentiles, + title="%i Risk Model Insurer" % risk_model) + self.insurer_fig.savefig("figures/"+str(risk_model)+"risk_model(s)_insurer_ensemble_timeseries.png") def create_reinsurer_timeseries(self, fig=None, axlst=None, percentiles=[25,75]): - # create the time series for each object in turn and superpose them? - fig = axlst = None - for vis,colour in zip(self.vis_list, self.colour_list): - (fig, axlst) = vis.reinsurer_time_series(fig=fig, axlst=axlst, colour=colour, percentiles=percentiles) + """Method to create separate reinsurer time series for all numbers of risk models using visualisations' + reinsurer_time_series method. Loops through each separately and they are then saved automatically. Used for + ensemble runs. + Accepts: + fig: Type figure. + axlst: Type axes list. + percentiles: Type List. + No return values.""" + risk_model = 0 + for vis, colour in zip(self.vis_list, self.colour_list): + risk_model += 1 + (self.reinsurer_fig, self.reinsurer_axlst) = vis.reinsurer_time_series(singlerun=False, fig=fig, axlst=axlst, + colour=colour, percentiles=percentiles, + title="%i Risk Model Reinsurer" % risk_model) + self.reinsurer_fig.savefig("figures/"+str(risk_model)+"risk_model(s)_reinsurer_ensemble_timeseries.png") def show(self): plt.show() - def save(self): - # logic to save plots - pass - -class CDF_distribution_plot(): - """Class for CDF/cCDF distribution plots using auxiliary class from visualisation_distribution_plots.py. - This class arranges as many such plots stacked in one diagram as there are series in the history - logs they are created from, i.e. len(vis_list).""" +class CDF_distribution_plot: + """Class for CDF/cCDF distribution plots using class CDFDistribution. This class arranges as many such plots stacked + in one diagram as there are series in the history logs they are created from, i.e. len(vis_list).""" def __init__(self, vis_list, colour_list, quantiles=[.25, .75], variable="reinsurance_firms_cash", timestep=-1, plot_cCDF=True): """Constructor. @@ -400,11 +465,12 @@ def generate_plot(self, xlabel=None, filename=None): Arguments: xlabel: str or None - the x axis label filename: str or None - the filename without ending - Returns None.""" + Returns None. + This method unpacks the variable wanted from the history log data then uses the CDFDistribution class to plot it""" """Set x axis label and filename to default if not provided""" xlabel = xlabel if xlabel is not None else self.variable - filename = filename if filename is not None else "CDF_plot_" + self.variable + filename = filename if filename is not None else "figures/CDF_plot_" + self.variable """Create figure with correct number of subplots""" self.fig, self.ax = plt.subplots(nrows=len(self.vis_list)) @@ -434,7 +500,6 @@ def generate_plot(self, xlabel=None, filename=None): series_x[j] = [firm[0] for firm in series_x[j] if firm[2]] """Create CDFDistribution object and populate the subfigure using it""" VDP = CDFDistribution(series_x) - # VDP.make_figure(upper_quantile=self.upper_quantile, lower_quantile=self.lower_quantile) c_xlabel = "" if i < len(self.vis_list) - 1 else xlabel VDP.plot(ax=self.ax[i], ylabel="cCDF " + str(i + 1) + "RM", xlabel=c_xlabel, upper_quantile=self.upper_quantile, lower_quantile=self.lower_quantile, color=self.colour_list[i], @@ -446,10 +511,9 @@ def generate_plot(self, xlabel=None, filename=None): self.fig.savefig(filename + ".png", density=300) -class Histogram_plot(): - """Class for CDF/cCDF distribution plots using auxiliary class from visualisation_distribution_plots.py. - This class arranges as many such plots stacked in one diagram as there are series in the history - logs they are created from, i.e. len(vis_list).""" +class Histogram_plot: + """Class for Histogram plots using class Histograms. This class arranges as many such plots stacked in one diagram + as there are series in the history logs they are created from, i.e. len(vis_list).""" def __init__(self, vis_list, colour_list, variable="bankruptcy_events"): """Constructor. Arguments: @@ -471,7 +535,7 @@ def generate_plot(self, xlabel=None, filename=None, logscale=False, minmax=None, """Set x axis label and filename to default if not provided""" xlabel = xlabel if xlabel is not None else self.variable - filename = filename if filename is not None else "Histogram_plot_" + self.variable + filename = filename if filename is not None else "figures/Histogram_plot_" + self.variable """Create figure with correct number of subplots""" self.fig, self.ax = plt.subplots(nrows=len(self.vis_list)) @@ -514,7 +578,7 @@ def generate_plot(self, xlabel=None, filename=None, logscale=False, minmax=None, self.fig.savefig(filename + ".png", density=300) -class CDFDistribution(): +class CDFDistribution: def __init__(self, samples_x): """Constructor. Arguments: @@ -537,7 +601,6 @@ def __init__(self, samples_x): self.quantile_series_y_upper = None def make_figure(self, upper_quantile=.25, lower_quantile=.75): - # pdb.set_trace() """Method to do the necessary computations to create the CDF plot (incl. mean, median, quantiles. This method populates the variables that are plotted. Arguments: @@ -646,7 +709,7 @@ def plot(self, ax=None, ylabel="CDF(x)", xlabel="y", upper_quantile=.25, lower_q plt.show() -class Histogram(): +class Histogram: """Class for plots of ensembles of distributions as CDF (cumulative distribution function) or cCDF (complementary cumulative distribution function) with mean, median, and quantiles""" def __init__(self, sample_x): @@ -706,17 +769,15 @@ def plot(self, ax=None, ylabel="PDF(x)", xtralabel="", xlabel="x", num_bins=50, parser.add_argument("--single", action="store_true", help="plot a single run of the insurance model") parser.add_argument("--pie", action="store_true", help="plot animated pie charts of contract and cash data") parser.add_argument("--timeseries", action="store_true", help="plot time series of firm data") - parser.add_argument("--comparison", action="store_true", help="plot the result of an ensemble of replicatons of the insurance model") + parser.add_argument("--comparison", action="store_true", help="plot time series for an ensemble of replicatons of " + "the insurance model") parser.add_argument("--firmdistribution", action="store_true", help="plot the CDFs of firm size distributions with quartiles indicating variation across " "ensemble") parser.add_argument("--bankruptcydistribution", action="store_true", - help="plot the histograms of bankruptcy events across ensemble") + help="plot the histograms of bankruptcy events/unrecovered claims across ensemble") args = parser.parse_args() - args.bankruptcydistribution = True - args.single = args.pie = True - if args.single: # load in data from the history_logs dictionarywith open("data/history_logs.dat","r") as rfile: @@ -729,37 +790,33 @@ def plot(self, ax=None, ylabel="PDF(x)", xtralabel="", xlabel="x", num_bins=50, vis.insurer_pie_animation() vis.reinsurer_pie_animation() if args.timeseries: - vis.insurer_time_series() - vis.reinsurer_time_series() + insurerfig, axs = vis.insurer_time_series() + reinsurerfig, axs = vis.reinsurer_time_series() + insurerfig.savefig("figures/insurer_singlerun_timeseries.png") + reinsurerfig.savefig("figures/reinsurer_singlerun_timeseries.png") vis.show() N = len(history_logs_list) if args.comparison or args.firmdistribution or args.bankruptcydistribution: - - # for each run, generate an animation and time series for insurer and reinsurer - # TODO: provide some way for these to be lined up nicely rather than having to manually arrange screen - # for i in range(N): - # vis.insurer_pie_animation(run=i) - # vis.insurer_time_series(runs=[i]) - # vis.reinsurer_pie_animation(run=i) - # vis.reinsurer_time_series(runs=[i]) - # vis.show() vis_list = [] + colour_list = ['red', 'blue', 'green', 'yellow'] + + # Loads all risk model history logs data (very long :'( ) and creates list of visualisation class instances. filenames = ["./data/" + x + "_history_logs.dat" for x in ["one", "two", "three", "four"]] for filename in filenames: with open(filename, 'r') as rfile: history_logs_list = [eval(k) for k in rfile] # one dict on each line vis_list.append(visualisation(history_logs_list)) - colour_list = ['red', 'blue', 'green', 'yellow'] - if args.comparison: + # Creates time series for all risk models in ensemble data. cmp_rsk = compare_riskmodels(vis_list, colour_list) cmp_rsk.create_insurer_timeseries(percentiles=[10, 90]) cmp_rsk.create_reinsurer_timeseries(percentiles=[10, 90]) cmp_rsk.show() if args.firmdistribution: + # Creates CDF for firm size using cash as measure of size. CP = CDF_distribution_plot(vis_list, colour_list, variable="insurance_firms_cash", timestep=-1, plot_cCDF=True) CP.generate_plot(xlabel="Firm size (capital)") if not isleconfig.simulation_parameters["reinsurance_off"]: @@ -768,27 +825,17 @@ def plot(self, ax=None, ylabel="PDF(x)", xtralabel="", xlabel="x", num_bins=50, CP.generate_plot(xlabel="Firm size (capital)") if args.bankruptcydistribution: + # Creates histogram for each number of risk models for size and frequency of bankruptcies/unrecovered claims. for vis in vis_list: vis.populate_scatter_data() - # HP = Histogram_plot(vis_list, colour_list, variable="bankruptcy_events") - # HP.generate_plot(logscale=True, xlabel="Number of bankruptcies") - # HP = Histogram_plot(vis_list, colour_list, variable="bankruptcy_events_relative") - # HP.generate_plot(logscale=True, xlabel="Share of bankrupt firms") - # HP = Histogram_plot(vis_list, colour_list, variable="bankruptcy_events_clustered") - # HP.generate_plot(logscale=True, xlabel="Number of bankruptcies") HP = Histogram_plot(vis_list, colour_list, variable="bankruptcy_events_relative_clustered") HP.generate_plot(logscale=True, xlabel="Share of bankrupt firms", minmax=[0, 0.5], - VaR005guess=0.1) # =0.056338028169014086) # this is the VaR threshold for 4 risk models with reinsurance - # HP.generate_plot(logscale=True, xlabel="Share of bankrupt firms", minmax=[0, 0.5], VaR005guess=0.04580152671755725) # this is the VaR threshold for 4 risk models without reinsurance + VaR005guess=0.1) # =0.056338028169014086) # this is the VaR threshold for 4 risk models with reinsuranc HP = Histogram_plot(vis_list, colour_list, variable="unrecovered_claims") HP.generate_plot(logscale=True, xlabel="Damages not recovered", minmax=[0, 6450000], VaR005guess=0.1) # =691186.8726311699) # this is the VaR threshold for 4 risk models with reinsurance - # HP.generate_plot(logscale=True, xlabel="Damages not recovered", minmax=[0, 6450000], VaR005guess=449707.1970911417) # this is the VaR threshold for 4 risk models without reinsurance - - # HP = Histogram_plot(vis_list, colour_list, variable="relative_unrecovered_claims") - # HP.generate_plot(logscale=True, xlabel="Damages not recovered")#, minmax=[0, 6450000]) # ਲ਼ From 88816ce0e193747d83c8493cf2e4db17a1a0d241 Mon Sep 17 00:00:00 2001 From: Chris Hughes Date: Thu, 18 Jul 2019 10:17:41 +0100 Subject: [PATCH 055/125] Fixed a bug causing contracts to be lost --- catbond.py | 56 ++++++---- genericclasses.py | 69 +++++++++--- insurancecontract.py | 10 +- insurancefirms.py | 16 +-- insurancesimulation.py | 33 ++---- isleconfig.py | 2 +- metainsurancecontract.py | 29 ++++- metainsuranceorg.py | 235 ++++++++++++++++++++------------------- reinsurancecontract.py | 2 +- riskmodel.py | 12 +- start.py | 6 +- 11 files changed, 272 insertions(+), 198 deletions(-) diff --git a/catbond.py b/catbond.py index 7d17fad..1f5b376 100644 --- a/catbond.py +++ b/catbond.py @@ -1,5 +1,10 @@ +from __future__ import annotations import isleconfig from metainsuranceorg import MetaInsuranceOrg +import genericclasses +import metainsurancecontract +import insurancesimulation +from typing import MutableSequence # TODO: This and MetaInsuranceOrg should probably both subclass something simple - a MetaAgent, say. MetaInsuranceOrg # can do more than a CatBond should be able to! @@ -8,7 +13,14 @@ # noinspection PyAbstractClass class CatBond(MetaInsuranceOrg): # noinspection PyMissingConstructor - def __init__(self, simulation, per_period_premium, owner, interest_rate=0): + # TODO inheret GenericAgent instead of MetaInsuranceOrg? + def __init__( + self, + simulation: insurancesimulation.InsuranceSimulation, + per_period_premium: float, + owner: genericclasses.GenericAgent, + interest_rate: float = 0, + ): """Initialising methods. Accepts: simulation: Type class @@ -16,23 +28,23 @@ def __init__(self, simulation, per_period_premium, owner, interest_rate=0): owner: Type class This initialised the catbond class instance, inheriting methods from MetaInsuranceOrg.""" self.simulation = simulation - self.id = 0 - self.underwritten_contracts = [] - self.cash = 0 - self.profits_losses = 0 - self.obligations = [] - self.operational = True - self.owner = owner - self.per_period_dividend = per_period_premium - self.interest_rate = interest_rate + self.id: int = 0 + self.underwritten_contracts: MutableSequence[ + metainsurancecontract.MetaInsuranceContract + ] = [] + self.cash: int = 0 + self.profits_losses: float = 0 + self.obligations: MutableSequence[genericclasses.Obligation] = [] + self.operational: bool = True + self.owner: genericclasses.GenericAgent = owner + self.per_period_dividend: float = per_period_premium + self.interest_rate: float = interest_rate # TODO: shift obtain_yield method to insurancesimulation, thereby making it unnecessary to drag parameters like # self.interest_rate from instance to instance and from class to class # self.simulation_no_risk_categories = self.simulation.simulation_parameters["no_categories"] # TODO: change start and InsuranceSimulation so that it iterates CatBonds - # old parent class init, cat bond class should be much smaller - - def iterate(self, time): + def iterate(self, time: int): """Method to perform CatBond duties for each time iteration. Accepts: time: Type Integer @@ -40,7 +52,7 @@ def iterate(self, time): For each time iteration this is called from insurancesimulation to perform duties: interest payments, _pay obligations, mature the contract if ended, make payments.""" self.obtain_yield(time) - self.effect_payments(time) + self._effect_payments(time) if isleconfig.verbose: print( time, @@ -78,7 +90,7 @@ def iterate(self, time): # self.estimate_var() # cannot compute VaR for catbond as catbond does not have a riskmodel - def set_owner(self, owner): + def set_owner(self, owner: genericclasses.GenericAgent): """Method to set owner of the Cat Bond. Accepts: owner: Type class @@ -87,7 +99,7 @@ def set_owner(self, owner): if isleconfig.verbose: print("SOLD") - def set_contract(self, contract): + def set_contract(self, contract: metainsurancecontract.MetaInsuranceContract): """Method to record new instances of CatBonds. Accepts: owner: Type class @@ -102,12 +114,12 @@ def mature_bond(self): When the catbond contract matures this is called which pays the value of the catbond to the simulation, and is then deleted from the list of agents.""" if self.operational: - obligation = { - "amount": self.cash, - "recipient": self.simulation, - "due_time": 1, - "purpose": "mature", - } + obligation = genericclasses.Obligation( + amount=self.cash, + recipient=self.simulation, + due_time=1, + purpose="mature", + ) self._pay(obligation) self.simulation.delete_agents([self]) self.operational = False diff --git a/genericclasses.py b/genericclasses.py index 760e444..5131946 100644 --- a/genericclasses.py +++ b/genericclasses.py @@ -1,43 +1,69 @@ from __future__ import annotations import dataclasses -from typing import Mapping +from typing import Mapping, MutableSequence import metainsurancecontract class GenericAgent: def __init__(self): - self.cash = 0 - self.obligations = [] - self.operational = True - self.profits_losses = 0 + self.cash: float = 0 + self.obligations: MutableSequence[Obligation] = [] + self.operational: bool = True + self.profits_losses: float = 0 - def _pay(self, obligation): + def _pay(self, obligation: Obligation): """Method to _pay other class instances. Accepts: Obligation: Type DataDict No return value Method removes value payed from the agents cash and adds it to recipient agents cash.""" - amount = obligation["amount"] - recipient = obligation["recipient"] - purpose = obligation["purpose"] + amount = obligation.amount + recipient = obligation.recipient + purpose = obligation.purpose if self.get_operational() and recipient.get_operational(): self.cash -= amount if purpose is not "dividend": self.profits_losses -= amount recipient.receive(amount) - def get_operational(self): + def get_operational(self) -> bool: """Method to return boolean of if agent is operational. Only used as check for payments. No accepted values Returns Boolean""" return self.operational - def iterate(self, time): + def iterate(self, time: int): raise NotImplementedError( "Iterate is not implemented in GenericAgent, should have be overridden" ) - def receive_obligation(self, amount, recipient, due_time, purpose): + def _effect_payments(self, time: int): + """Method for checking if any payments are due. + Accepts: + time: Type Integer + No return value + Method checks firms list of obligations to see if ay are due for this time, then pays them. If the firm + does not have enough cash then it enters illiquity, leaves the market, and matures all contracts.""" + # TODO: don't really want to be reconstructing lists every time (unless the obligations are naturally sorted by + # time, in which case this could be done slightly better). Low priority, but something to consider + due = [item for item in self.obligations if item.due_time <= time] + self.obligations = [item for item in self.obligations if item.due_time > time] + # QUERY: could this cause a firm to enter illiquidity if it has obligations to non-operational firms? Such + # firms can't recieve payment, so this possibly shouldn't happen. + sum_due = sum([item.amount for item in due]) + if sum_due > self.cash: + self.obligations += due + self.enter_illiquidity(time, sum_due) + else: + for obligation in due: + self._pay(obligation) + + def enter_illiquidity(self, time: int, sum_due: float): + raise NotImplementedError() + + def receive_obligation( + self, amount: float, recipient: GenericAgent, due_time: int, purpose: str + ): """Method for receiving obligations that the firm will have to _pay. Accepts: amount: Type integer, how much will be payed @@ -47,12 +73,9 @@ def receive_obligation(self, amount, recipient, due_time, purpose): No return value Adds obligation (Type DataDict) to list of obligations owed by the firm.""" - obligation = { - "amount": amount, - "recipient": recipient, - "due_time": due_time, - "purpose": purpose, - } + obligation = Obligation( + amount=amount, recipient=recipient, due_time=due_time, purpose=purpose + ) self.obligations.append(obligation) def receive(self, amount: float): @@ -102,3 +125,13 @@ class AgentProperties: capacity_target_decrement_factor: float capacity_target_increment_factor: float interest_rate: float + + +@dataclasses.dataclass +class Obligation: + """Class for holding the properties of an obligation""" + + amount: float + recipient: GenericAgent + due_time: int + purpose: str diff --git a/insurancecontract.py b/insurancecontract.py index 1c32545..2c5f08c 100644 --- a/insurancecontract.py +++ b/insurancecontract.py @@ -43,7 +43,7 @@ def __init__( reinsurance, ) - def explode(self, time, uniform_value, damage_extent): + def explode(self, time, uniform_value=None, damage_extent=None): """Explode method. Accepts arguments time: Type integer. The current time. @@ -54,6 +54,14 @@ def explode(self, time, uniform_value, damage_extent): No return value. For registering damage and creating resulting claims (and payment obligations).""" # np.mean(np.random.beta(1, 1./mu -1, size=90000)) + if uniform_value is None: + raise ValueError( + "uniform_value must be passed to InsuranceContract.explode" + ) + if damage_extent is None: + raise ValueError( + "damage_extent must be passed to InsuranceContract.explode" + ) if uniform_value < self.risk_factor: claim = min(self.excess, damage_extent * self.value) - self.deductible self.insurer.register_claim( diff --git a/insurancefirms.py b/insurancefirms.py index 33a3df5..f9d630a 100644 --- a/insurancefirms.py +++ b/insurancefirms.py @@ -453,12 +453,12 @@ def issue_cat_bond( self.simulation.receive_obligation(var_this_risk, self, time, "bond") new_catbond.set_owner(self.simulation) """hand cash over to cat bond such that var_this_risk is covered""" - obligation = { - "amount": var_this_risk + total_premium, - "recipient": new_catbond, - "due_time": time, - "purpose": "bond", - } + obligation = genericclasses.Obligation( + amount=var_this_risk + total_premium, + recipient=new_catbond, + due_time=time, + purpose="bond", + ) self._pay(obligation) # TODO: is var_this_risk the correct amount? """register catbond""" self.simulation.add_agents(catbond.CatBond, "catbond", [new_catbond]) @@ -479,7 +479,7 @@ def make_reinsurance_claims(self, time: int): if is_proportional: claims_this_turn[categ_id] += claims if contract.reincontract: - contract.reincontract.explode(time, claims) + contract.reincontract.explode(time, damage_extent=claims) for categ_id in range(self.simulation_no_risk_categories): if ( @@ -487,7 +487,7 @@ def make_reinsurance_claims(self, time: int): and self.category_reinsurance[categ_id] is not None ): self.category_reinsurance[categ_id].explode( - time, claims_this_turn[categ_id] + time, damage_extent=claims_this_turn[categ_id] ) def get_excess_of_loss_reinsurance(self): diff --git a/insurancesimulation.py b/insurancesimulation.py index 543f7c5..1aaf757 100644 --- a/insurancesimulation.py +++ b/insurancesimulation.py @@ -124,7 +124,6 @@ def __init__( "Set up monetary system (should instead be with the customers, if customers are modeled explicitly)" self.cash: float = self.simulation_parameters["money_supply"] - # QUERY Why is this a property of the simulation rather than of the obligated parties? "set up risk categories" # QUERY What do risk categories represent? Different types of catastrophes? @@ -166,14 +165,19 @@ def __init__( ) self.risks: MutableSequence[RiskProperties] = [ - RiskProperties(rrisk_factors[i], rvalues[i], rcategories[i], self) + RiskProperties( + risk_factor=rrisk_factors[i], + value=rvalues[i], + category=rcategories[i], + owner=self, + ) for i in range(self.simulation_parameters["no_risks"]) ] self.risks_counter: MutableSequence[int] = [0, 0, 0, 0] - for item in self.risks: - self.risks_counter[item.category] = self.risks_counter[item.category] + 1 + for risk in self.risks: + self.risks_counter[risk.category] += 1 self.inaccuracy: Sequence[Sequence[int]] = self._get_all_riskmodel_combinations( self.simulation_parameters["riskmodel_inaccuracy_parameter"] @@ -587,7 +591,7 @@ def save_data(self): ) operational_no = sum([firm.operational for firm in self.insurancefirms]) reinoperational_no = sum([firm.operational for firm in self.reinsurancefirms]) - catbondsoperational_no = sum([catbond.operational for catbond in self.catbonds]) + catbondsoperational_no = sum([cb.operational for cb in self.catbonds]) """ collect agent-level data """ insurance_firms = [ @@ -673,23 +677,8 @@ def _inflict_peril(self, categ_id: int, damage: float, t: int): for i, contract in enumerate(affected_contracts): contract.explode(t, uniformvalues[i], damagevalues[i]) - def _effect_payments(self, time: int): - """Method for checking and paying obligation if due. - Arguments - Current time to allow check if due - Returns None""" - if self.get_operational(): - due = [item for item in self.obligations if item["due_time"] <= time] - self.obligations = [ - item for item in self.obligations if item["due_time"] > time - ] - # sum_due = sum([item["amount"] for item in due]) - for obligation in due: - if self.cash < obligation["amount"]: - warnings.warn( - "Something wrong: economy out of money", RuntimeWarning - ) - self._pay(obligation) + def enter_illiquidity(self, time: int, sum_due: float): + raise RuntimeError("Oh no, economy has run out of money!") def _reduce_money_supply(self, amount: float): """Method to reduce money supply immediately and without payment recipient (used to adjust money supply diff --git a/isleconfig.py b/isleconfig.py index e45855d..103840c 100644 --- a/isleconfig.py +++ b/isleconfig.py @@ -82,7 +82,7 @@ "insurance_permanency_ratio_limit": 0.6, # If insurers stay for too long under this limit they deccide to leave the market because they have too much capital. "insurance_permanency_time_constraint": 24, - # This parameter defines the period that the insurers wait if they have few capital or few contract before leaving the market. + # The period that the insurers wait before leaving the market if they have few capital or few contract . "reinsurance_permanency_contracts_limit": 2, # If reinsurers stay for too long under this limit of contracts they deccide to leave the market. "reinsurance_permanency_ratio_limit": 0.8, diff --git a/metainsurancecontract.py b/metainsurancecontract.py index 712b709..2153ed8 100644 --- a/metainsurancecontract.py +++ b/metainsurancecontract.py @@ -61,7 +61,9 @@ def __init__( self.deductible_fraction = ( deductible_fraction if deductible_fraction is not None - else risk.deductible_fraction or default_deductible_fraction + else risk.deductible_fraction + if risk.deductible_fraction is not None + else default_deductible_fraction ) self.deductible = self.deductible_fraction * self.value @@ -71,7 +73,9 @@ def __init__( self.excess_fraction = ( excess_fraction if excess_fraction is not None - else risk.excess_fraction or default_excess_fraction + else risk.excess_fraction + if risk.excess_fraction is not None + else default_excess_fraction ) self.excess = self.excess_fraction * self.value @@ -178,3 +182,24 @@ def unreinsure(self): self.reincontract = None self.reinsurance = 0 self.reinsurance_share = None + + def explode(self, time, uniform_value=None, damage_extent=None): + """Explode method. + Accepts arguments + time: Type integer. The current time. + uniform_value: Not used + damage_extent: Type float. The absolute damage in excess-of-loss reinsurance (not relative as in + proportional contracts. + No return value. + Method marks the contract for termination. + """ + raise NotImplementedError() + + def mature(self, time): + """Mature method. + Accepts arguments + time: Type integer. The current time. + No return value. + Removes any reinsurance functions this contract has and terminates any reinsurance contracts for this + contract.""" + raise NotImplementedError() diff --git a/metainsuranceorg.py b/metainsuranceorg.py index d0b0b09..3b22ebc 100644 --- a/metainsuranceorg.py +++ b/metainsuranceorg.py @@ -2,8 +2,17 @@ import copy import math import functools -from typing import Optional, Tuple, Sequence, Mapping, MutableSequence -from itertools import zip_longest, chain +from typing import ( + Optional, + Tuple, + Sequence, + Mapping, + MutableSequence, + Iterable, + Callable, + Any, +) +from itertools import cycle, islice import numpy as np import scipy.stats @@ -14,7 +23,22 @@ from reinsurancecontract import ReinsuranceContract import metainsurancecontract from riskmodel import RiskModel -from genericclasses import GenericAgent, RiskProperties, AgentProperties +from genericclasses import GenericAgent, RiskProperties, AgentProperties, Obligation + + +def roundrobin(iterables: Sequence[Iterable]) -> Iterable: + """roundrobin(['ABC', 'D', 'EF']) --> A D E B F C""" + # Recipe credited to George Sakkis + num_active = len(iterables) + nexts: Iterable[Callable[[], Any]] = cycle(iter(it).__next__ for it in iterables) + while num_active: + try: + for next_fun in nexts: + yield next_fun() + except StopIteration: + # Remove the iterator we just exhausted from the cycle. + num_active -= 1 + nexts = cycle(islice(nexts, num_active)) def get_mean(x: Sequence[float]) -> float: @@ -151,7 +175,9 @@ def __init__( self.np_reinsurance_premium_share = simulation_parameters[ "default_non-proportional_reinsurance_premium_share" ] - self.underwritten_contracts: MutableSequence[metainsurancecontract.MetaInsuranceContract] = [] + self.underwritten_contracts: MutableSequence[ + metainsurancecontract.MetaInsuranceContract + ] = [] # self.reinsurance_contracts = [] self.is_insurer = True self.is_reinsurer = False @@ -186,13 +212,13 @@ def iterate(self, time: int): No return value For each time step this method obtains every firms interest payments, pays obligations, claim reinsurance, matures necessary contracts. Check condition for operational firms (as never removed) - so only operational firms receive new risks to evaluate, _pay dividends, adjust capacity.""" + so only operational firms receive new risks to evaluate, pay dividends, adjust capacity.""" """obtain investments yield""" self.obtain_yield(time) """realize due payments""" - self.effect_payments(time) + self._effect_payments(time) if isleconfig.verbose: print( time, @@ -348,14 +374,20 @@ def collect_process_evaluate_risks( # print(self.id, " now has ", len(self.underwritten_contracts), " & returns ", len(not_accepted_risks)) - def enter_illiquidity(self, time: int): + def enter_illiquidity(self, time: int, sum_due: float): """Enter_illiquidity Method. Accepts arguments time: Type integer. The current time. + sum_due: the outstanding sum that the firm couldn't pay No return value. - This method is called when a firm does not have enough cash to _pay all its obligations. It is only called + This method is called when a firm does not have enough cash to pay all its obligations. It is only called from the method self._effect_payments() which is called at the beginning of the self.iterate() method of this class. This method formalizes the bankruptcy through the method self.enter_bankruptcy().""" + self.simulation.record_unrecovered_claims(sum_due - self.cash) + # TODO: is this record of uncovered claims correct or should it be sum_due (since the company is + # impounded and self.cash will also not be paid out for quite some time)? + # QUERY: should failed payments to other firms (rein premiums, catbond payouts) count as unrecovered claims? + # TODO: effect partial payment self.enter_bankruptcy(time) def enter_bankruptcy(self, time: int): @@ -363,7 +395,7 @@ def enter_bankruptcy(self, time: int): Accepts arguments time: Type integer. The current time. No return value. - This method is used when a firm does not have enough cash to _pay all its obligations. It is only called from + This method is used when a firm does not have enough cash to pay all its obligations. It is only called from the method self.enter_illiquidity() which is only called from the method self._effect_payments(). This method dissolves the firm through the method self.dissolve().""" self.dissolve(time, "record_bankruptcy") @@ -406,22 +438,20 @@ def dissolve(self, time: int, record: str): self.simulation.return_risks(self.risks_kept) self.risks_kept = [] self.reinrisks_kept = [] - obligation = { - "amount": self.cash, - "recipient": self.simulation, - "due_time": time, - "purpose": "Dissolution", - } - self._pay( - obligation - ) # This MUST be the last obligation before the dissolution of the firm. - self.excess_capital = ( - 0 - ) # Excess of capital is 0 after bankruptcy or market exit. - self.profits_losses = ( - 0 - ) # Profits and losses are 0 after bankruptcy or market exit. + obligation = Obligation( + amount=self.cash, + recipient=self.simulation, + due_time=time, + purpose="Dissolution", + ) + # This MUST be the last obligation before the dissolution of the firm. + self._pay(obligation) + # Excess of capital is 0 after bankruptcy or market exit. + self.excess_capital = 0 + # Profits and losses are 0 after bankruptcy or market exit. + self.profits_losses = 0 if self.operational: + # TODO: This seems... odd? method_to_call = getattr(self.simulation, record) method_to_call() for category_reinsurance in self.category_reinsurance: @@ -429,39 +459,12 @@ def dissolve(self, time: int, record: str): category_reinsurance.dissolve(time) self.operational = False - def effect_payments(self, time: int): - """Method for checking if any payments are due. - Accepts: - time: Type Integer - No return value - Method checks firms list of obligations to see if ay are due for this time, then pays them. If the firm - does not have enough cash then it enters illiquity, leaves the market, and matures all contracts.""" - # TODO: don't really want to be reconstructing lists every time (unless the oblications are naturally sorted by - # time, in which case this could be done slightly better). Low priority, but something to consider - due = [item for item in self.obligations if item["due_time"] <= time] - self.obligations = [ - item for item in self.obligations if item["due_time"] > time - ] - # TODO: could this cause a firm to enter illiquidity if it has obligations to non-operational firms? Such - # firms can't recieve payment, so this possibly shouldn't happen. - sum_due = sum([item["amount"] for item in due]) - if sum_due > self.cash: - self.obligations += due - self.enter_illiquidity(time) - self.simulation.record_unrecovered_claims(sum_due - self.cash) - # TODO: is this record of uncovered claims correct or should it be sum_due (since the company is - # impounded and self.cash will also not be paid out for quite some time)? - # TODO: effect partial payment - else: - for obligation in due: - self._pay(obligation) - def pay_dividends(self, time: int): """Method to receive dividend obligation. Accepts: time: Type integer No return value - If firm has positive profits will _pay percentage of them as dividends. + If firm has positive profits will pay percentage of them as dividends. Currently pays to simulation. """ @@ -708,67 +711,69 @@ def process_newrisks_reinsurer( For that reason we process risk[C1], risk[C2], risk[C3], risk[C4], risk[C1], risk[C2], ... and so forth. If risks are accepted then a contract is written.""" not_accepted_reinrisks = [] - for risk in chain.from_iterable(zip_longest(*reinrisks_per_categ)): + for risk in roundrobin(reinrisks_per_categ): # Here we take only one risk per category at a time to achieve risk[C1], risk[C2], risk[C3], # risk[C4], risk[C1], risk[C2], ... if possible. - if risk: - # TODO: Move this out of the loop (maybe somewhere else entirely) and update it when needed - underwritten_risks = [ - RiskProperties( - owner=self, - value=contract.value, - category=contract.category, - risk_factor=contract.risk_factor, - deductible=contract.deductible, - excess=contract.excess, - insurancetype=contract.insurancetype, - runtime_left=(contract.expiration - time), - ) - for contract in self.underwritten_contracts - if contract.insurancetype == "excess-of-loss" - ] - accept, cash_left_by_categ, var_this_risk, self.excess_capital = self.riskmodel.evaluate( - underwritten_risks, self.cash, risk + assert risk + # TODO: Move this out of the loop (maybe somewhere else entirely) and update it when needed + underwritten_risks = [ + RiskProperties( + owner=self, + value=contract.value, + category=contract.category, + risk_factor=contract.risk_factor, + deductible=contract.deductible, + excess=contract.excess, + insurancetype=contract.insurancetype, + runtime_left=(contract.expiration - time), ) - # TODO: change riskmodel.evaluate() to accept new risk to be evaluated and - # to account for existing non-proportional risks correctly -> DONE. - if accept: - # TODO: rename this to per_value_premium in insurancecontract.py to avoid confusion - per_value_reinsurance_premium = ( - self.np_reinsurance_premium_share - * risk.periodized_total_premium - * risk.runtime - * ( - self.simulation.get_market_reinpremium() - / self.simulation.get_market_premium() - ) - / risk.value + for contract in self.underwritten_contracts + if contract.insurancetype == "excess-of-loss" + ] + accept, cash_left_by_categ, var_this_risk, self.excess_capital = self.riskmodel.evaluate( + underwritten_risks, self.cash, risk + ) + # TODO: change riskmodel.evaluate() to accept new risk to be evaluated and + # to account for existing non-proportional risks correctly -> DONE. + if accept: + # TODO: rename this to per_value_premium in insurancecontract.py to avoid confusion + per_value_reinsurance_premium = ( + self.np_reinsurance_premium_share + * risk.periodized_total_premium + * risk.runtime + * ( + self.simulation.get_market_reinpremium() + / self.simulation.get_market_premium() ) + / risk.value + ) - # Here it is check whether the portfolio is balanced or not if the reinrisk - # (risk_to_insure) is underwritten. Return True if it is balanced. False otherwise. - condition, cash_left_by_categ = self.balanced_portfolio( - risk, cash_left_by_categ, None - ) + # Here it is check whether the portfolio is balanced or not if the reinrisk + # (risk_to_insure) is underwritten. Return True if it is balanced. False otherwise. + condition, cash_left_by_categ = self.balanced_portfolio( + risk, cash_left_by_categ, None + ) - if condition: - contract = ReinsuranceContract( - self, - risk, - time, - per_value_reinsurance_premium, - risk.runtime, - self.default_contract_payment_period, - expire_immediately=self.simulation_parameters[ - "expire_immediately" - ], - initial_var=var_this_risk, - insurancetype=risk.insurancetype, - ) # TODO: implement excess of loss for reinsurance contracts - self.underwritten_contracts.append(contract) - self.cash_left_by_categ = cash_left_by_categ - else: - not_accepted_reinrisks.append(risk) + if condition: + contract = ReinsuranceContract( + self, + risk, + time, + per_value_reinsurance_premium, + risk.runtime, + self.default_contract_payment_period, + expire_immediately=self.simulation_parameters[ + "expire_immediately" + ], + initial_var=var_this_risk, + insurancetype=risk.insurancetype, + ) # TODO: implement excess of loss for reinsurance contracts + self.underwritten_contracts.append(contract) + self.cash_left_by_categ = cash_left_by_categ + else: + not_accepted_reinrisks.append(risk) + else: + not_accepted_reinrisks.append(risk) return reinrisks_per_categ, not_accepted_reinrisks @@ -796,9 +801,9 @@ def process_newrisks_insurer( risks are accepted then a contract is written.""" _cached_rvs = self.contract_runtime_dist.rvs() not_accepted_risks = [] - for risk in chain.from_iterable(zip_longest(*risks_per_categ)): - if risk and acceptable_by_category[risk.category] > 0: - categ_id = risk.category + for risk in roundrobin(risks_per_categ): + assert risk + if acceptable_by_category[risk.category] > 0: if risk.contract and risk.contract.expiration > time: # In this case the risk being inspected already has a contract, so we are deciding whether to # give reinsurance for it # QUERY: is this correct? @@ -821,6 +826,7 @@ def process_newrisks_insurer( ) self.underwritten_contracts.append(contract) self.cash_left_by_categ = cash_left_by_categ + acceptable_by_category[risk.category] -= 1 else: not_accepted_risks.append(risk) @@ -843,17 +849,18 @@ def process_newrisks_insurer( expire_immediately=self.simulation_parameters[ "expire_immediately" ], - initial_var=var_per_risk_per_categ[categ_id], + initial_var=var_per_risk_per_categ[risk.category], ) self.underwritten_contracts.append(contract) self.cash_left_by_categ = cash_left_by_categ + acceptable_by_category[risk.category] -= 1 else: not_accepted_risks.append(risk) - # QUERY: should we only decrease this if the risk is accepted? - acceptable_by_category[categ_id] -= 1 + else: + not_accepted_risks.append(risk) # TODO: allow different values per risk (i.e. sum over value (and reinsurance_share) or # exposure instead of counting) - + # QUERY: should we only decrease this if the risk is accepted? return risks_per_categ, not_accepted_risks def market_permanency(self, time: int): diff --git a/reinsurancecontract.py b/reinsurancecontract.py index 88905d1..f7ad879 100644 --- a/reinsurancecontract.py +++ b/reinsurancecontract.py @@ -52,7 +52,7 @@ def __init__( else: assert self.contract is not None - def explode(self, time, damage_extent=None): + def explode(self, time, uniform_value=None, damage_extent=None): """Explode method. Accepts arguments time: Type integer. The current time. diff --git a/riskmodel.py b/riskmodel.py index 357e586..060770e 100644 --- a/riskmodel.py +++ b/riskmodel.py @@ -1,5 +1,5 @@ import math -from typing import Sequence, Tuple, Union, Optional, List +from typing import Sequence, Tuple, Union, Optional, MutableSequence import numpy as np @@ -36,15 +36,15 @@ def __init__( self.margin_of_safety = margin_of_safety """damage_distribution is some scipy frozen rv distribution which is bound between 0 and 1 and indicates the share of risks suffering damage as part of any single catastrophic peril""" - self.damage_distribution: List = [ + self.damage_distribution: MutableSequence = [ damage_distribution for _ in range(self.category_number) ] # TODO: separate that category wise? -> DONE. - self.damage_distribution_stack: Sequence[List] = [ - [] for _ in range(self.category_number) - ] - self.reinsurance_contract_stack: Sequence[List[MetaInsuranceContract]] = [ + self.damage_distribution_stack: Sequence[MutableSequence] = [ [] for _ in range(self.category_number) ] + self.reinsurance_contract_stack: Sequence[ + MutableSequence[MetaInsuranceContract] + ] = [[] for _ in range(self.category_number)] # self.inaccuracy = np.random.uniform(9/10., 10/9., size=self.category_number) self.inaccuracy: Sequence[float] = inaccuracy diff --git a/start.py b/start.py index 14c9338..746675c 100644 --- a/start.py +++ b/start.py @@ -63,7 +63,6 @@ def main( simulation_parameters = d["simulation_parameters"] for key in d["isleconfig"]: isleconfig.__dict__[key] = d["isleconfig"][key] - simulation = copy.deepcopy(simulation) for t in range(time, simulation_parameters["max_time"]): # Main time iteration loop simulation.iterate(t) @@ -221,7 +220,8 @@ def load_simulation(): if args.save_iterations: save_iter = args.save_iterations else: - save_iter = 100 + # Disable saving unless save_iter is given. It doesn't work anyway # TODO + save_iter = isleconfig.simulation_parameters["max_time"] + 2 if not args.resume: from setup_simulation import SetupSim @@ -254,7 +254,7 @@ def load_simulation(): resume=args.resume, ) - replic_ID = filepath + replic_ID = 1 """ Restore the log at the end of the single simulation run for saving and for potential further study """ is_background = (not isleconfig.force_foreground) and ( isleconfig.replicating or (replic_ID in locals()) From 0f0892041e0124196095cbb32928091ef3808f6b Mon Sep 17 00:00:00 2001 From: Chris Hughes Date: Thu, 18 Jul 2019 17:11:36 +0100 Subject: [PATCH 056/125] Even more type hints --- genericclasses.py | 34 ++++++++++++++++++++++++--- insurancesimulation.py | 29 ++++++++++++----------- metainsurancecontract.py | 6 +++-- metainsuranceorg.py | 10 ++++---- riskmodel.py | 10 ++++---- setup_simulation.py | 18 ++++++++++----- start.py | 50 ++++++++++++++++++++++------------------ utils.py | 21 ----------------- 8 files changed, 98 insertions(+), 80 deletions(-) delete mode 100644 utils.py diff --git a/genericclasses.py b/genericclasses.py index 5131946..4fd5d64 100644 --- a/genericclasses.py +++ b/genericclasses.py @@ -1,7 +1,16 @@ from __future__ import annotations + +import numpy as np +from scipy import stats import dataclasses -from typing import Mapping, MutableSequence -import metainsurancecontract +from typing import Mapping, MutableSequence, Union + +# Not totally sure about the best way to resolve circular dependencies caused by type hinting +# from metainsurancecontract import MetaInsuranceContract +from distributiontruncated import TruncatedDistWrapper +from distributionreinsurance import ReinsuranceDistWrapper + +Distribution = Union[stats.rv_continuous, TruncatedDistWrapper, ReinsuranceDistWrapper] class GenericAgent: @@ -94,7 +103,7 @@ class RiskProperties: owner: GenericAgent number_risks: int = 1 - contract: metainsurancecontract.MetaInsuranceContract = None + contract: "metainsurancecontract.MetaInsuranceContract" = None insurancetype: str = None deductible: float = None runtime: int = None @@ -135,3 +144,22 @@ class Obligation: recipient: GenericAgent due_time: int purpose: str + + +class ConstantGen(stats.rv_continuous): + def _pdf(self, x: float, *args) -> float: + a = np.float_(x == 0) + a[a == 1.0] = np.float_("inf") + return a + + def _cdf(self, x: float, *args) -> float: + return np.float_(x >= 0) + + def _rvs(self, *args) -> Union[np.ndarray, float]: + if self._size is None: + return 0.0 + else: + return np.zeros(shape=self._size) + + +Constant = ConstantGen(name="constant") diff --git a/insurancesimulation.py b/insurancesimulation.py index 1aaf757..13e7104 100644 --- a/insurancesimulation.py +++ b/insurancesimulation.py @@ -2,6 +2,8 @@ import math import random import copy + +import genericclasses import logger import warnings from typing import Mapping, MutableMapping, MutableSequence, Sequence, Any, Optional @@ -13,8 +15,7 @@ import visualization_network import insurancefirms import isleconfig -import utils -from genericclasses import GenericAgent, RiskProperties, AgentProperties +from genericclasses import GenericAgent, RiskProperties, AgentProperties, Distribution from metainsuranceorg import MetaInsuranceOrg import catbond @@ -33,12 +34,12 @@ class InsuranceSimulation(GenericAgent): def __init__( self, - override_no_riskmodels, - replic_id, - simulation_parameters, - rc_event_schedule, - rc_event_damage, - damage_distribution=TruncatedDistWrapper( + override_no_riskmodels: bool, + replic_id: int, + simulation_parameters: MutableMapping, + rc_event_schedule: MutableSequence[MutableSequence[int]], + rc_event_damage: MutableSequence[MutableSequence[float]], + damage_distribution: Distribution = TruncatedDistWrapper( lower_bound=0.25, upper_bound=1.0, dist=scipy.stats.pareto(b=2, loc=0, scale=0.25), @@ -68,7 +69,7 @@ def __init__( self.simulation_parameters["simulation"] = self "Unpacks parameters and sets distributions" - self.damage_distribution = damage_distribution + self.damage_distribution: Distribution = damage_distribution self.catbonds_off: bool = simulation_parameters["catbonds_off"] self.reinsurance_off: bool = simulation_parameters["reinsurance_off"] @@ -91,9 +92,9 @@ def __init__( loc=self.risk_factor_lower_bound, scale=self.risk_factor_spread ) else: - self.risk_factor_distribution = utils.constant(loc=1.0) + self.risk_factor_distribution = genericclasses.Constant(loc=1.0) # self.risk_value_distribution = scipy.stats.uniform(loc=100, scale=9900) - self.risk_value_distribution = utils.constant(loc=1000) + self.risk_value_distribution = genericclasses.Constant(loc=1000) risk_factor_mean = self.risk_factor_distribution.mean() @@ -642,7 +643,7 @@ def save_data(self): # This function allows to return in a list all the data generated by the model. There is no other way to transfer # it back from the cloud. - def obtain_log(self, requested_logs: Mapping = None): + def obtain_log(self, requested_logs: Mapping = None) -> MutableSequence: return self.logger.obtain_log(requested_logs) def finalize(self, *args): @@ -1123,8 +1124,8 @@ def reset_pls(self): for reininsurancefirm in self.reinsurancefirms: reininsurancefirm.reset_pl() - for catbond in self.catbonds: - catbond.reset_pl() + for cb in self.catbonds: + cb.reset_pl() def get_risk_share(self, firm: MetaInsuranceOrg) -> float: """Method to determine the total percentage of risks in the market that are held by a particular firm. diff --git a/metainsurancecontract.py b/metainsurancecontract.py index 2153ed8..435f459 100644 --- a/metainsurancecontract.py +++ b/metainsurancecontract.py @@ -1,10 +1,12 @@ -from genericclasses import GenericAgent, RiskProperties +from __future__ import annotations +from genericclasses import RiskProperties +import metainsuranceorg class MetaInsuranceContract: def __init__( self, - insurer: GenericAgent, + insurer: metainsuranceorg.MetaInsuranceOrg, risk: RiskProperties, time: int, premium: float, diff --git a/metainsuranceorg.py b/metainsuranceorg.py index 3b22ebc..64c6c3e 100644 --- a/metainsuranceorg.py +++ b/metainsuranceorg.py @@ -271,7 +271,7 @@ def collect_process_evaluate_risks( then with proportional ones""" # Here the new reinrisks are organized by category. - reinrisks_per_categ, number_reinrisks_categ = self.risks_reinrisks_organizer( + reinrisks_per_categ = self.risks_reinrisks_organizer( new_nonproportional_risks ) @@ -349,9 +349,7 @@ def collect_process_evaluate_risks( acceptable_by_category = np.int64(np.round(acceptable_by_category)) # Here the new risks are organized by category. - [risks_per_categ, number_risks_categ] = self.risks_reinrisks_organizer( - new_risks - ) + risks_per_categ = self.risks_reinrisks_organizer(new_risks) for repetition in range(self.recursion_limit): # TODO: find an efficient way to stop the recursion if there are no more risks to accept or if it is @@ -602,7 +600,7 @@ def adjust_capacity_target(self, time): def risks_reinrisks_organizer( self, new_risks: Sequence[RiskProperties] - ) -> Tuple[Sequence[Sequence[RiskProperties]], Sequence[int]]: + ) -> Sequence[Sequence[RiskProperties]]: """This method organizes the new risks received by the insurer (or reinsurer) by category. Accepts: new_risks: Type list of DataDicts @@ -623,7 +621,7 @@ def risks_reinrisks_organizer( number_risks_categ[categ_id] = len(risks_by_category[categ_id]) # The method returns both risks_by_category and number_risks_categ. - return risks_by_category, number_risks_categ + return risks_by_category def balanced_portfolio( self, diff --git a/riskmodel.py b/riskmodel.py index 060770e..72e99e0 100644 --- a/riskmodel.py +++ b/riskmodel.py @@ -5,16 +5,16 @@ import isleconfig from distributionreinsurance import ReinsuranceDistWrapper -from genericclasses import RiskProperties +from genericclasses import RiskProperties, Distribution from metainsurancecontract import MetaInsuranceContract class RiskModel: def __init__( self, - damage_distribution, + damage_distribution: Distribution, expire_immediately: bool, - cat_separation_distribution, + cat_separation_distribution: Distribution, norm_premium: float, category_number: int, init_average_exposure: float, @@ -36,10 +36,10 @@ def __init__( self.margin_of_safety = margin_of_safety """damage_distribution is some scipy frozen rv distribution which is bound between 0 and 1 and indicates the share of risks suffering damage as part of any single catastrophic peril""" - self.damage_distribution: MutableSequence = [ + self.damage_distribution: MutableSequence[Distribution] = [ damage_distribution for _ in range(self.category_number) ] # TODO: separate that category wise? -> DONE. - self.damage_distribution_stack: Sequence[MutableSequence] = [ + self.damage_distribution_stack: Sequence[MutableSequence[Distribution]] = [ [] for _ in range(self.category_number) ] self.reinsurance_contract_stack: Sequence[ diff --git a/setup_simulation.py b/setup_simulation.py index 6e4ab6f..77cb3dc 100644 --- a/setup_simulation.py +++ b/setup_simulation.py @@ -2,7 +2,7 @@ Event schedule sets are written to files and include event schedules for every replication as dictionaries in a list. Every event schedule dictionary has: - event_times: list of list of int - iteration periods of risk events in each category - - event_damages: list of list of float (0, 1) - damage as share of theoretically possible damage for each risk event + - event_damages: list of list of float (0, 1) - damage as share of possible damage for each risk event - num_categories: int - number of risk categories - np_seed: int - numpy module random seed - random_seed: int - random module random seed @@ -15,7 +15,7 @@ """ import math - +from typing import MutableSequence, Tuple import os import pickle import scipy.stats @@ -53,7 +53,11 @@ def __init__(self): self.overwrite = False self.replications = None - def schedule(self, replications): + def schedule( + self, replications: int + ) -> Tuple[ + MutableSequence[MutableSequence[int]], MutableSequence[MutableSequence[float]] + ]: for i in range(replications): # In this list will be stored the lists of times when there will be catastrophes for every category of the # model during a single run. ([[times for C1],[times for C2],[times for C3],[times for C4]]) @@ -81,7 +85,7 @@ def schedule(self, replications): return self.general_rc_event_schedule, self.general_rc_event_damage - def seeds(self, replications): + def seeds(self, replications: int): # This method sets (and returns) the seeds required for an ensemble of replications of the model. # The argument (replications) is the number of replications. """draw random variates for random seeds""" @@ -155,7 +159,9 @@ def recall(self): "num_categories" ] - def obtain_ensemble(self, replications, filepath=None, overwrite=False): + def obtain_ensemble( + self, replications: int, filepath: str = None, overwrite: bool = False + ) -> Tuple: # This method returns all the information (schedules and seeds) required to run an ensemble of simulations of # the model. Since it also stores the information in a file it will be possible to replicate the ensemble at a # later time. The argument (replications) is the number of replications. @@ -197,7 +203,7 @@ def obtain_ensemble(self, replications, filepath=None, overwrite=False): ) @staticmethod - def to_filename(filepath): + def to_filename(filepath: str) -> str: if len(filepath) >= 10 and filepath[-10:] == ".islestore": return filepath else: diff --git a/start.py b/start.py index 746675c..a547891 100644 --- a/start.py +++ b/start.py @@ -1,11 +1,12 @@ # import common packages +from __future__ import annotations import argparse import hashlib import numpy as np import os import pickle import random -import copy +from typing import MutableMapping, MutableSequence import calibrationscore import insurancesimulation @@ -30,26 +31,24 @@ # main function def main( - simulation_parameters, - rc_event_schedule, - rc_event_damage, - np_seed, - random_seed, - save_iter: int, - replic_ID, - requested_logs=None, - resume=False, -): + sim_params: MutableMapping, + rc_event_schedule: MutableSequence[MutableSequence[int]], + rc_event_damage: MutableSequence[MutableSequence[float]], + np_seed: int, + random_seed: int, + save_iteration: int, + replic_id: int, + requested_logs: MutableSequence = None, + resume: bool = False, +) -> MutableSequence: if not resume: np.random.seed(np_seed) random.seed(random_seed) - simulation_parameters[ - "simulation" - ] = simulation = insurancesimulation.InsuranceSimulation( + sim_params["simulation"] = simulation = insurancesimulation.InsuranceSimulation( override_no_riskmodels, - replic_ID, - simulation_parameters, + replic_id, + sim_params, rc_event_schedule, rc_event_damage, ) @@ -60,10 +59,10 @@ def main( random.setstate(d["random_seed"]) time = d["time"] simulation = d["simulation"] - simulation_parameters = d["simulation_parameters"] + sim_params = d["simulation_parameters"] for key in d["isleconfig"]: isleconfig.__dict__[key] = d["isleconfig"][key] - for t in range(time, simulation_parameters["max_time"]): + for t in range(time, sim_params["max_time"]): # Main time iteration loop simulation.iterate(t) @@ -71,9 +70,9 @@ def main( simulation.save_data() # Don't save at t=0 or if the simulation has just finished - if t % save_iter == 0 and 0 < t < simulation_parameters["max_time"]: + if t % save_iteration == 0 and 0 < t < sim_params["max_time"]: # Need to use t+1 as resume will start at time saved - save_simulation(t + 1, simulation, simulation_parameters, exit_now=False) + save_simulation(t + 1, simulation, sim_params, exit_now=False) # Finish simulation, write logs simulation.finalize() @@ -82,7 +81,12 @@ def main( return simulation.obtain_log(requested_logs) -def save_simulation(t, sim, sim_param, exit_now=False): +def save_simulation( + t: int, + sim: insurancesimulation.InsuranceSimulation, + sim_param: MutableMapping, + exit_now: bool = False, +) -> None: d = { "np_seed": np.random.get_state(), "random_seed": random.getstate(), @@ -108,7 +112,7 @@ def save_simulation(t, sim, sim_param, exit_now=False): exit(0) -def load_simulation(): +def load_simulation() -> dict: # TODO: Fix! This doesn't work, the retrieved file is different to the saved one. with open("data/simulation_save.pkl", "br") as rfile: print( @@ -250,7 +254,7 @@ def load_simulation(): np_seeds[0], random_seeds[0], save_iter, - replic_ID=1, + replic_id=1, resume=args.resume, ) diff --git a/utils.py b/utils.py deleted file mode 100644 index 22efa3e..0000000 --- a/utils.py +++ /dev/null @@ -1,21 +0,0 @@ -from scipy import stats -import numpy as np - - -class ConstantGen(stats.rv_continuous): - def _pdf(self, x, *args): - a = np.float_(x == 0) - a[a == 1.0] = np.float_("inf") - return a - - def _cdf(self, x, *args): - return np.float_(x >= 0) - - def _rvs(self, *args): - if self._size is None: - return 0.0 - else: - return np.zeros(shape=self._size) - - -constant = ConstantGen(name="constant") From b08ce8cd1bf92d721e27d0aa3bbfa83366a86c01 Mon Sep 17 00:00:00 2001 From: KloskaT Date: Thu, 18 Jul 2019 17:42:57 +0100 Subject: [PATCH 057/125] Can plot specific data types against each other for all riskmodels, with a subplot for each type of firm. Needs automating. --- visualisation.py | 113 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 113 insertions(+) diff --git a/visualisation.py b/visualisation.py index fded500..6a4c5eb 100644 --- a/visualisation.py +++ b/visualisation.py @@ -8,6 +8,8 @@ import scipy import scipy.stats from matplotlib.offsetbox import AnchoredText +import time +import os class TimeSeries(object): @@ -763,6 +765,111 @@ def plot(self, ax=None, ylabel="PDF(x)", xtralabel="", xlabel="x", num_bins=50, plt.show() +class RiskModelSpecificCompare: # TODO: Automate for all filetypes + def __init__(self, filetype="_premium.dat", refiletype="_reinpremium.dat", number_riskmodels=4): + """Initialises class that plots the insurance and reinsurance data for all risk models for specified data. + Currently plots premium data. Need to automate for number of risk models and filenames.""" + self.timeseries_dict = {} + self.timeseries_dict["mean"] = {} + self.timeseries_dict["median"] = {} + self.timeseries_dict["quantile25"] = {} + self.timeseries_dict["quantile75"] = {} + + self.filetypes = [filetype, refiletype] + self.riskmodels = ["one", "two", "three", "four"] + + for i in range(number_riskmodels): + for j in range(len(self.filetypes)): + filename = "data/"+self.riskmodels[i]+self.filetypes[j] + rfile = open(filename, "r") + data = [eval(k) for k in rfile] + rfile.close() + + # compute data series + data_means = [] + data_medians = [] + data_q25 = [] + data_q75 = [] + for k in range(len(data[0])): + data_means.append(np.mean([item[k] for item in data])) + data_q25.append(np.percentile([item[k] for item in data], 25)) + data_q75.append(np.percentile([item[k] for item in data], 75)) + data_medians.append(np.median([item[k] for item in data])) + data_means = np.array(data_means) + data_medians = np.array(data_medians) + data_q25 = np.array(data_q25) + data_q75 = np.array(data_q75) + + # record data series + self.timeseries_dict["mean"][filename] = data_means + self.timeseries_dict["median"][filename] = data_medians + self.timeseries_dict["quantile25"][filename] = data_q25 + self.timeseries_dict["quantile75"][filename] = data_q75 + + def plot(self, outputfile): + colors = {"one": "red", "two": "blue", "three": "green", "four": "yellow"} + labels = {"reinexcess_capital": "Excess Capital (Reinsurers)", "excess_capital": "Excess Capital (Insurers)", + "cumulative_unrecovered_claims": "Uncovered Claims (cumulative)", + "cumulative_bankruptcies": "Bankruptcies (cumulative)", + "profitslosses": "Profits and Losses (Insurer)", "contracts": "Contracts (Insurers)", + "cash": "Liquidity (Insurers)", "operational": "Active Insurers", "premium": "Premium (Insurance)", + "reinprofitslosses": "Profits and Losses (Reinsurer)", "reincash": "Liquidity (Reinsurers)", + "reincontracts": "Contracts (Reinsurers)", "reinoperational": "Active Reinsurers", + "reinpremium": "Premium (Reinsurance)", "noninsured_risks": "Non-insured risks (Insurance)", + "noninsured_reinrisks": "Non-insured risks (Insurance)"} + + # Backup existing figures (so as not to overwrite them) + outputfilename = "data/" + outputfile + ".pdf" + backupfilename = "data/" + outputfile + "_old_" + time.strftime('%Y_%b_%d_%H_%M') + ".pdf" + if os.path.exists(outputfilename): + os.rename(outputfilename, backupfilename) + + self.fig = plt.figure() + + self.ax0 = self.fig.add_subplot(211) + maxlen_plots = 0 + self.ax0.plot(range(len(self.timeseries_dict["mean"]["data/one_premium.dat"]))[1200:], self.timeseries_dict["mean"]["data/one_premium.dat"][1200:], color="red", label="One Riskmodel") + self.ax0.plot(range(len(self.timeseries_dict["mean"]["data/two_premium.dat"]))[1200:], self.timeseries_dict["mean"]["data/two_premium.dat"][1200:], color="blue", label="Two Riskmodels") + self.ax0.plot(range(len(self.timeseries_dict["mean"]["data/three_premium.dat"]))[1200:], self.timeseries_dict["mean"]["data/three_premium.dat"][1200:], color="green", label="Three Riskmodels") + self.ax0.plot(range(len(self.timeseries_dict["mean"]["data/four_premium.dat"]))[1200:], self.timeseries_dict["mean"]["data/four_premium.dat"][1200:], color="yellow", label="Four Riskmodels") + + self.ax0.fill_between(range(len(self.timeseries_dict["quantile25"]["data/one_premium.dat"]))[1200:],self.timeseries_dict["quantile25"]["data/one_premium.dat"][1200:], self.timeseries_dict["quantile75"]["data/one_premium.dat"][1200:],facecolor="red", alpha=0.25) + self.ax0.fill_between(range(len(self.timeseries_dict["quantile25"]["data/one_premium.dat"]))[1200:],self.timeseries_dict["quantile25"]["data/two_premium.dat"][1200:], self.timeseries_dict["quantile75"]["data/two_premium.dat"][1200:],facecolor="blue", alpha=0.25) + self.ax0.fill_between(range(len(self.timeseries_dict["quantile25"]["data/two_premium.dat"]))[1200:],self.timeseries_dict["quantile25"]["data/three_premium.dat"][1200:], self.timeseries_dict["quantile75"]["data/three_premium.dat"][1200:],facecolor="green", alpha=0.25) + self.ax0.fill_between(range(len(self.timeseries_dict["quantile25"]["data/three_premium.dat"]))[1200:],self.timeseries_dict["quantile25"]["data/four_premium.dat"][1200:], self.timeseries_dict["quantile75"]["data/four_premium.dat"][1200:],facecolor="yellow", alpha=0.25) + + maxlen_plots = max(maxlen_plots, len(self.timeseries_dict["mean"]["data/one_premium.dat"]), len(self.timeseries_dict["mean"]["data/two_premium.dat"]), len(self.timeseries_dict["mean"]["data/three_premium.dat"]), len(self.timeseries_dict["mean"]["data/four_premium.dat"])) + xticks = np.arange(1200, maxlen_plots, step=600) + self.ax0.set_xticks(xticks) + self.ax0.set_xticklabels(["${0:d}$".format(int((xtc - 1200) / 12)) for xtc in xticks]); + self.ax0.legend(loc='best') + self.ax0.set_ylabel(labels["premium"]) + + self.ax1 = self.fig.add_subplot(212) + maxlen_plots = 0 + self.ax1.plot(range(len(self.timeseries_dict["mean"]["data/one_reinpremium.dat"]))[1200:], self.timeseries_dict["mean"]["data/one_reinpremium.dat"][1200:], color="red", label="One Riskmodel") + self.ax1.plot(range(len(self.timeseries_dict["mean"]["data/two_reinpremium.dat"]))[1200:], self.timeseries_dict["mean"]["data/two_reinpremium.dat"][1200:], color="blue", label="Two Riskmodels") + self.ax1.plot(range(len(self.timeseries_dict["mean"]["data/three_reinpremium.dat"]))[1200:], self.timeseries_dict["mean"]["data/three_reinpremium.dat"][1200:], color="green", label="Three Riskmodels") + self.ax1.plot(range(len(self.timeseries_dict["mean"]["data/four_reinpremium.dat"]))[1200:], self.timeseries_dict["mean"]["data/four_reinpremium.dat"][1200:], color="yellow", label="Four Riskmodels") + + self.ax1.fill_between(range(len(self.timeseries_dict["quantile25"]["data/one_reinpremium.dat"]))[1200:], self.timeseries_dict["quantile25"]["data/one_reinpremium.dat"][1200:], self.timeseries_dict["quantile75"]["data/one_reinpremium.dat"][1200:], facecolor="red", alpha=0.25) + self.ax1.fill_between(range(len(self.timeseries_dict["quantile25"]["data/one_reinpremium.dat"]))[1200:], self.timeseries_dict["quantile25"]["data/two_reinpremium.dat"][1200:], self.timeseries_dict["quantile75"]["data/two_reinpremium.dat"][1200:], facecolor="blue", alpha=0.25) + self.ax1.fill_between(range(len(self.timeseries_dict["quantile25"]["data/two_reinpremium.dat"]))[1200:], self.timeseries_dict["quantile25"]["data/three_reinpremium.dat"][1200:], self.timeseries_dict["quantile75"]["data/three_reinpremium.dat"][1200:], facecolor="green", alpha=0.25) + self.ax1.fill_between(range(len(self.timeseries_dict["quantile25"]["data/three_reinpremium.dat"]))[1200:], self.timeseries_dict["quantile25"]["data/four_reinpremium.dat"][1200:], self.timeseries_dict["quantile75"]["data/four_reinpremium.dat"][1200:], facecolor="yellow", alpha=0.25) + + maxlen_plots = max(maxlen_plots, len(self.timeseries_dict["mean"]["data/one_reinpremium.dat"]), len(self.timeseries_dict["mean"]["data/two_reinpremium.dat"]), len(self.timeseries_dict["mean"]["data/three_reinpremium.dat"]), len(self.timeseries_dict["mean"]["data/four_reinpremium.dat"])) + + xticks = np.arange(1200, maxlen_plots, step=600) + self.ax1.set_xticks(xticks) + self.ax1.set_xticklabels(["${0:d}$".format(int((xtc - 1200) / 12)) for xtc in xticks]); + self.ax1.set_ylabel(labels["reinpremium"]) + self.ax1.set_xlabel("Years") + + plt.tight_layout() + self.fig.savefig(outputfilename) + plt.show() + + if __name__ == "__main__": # Use argparse to handle command line arguments parser = argparse.ArgumentParser(description='Model the Insurance sector') @@ -776,8 +883,11 @@ def plot(self, ax=None, ylabel="PDF(x)", xtralabel="", xlabel="x", num_bins=50, "ensemble") parser.add_argument("--bankruptcydistribution", action="store_true", help="plot the histograms of bankruptcy events/unrecovered claims across ensemble") + parser.add_argument("--riskmodel_comparison", action="store_true") args = parser.parse_args() + args.riskmodel_comparison = True + if args.single: # load in data from the history_logs dictionarywith open("data/history_logs.dat","r") as rfile: @@ -837,5 +947,8 @@ def plot(self, ax=None, ylabel="PDF(x)", xtralabel="", xlabel="x", num_bins=50, HP.generate_plot(logscale=True, xlabel="Damages not recovered", minmax=[0, 6450000], VaR005guess=0.1) # =691186.8726311699) # this is the VaR threshold for 4 risk models with reinsurance + if args.riskmodel_comparison: + compare = RiskModelSpecificCompare() + compare.plot("figures/premium.data") # ਲ਼ From e7f442a7619a9a9b3b70356303caab61f440862c Mon Sep 17 00:00:00 2001 From: Chris Hughes Date: Fri, 19 Jul 2019 13:13:25 +0100 Subject: [PATCH 058/125] Misc cleanups --- catbond.py | 2 +- distributionreinsurance.py | 16 +++++++++++++--- insurancecontract.py | 5 ++++- insurancefirms.py | 4 ++-- insurancesimulation.py | 8 +++++--- isleconfig.py | 2 +- logger.py | 8 ++++---- metainsurancecontract.py | 12 +++++------- metainsuranceorg.py | 17 +++++++---------- reinsurancecontract.py | 5 ++++- riskmodel.py | 2 +- 11 files changed, 47 insertions(+), 34 deletions(-) diff --git a/catbond.py b/catbond.py index 1f5b376..6907558 100644 --- a/catbond.py +++ b/catbond.py @@ -32,7 +32,7 @@ def __init__( self.underwritten_contracts: MutableSequence[ metainsurancecontract.MetaInsuranceContract ] = [] - self.cash: int = 0 + self.cash: float = 0 self.profits_losses: float = 0 self.obligations: MutableSequence[genericclasses.Obligation] = [] self.operational: bool = True diff --git a/distributionreinsurance.py b/distributionreinsurance.py index 904a93c..0e70cdb 100644 --- a/distributionreinsurance.py +++ b/distributionreinsurance.py @@ -4,7 +4,10 @@ class ReinsuranceDistWrapper: - # QUERY: Is this the distribution of the risk when excess of loss reinsurance is applied? + """ Wrapper for modifying the risk to an insurance company when they have EoL reinsurance + + lower_bound is the least reinsured risk (lowest priority), upper_bound is the greatest reinsured risk + Note that the bounds are in terms of the values of the distribution, not the probabilities.""" def __init__(self, dist, lower_bound=None, upper_bound=None): assert lower_bound is not None or upper_bound is not None self.dist = dist @@ -82,10 +85,17 @@ def rvs(self, size=1): non_truncated = scipy.stats.pareto(b=2, loc=0, scale=0.5) # truncated = ReinsuranceDistWrapper(lower_bound=0, upper_bound=1, dist=non_truncated) truncated = ReinsuranceDistWrapper( - lower_bound=0.9, upper_bound=1.1, dist=non_truncated + lower_bound=0.7, upper_bound=1.5, dist=non_truncated ) x1 = np.linspace(non_truncated.ppf(0.01), non_truncated.ppf(0.99), 100) - x2 = np.linspace(truncated.ppf(0.01), truncated.ppf(0.99), 100) + # x2 = np.linspace(truncated.ppf(0.01), truncated.ppf(0.99), 100) + import matplotlib.pyplot as plt + y1 = non_truncated.pdf(x1) + y2 = truncated.pdf(x1) + plt.plot(x1, y1, 'r+') + plt.plot(x1, y2, 'bx') + plt.legend(["non truncated", "truncated"]) + plt.show() # pdb.set_trace() diff --git a/insurancecontract.py b/insurancecontract.py index 2c5f08c..21a68fc 100644 --- a/insurancecontract.py +++ b/insurancecontract.py @@ -3,6 +3,7 @@ import metainsurancecontract import genericclasses import metainsuranceorg +import insurancesimulation class InsuranceContract(metainsurancecontract.MetaInsuranceContract): @@ -42,6 +43,9 @@ def __init__( excess_fraction, reinsurance, ) + # the property holder in an insurance contract should always be the simulation + assert self.property_holder is self.insurer.simulation + self.property_holder: insurancesimulation.InsuranceSimulation def explode(self, time, uniform_value=None, damage_extent=None): """Explode method. @@ -53,7 +57,6 @@ def explode(self, time, uniform_value=None, damage_extent=None): damage caused in the risk insured by this contract. No return value. For registering damage and creating resulting claims (and payment obligations).""" - # np.mean(np.random.beta(1, 1./mu -1, size=90000)) if uniform_value is None: raise ValueError( "uniform_value must be passed to InsuranceContract.explode" diff --git a/insurancefirms.py b/insurancefirms.py index 5ef29bc..f64c80a 100644 --- a/insurancefirms.py +++ b/insurancefirms.py @@ -1,5 +1,5 @@ from __future__ import annotations -from typing import Optional, Tuple +from typing import Optional, Tuple, MutableSequence, Mapping import numpy as np @@ -490,7 +490,7 @@ def make_reinsurance_claims(self, time: int): time, damage_extent=claims_this_turn[categ_id] ) - def get_excess_of_loss_reinsurance(self): + def get_excess_of_loss_reinsurance(self) -> MutableSequence[Mapping]: """Method to return list containing the reinsurance for each category interms of the reinsurer, value of contract and category. Only used for network visualisation. No accepted values. diff --git a/insurancesimulation.py b/insurancesimulation.py index 13e7104..990755d 100644 --- a/insurancesimulation.py +++ b/insurancesimulation.py @@ -882,11 +882,12 @@ def solicit_insurance_requests( risks_to_be_sent: Type List""" risks_to_be_sent = self.risks[: int(self.insurers_weights[insurer.id])] self.risks = self.risks[int(self.insurers_weights[insurer.id]) :] - for risk in insurer.risks_kept: + for risk in insurer.risks_retained: risks_to_be_sent.append(risk) - # QUERY: what actually is InsuranceFirm.risks_kept? - insurer.risks_kept = [] + # QUERY: what actually is InsuranceFirm.risks_kept? Are we resending all their existing risks? + # Or is it just a list of risk that have rolled over and so need to be re-evaluated + insurer.risks_retained = [] np.random.shuffle(risks_to_be_sent) @@ -1003,6 +1004,7 @@ def record_unrecovered_claims(self, loss: float): def record_claims(self, claims: float): """This method records every claim made to insurers and reinsurers. It is called from both insurers and reinsurers (metainsuranceorg.py).""" + # QUERY: Should insurance and reinsurance claims really be recorded together? self.cumulative_claims += claims def log(self): diff --git a/isleconfig.py b/isleconfig.py index 103840c..8510659 100644 --- a/isleconfig.py +++ b/isleconfig.py @@ -29,7 +29,7 @@ "mean_contract_runtime": 12, "contract_runtime_halfspread": 2, "default_contract_payment_period": 3, - "max_time": 300, + "max_time": 1000, "money_supply": 2000000000, "event_time_mean_separation": 100 / 3.0, "expire_immediately": False, diff --git a/logger.py b/logger.py index 23ad149..f572760 100644 --- a/logger.py +++ b/logger.py @@ -94,9 +94,9 @@ def record_data(self, data_dict): data_dict["reinsurance_contracts"][i] ) - def obtain_log( - self, requested_logs=LOG_DEFAULT - ): # This function allows to return in a list all the data generated by the model. There is no other way to transfer it back from the cloud. + def obtain_log(self, requested_logs=None): + if requested_logs is None: + requested_logs = LOG_DEFAULT """Method to transfer entire log (self.history_log as well as risk event schedule). This is used to transfer the log to master core from work cores in ensemble runs in the cloud. No arguments. @@ -108,7 +108,7 @@ def obtain_log( self.history_logs["rc_event_schedule_initial"] = self.rc_event_schedule_initial """Parse logs to be returned""" - if requested_logs == None: + if requested_logs is None: requested_logs = LOG_DEFAULT log = {name: self.history_logs[name] for name in requested_logs} diff --git a/metainsurancecontract.py b/metainsurancecontract.py index 435f459..25fddcd 100644 --- a/metainsurancecontract.py +++ b/metainsurancecontract.py @@ -1,5 +1,5 @@ from __future__ import annotations -from genericclasses import RiskProperties +from genericclasses import RiskProperties, GenericAgent import metainsuranceorg @@ -41,16 +41,15 @@ def __init__( # TODO: argument reinsurance seems senseless; remove? # Save parameters - self.insurer = insurer + self.insurer: metainsuranceorg.MetaInsuranceOrg = insurer self.risk_factor = risk.risk_factor self.category = risk.category - self.property_holder = risk.owner + self.property_holder: GenericAgent = risk.owner self.value = risk.value self.contract = risk.contract # May be None self.risk = risk - self.insurancetype = ( - risk.insurancetype if insurancetype is None else insurancetype - ) + self.insurancetype = insurancetype or risk.insurancetype + self.runtime = runtime self.starttime = time self.expiration = runtime + time @@ -67,7 +66,6 @@ def __init__( if risk.deductible_fraction is not None else default_deductible_fraction ) - self.deductible = self.deductible_fraction * self.value # set excess from argument, risk property or default value, whichever first is not None diff --git a/metainsuranceorg.py b/metainsuranceorg.py index 64c6c3e..0942734 100644 --- a/metainsuranceorg.py +++ b/metainsuranceorg.py @@ -143,7 +143,7 @@ def __init__( 1 - isleconfig.simulation_parameters["scale_inaccuracy"] ) - self.riskmodel = RiskModel( + self.riskmodel: RiskModel = RiskModel( damage_distribution=rm_config["damage_distribution"], expire_immediately=rm_config["expire_immediately"], cat_separation_distribution=rm_config["cat_separation_distribution"], @@ -192,8 +192,7 @@ def __init__( self.var_category = np.zeros( self.simulation_no_risk_categories ) # var_sum disaggregated by category - self.naccep = [] - self.risks_kept = [] + self.risks_retained = [] self.reinrisks_kept = [] self.balance_ratio = simulation_parameters["insurers_balance_ratio"] self.recursion_limit = simulation_parameters["insurers_recursion_limit"] @@ -433,8 +432,8 @@ def dissolve(self, time: int, record: str): # removing (dissolving) all risks immediately after bankruptcy (may not be realistic, # they might instead be bought by another company) # TODO: implement buyouts - self.simulation.return_risks(self.risks_kept) - self.risks_kept = [] + self.simulation.return_risks(self.risks_retained) + self.risks_retained = [] self.reinrisks_kept = [] obligation = Obligation( amount=self.cash, @@ -474,8 +473,6 @@ def obtain_yield(self, time: int): time: Type integer No return value""" amount = self.cash * self.interest_rate - # TODO: agent should not award her own interest. - # This interest rate should be taken from self.simulation with a getter method self.simulation.receive_obligation(amount, self, time, "yields") def mature_contracts(self, time: int) -> int: @@ -797,7 +794,7 @@ def process_newrisks_insurer( they should be underwritten or not. It is done in this way to maintain the portfolio as balanced as possible. For that reason we process risk[C1], risk[C2], risk[C3], risk[C4], risk[C1], risk[C2], ... and so forth. If risks are accepted then a contract is written.""" - _cached_rvs = self.contract_runtime_dist.rvs() + random_runtime = self.contract_runtime_dist.rvs() not_accepted_risks = [] for risk in roundrobin(risks_per_categ): assert risk @@ -842,7 +839,7 @@ def process_newrisks_insurer( risk, time, self.simulation.get_market_premium(), - _cached_rvs, + random_runtime, self.default_contract_payment_period, expire_immediately=self.simulation_parameters[ "expire_immediately" @@ -983,7 +980,7 @@ def roll_over(self, time: int): [contract.risk] ) # TODO: This is not a retention, so the roll_over_flag might be confusing in this case else: - self.risks_kept.append(contract.risk) + self.risks_retained.append(contract.risk) if self.is_reinsurer: for reincontract in maturing_next: diff --git a/reinsurancecontract.py b/reinsurancecontract.py index f7ad879..ca2a0b3 100644 --- a/reinsurancecontract.py +++ b/reinsurancecontract.py @@ -1,4 +1,5 @@ from metainsurancecontract import MetaInsuranceContract +import insurancefirms class ReinsuranceContract(MetaInsuranceContract): @@ -39,7 +40,8 @@ def __init__( reinsurance, ) # self.is_reinsurancecontract = True - + assert type(self.property_holder) is insurancefirms.InsuranceFirm + self.property_holder: insurancefirms.InsuranceFirm if self.insurancetype not in ["excess-of-loss", "proportional"]: raise ValueError(f'Unrecognised insurance type "{self.insurancetype}"') if self.insurancetype == "excess-of-loss": @@ -62,6 +64,7 @@ def explode(self, time, uniform_value=None, damage_extent=None): No return value. Method marks the contract for termination. """ + assert uniform_value is None # QUERY: What is the difference? Also, what happens if damage_extent = None? if damage_extent > self.deductible: diff --git a/riskmodel.py b/riskmodel.py index 72e99e0..e804970 100644 --- a/riskmodel.py +++ b/riskmodel.py @@ -420,7 +420,7 @@ def add_reinsurance( ): """Method to add any instance of reinsurance to risk models list of reinsurance contracts, and add damage distribution to stack of damage distributions per category, then replace with a new distribution. Only used in - thad add_reinsurance method of insurancefirm. + the add_reinsurance method of insurancefirm. Accepts: categ_id: Type Integer. excess_fraction: Type Decimal. From 7f910e8a3db6e679a16f37fd69f2db3e06b8cf92 Mon Sep 17 00:00:00 2001 From: Chris Hughes Date: Fri, 19 Jul 2019 13:40:34 +0100 Subject: [PATCH 059/125] Allow firms to issue multi-layered requests for reinsurance --- distributionreinsurance.py | 6 +++-- insurancefirms.py | 54 ++++++++++++++++++++++++-------------- riskmodel.py | 3 ++- 3 files changed, 40 insertions(+), 23 deletions(-) diff --git a/distributionreinsurance.py b/distributionreinsurance.py index 0e70cdb..dbca90a 100644 --- a/distributionreinsurance.py +++ b/distributionreinsurance.py @@ -8,6 +8,7 @@ class ReinsuranceDistWrapper: lower_bound is the least reinsured risk (lowest priority), upper_bound is the greatest reinsured risk Note that the bounds are in terms of the values of the distribution, not the probabilities.""" + def __init__(self, dist, lower_bound=None, upper_bound=None): assert lower_bound is not None or upper_bound is not None self.dist = dist @@ -91,10 +92,11 @@ def rvs(self, size=1): x1 = np.linspace(non_truncated.ppf(0.01), non_truncated.ppf(0.99), 100) # x2 = np.linspace(truncated.ppf(0.01), truncated.ppf(0.99), 100) import matplotlib.pyplot as plt + y1 = non_truncated.pdf(x1) y2 = truncated.pdf(x1) - plt.plot(x1, y1, 'r+') - plt.plot(x1, y2, 'bx') + plt.plot(x1, y1, "r+") + plt.plot(x1, y2, "bx") plt.legend(["non truncated", "truncated"]) plt.show() diff --git a/insurancefirms.py b/insurancefirms.py index f64c80a..980a122 100644 --- a/insurancefirms.py +++ b/insurancefirms.py @@ -278,7 +278,7 @@ def characterize_underwritten_risks_by_category( return total_value, avg_risk_factor, number_risks, periodized_total_premium def ask_reinsurance_non_proportional_by_category( - self, time: int, categ_id: int, purpose: str = "newrisk" + self, time: int, categ_id: int, purpose: str = "newrisk", tranches: int = 1 ) -> Optional[genericclasses.RiskProperties]: """Method to create a reinsurance risk for a given category for firm that calls it. Called from increase_ capacity_by_category, ask_reinsurance_non_proportional, and roll_over in metainsuranceorg. @@ -286,11 +286,13 @@ def ask_reinsurance_non_proportional_by_category( time: Type Integer. categ_id: Type Integer. purpose: Type String. Needed for when called from roll_over method as the risk is then returned. + tranches: Type int. Determines how many layers of reinsurance the risk is split over Returns: risk: Type DataDict. Only returned when method used for roll_over. This method is given a category, then characterises all the underwritten risks in that category for the firm - and, assuming firms has underwritten risks in category, creates new reinsurance risk with values based on firms - existing underwritten risks. If the method was called to create a new risks then it is appended to list of + and, assuming firms has underwritten risks in category, creates new reinsurance risks with values based on firms + existing underwritten risks. If tranches > 1, the risk is split between mutliple layers of reinsurance, each of + the same size. If the method was called to create a new risks then it is appended to list of 'reinrisks', otherwise used for creating the risk when a reinsurance contract rolls over.""" [ total_value, @@ -299,23 +301,35 @@ def ask_reinsurance_non_proportional_by_category( periodized_total_premium, ] = self.characterize_underwritten_risks_by_category(categ_id) if number_risks > 0: - risk = genericclasses.RiskProperties( - value=total_value, - category=categ_id, - owner=self, - insurancetype="excess-of-loss", - number_risks=number_risks, - deductible_fraction=self.np_reinsurance_deductible_fraction, - excess_fraction=self.np_reinsurance_excess_fraction, - periodized_total_premium=periodized_total_premium, - runtime=12, - expiration=time + 12, - risk_factor=avg_risk_factor, - ) # TODO: make runtime into a parameter - if purpose == "newrisk": - self.simulation.append_reinrisks(risk) - elif purpose == "rollover": - return risk + lower_boundary, upper_boundary = ( + self.np_reinsurance_deductible_fraction, + self.np_reinsurance_excess_fraction, + ) + # TODO: think about tranche sizes + tranche_boundaries = [ + lower_boundary + n * (upper_boundary - lower_boundary) / tranches + for n in range(tranches + 1) + ] + for tranche in range(tranches): + tranche_lower_bound = tranche_boundaries[tranche] + tranche_upper_bound = tranche_boundaries[tranche + 1] + risk = genericclasses.RiskProperties( + value=total_value, + category=categ_id, + owner=self, + insurancetype="excess-of-loss", + number_risks=number_risks, + deductible_fraction=tranche_lower_bound, + excess_fraction=tranche_upper_bound, + periodized_total_premium=periodized_total_premium, + runtime=12, + expiration=time + 12, + risk_factor=avg_risk_factor, + ) # TODO: make runtime into a parameter + if purpose == "newrisk": + self.simulation.append_reinrisks(risk) + elif purpose == "rollover": + return risk elif number_risks == 0 and purpose == "rollover": return None diff --git a/riskmodel.py b/riskmodel.py index e804970..a98f281 100644 --- a/riskmodel.py +++ b/riskmodel.py @@ -418,7 +418,7 @@ def add_reinsurance( deductible_fraction: float, contract: MetaInsuranceContract, ): - """Method to add any instance of reinsurance to risk models list of reinsurance contracts, and add damage + """Method to add any instance of EoL reinsurance to risk models list of reinsurance contracts, and add damage distribution to stack of damage distributions per category, then replace with a new distribution. Only used in the add_reinsurance method of insurancefirm. Accepts: @@ -427,6 +427,7 @@ def add_reinsurance( deductible_fraction: Type Decimal. contract: Type DataDict. No return values.""" + assert contract.insurancetype == "excess-of-loss" self.damage_distribution_stack[categ_id].append( self.damage_distribution[categ_id] ) From bb16e6951bd3851e34387e3c36cc6d7374ece8a0 Mon Sep 17 00:00:00 2001 From: KloskaT Date: Fri, 19 Jul 2019 14:24:04 +0100 Subject: [PATCH 060/125] Properly integrated metaplotter_pl_timescale_additional_measures_alpha.py. Can now take any provided data file and plot insurance and reinsurance data for each risk model. Saves files automatically. --- visualisation.py | 162 ++++++++++++++++++++++++++++------------------- 1 file changed, 97 insertions(+), 65 deletions(-) diff --git a/visualisation.py b/visualisation.py index 6a4c5eb..646106c 100644 --- a/visualisation.py +++ b/visualisation.py @@ -765,49 +765,89 @@ def plot(self, ax=None, ylabel="PDF(x)", xtralabel="", xlabel="x", num_bins=50, plt.show() -class RiskModelSpecificCompare: # TODO: Automate for all filetypes - def __init__(self, filetype="_premium.dat", refiletype="_reinpremium.dat", number_riskmodels=4): +class RiskModelSpecificCompare: + def __init__(self, infiletype="_premium.dat", refiletype="_reinpremium.dat", number_riskmodels=4): """Initialises class that plots the insurance and reinsurance data for all risk models for specified data. - Currently plots premium data. Need to automate for number of risk models and filenames.""" + Accepts: + infiletype: Type String. The insurance data to be plotted. + refiletype: Type String. The reinsurance data to be plotted. + number_riskmodels. Type Integer. The number of riskmodels used in the data. + This initialises the class by taking the specififed data from its file, calculating its mean, median, and + 25/75th percentiles. then adding it to a dictionary. Defaults to premium data however can plot any data.""" self.timeseries_dict = {} self.timeseries_dict["mean"] = {} self.timeseries_dict["median"] = {} self.timeseries_dict["quantile25"] = {} self.timeseries_dict["quantile75"] = {} - self.filetypes = [filetype, refiletype] + # Original filetypes is needed for the specific case of non insured risks in order to get correct y axis label. + self.original_filetypes = self.filetypes = [infiletype, refiletype] + self.number_riskmodels = number_riskmodels self.riskmodels = ["one", "two", "three", "four"] + # Non insured risks is special as it needs contract data. + uninsured_risks = False + if self.filetypes[0] == "_noninsured_risks.dat": + uninsured_risks = True + num_risks = 20000 + self.filetypes = ["_contracts.dat", "_reincontracts.dat"] + for i in range(number_riskmodels): for j in range(len(self.filetypes)): + # Get required data out of file. filename = "data/"+self.riskmodels[i]+self.filetypes[j] rfile = open(filename, "r") data = [eval(k) for k in rfile] rfile.close() - # compute data series + if uninsured_risks and j == 1: # Need operational data for calculating uninsured reinsurance risks. + rfile = open("data/" + self.riskmodels[i] + "_operational.dat", "r") + n_insurers = [eval(d) for d in rfile] + rfile.close() + n_pr = 4 # Number of peril categories + + # Compute data series data_means = [] data_medians = [] data_q25 = [] data_q75 = [] for k in range(len(data[0])): - data_means.append(np.mean([item[k] for item in data])) - data_q25.append(np.percentile([item[k] for item in data], 25)) - data_q75.append(np.percentile([item[k] for item in data], 75)) - data_medians.append(np.median([item[k] for item in data])) + if not uninsured_risks: # Used for most risks. + data_means.append(np.mean([item[k] for item in data])) + data_q25.append(np.percentile([item[k] for item in data], 25)) + data_q75.append(np.percentile([item[k] for item in data], 75)) + data_medians.append(np.median([item[k] for item in data])) + elif uninsured_risks and j == 0: # Used for uninsured insurance risks. + data_means.append(np.mean([num_risks - item[k] for item in data])) + data_q25.append(np.percentile([num_risks - item[k] for item in data], 25)) + data_q75.append(np.percentile([num_risks - item[k] for item in data], 75)) + data_medians.append(np.median([num_risks - item[k] for item in data])) + elif uninsured_risks and j ==1: # Used for uninsured reinsurance risks. + data_means.append(np.mean([n_pr * n_insurers[n][k] - data[n][k] for n in range(len(n_insurers))])) + data_q25.append(np.percentile([n_pr * n_insurers[n][k] - data[n][k] for n in range(len(n_insurers))],25)) + data_q75.append(np.percentile([n_pr * n_insurers[n][k] - data[n][k] for n in range(len(n_insurers))],75)) + data_medians.append(np.median([n_pr * n_insurers[n][k] - data[n][k] for n in range(len(n_insurers))])) + data_means = np.array(data_means) data_medians = np.array(data_medians) data_q25 = np.array(data_q25) data_q75 = np.array(data_q75) - # record data series + # Record data series self.timeseries_dict["mean"][filename] = data_means self.timeseries_dict["median"][filename] = data_medians self.timeseries_dict["quantile25"][filename] = data_q25 self.timeseries_dict["quantile75"][filename] = data_q75 def plot(self, outputfile): - colors = {"one": "red", "two": "blue", "three": "green", "four": "yellow"} + """Method to plot the insurance and reinsurance data for each risk model already initialised. Automatically + saves data as pdf using argument provided. + Accepts: + outputfile: Type string. Used in naming of file to be saved to. + No return values.""" + + # List of colours and labels used in plotting + colours = {"one": "red", "two": "blue", "three": "green", "four": "yellow"} labels = {"reinexcess_capital": "Excess Capital (Reinsurers)", "excess_capital": "Excess Capital (Insurers)", "cumulative_unrecovered_claims": "Uncovered Claims (cumulative)", "cumulative_bankruptcies": "Bankruptcies (cumulative)", @@ -816,58 +856,44 @@ def plot(self, outputfile): "reinprofitslosses": "Profits and Losses (Reinsurer)", "reincash": "Liquidity (Reinsurers)", "reincontracts": "Contracts (Reinsurers)", "reinoperational": "Active Reinsurers", "reinpremium": "Premium (Reinsurance)", "noninsured_risks": "Non-insured risks (Insurance)", - "noninsured_reinrisks": "Non-insured risks (Insurance)"} + "noninsured_reinrisks": "Non-insured risks (Reinsurance)"} # Backup existing figures (so as not to overwrite them) - outputfilename = "data/" + outputfile + ".pdf" - backupfilename = "data/" + outputfile + "_old_" + time.strftime('%Y_%b_%d_%H_%M') + ".pdf" + outputfilename = "figures/" + outputfile + "_riskmodel_comparison.pdf" + backupfilename = "figures/" + outputfile + "_riskmodel_comparison_old_" + time.strftime('%Y_%b_%d_%H_%M') + ".pdf" if os.path.exists(outputfilename): os.rename(outputfilename, backupfilename) - self.fig = plt.figure() - - self.ax0 = self.fig.add_subplot(211) - maxlen_plots = 0 - self.ax0.plot(range(len(self.timeseries_dict["mean"]["data/one_premium.dat"]))[1200:], self.timeseries_dict["mean"]["data/one_premium.dat"][1200:], color="red", label="One Riskmodel") - self.ax0.plot(range(len(self.timeseries_dict["mean"]["data/two_premium.dat"]))[1200:], self.timeseries_dict["mean"]["data/two_premium.dat"][1200:], color="blue", label="Two Riskmodels") - self.ax0.plot(range(len(self.timeseries_dict["mean"]["data/three_premium.dat"]))[1200:], self.timeseries_dict["mean"]["data/three_premium.dat"][1200:], color="green", label="Three Riskmodels") - self.ax0.plot(range(len(self.timeseries_dict["mean"]["data/four_premium.dat"]))[1200:], self.timeseries_dict["mean"]["data/four_premium.dat"][1200:], color="yellow", label="Four Riskmodels") - - self.ax0.fill_between(range(len(self.timeseries_dict["quantile25"]["data/one_premium.dat"]))[1200:],self.timeseries_dict["quantile25"]["data/one_premium.dat"][1200:], self.timeseries_dict["quantile75"]["data/one_premium.dat"][1200:],facecolor="red", alpha=0.25) - self.ax0.fill_between(range(len(self.timeseries_dict["quantile25"]["data/one_premium.dat"]))[1200:],self.timeseries_dict["quantile25"]["data/two_premium.dat"][1200:], self.timeseries_dict["quantile75"]["data/two_premium.dat"][1200:],facecolor="blue", alpha=0.25) - self.ax0.fill_between(range(len(self.timeseries_dict["quantile25"]["data/two_premium.dat"]))[1200:],self.timeseries_dict["quantile25"]["data/three_premium.dat"][1200:], self.timeseries_dict["quantile75"]["data/three_premium.dat"][1200:],facecolor="green", alpha=0.25) - self.ax0.fill_between(range(len(self.timeseries_dict["quantile25"]["data/three_premium.dat"]))[1200:],self.timeseries_dict["quantile25"]["data/four_premium.dat"][1200:], self.timeseries_dict["quantile75"]["data/four_premium.dat"][1200:],facecolor="yellow", alpha=0.25) - - maxlen_plots = max(maxlen_plots, len(self.timeseries_dict["mean"]["data/one_premium.dat"]), len(self.timeseries_dict["mean"]["data/two_premium.dat"]), len(self.timeseries_dict["mean"]["data/three_premium.dat"]), len(self.timeseries_dict["mean"]["data/four_premium.dat"])) - xticks = np.arange(1200, maxlen_plots, step=600) - self.ax0.set_xticks(xticks) - self.ax0.set_xticklabels(["${0:d}$".format(int((xtc - 1200) / 12)) for xtc in xticks]); - self.ax0.legend(loc='best') - self.ax0.set_ylabel(labels["premium"]) - - self.ax1 = self.fig.add_subplot(212) - maxlen_plots = 0 - self.ax1.plot(range(len(self.timeseries_dict["mean"]["data/one_reinpremium.dat"]))[1200:], self.timeseries_dict["mean"]["data/one_reinpremium.dat"][1200:], color="red", label="One Riskmodel") - self.ax1.plot(range(len(self.timeseries_dict["mean"]["data/two_reinpremium.dat"]))[1200:], self.timeseries_dict["mean"]["data/two_reinpremium.dat"][1200:], color="blue", label="Two Riskmodels") - self.ax1.plot(range(len(self.timeseries_dict["mean"]["data/three_reinpremium.dat"]))[1200:], self.timeseries_dict["mean"]["data/three_reinpremium.dat"][1200:], color="green", label="Three Riskmodels") - self.ax1.plot(range(len(self.timeseries_dict["mean"]["data/four_reinpremium.dat"]))[1200:], self.timeseries_dict["mean"]["data/four_reinpremium.dat"][1200:], color="yellow", label="Four Riskmodels") - - self.ax1.fill_between(range(len(self.timeseries_dict["quantile25"]["data/one_reinpremium.dat"]))[1200:], self.timeseries_dict["quantile25"]["data/one_reinpremium.dat"][1200:], self.timeseries_dict["quantile75"]["data/one_reinpremium.dat"][1200:], facecolor="red", alpha=0.25) - self.ax1.fill_between(range(len(self.timeseries_dict["quantile25"]["data/one_reinpremium.dat"]))[1200:], self.timeseries_dict["quantile25"]["data/two_reinpremium.dat"][1200:], self.timeseries_dict["quantile75"]["data/two_reinpremium.dat"][1200:], facecolor="blue", alpha=0.25) - self.ax1.fill_between(range(len(self.timeseries_dict["quantile25"]["data/two_reinpremium.dat"]))[1200:], self.timeseries_dict["quantile25"]["data/three_reinpremium.dat"][1200:], self.timeseries_dict["quantile75"]["data/three_reinpremium.dat"][1200:], facecolor="green", alpha=0.25) - self.ax1.fill_between(range(len(self.timeseries_dict["quantile25"]["data/three_reinpremium.dat"]))[1200:], self.timeseries_dict["quantile25"]["data/four_reinpremium.dat"][1200:], self.timeseries_dict["quantile75"]["data/four_reinpremium.dat"][1200:], facecolor="yellow", alpha=0.25) - - maxlen_plots = max(maxlen_plots, len(self.timeseries_dict["mean"]["data/one_reinpremium.dat"]), len(self.timeseries_dict["mean"]["data/two_reinpremium.dat"]), len(self.timeseries_dict["mean"]["data/three_reinpremium.dat"]), len(self.timeseries_dict["mean"]["data/four_reinpremium.dat"])) - - xticks = np.arange(1200, maxlen_plots, step=600) - self.ax1.set_xticks(xticks) - self.ax1.set_xticklabels(["${0:d}$".format(int((xtc - 1200) / 12)) for xtc in xticks]); - self.ax1.set_ylabel(labels["reinpremium"]) - self.ax1.set_xlabel("Years") - + # Create figure and two subplot axes then plot on them using loop. + self.fig, self.axs = plt.subplots(2, 1) + for f in range(len(self.filetypes)): # Loop for plotting insurance then reinsurance data (length 2). + maxlen_plots = 0 + for i in range(self.number_riskmodels): # Loop through number of risk models. + # Needed for the fill_between method for plotting percentile data. + if i <= 1: + j = 0 + else: + j = i-1 + filename = "data/"+self.riskmodels[i]+self.filetypes[f] + self.axs[f].plot(range(len(self.timeseries_dict["mean"][filename]))[1200:], self.timeseries_dict["mean"][filename][1200:], color=colours[self.riskmodels[i]], label=self.riskmodels[i]+" riskmodel(s)") + self.axs[f].fill_between(range(len(self.timeseries_dict["quantile25"]["data/"+self.riskmodels[j]+self.filetypes[f]]))[1200:],self.timeseries_dict["quantile25"][filename][1200:],self.timeseries_dict["quantile75"][filename][1200:],facecolor=colours[self.riskmodels[i]], alpha=0.25) + maxlen_plots = max(maxlen_plots, len(self.timeseries_dict["mean"][filename])) + + # Labels axes. + xticks = np.arange(1200, maxlen_plots, step=600) + self.axs[f].set_xticks(xticks) + self.axs[f].set_xticklabels(["${0:d}$".format(int((xtc - 1200) / 12)) for xtc in xticks]); + ylabel = self.original_filetypes[f][1:-4] + self.axs[f].set_ylabel(labels[ylabel]) + + # Adds legend to top subplot and x axis label to bottom subplot. + self.axs[0].legend(loc='best') + self.axs[1].set_xlabel('Years') plt.tight_layout() + + # Saves figure and notifies user (no plt.show so allows progress tracking) self.fig.savefig(outputfilename) - plt.show() + print("Have saved " + outputfile + " data") if __name__ == "__main__": @@ -876,21 +902,21 @@ def plot(self, outputfile): parser.add_argument("--single", action="store_true", help="plot a single run of the insurance model") parser.add_argument("--pie", action="store_true", help="plot animated pie charts of contract and cash data") parser.add_argument("--timeseries", action="store_true", help="plot time series of firm data") - parser.add_argument("--comparison", action="store_true", help="plot time series for an ensemble of replicatons of " + parser.add_argument("--timeseries_comparison", action="store_true", help="plot insurance and reinsurance " + "time series for an ensemble of replications of " "the insurance model") parser.add_argument("--firmdistribution", action="store_true", help="plot the CDFs of firm size distributions with quartiles indicating variation across " "ensemble") parser.add_argument("--bankruptcydistribution", action="store_true", help="plot the histograms of bankruptcy events/unrecovered claims across ensemble") - parser.add_argument("--riskmodel_comparison", action="store_true") + parser.add_argument("--riskmodel_comparison", action="store_true", + help="Plot data comparing risk models for both insurance and reinsurance firms.") args = parser.parse_args() - args.riskmodel_comparison = True - if args.single: - # load in data from the history_logs dictionarywith open("data/history_logs.dat","r") as rfile: + # load in data from the history_logs dictionary with open("data/history_logs.dat","r") as rfile: with open("data/history_logs.dat","r") as rfile: history_logs_list = [eval(k) for k in rfile] # one dict on each line @@ -907,7 +933,7 @@ def plot(self, outputfile): vis.show() N = len(history_logs_list) - if args.comparison or args.firmdistribution or args.bankruptcydistribution: + if args.timeseries_comparison or args.firmdistribution or args.bankruptcydistribution: vis_list = [] colour_list = ['red', 'blue', 'green', 'yellow'] @@ -918,7 +944,7 @@ def plot(self, outputfile): history_logs_list = [eval(k) for k in rfile] # one dict on each line vis_list.append(visualisation(history_logs_list)) - if args.comparison: + if args.timeseries_comparison: # Creates time series for all risk models in ensemble data. cmp_rsk = compare_riskmodels(vis_list, colour_list) cmp_rsk.create_insurer_timeseries(percentiles=[10, 90]) @@ -948,7 +974,13 @@ def plot(self, outputfile): VaR005guess=0.1) # =691186.8726311699) # this is the VaR threshold for 4 risk models with reinsurance if args.riskmodel_comparison: - compare = RiskModelSpecificCompare() - compare.plot("figures/premium.data") + # Lists of insurance and reinsurance data files to be compared (Must be same size and equivalent). + data_types = ["_noninsured_risks.dat", "_excess_capital.dat", "_cash.dat", "_contracts.dat", "_premium.dat", "_operational.dat"] + rein_data_types = ["_noninsured_reinrisks.dat", "_reinexcess_capital.dat", "_reincash.dat", "_reincontracts.dat", "_reinpremium.dat", "_reinoperational.dat"] + + # Loops through data types and loads, plots, and saves each one. + for type in range(len(data_types)): + compare = RiskModelSpecificCompare(infiletype=data_types[type], refiletype=rein_data_types[type]) + compare.plot(outputfile=data_types[type][1:-4]) # ਲ਼ From 868a7d8e53a91d8fe46fc738be3441528e144288 Mon Sep 17 00:00:00 2001 From: KloskaT Date: Fri, 19 Jul 2019 14:31:22 +0100 Subject: [PATCH 061/125] Updated README for visualisation. --- README.md | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index cdf0bfb..16670c6 100644 --- a/README.md +++ b/README.md @@ -58,7 +58,12 @@ which takes the arguments ```[--save] [--number_iterations]``` if you want the a many time iterations you want in the animation. #### Ensemble runs -Ensemble runs can be plotted if the correct data is available using ``visualisation.py``. Takes the arguments -``[--comparison]`` for an averaged time series, ``[--firmdistribution]`` for a CDF of firm size using amount of cash, -and ``[--bankruptcydistribution]`` for histograms of bankruptcy events per number of risk models. +Ensemble runs can be plotted if the correct data is available using ``visualisation.py`` which has a number of arguments. + +``` +visualiation.py [--timeseries_comparison] [--firmdistribution] + [--bankruptcydistribution] [--compare_riskmodels] +``` + +See help for more information. From 695929d55c6cd6ea6512cf872df93644bae0cbe3 Mon Sep 17 00:00:00 2001 From: Chris Hughes Date: Fri, 19 Jul 2019 14:44:51 +0100 Subject: [PATCH 062/125] Start creating a way for firms to track reinsurance --- genericclasses.py | 24 +++++++++++++++++++++++- insurancesimulation.py | 1 + riskmodel.py | 4 +++- 3 files changed, 27 insertions(+), 2 deletions(-) diff --git a/genericclasses.py b/genericclasses.py index 4fd5d64..25fe371 100644 --- a/genericclasses.py +++ b/genericclasses.py @@ -3,12 +3,14 @@ import numpy as np from scipy import stats import dataclasses -from typing import Mapping, MutableSequence, Union +from typing import Mapping, MutableSequence, Union, Tuple # Not totally sure about the best way to resolve circular dependencies caused by type hinting # from metainsurancecontract import MetaInsuranceContract from distributiontruncated import TruncatedDistWrapper from distributionreinsurance import ReinsuranceDistWrapper +from reinsurancecontract import ReinsuranceContract +import isleconfig Distribution = Union[stats.rv_continuous, TruncatedDistWrapper, ReinsuranceDistWrapper] @@ -163,3 +165,23 @@ def _rvs(self, *args) -> Union[np.ndarray, float]: Constant = ConstantGen(name="constant") + + +class ReinsuranceProfile: + """Class for keeping track of the reinsurance that an insurance firm holds + + All reinsurance is assumed to be on open intervals""" + # TODO: add, remove, explode, get uninsured regions + def __init__(self): + self.reinsured_regions: MutableSequence[MutableSequence[Tuple[float, float, ReinsuranceContract]]] = [[] for _ in range(isleconfig.simulation_parameters["no_categories"])] + self.not_reinsured_regions: MutableSequence[MutableSequence[Tuple[float, float]]] = [[(0, np.float_("inf"))] for _ in range(isleconfig.simulation_parameters["no_categories"])] + + def add_reinsurance(self, contract: ReinsuranceContract): + lower_bound = contract.deductible + upper_bound = contract.excess + for index, region in enumerate(self.not_reinsured_regions): + if region[0] <= lower_bound: + pass + self.reinsured_regions[contract.category].append( + (contract.deductible_fraction, contract.excess_fraction, contract) + ) diff --git a/insurancesimulation.py b/insurancesimulation.py index 990755d..6eccbb5 100644 --- a/insurancesimulation.py +++ b/insurancesimulation.py @@ -68,6 +68,7 @@ def __init__( self.simulation_parameters: MutableMapping = simulation_parameters self.simulation_parameters["simulation"] = self + # QUERY: The distribution given is bounded by [0.25, 1.0]. Should this always be the case? "Unpacks parameters and sets distributions" self.damage_distribution: Distribution = damage_distribution diff --git a/riskmodel.py b/riskmodel.py index a98f281..e96a967 100644 --- a/riskmodel.py +++ b/riskmodel.py @@ -38,7 +38,7 @@ def __init__( the share of risks suffering damage as part of any single catastrophic peril""" self.damage_distribution: MutableSequence[Distribution] = [ damage_distribution for _ in range(self.category_number) - ] # TODO: separate that category wise? -> DONE. + ] self.damage_distribution_stack: Sequence[MutableSequence[Distribution]] = [ [] for _ in range(self.category_number) ] @@ -432,6 +432,8 @@ def add_reinsurance( self.damage_distribution[categ_id] ) self.reinsurance_contract_stack[categ_id].append(contract) + # QUERY: The riskmodel is based on the fractions, but these do not precisely correspond to the actual recovered + # claim if the value insured in that category grows or shrinks self.damage_distribution[categ_id] = ReinsuranceDistWrapper( lower_bound=deductible_fraction, upper_bound=excess_fraction, From aee9048cbfd9d9f433f2da1c508a0944df6b7720 Mon Sep 17 00:00:00 2001 From: Chris Hughes Date: Tue, 23 Jul 2019 12:14:27 +0100 Subject: [PATCH 063/125] Merge in changes for python 3.6 (and notes from meeting) --- catbond.py | 27 +++++++------- distributionreinsurance.py | 1 + genericclasses.py | 44 ++++++++++++++--------- insurancecontract.py | 17 ++++----- insurancefirms.py | 16 +++++---- insurancesimulation.py | 35 ++++++++++-------- metainsurancecontract.py | 15 ++++---- metainsuranceorg.py | 62 ++++++++++++++++---------------- reinsurancecontract.py | 47 +++++++++++++----------- requirements.txt | 1 + riskmodel.py | 74 +++++++++++++++++--------------------- start.py | 1 - 12 files changed, 179 insertions(+), 161 deletions(-) diff --git a/catbond.py b/catbond.py index 6907558..1fd4b81 100644 --- a/catbond.py +++ b/catbond.py @@ -1,10 +1,13 @@ -from __future__ import annotations import isleconfig from metainsuranceorg import MetaInsuranceOrg -import genericclasses -import metainsurancecontract -import insurancesimulation +from genericclasses import Obligation, GenericAgent + from typing import MutableSequence +from typing import TYPE_CHECKING +if TYPE_CHECKING: + from insurancesimulation import InsuranceSimulation + from metainsurancecontract import MetaInsuranceContract + # TODO: This and MetaInsuranceOrg should probably both subclass something simple - a MetaAgent, say. MetaInsuranceOrg # can do more than a CatBond should be able to! @@ -16,9 +19,9 @@ class CatBond(MetaInsuranceOrg): # TODO inheret GenericAgent instead of MetaInsuranceOrg? def __init__( self, - simulation: insurancesimulation.InsuranceSimulation, + simulation: "InsuranceSimulation", per_period_premium: float, - owner: genericclasses.GenericAgent, + owner: GenericAgent, interest_rate: float = 0, ): """Initialising methods. @@ -30,13 +33,13 @@ def __init__( self.simulation = simulation self.id: int = 0 self.underwritten_contracts: MutableSequence[ - metainsurancecontract.MetaInsuranceContract + "MetaInsuranceContract" ] = [] self.cash: float = 0 self.profits_losses: float = 0 - self.obligations: MutableSequence[genericclasses.Obligation] = [] + self.obligations: MutableSequence[Obligation] = [] self.operational: bool = True - self.owner: genericclasses.GenericAgent = owner + self.owner: GenericAgent = owner self.per_period_dividend: float = per_period_premium self.interest_rate: float = interest_rate # TODO: shift obtain_yield method to insurancesimulation, thereby making it unnecessary to drag parameters like @@ -90,7 +93,7 @@ def iterate(self, time: int): # self.estimate_var() # cannot compute VaR for catbond as catbond does not have a riskmodel - def set_owner(self, owner: genericclasses.GenericAgent): + def set_owner(self, owner: GenericAgent): """Method to set owner of the Cat Bond. Accepts: owner: Type class @@ -99,7 +102,7 @@ def set_owner(self, owner: genericclasses.GenericAgent): if isleconfig.verbose: print("SOLD") - def set_contract(self, contract: metainsurancecontract.MetaInsuranceContract): + def set_contract(self, contract: "MetaInsuranceContract"): """Method to record new instances of CatBonds. Accepts: owner: Type class @@ -114,7 +117,7 @@ def mature_bond(self): When the catbond contract matures this is called which pays the value of the catbond to the simulation, and is then deleted from the list of agents.""" if self.operational: - obligation = genericclasses.Obligation( + obligation = Obligation( amount=self.cash, recipient=self.simulation, due_time=1, diff --git a/distributionreinsurance.py b/distributionreinsurance.py index 0e70cdb..a703b67 100644 --- a/distributionreinsurance.py +++ b/distributionreinsurance.py @@ -1,6 +1,7 @@ import functools import numpy as np import scipy.stats +import warnings class ReinsuranceDistWrapper: diff --git a/genericclasses.py b/genericclasses.py index 4fd5d64..5d3135f 100644 --- a/genericclasses.py +++ b/genericclasses.py @@ -1,26 +1,36 @@ -from __future__ import annotations +from itertools import chain +import dataclasses +from sortedcontainers import SortedList import numpy as np from scipy import stats -import dataclasses -from typing import Mapping, MutableSequence, Union -# Not totally sure about the best way to resolve circular dependencies caused by type hinting -# from metainsurancecontract import MetaInsuranceContract -from distributiontruncated import TruncatedDistWrapper -from distributionreinsurance import ReinsuranceDistWrapper +import isleconfig -Distribution = Union[stats.rv_continuous, TruncatedDistWrapper, ReinsuranceDistWrapper] +from typing import Mapping, MutableSequence, Union, Tuple +from typing import TYPE_CHECKING +if TYPE_CHECKING: + from metainsurancecontract import MetaInsuranceContract + from distributiontruncated import TruncatedDistWrapper + from distributionreinsurance import ReinsuranceDistWrapper + from reinsurancecontract import ReinsuranceContract + from riskmodel import RiskModel + + Distribution = Union[ + "stats.rv_continuous", + "TruncatedDistWrapper", + "ReinsuranceDistWrapper", + ] class GenericAgent: def __init__(self): self.cash: float = 0 - self.obligations: MutableSequence[Obligation] = [] + self.obligations: MutableSequence["Obligation"] = [] self.operational: bool = True self.profits_losses: float = 0 - def _pay(self, obligation: Obligation): + def _pay(self, obligation: "Obligation"): """Method to _pay other class instances. Accepts: Obligation: Type DataDict @@ -29,6 +39,7 @@ def _pay(self, obligation: Obligation): amount = obligation.amount recipient = obligation.recipient purpose = obligation.purpose + # TODO: Think about what happens when paying non-operational firms if self.get_operational() and recipient.get_operational(): self.cash -= amount if purpose is not "dividend": @@ -57,8 +68,7 @@ def _effect_payments(self, time: int): # time, in which case this could be done slightly better). Low priority, but something to consider due = [item for item in self.obligations if item.due_time <= time] self.obligations = [item for item in self.obligations if item.due_time > time] - # QUERY: could this cause a firm to enter illiquidity if it has obligations to non-operational firms? Such - # firms can't recieve payment, so this possibly shouldn't happen. + sum_due = sum([item.amount for item in due]) if sum_due > self.cash: self.obligations += due @@ -71,7 +81,7 @@ def enter_illiquidity(self, time: int, sum_due: float): raise NotImplementedError() def receive_obligation( - self, amount: float, recipient: GenericAgent, due_time: int, purpose: str + self, amount: float, recipient: "GenericAgent", due_time: int, purpose: str ): """Method for receiving obligations that the firm will have to _pay. Accepts: @@ -100,10 +110,10 @@ class RiskProperties: risk_factor: float value: float category: int - owner: GenericAgent + owner: "GenericAgent" number_risks: int = 1 - contract: "metainsurancecontract.MetaInsuranceContract" = None + contract: "MetaInsuranceContract" = None insurancetype: str = None deductible: float = None runtime: int = None @@ -141,7 +151,7 @@ class Obligation: """Class for holding the properties of an obligation""" amount: float - recipient: GenericAgent + recipient: "GenericAgent" due_time: int purpose: str @@ -149,7 +159,7 @@ class Obligation: class ConstantGen(stats.rv_continuous): def _pdf(self, x: float, *args) -> float: a = np.float_(x == 0) - a[a == 1.0] = np.float_("inf") + a[a == 1.0] = np.inf return a def _cdf(self, x: float, *args) -> float: diff --git a/insurancecontract.py b/insurancecontract.py index 21a68fc..893d039 100644 --- a/insurancecontract.py +++ b/insurancecontract.py @@ -1,9 +1,10 @@ -from __future__ import annotations - import metainsurancecontract -import genericclasses -import metainsuranceorg -import insurancesimulation + +from typing import TYPE_CHECKING +if TYPE_CHECKING: + from metainsuranceorg import MetaInsuranceOrg + from insurancesimulation import InsuranceSimulation + from genericclasses import RiskProperties class InsuranceContract(metainsurancecontract.MetaInsuranceContract): @@ -16,8 +17,8 @@ class InsuranceContract(metainsurancecontract.MetaInsuranceContract): def __init__( self, - insurer: metainsuranceorg.MetaInsuranceOrg, - risk: genericclasses.RiskProperties, + insurer: "MetaInsuranceOrg", + risk: "RiskProperties", time: int, premium: float, runtime: int, @@ -45,7 +46,7 @@ def __init__( ) # the property holder in an insurance contract should always be the simulation assert self.property_holder is self.insurer.simulation - self.property_holder: insurancesimulation.InsuranceSimulation + self.property_holder: "InsuranceSimulation" def explode(self, time, uniform_value=None, damage_extent=None): """Explode method. diff --git a/insurancefirms.py b/insurancefirms.py index f64c80a..85f0858 100644 --- a/insurancefirms.py +++ b/insurancefirms.py @@ -1,16 +1,18 @@ -from __future__ import annotations -from typing import Optional, Tuple, MutableSequence, Mapping - import numpy as np -from metainsuranceorg import MetaInsuranceOrg +import metainsuranceorg import catbond from reinsurancecontract import ReinsuranceContract import isleconfig import genericclasses +from typing import Optional, MutableSequence, Mapping, Tuple + +from typing import TYPE_CHECKING +if TYPE_CHECKING: + pass -class InsuranceFirm(MetaInsuranceOrg): +class InsuranceFirm(metainsuranceorg.MetaInsuranceOrg): """ReinsuranceFirm class. Inherits from MetaInsuranceFirm.""" @@ -41,7 +43,7 @@ def adjust_dividends(self, time: int, actual_capacity: float): self.per_period_dividend = 0 def get_reinsurance_var_estimate(self, max_var: float) -> float: - """Method to estimate the VaR if another reinsurance contract were to be taken. + """Method to estimate the VaR if another reinsurance contract were to be taken out. Accepts: max_var: Type Decimal. Max value at risk Returns: @@ -72,7 +74,7 @@ def adjust_capacity_target(self, max_var: float): reinsurance_var_estimate = self.get_reinsurance_var_estimate(max_var) if max_var + reinsurance_var_estimate == 0: # TODO: why is this being called with max_var = 0 anyway? - capacity_target_var_ratio_estimate = float("inf") + capacity_target_var_ratio_estimate = np.inf else: capacity_target_var_ratio_estimate = ( (self.capacity_target + reinsurance_var_estimate) diff --git a/insurancesimulation.py b/insurancesimulation.py index 990755d..b6e99e3 100644 --- a/insurancesimulation.py +++ b/insurancesimulation.py @@ -1,12 +1,8 @@ -from __future__ import annotations import math import random import copy - -import genericclasses import logger import warnings -from typing import Mapping, MutableMapping, MutableSequence, Sequence, Any, Optional import scipy.stats import numpy as np @@ -15,10 +11,20 @@ import visualization_network import insurancefirms import isleconfig -from genericclasses import GenericAgent, RiskProperties, AgentProperties, Distribution -from metainsuranceorg import MetaInsuranceOrg +from genericclasses import ( + GenericAgent, + RiskProperties, + AgentProperties, + Constant, +) import catbond +from typing import Mapping, MutableMapping, MutableSequence, Sequence, Any, Optional +from typing import TYPE_CHECKING +if TYPE_CHECKING: + from genericclasses import Distribution +from metainsuranceorg import MetaInsuranceOrg + class InsuranceSimulation(GenericAgent): """ Simulation object that is responsible for handling all aspects of the world. @@ -39,7 +45,7 @@ def __init__( simulation_parameters: MutableMapping, rc_event_schedule: MutableSequence[MutableSequence[int]], rc_event_damage: MutableSequence[MutableSequence[float]], - damage_distribution: Distribution = TruncatedDistWrapper( + damage_distribution: "Distribution" = TruncatedDistWrapper( lower_bound=0.25, upper_bound=1.0, dist=scipy.stats.pareto(b=2, loc=0, scale=0.25), @@ -68,8 +74,9 @@ def __init__( self.simulation_parameters: MutableMapping = simulation_parameters self.simulation_parameters["simulation"] = self + # QUERY: The distribution given is bounded by [0.25, 1.0]. Should this always be the case? "Unpacks parameters and sets distributions" - self.damage_distribution: Distribution = damage_distribution + self.damage_distribution: "Distribution" = damage_distribution self.catbonds_off: bool = simulation_parameters["catbonds_off"] self.reinsurance_off: bool = simulation_parameters["reinsurance_off"] @@ -92,9 +99,9 @@ def __init__( loc=self.risk_factor_lower_bound, scale=self.risk_factor_spread ) else: - self.risk_factor_distribution = genericclasses.Constant(loc=1.0) + self.risk_factor_distribution = Constant(loc=1.0) # self.risk_value_distribution = scipy.stats.uniform(loc=100, scale=9900) - self.risk_value_distribution = genericclasses.Constant(loc=1000) + self.risk_value_distribution = Constant(loc=1000) risk_factor_mean = self.risk_factor_distribution.mean() @@ -361,7 +368,7 @@ def add_agents( self, agent_class: type, agent_class_string: str, - agents: Sequence[GenericAgent] = None, + agents: Sequence["GenericAgent"] = None, n: int = 1, ): """Method for building agents and adding them to the simulation. Can also add pre-made catbond agents directly @@ -873,7 +880,7 @@ def get_reinrisks(self) -> Sequence[RiskProperties]: return self.reinrisks def solicit_insurance_requests( - self, insurer: MetaInsuranceOrg + self, insurer: "MetaInsuranceOrg" ) -> Sequence[RiskProperties]: """Method for determining which risks are to be assessed by firms based on insurer weights Accepts: @@ -894,7 +901,7 @@ def solicit_insurance_requests( return risks_to_be_sent def solicit_reinsurance_requests( - self, reinsurer: MetaInsuranceOrg + self, reinsurer: "MetaInsuranceOrg" ) -> Sequence[RiskProperties]: """Method for determining which reinsurance risks are to be assessed by firms based on reinsurer weights Accepts: @@ -1129,7 +1136,7 @@ def reset_pls(self): for cb in self.catbonds: cb.reset_pl() - def get_risk_share(self, firm: MetaInsuranceOrg) -> float: + def get_risk_share(self, firm: "MetaInsuranceOrg") -> float: """Method to determine the total percentage of risks in the market that are held by a particular firm. For insurers uses insurance risks, for reinsurers uses reinsurance risks diff --git a/metainsurancecontract.py b/metainsurancecontract.py index 25fddcd..b4a1c45 100644 --- a/metainsurancecontract.py +++ b/metainsurancecontract.py @@ -1,13 +1,14 @@ -from __future__ import annotations -from genericclasses import RiskProperties, GenericAgent -import metainsuranceorg +from typing import TYPE_CHECKING +if TYPE_CHECKING: + from metainsuranceorg import MetaInsuranceOrg + from genericclasses import GenericAgent, RiskProperties class MetaInsuranceContract: def __init__( self, - insurer: metainsuranceorg.MetaInsuranceOrg, - risk: RiskProperties, + insurer: "MetaInsuranceOrg", + risk: "RiskProperties", time: int, premium: float, runtime: int, @@ -41,10 +42,10 @@ def __init__( # TODO: argument reinsurance seems senseless; remove? # Save parameters - self.insurer: metainsuranceorg.MetaInsuranceOrg = insurer + self.insurer: "MetaInsuranceOrg" = insurer self.risk_factor = risk.risk_factor self.category = risk.category - self.property_holder: GenericAgent = risk.owner + self.property_holder: "GenericAgent" = risk.owner self.value = risk.value self.contract = risk.contract # May be None self.risk = risk diff --git a/metainsuranceorg.py b/metainsuranceorg.py index 0942734..4561b96 100644 --- a/metainsuranceorg.py +++ b/metainsuranceorg.py @@ -1,29 +1,28 @@ -from __future__ import annotations -import copy import math import functools -from typing import ( - Optional, - Tuple, - Sequence, - Mapping, - MutableSequence, - Iterable, - Callable, - Any, -) -from itertools import cycle, islice +import copy +from itertools import cycle, islice, chain import numpy as np import scipy.stats import isleconfig -from insurancecontract import InsuranceContract -import insurancesimulation -from reinsurancecontract import ReinsuranceContract -import metainsurancecontract -from riskmodel import RiskModel -from genericclasses import GenericAgent, RiskProperties, AgentProperties, Obligation +import insurancecontract +import reinsurancecontract +import riskmodel +from genericclasses import ( + GenericAgent, + RiskProperties, + AgentProperties, + Obligation, +) + +from typing import Optional, Tuple, Sequence, Mapping, MutableSequence, Iterable, Callable, Any +from typing import TYPE_CHECKING +if TYPE_CHECKING: + from insurancesimulation import InsuranceSimulation + from metainsurancecontract import MetaInsuranceContract + from reinsurancecontract import ReinsuranceContract def roundrobin(iterables: Sequence[Iterable]) -> Iterable: @@ -75,7 +74,7 @@ def __init__( Constructor creates general instance of an insurance company which is inherited by the reinsurance and insurance firm classes. Initialises all necessary values provided by config file.""" super().__init__() - self.simulation: insurancesimulation.InsuranceSimulation = simulation_parameters[ + self.simulation: "InsuranceSimulation" = simulation_parameters[ "simulation" ] self.simulation_parameters: Mapping = simulation_parameters @@ -143,7 +142,7 @@ def __init__( 1 - isleconfig.simulation_parameters["scale_inaccuracy"] ) - self.riskmodel: RiskModel = RiskModel( + self.riskmodel: riskmodel.RiskModel = riskmodel.RiskModel( damage_distribution=rm_config["damage_distribution"], expire_immediately=rm_config["expire_immediately"], cat_separation_distribution=rm_config["cat_separation_distribution"], @@ -176,9 +175,8 @@ def __init__( "default_non-proportional_reinsurance_premium_share" ] self.underwritten_contracts: MutableSequence[ - metainsurancecontract.MetaInsuranceContract + "MetaInsuranceContract" ] = [] - # self.reinsurance_contracts = [] self.is_insurer = True self.is_reinsurer = False @@ -476,7 +474,7 @@ def obtain_yield(self, time: int): self.simulation.receive_obligation(amount, self, time, "yields") def mature_contracts(self, time: int) -> int: - """Method to mature contracts that have expired + """Method to mature underwritten contracts that have expired Accepts: time: Type integer Returns: @@ -511,7 +509,7 @@ def number_underwritten_contracts(self) -> int: def get_underwritten_contracts( self - ) -> Sequence[metainsurancecontract.MetaInsuranceContract]: + ) -> Sequence["MetaInsuranceContract"]: return self.underwritten_contracts def get_profitslosses(self) -> float: @@ -650,14 +648,14 @@ def balanced_portfolio( percentage_value_at_risk = self.riskmodel.get_ppf( categ_id=risk.category, tail_size=self.riskmodel.var_tail_prob ) - expected_damage = ( + var_damage = ( percentage_value_at_risk * risk.value * risk.risk_factor * self.riskmodel.inaccuracy[risk.category] ) - expected_claim = ( - min(expected_damage, risk.value * risk.excess_fraction) + var_claim = ( + min(var_damage, risk.value * risk.excess_fraction) - risk.value * risk.deductible_fraction ) @@ -665,7 +663,7 @@ def balanced_portfolio( # Compute how the cash reserved by category would change if the new reinsurance risk was accepted cash_reserved_by_categ_store[risk.category] += ( - expected_claim * self.riskmodel.margin_of_safety + var_claim * self.riskmodel.margin_of_safety ) else: @@ -750,7 +748,7 @@ def process_newrisks_reinsurer( ) if condition: - contract = ReinsuranceContract( + contract = reinsurancecontract.ReinsuranceContract( self, risk, time, @@ -808,7 +806,7 @@ def process_newrisks_insurer( # Here we check whether the portfolio is balanced or not if the reinrisk (risk_to_insure) is # underwritten. Return True if it is balanced. False otherwise. if condition: - contract = ReinsuranceContract( + contract = reinsurancecontract.ReinsuranceContract( self, risk, time, @@ -834,7 +832,7 @@ def process_newrisks_insurer( # Here it is check whether the portfolio is balanced or not if the risk (risk_to_insure) is # underwritten. Return True if it is balanced. False otherwise. if condition: - contract = InsuranceContract( + contract = insurancecontract.InsuranceContract( self, risk, time, diff --git a/reinsurancecontract.py b/reinsurancecontract.py index ca2a0b3..b7a150d 100644 --- a/reinsurancecontract.py +++ b/reinsurancecontract.py @@ -1,8 +1,14 @@ -from metainsurancecontract import MetaInsuranceContract -import insurancefirms +import metainsurancecontract +from typing import Optional +from typing import TYPE_CHECKING +if TYPE_CHECKING: + from insurancefirms import InsuranceFirm + from metainsuranceorg import MetaInsuranceOrg + from genericclasses import RiskProperties -class ReinsuranceContract(MetaInsuranceContract): + +class ReinsuranceContract(metainsurancecontract.MetaInsuranceContract): """ReinsuranceContract class. Inherits from InsuranceContract. Constructor is not currently required but may be used in the future to distinguish InsuranceContract @@ -12,18 +18,18 @@ class ReinsuranceContract(MetaInsuranceContract): def __init__( self, - insurer, - risk, - time, - premium, - runtime, - payment_period, - expire_immediately, - initial_var=0.0, - insurancetype="proportional", - deductible_fraction=None, - excess_fraction=None, - reinsurance=0, + insurer: "MetaInsuranceOrg", + risk: "RiskProperties", + time: int, + premium: float, + runtime: int, + payment_period: int, + expire_immediately: bool, + initial_var: float = 0.0, + insurancetype: str = "proportional", + deductible_fraction: "Optional[float]"=None, + excess_fraction: "Optional[float]"=None, + reinsurance: float=0, ): super().__init__( insurer, @@ -40,8 +46,7 @@ def __init__( reinsurance, ) # self.is_reinsurancecontract = True - assert type(self.property_holder) is insurancefirms.InsuranceFirm - self.property_holder: insurancefirms.InsuranceFirm + self.property_holder: "InsuranceFirm" if self.insurancetype not in ["excess-of-loss", "proportional"]: raise ValueError(f'Unrecognised insurance type "{self.insurancetype}"') if self.insurancetype == "excess-of-loss": @@ -54,7 +59,7 @@ def __init__( else: assert self.contract is not None - def explode(self, time, uniform_value=None, damage_extent=None): + def explode(self, time: int, uniform_value: None=None, damage_extent: float=None): """Explode method. Accepts arguments time: Type integer. The current time. @@ -65,7 +70,8 @@ def explode(self, time, uniform_value=None, damage_extent=None): Method marks the contract for termination. """ assert uniform_value is None - + if damage_extent is None: + raise ValueError("Damage extend should be given") # QUERY: What is the difference? Also, what happens if damage_extent = None? if damage_extent > self.deductible: # QUERY: Changed this, for the better? @@ -81,7 +87,6 @@ def explode(self, time, uniform_value=None, damage_extent=None): ) else: raise ValueError(f"Unexpected insurance type {self.insurancetype}") - # Reinsurer pays as soon as possible. # Every reinsurance claim made is immediately registered. self.insurer.register_claim(claim) @@ -93,7 +98,7 @@ def explode(self, time, uniform_value=None, damage_extent=None): self.expiration = time # self.terminating = True - def mature(self, time): + def mature(self, time: int): """Mature method. Accepts arguments time: Type integer. The current time. diff --git a/requirements.txt b/requirements.txt index be2bd82..ed29d75 100644 --- a/requirements.txt +++ b/requirements.txt @@ -3,3 +3,4 @@ numpy>=1.13.3 matplotlib>=2.1.1 networkx>=2.0 argparse>=1.1 +dataclasses>=0.6 \ No newline at end of file diff --git a/riskmodel.py b/riskmodel.py index e804970..8c3264b 100644 --- a/riskmodel.py +++ b/riskmodel.py @@ -1,20 +1,23 @@ import math -from typing import Sequence, Tuple, Union, Optional, MutableSequence import numpy as np import isleconfig from distributionreinsurance import ReinsuranceDistWrapper -from genericclasses import RiskProperties, Distribution -from metainsurancecontract import MetaInsuranceContract +from typing import Sequence, Tuple, Union, Optional, MutableSequence + +from typing import TYPE_CHECKING +if TYPE_CHECKING: + from genericclasses import Distribution, RiskProperties + from metainsurancecontract import MetaInsuranceContract class RiskModel: def __init__( self, - damage_distribution: Distribution, + damage_distribution: "Distribution", expire_immediately: bool, - cat_separation_distribution: Distribution, + cat_separation_distribution: "Distribution", norm_premium: float, category_number: int, init_average_exposure: float, @@ -36,14 +39,14 @@ def __init__( self.margin_of_safety = margin_of_safety """damage_distribution is some scipy frozen rv distribution which is bound between 0 and 1 and indicates the share of risks suffering damage as part of any single catastrophic peril""" - self.damage_distribution: MutableSequence[Distribution] = [ + self.damage_distribution: MutableSequence["Distribution"] = [ damage_distribution for _ in range(self.category_number) ] # TODO: separate that category wise? -> DONE. - self.damage_distribution_stack: Sequence[MutableSequence[Distribution]] = [ + self.damage_distribution_stack: Sequence[MutableSequence["Distribution"]] = [ [] for _ in range(self.category_number) ] self.reinsurance_contract_stack: Sequence[ - MutableSequence[MetaInsuranceContract] + MutableSequence["MetaInsuranceContract"] ] = [[] for _ in range(self.category_number)] # self.inaccuracy = np.random.uniform(9/10., 10/9., size=self.category_number) self.inaccuracy: Sequence[float] = inaccuracy @@ -57,8 +60,8 @@ def get_ppf(self, categ_id: int, tail_size: float) -> float: return self.damage_distribution[categ_id].ppf(1 - tail_size) def get_risks_by_categ( - self, risks: Sequence[RiskProperties] - ) -> Sequence[Sequence[RiskProperties]]: + self, risks: Sequence["RiskProperties"] + ) -> Sequence[Sequence["RiskProperties"]]: """Method splits list of risks by category Accepts: risks: Type List of DataDicts @@ -70,7 +73,7 @@ def get_risks_by_categ( return risks_by_categ def compute_expectation( - self, categ_risks: Sequence[RiskProperties], categ_id: int + self, categ_risks: Sequence["RiskProperties"], categ_id: int ) -> Tuple[float, float, float]: # TODO: more intuitive name? """Method to compute the average exposure and risk factor as well as the increase in expected profits for the @@ -131,7 +134,7 @@ def compute_expectation( return average_risk_factor, average_exposure, incr_expected_profits def evaluate_proportional( - self, risks: Sequence[RiskProperties], cash: Sequence[float] + self, risks: Sequence["RiskProperties"], cash: Sequence[float] ) -> Tuple[float, Sequence[int], Sequence[int], Sequence[float]]: """Method to evaluate proportional type risks. Accepts: @@ -184,9 +187,7 @@ def evaluate_proportional( # QUERY: Is the margin of safety appiled twice? (above and below) # record liquidity requirement and apply margin of safety for liquidity requirement - necessary_liquidity += ( - var_per_risk * self.margin_of_safety * len(categ_risks) - ) + necessary_liquidity += var_per_risk * len(categ_risks) if isleconfig.verbose: print(self.inaccuracy) print( @@ -221,16 +222,6 @@ def evaluate_proportional( else: expected_profits /= necessary_liquidity - max_cash_by_categ = max(cash_left_by_category) - floored_cash_by_categ = cash_left_by_category.copy() - floored_cash_by_categ[floored_cash_by_categ < 0] = 0 - # remaining_acceptable_by_category_old = remaining_acceptable_by_category.copy() - for categ_id in range(self.category_number): - # QUERY: Where does this come from? - remaining_acceptable_by_category[categ_id] = math.floor( - remaining_acceptable_by_category[categ_id] - * pow(floored_cash_by_categ[categ_id] / max_cash_by_categ, 5) - ) if isleconfig.verbose: print( "RISKMODEL returns: ", @@ -246,9 +237,9 @@ def evaluate_proportional( def evaluate_excess_of_loss( self, - risks: Sequence[RiskProperties], + risks: Sequence["RiskProperties"], cash: Sequence[float], - offered_risk: Optional[RiskProperties] = None, + offered_risk: Optional["RiskProperties"] = None, ) -> Tuple[Sequence[float], Sequence[float], float]: """Method to evaluate excess-of-loss type risks. Accepts: @@ -283,38 +274,36 @@ def evaluate_excess_of_loss( # compute liquidity requirements from existing contracts for risk in categ_risks: - expected_damage = ( + var_damage = ( percentage_value_at_risk * risk.value * risk.risk_factor * self.inaccuracy[categ_id] ) - # QUERY: This doesn't look accurate to me - E(f(X)) != f(E(X)) in general - # QUERY: Isn't this wrong? - expected_claim = min(expected_damage, risk.excess) - risk.deductible + var_claim = max(min(var_damage, risk.excess) - risk.deductible, 0) # record liquidity requirement and apply margin of safety for liquidity requirement - cash_left_by_categ[categ_id] -= expected_claim * self.margin_of_safety + cash_left_by_categ[categ_id] -= var_claim * self.margin_of_safety # compute additional liquidity requirements from newly offered contract if (offered_risk is not None) and (offered_risk.category == categ_id): - expected_damage_fraction = ( + var_damage_fraction = ( percentage_value_at_risk * offered_risk.risk_factor * self.inaccuracy[categ_id] ) - expected_claim_fraction = ( - min(expected_damage_fraction, offered_risk.excess_fraction) + var_claim_fraction = ( + min(var_damage_fraction, offered_risk.excess_fraction) - offered_risk.deductible_fraction ) - expected_claim_total = expected_claim_fraction * offered_risk.value + var_claim_total = var_claim_fraction * offered_risk.value # record liquidity requirement and apply margin of safety for liquidity requirement additional_required[categ_id] += ( - expected_claim_total * self.margin_of_safety + var_claim_total * self.margin_of_safety ) - additional_var_per_categ[categ_id] += expected_claim_total + additional_var_per_categ[categ_id] += var_claim_total # Additional value at risk should only occur in one category. Assert that this is the case. assert sum(additional_var_per_categ > 0) <= 1 @@ -325,9 +314,9 @@ def evaluate_excess_of_loss( # noinspection PyUnboundLocalVariable def evaluate( self, - risks: Sequence[RiskProperties], + risks: Sequence["RiskProperties"], cash: Union[float, Sequence[float]], - offered_risk: Optional[RiskProperties] = None, + offered_risk: Optional["RiskProperties"] = None, ) -> Union[ Tuple[float, Sequence[int], Sequence[float], Sequence[float], float], Tuple[bool, Sequence[float], float, float], @@ -359,6 +348,7 @@ def evaluate( the offered_risk argument, whereas proportional risks are processed all at once leaving offered_risk = 0. This results in two sets of return values being used. These return values are what is used to determine if risks are underwritten or not.""" + # TODO: split this into two functions # ensure that any risk to be considered supplied directly as argument is non-proportional/excess-of-loss assert (offered_risk is None) or offered_risk.insurancetype == "excess-of-loss" @@ -416,7 +406,7 @@ def add_reinsurance( categ_id: int, excess_fraction: float, deductible_fraction: float, - contract: MetaInsuranceContract, + contract: "MetaInsuranceContract", ): """Method to add any instance of reinsurance to risk models list of reinsurance contracts, and add damage distribution to stack of damage distributions per category, then replace with a new distribution. Only used in @@ -437,7 +427,7 @@ def add_reinsurance( dist=self.damage_distribution[categ_id], ) - def delete_reinsurance(self, categ_id: int, contract: MetaInsuranceContract): + def delete_reinsurance(self, categ_id: int, contract: "MetaInsuranceContract"): """Method to remove any instance of reinsurance to risk models list of reinsurance contracts, and remove its damage distribution from the stack of damage distributions per category. Only used in the delete_reinsurance method of insurancefirm. diff --git a/start.py b/start.py index a547891..8e6b5a2 100644 --- a/start.py +++ b/start.py @@ -1,5 +1,4 @@ # import common packages -from __future__ import annotations import argparse import hashlib import numpy as np From 5da97e55f91fbb1fa51a874c48ab0f1ccba138cd Mon Sep 17 00:00:00 2001 From: Chris Hughes Date: Tue, 23 Jul 2019 11:55:57 +0100 Subject: [PATCH 064/125] Made multi-layer reinsurance work. Also fix for python 3.6 --- catbond.py | 27 ++-- distribution_wrapper_test.py | 25 ---- distributionreinsurance.py | 163 ++++++++++++---------- distributiontruncated.py | 3 + genericclasses.py | 161 ++++++++++++++++++---- insurancecontract.py | 17 +-- insurancefirms.py | 253 ++++++++++++++++++----------------- insurancesimulation.py | 46 ++++--- isleconfig.py | 3 + metainsurancecontract.py | 19 +-- metainsuranceorg.py | 190 ++++++++++++++++---------- reinsurancecontract.py | 58 ++++---- requirements.txt | 2 + riskmodel.py | 126 ++++++----------- start.py | 2 +- 15 files changed, 615 insertions(+), 480 deletions(-) delete mode 100644 distribution_wrapper_test.py diff --git a/catbond.py b/catbond.py index 6907558..1fd4b81 100644 --- a/catbond.py +++ b/catbond.py @@ -1,10 +1,13 @@ -from __future__ import annotations import isleconfig from metainsuranceorg import MetaInsuranceOrg -import genericclasses -import metainsurancecontract -import insurancesimulation +from genericclasses import Obligation, GenericAgent + from typing import MutableSequence +from typing import TYPE_CHECKING +if TYPE_CHECKING: + from insurancesimulation import InsuranceSimulation + from metainsurancecontract import MetaInsuranceContract + # TODO: This and MetaInsuranceOrg should probably both subclass something simple - a MetaAgent, say. MetaInsuranceOrg # can do more than a CatBond should be able to! @@ -16,9 +19,9 @@ class CatBond(MetaInsuranceOrg): # TODO inheret GenericAgent instead of MetaInsuranceOrg? def __init__( self, - simulation: insurancesimulation.InsuranceSimulation, + simulation: "InsuranceSimulation", per_period_premium: float, - owner: genericclasses.GenericAgent, + owner: GenericAgent, interest_rate: float = 0, ): """Initialising methods. @@ -30,13 +33,13 @@ def __init__( self.simulation = simulation self.id: int = 0 self.underwritten_contracts: MutableSequence[ - metainsurancecontract.MetaInsuranceContract + "MetaInsuranceContract" ] = [] self.cash: float = 0 self.profits_losses: float = 0 - self.obligations: MutableSequence[genericclasses.Obligation] = [] + self.obligations: MutableSequence[Obligation] = [] self.operational: bool = True - self.owner: genericclasses.GenericAgent = owner + self.owner: GenericAgent = owner self.per_period_dividend: float = per_period_premium self.interest_rate: float = interest_rate # TODO: shift obtain_yield method to insurancesimulation, thereby making it unnecessary to drag parameters like @@ -90,7 +93,7 @@ def iterate(self, time: int): # self.estimate_var() # cannot compute VaR for catbond as catbond does not have a riskmodel - def set_owner(self, owner: genericclasses.GenericAgent): + def set_owner(self, owner: GenericAgent): """Method to set owner of the Cat Bond. Accepts: owner: Type class @@ -99,7 +102,7 @@ def set_owner(self, owner: genericclasses.GenericAgent): if isleconfig.verbose: print("SOLD") - def set_contract(self, contract: metainsurancecontract.MetaInsuranceContract): + def set_contract(self, contract: "MetaInsuranceContract"): """Method to record new instances of CatBonds. Accepts: owner: Type class @@ -114,7 +117,7 @@ def mature_bond(self): When the catbond contract matures this is called which pays the value of the catbond to the simulation, and is then deleted from the list of agents.""" if self.operational: - obligation = genericclasses.Obligation( + obligation = Obligation( amount=self.cash, recipient=self.simulation, due_time=1, diff --git a/distribution_wrapper_test.py b/distribution_wrapper_test.py deleted file mode 100644 index 81734f9..0000000 --- a/distribution_wrapper_test.py +++ /dev/null @@ -1,25 +0,0 @@ -import scipy.stats -import numpy as np -from distributiontruncated import TruncatedDistWrapper -from distributionreinsurance import ReinsuranceDistWrapper -import pdb - -non_truncated_dist = scipy.stats.pareto(b=2, loc=0, scale=0.5) -truncated_dist = TruncatedDistWrapper( - lower_bound=0.6, upper_bound=1.0, dist=non_truncated_dist -) -reinsurance_dist = ReinsuranceDistWrapper( - lower_bound=0.85, upper_bound=0.95, dist=truncated_dist -) - -x1 = np.linspace(non_truncated_dist.ppf(0.01), non_truncated_dist.ppf(0.99), 100) -x2 = np.linspace(truncated_dist.ppf(0.01), truncated_dist.ppf(1.0), 100) -x3 = np.linspace(reinsurance_dist.ppf(0.01), reinsurance_dist.ppf(1.0), 100) -x_val_1 = reinsurance_dist.lower_bound -x_val_2 = truncated_dist.upper_bound - ( - reinsurance_dist.upper_bound - reinsurance_dist.lower_bound -) -x_val_3 = reinsurance_dist.upper_bound -x_val_4 = truncated_dist.upper_bound - -pdb.set_trace() diff --git a/distributionreinsurance.py b/distributionreinsurance.py index dbca90a..15640e0 100644 --- a/distributionreinsurance.py +++ b/distributionreinsurance.py @@ -1,100 +1,129 @@ import functools import numpy as np import scipy.stats +import warnings class ReinsuranceDistWrapper: """ Wrapper for modifying the risk to an insurance company when they have EoL reinsurance + dist is a distribution taking values in [0, 1] (as the damage distribution should) # QUERY: Check this lower_bound is the least reinsured risk (lowest priority), upper_bound is the greatest reinsured risk - Note that the bounds are in terms of the values of the distribution, not the probabilities.""" + Note that the bounds are in terms of the values of the distribution, not the probabilities. - def __init__(self, dist, lower_bound=None, upper_bound=None): - assert lower_bound is not None or upper_bound is not None + Coverage is a list of tuples, each tuple representing a region that is reinsured. Coverage is in money, will be + divided by value.""" + + def __init__( + self, dist, lower_bound=None, upper_bound=None, coverage=None, value=None + ): + if coverage is not None: + if value is None: + raise ValueError( + "coverage and value must both be passed or neither be passed" + ) + if upper_bound is not None or lower_bound is not None: + raise ValueError( + "lower_bound and upper_bound can't be used with coverage and value" + ) + else: + if value is not None: + raise ValueError( + "coverage and value must both be passed or neither be passed" + ) + if upper_bound is None and lower_bound is None: + raise ValueError("no restriction arguments passed!") self.dist = dist - self.lower_bound = lower_bound - self.upper_bound = upper_bound - if lower_bound is None: - self.lower_bound = -np.inf - elif upper_bound is None: - self.upper_bound = np.inf - assert self.upper_bound > self.lower_bound - self.redistributed_share = dist.cdf(upper_bound) - dist.cdf(lower_bound) + if coverage is None: + if lower_bound is None: + lower_bound = 0 + elif upper_bound is None: + upper_bound = 1 + assert 0 <= lower_bound < upper_bound <= 1 + self.coverage = [(lower_bound, upper_bound)] + else: + self.coverage = [ + (region[0] / value, region[1] / value) for region in coverage + ] + if self.coverage and self.coverage[0][0] == 0: + warnings.warn("Adding reinsurance for 0 damage - probably not right!") + # TODO: verify distribution bounds here + # self.redistributed_share = dist.cdf(upper_bound) - dist.cdf(lower_bound) + + @functools.lru_cache(maxsize=512) + def truncation(self, x): + """ Takes a value x and returns the ammount of damage required for x damage to be absorbed by the firm. + Also returns whether the value was on a boundary (point of discontinuity) (to make pdf, cdf work on edge cases) + """ + # TODO: doesn't work with arrays, fix? + if not np.isscalar(x): + x = x[0] + boundary = False + for region in self.coverage: + if x < region[0]: + return x, boundary + else: + if x == region[0]: + boundary = True + x += region[1] - region[0] + return x, boundary + + def inverse_truncation(self, p): + """ Returns the inverse of the above function, which is continuous and well-defined """ + # TODO: needs to work with arrays + adjustment = 0 + for region in self.coverage: + # These bounds are probabilities + if p <= region[0]: + return p - adjustment + elif p < region[1]: + return region[0] - adjustment + else: + adjustment += region[1] - region[0] + return p - adjustment @functools.lru_cache(maxsize=512) def pdf(self, x): - x = np.array(x, ndmin=1) - r = map( - lambda y: self.dist.pdf(y) - if y < self.lower_bound - else np.inf - if y == self.lower_bound - else self.dist.pdf(y + self.upper_bound - self.lower_bound), - x, - ) - r = np.array(list(r)) - if len(r.flatten()) == 1: - r = float(r) - return r + # derivative of truncation is 1 at all points of continuity, so only need to modify at boundaries + result, boundary = self.truncation(x) + if boundary: + return np.inf + else: + return self.dist.pdf(result) @functools.lru_cache(maxsize=512) def cdf(self, x): - x = np.array(x, ndmin=1) - r = map( - lambda y: self.dist.cdf(y) - if y < self.lower_bound - else self.dist.cdf(y + self.upper_bound - self.lower_bound), - x, - ) - r = np.array(list(r)) - if len(r.flatten()) == 1: - r = float(r) - return r + # cdf is right-continuous modification, so doesn't care about the discontinuity + result, _ = self.truncation(x) + return self.dist.cdf(result) @functools.lru_cache(maxsize=512) - def ppf(self, x): - x = np.array(x, ndmin=1) - assert (x >= 0).all() and (x <= 1).all() - r = map( - lambda y: self.dist.ppf(y) - if y <= self.dist.cdf(self.lower_bound) - else self.dist.ppf(self.dist.cdf(self.lower_bound)) - if y <= self.dist.cdf(self.upper_bound) - else self.dist.ppf(y) - self.upper_bound + self.lower_bound, - x, - ) - r = np.array(list(r)) - if len(r.flatten()) == 1: - r = float(r) - return r + def ppf(self, p): + if type(p) is not float: + p = p[0] + return self.inverse_truncation(self.dist.ppf(p)) def rvs(self, size=1): sample = self.dist.rvs(size=size) - sample1 = sample[sample <= self.lower_bound] - sample2 = sample[sample > self.lower_bound] - sample3 = sample2[sample2 >= self.upper_bound] - sample2 = sample2[sample2 < self.upper_bound] - - sample2 = np.ones(len(sample2)) * self.lower_bound - sample3 = sample3 - self.upper_bound + self.lower_bound - - sample = np.append(np.append(sample1, sample2), sample3) - return sample[:size] + sample = map(self.inverse_truncation, sample) + return sample if __name__ == "__main__": - non_truncated = scipy.stats.pareto(b=2, loc=0, scale=0.5) + # TODO: Check with coverage = [] + from distributiontruncated import TruncatedDistWrapper + import matplotlib.pyplot as plt + + non_truncated = TruncatedDistWrapper(scipy.stats.pareto(b=2, loc=0, scale=0.5)) # truncated = ReinsuranceDistWrapper(lower_bound=0, upper_bound=1, dist=non_truncated) truncated = ReinsuranceDistWrapper( - lower_bound=0.7, upper_bound=1.5, dist=non_truncated + dist=non_truncated, value=10, coverage=[(6.5, 7), (8, 9)] ) - x1 = np.linspace(non_truncated.ppf(0.01), non_truncated.ppf(0.99), 100) - # x2 = np.linspace(truncated.ppf(0.01), truncated.ppf(0.99), 100) - import matplotlib.pyplot as plt + x1 = np.linspace(non_truncated.ppf(0.01), non_truncated.ppf(0.99), 100).flatten() - y1 = non_truncated.pdf(x1) - y2 = truncated.pdf(x1) + y1 = list(map(non_truncated.pdf, x1)) + y2 = list(map(truncated.pdf, x1)) plt.plot(x1, y1, "r+") plt.plot(x1, y2, "bx") plt.legend(["non truncated", "truncated"]) diff --git a/distributiontruncated.py b/distributiontruncated.py index e4ceb0f..a6916ca 100644 --- a/distributiontruncated.py +++ b/distributiontruncated.py @@ -15,6 +15,7 @@ def __init__(self, dist, lower_bound=0.0, upper_bound=1.0): @functools.lru_cache(maxsize=1024) def pdf(self, x): + # TODO: begone, arrays x = np.array(x, ndmin=1) r = map( lambda y: self.dist.pdf(y) / self.normalizing_factor @@ -29,6 +30,7 @@ def pdf(self, x): @functools.lru_cache(maxsize=1024) def cdf(self, x): + # TODO: rm arrays x = np.array(x, ndmin=1) r = map( lambda y: 0 @@ -46,6 +48,7 @@ def cdf(self, x): @functools.lru_cache(maxsize=1024) def ppf(self, x): + # TODO: probably no need for arrays x = np.array(x, ndmin=1) assert (x >= 0).all() and (x <= 1).all() return self.dist.ppf( diff --git a/genericclasses.py b/genericclasses.py index 25fe371..65fd740 100644 --- a/genericclasses.py +++ b/genericclasses.py @@ -1,28 +1,36 @@ -from __future__ import annotations +from itertools import chain +import dataclasses +from sortedcontainers import SortedList import numpy as np from scipy import stats -import dataclasses -from typing import Mapping, MutableSequence, Union, Tuple -# Not totally sure about the best way to resolve circular dependencies caused by type hinting -# from metainsurancecontract import MetaInsuranceContract -from distributiontruncated import TruncatedDistWrapper -from distributionreinsurance import ReinsuranceDistWrapper -from reinsurancecontract import ReinsuranceContract import isleconfig -Distribution = Union[stats.rv_continuous, TruncatedDistWrapper, ReinsuranceDistWrapper] +from typing import Mapping, MutableSequence, Union, Tuple +from typing import TYPE_CHECKING +if TYPE_CHECKING: + from metainsurancecontract import MetaInsuranceContract + from distributiontruncated import TruncatedDistWrapper + from distributionreinsurance import ReinsuranceDistWrapper + from reinsurancecontract import ReinsuranceContract + from riskmodel import RiskModel + + Distribution = Union[ + "stats.rv_continuous", + "TruncatedDistWrapper", + "ReinsuranceDistWrapper", + ] class GenericAgent: def __init__(self): self.cash: float = 0 - self.obligations: MutableSequence[Obligation] = [] + self.obligations: MutableSequence["Obligation"] = [] self.operational: bool = True self.profits_losses: float = 0 - def _pay(self, obligation: Obligation): + def _pay(self, obligation: "Obligation"): """Method to _pay other class instances. Accepts: Obligation: Type DataDict @@ -31,6 +39,7 @@ def _pay(self, obligation: Obligation): amount = obligation.amount recipient = obligation.recipient purpose = obligation.purpose + # TODO: Think about what happens when paying non-operational firms if self.get_operational() and recipient.get_operational(): self.cash -= amount if purpose is not "dividend": @@ -59,8 +68,7 @@ def _effect_payments(self, time: int): # time, in which case this could be done slightly better). Low priority, but something to consider due = [item for item in self.obligations if item.due_time <= time] self.obligations = [item for item in self.obligations if item.due_time > time] - # QUERY: could this cause a firm to enter illiquidity if it has obligations to non-operational firms? Such - # firms can't recieve payment, so this possibly shouldn't happen. + sum_due = sum([item.amount for item in due]) if sum_due > self.cash: self.obligations += due @@ -73,7 +81,7 @@ def enter_illiquidity(self, time: int, sum_due: float): raise NotImplementedError() def receive_obligation( - self, amount: float, recipient: GenericAgent, due_time: int, purpose: str + self, amount: float, recipient: "GenericAgent", due_time: int, purpose: str ): """Method for receiving obligations that the firm will have to _pay. Accepts: @@ -102,10 +110,10 @@ class RiskProperties: risk_factor: float value: float category: int - owner: GenericAgent + owner: "GenericAgent" number_risks: int = 1 - contract: "metainsurancecontract.MetaInsuranceContract" = None + contract: "MetaInsuranceContract" = None insurancetype: str = None deductible: float = None runtime: int = None @@ -143,7 +151,7 @@ class Obligation: """Class for holding the properties of an obligation""" amount: float - recipient: GenericAgent + recipient: "GenericAgent" due_time: int purpose: str @@ -151,7 +159,7 @@ class Obligation: class ConstantGen(stats.rv_continuous): def _pdf(self, x: float, *args) -> float: a = np.float_(x == 0) - a[a == 1.0] = np.float_("inf") + a[a == 1.0] = np.inf return a def _cdf(self, x: float, *args) -> float: @@ -170,18 +178,115 @@ def _rvs(self, *args) -> Union[np.ndarray, float]: class ReinsuranceProfile: """Class for keeping track of the reinsurance that an insurance firm holds - All reinsurance is assumed to be on open intervals""" + All reinsurance is assumed to be on open intervals + + regions are tuples, (priority, priority+limit, contract), so the contract covers losses in the region (priority, + priority + limit)""" + # TODO: add, remove, explode, get uninsured regions - def __init__(self): - self.reinsured_regions: MutableSequence[MutableSequence[Tuple[float, float, ReinsuranceContract]]] = [[] for _ in range(isleconfig.simulation_parameters["no_categories"])] - self.not_reinsured_regions: MutableSequence[MutableSequence[Tuple[float, float]]] = [[(0, np.float_("inf"))] for _ in range(isleconfig.simulation_parameters["no_categories"])] + def __init__(self, riskmodel: "RiskModel"): + self.reinsured_regions: MutableSequence[SortedList[Tuple[int, int, "ReinsuranceContract"]]] + + self.reinsured_regions = [ + SortedList(key=lambda x: x[0]) + for _ in range(isleconfig.simulation_parameters["no_categories"]) + ] + + # Used for automatically updating the riskmodel when reinsurance is modified + self.riskmodel = riskmodel + + def add( + self, contract: "ReinsuranceContract", value: float + ) -> None: + lower_bound: int = contract.deductible + upper_bound: int = contract.excess + category = contract.category + + self.reinsured_regions[category].add((lower_bound, upper_bound, contract)) + index = self.reinsured_regions[category].index( + (lower_bound, upper_bound, contract) + ) + + # Check for overlap with region to the right... + if index + 1 < len(self.reinsured_regions[category]) and self.reinsured_regions[category][index + 1][0] < upper_bound: + raise ValueError( + "Attempted to add reinsurance overlapping with existing reinsurance" + ) + + # ... and to the left + if index != 0 and self.reinsured_regions[category][index - 1][1] > lower_bound: + raise ValueError( + "Attempted to add reinsurance overlapping with existing reinsurance" + ) + + self.riskmodel.set_reinsurance_coverage( + value=value, coverage=self.reinsured_regions[category], category=category + ) - def add_reinsurance(self, contract: ReinsuranceContract): + def remove( + self, contract: "ReinsuranceContract", value: float + ) -> None: lower_bound = contract.deductible upper_bound = contract.excess - for index, region in enumerate(self.not_reinsured_regions): - if region[0] <= lower_bound: - pass - self.reinsured_regions[contract.category].append( - (contract.deductible_fraction, contract.excess_fraction, contract) + category = contract.category + + try: + self.reinsured_regions[category].remove( + (lower_bound, upper_bound, contract) + ) + except ValueError: + raise ValueError( + "Attempting to remove a reinsurance contract that doesn't exist!" + ) + self.riskmodel.set_reinsurance_coverage( + value=value, coverage=self.reinsured_regions[category], category=category + ) + + def uncovered(self, category: int) -> MutableSequence[Tuple[float, float]]: + uncovered_regions = [] + upper = 0 + for region in self.reinsured_regions[category]: + if region[0] - upper > 1: + # There's a gap in coverage! + uncovered_regions.append((upper, region[0])) + upper = region[1] + uncovered_regions.append((upper, np.inf)) + return uncovered_regions + + def contracts_to_explode( + self, category: int, damage: float + ) -> MutableSequence["ReinsuranceContract"]: + contracts = [] + for region in self.reinsured_regions[category]: + if region[0] < damage: + contracts.append(region[2]) + if region[1] >= damage: + break + return contracts + + def all_contracts(self) -> MutableSequence["ReinsuranceContract"]: + regions = chain.from_iterable(self.reinsured_regions) + contracts = map(lambda x: x[2], regions) + return list(contracts) + + def update_value(self, value: float, category: int) -> None: + self.riskmodel.set_reinsurance_coverage( + value=value, coverage=self.reinsured_regions[category], category=category ) + + @staticmethod + def split_longest(l: MutableSequence[Tuple[float, float]]) -> MutableSequence[Tuple[float, float]]: + max_width = 0 + max_width_index = None + for i, region in enumerate(l): + if region[1] - region[0] > max_width: + max_width = region[1] - region[0] + max_width_index = i + if max_width == 0: + raise RuntimeError("All regions have zero width!") + lower, upper = l[max_width_index] + mid = (lower + upper) / 2 + del l[max_width_index] + l.insert(max_width_index, (mid, upper)) + l.insert(max_width_index, (lower, mid)) + return l diff --git a/insurancecontract.py b/insurancecontract.py index 21a68fc..893d039 100644 --- a/insurancecontract.py +++ b/insurancecontract.py @@ -1,9 +1,10 @@ -from __future__ import annotations - import metainsurancecontract -import genericclasses -import metainsuranceorg -import insurancesimulation + +from typing import TYPE_CHECKING +if TYPE_CHECKING: + from metainsuranceorg import MetaInsuranceOrg + from insurancesimulation import InsuranceSimulation + from genericclasses import RiskProperties class InsuranceContract(metainsurancecontract.MetaInsuranceContract): @@ -16,8 +17,8 @@ class InsuranceContract(metainsurancecontract.MetaInsuranceContract): def __init__( self, - insurer: metainsuranceorg.MetaInsuranceOrg, - risk: genericclasses.RiskProperties, + insurer: "MetaInsuranceOrg", + risk: "RiskProperties", time: int, premium: float, runtime: int, @@ -45,7 +46,7 @@ def __init__( ) # the property holder in an insurance contract should always be the simulation assert self.property_holder is self.insurer.simulation - self.property_holder: insurancesimulation.InsuranceSimulation + self.property_holder: "InsuranceSimulation" def explode(self, time, uniform_value=None, damage_extent=None): """Explode method. diff --git a/insurancefirms.py b/insurancefirms.py index 980a122..9d0736e 100644 --- a/insurancefirms.py +++ b/insurancefirms.py @@ -1,16 +1,18 @@ -from __future__ import annotations -from typing import Optional, Tuple, MutableSequence, Mapping - import numpy as np -from metainsuranceorg import MetaInsuranceOrg +import metainsuranceorg import catbond from reinsurancecontract import ReinsuranceContract import isleconfig import genericclasses +from typing import Optional, MutableSequence, Mapping + +from typing import TYPE_CHECKING +if TYPE_CHECKING: + pass -class InsuranceFirm(MetaInsuranceOrg): +class InsuranceFirm(metainsuranceorg.MetaInsuranceOrg): """ReinsuranceFirm class. Inherits from MetaInsuranceFirm.""" @@ -41,27 +43,37 @@ def adjust_dividends(self, time: int, actual_capacity: float): self.per_period_dividend = 0 def get_reinsurance_var_estimate(self, max_var: float) -> float: - """Method to estimate the VaR if another reinsurance contract were to be taken. + """Method to estimate the VaR if another reinsurance contract were to be taken out. Accepts: max_var: Type Decimal. Max value at risk Returns: reinsurance_VaR_estimate: Type Decimal. This method takes the max VaR and mulitiplies it by a factor that estimates the VaR if another reinsurance contract was to be taken. Called by the adjust_target_capacity and get_capacity methods.""" - reinsurance_factor_estimate = ( - len( - [ - 1 - for categ_id in range(self.simulation_no_risk_categories) - if (self.category_reinsurance[categ_id] is None) - ] - ) - * 1.0 - / self.simulation_no_risk_categories - ) * (1.0 - self.np_reinsurance_deductible_fraction) + values = [ + self.underwritten_risk_characterisation[categ][2] + for categ in range(self.simulation_parameters["no_categories"]) + ] + reinsurance_factor_estimate = self.get_reinsurable_fraction(values) reinsurance_var_estimate = max_var * (1.0 + reinsurance_factor_estimate) return reinsurance_var_estimate + def get_reinsurable_fraction(self, value_by_category): + """Returns the proportion of the value of risk held overall that is eligible for reinsurance""" + total = 0 + for categ in range(len(value_by_category)): + value: float = value_by_category[categ] + uncovered = self.reinsurance_profile.uncovered(categ) + maximum_excess: float = self.np_reinsurance_excess_fraction * value + miniumum_deductible: float = self.np_reinsurance_deductible_fraction * value + for region in uncovered: + if region[1] > miniumum_deductible and region[0] < maximum_excess: + total += min( + region[1] / value, self.np_reinsurance_excess_fraction + ) - max(region[0] / value, self.np_reinsurance_deductible_fraction) + total = total / len(value_by_category) + return total + def adjust_capacity_target(self, max_var: float): """Method to adjust capacity target. Accepts: @@ -72,7 +84,7 @@ def adjust_capacity_target(self, max_var: float): reinsurance_var_estimate = self.get_reinsurance_var_estimate(max_var) if max_var + reinsurance_var_estimate == 0: # TODO: why is this being called with max_var = 0 anyway? - capacity_target_var_ratio_estimate = float("inf") + capacity_target_var_ratio_estimate = np.inf else: capacity_target_var_ratio_estimate = ( (self.capacity_target + reinsurance_var_estimate) @@ -131,9 +143,7 @@ def increase_capacity(self, time: int, max_var: float) -> float: capacity = None if not reinsurance_price == cat_bond_price == float("inf"): categ_ids = [ - categ_id - for categ_id in range(self.simulation_no_risk_categories) - if (self.category_reinsurance[categ_id] is None) + categ_id for categ_id in range(self.simulation_no_risk_categories) ] if len(categ_ids) > 1: np.random.shuffle(categ_ids) @@ -245,40 +255,11 @@ def ask_reinsurance_non_proportional(self, time: int): Returns None.""" """Evaluate by risk category""" for categ_id in range(self.simulation_no_risk_categories): - """Seek reinsurance only with probability 10% if not already reinsured""" - # QUERY It doesn't actually have the 10% chance? - # TODO: find a more generic way to decide whether to request reinsurance for category in this period - if self.category_reinsurance[categ_id] is None: - self.ask_reinsurance_non_proportional_by_category(time, categ_id) - - def characterize_underwritten_risks_by_category( - self, categ_id: int - ) -> Tuple[float, float, int, float]: - """Method to characterise associated risks in a given category in terms of value, number, avg risk factor, and - total premium per iteration. - Accepts: - categ_id: Type Integer. The given category for characterising risks. - Returns: - total_value: Type Decimal. Total value of all contracts in the category. - avg_risk_facotr: Type Decimal. Avg risk factor of all contracted risks in category. - number_risks: Type Integer. Total number of contracted risks in category. - periodised_total_premium: Total value per month of all contracts premium payments.""" - total_value = 0 - avg_risk_factor = 0 - number_risks = 0 - periodized_total_premium = 0 - for contract in self.underwritten_contracts: - if contract.category == categ_id: - total_value += contract.value - avg_risk_factor += contract.risk_factor - number_risks += 1 - periodized_total_premium += contract.periodized_premium - if number_risks > 0: - avg_risk_factor /= number_risks - return total_value, avg_risk_factor, number_risks, periodized_total_premium + # TODO: find a way to decide whether to request reinsurance for category in this period, maybe a threshold? + self.ask_reinsurance_non_proportional_by_category(time, categ_id) def ask_reinsurance_non_proportional_by_category( - self, time: int, categ_id: int, purpose: str = "newrisk", tranches: int = 1 + self, time: int, categ_id: int, purpose: str = "newrisk", min_tranches: int = None ) -> Optional[genericclasses.RiskProperties]: """Method to create a reinsurance risk for a given category for firm that calls it. Called from increase_ capacity_by_category, ask_reinsurance_non_proportional, and roll_over in metainsuranceorg. @@ -286,7 +267,7 @@ def ask_reinsurance_non_proportional_by_category( time: Type Integer. categ_id: Type Integer. purpose: Type String. Needed for when called from roll_over method as the risk is then returned. - tranches: Type int. Determines how many layers of reinsurance the risk is split over + min_tranches: Type int. Determines how many layers of reinsurance the risk is split over Returns: risk: Type DataDict. Only returned when method used for roll_over. This method is given a category, then characterises all the underwritten risks in that category for the firm @@ -294,33 +275,65 @@ def ask_reinsurance_non_proportional_by_category( existing underwritten risks. If tranches > 1, the risk is split between mutliple layers of reinsurance, each of the same size. If the method was called to create a new risks then it is appended to list of 'reinrisks', otherwise used for creating the risk when a reinsurance contract rolls over.""" + # TODO: how do we decide how many tranches? + if min_tranches is None: + min_tranches = isleconfig.simulation_parameters["min_tranches"] [ total_value, avg_risk_factor, number_risks, periodized_total_premium, - ] = self.characterize_underwritten_risks_by_category(categ_id) + ] = self.underwritten_risk_characterisation[categ_id] if number_risks > 0: - lower_boundary, upper_boundary = ( - self.np_reinsurance_deductible_fraction, - self.np_reinsurance_excess_fraction, - ) - # TODO: think about tranche sizes - tranche_boundaries = [ - lower_boundary + n * (upper_boundary - lower_boundary) / tranches - for n in range(tranches + 1) - ] - for tranche in range(tranches): - tranche_lower_bound = tranche_boundaries[tranche] - tranche_upper_bound = tranche_boundaries[tranche + 1] + tranches = self.reinsurance_profile.uncovered(categ_id) + + # Don't get reinsurance above maximum excess + while tranches[-1][1] > self.np_reinsurance_excess_fraction * total_value: + if tranches[-1][0] >= self.np_reinsurance_excess_fraction * total_value: + tranches.pop() + else: + tranches[-1] = ( + tranches[-1][0], + self.np_reinsurance_excess_fraction * total_value, + ) + while ( + tranches[0][0] < self.np_reinsurance_deductible_fraction * total_value + ): + if ( + tranches[0][1] + <= self.np_reinsurance_deductible_fraction * total_value + ): + tranches = tranches[1:] + if len(tranches) == 0: + break + else: + tranches[0] = ( + self.np_reinsurance_deductible_fraction * total_value, + tranches[0][1], + ) + for tranche in tranches: + if tranche[1] - tranche[0] <= 2: + # Small gaps are acceptable to avoid having trivial contracts + tranches.remove(tranche) + + if not tranches: + # If we've ended up with no tranches, give up and return + return None + + while len(tranches) < min_tranches: + tranches = self.reinsurance_profile.split_longest(tranches) + if purpose == "rollover": + risks_to_return = [] + for tranche in tranches: + assert tranche[1] > tranche[0] risk = genericclasses.RiskProperties( value=total_value, category=categ_id, owner=self, insurancetype="excess-of-loss", number_risks=number_risks, - deductible_fraction=tranche_lower_bound, - excess_fraction=tranche_upper_bound, + deductible_fraction=tranche[0] / total_value, + excess_fraction=tranche[1] / total_value, periodized_total_premium=periodized_total_premium, runtime=12, expiration=time + 12, @@ -329,7 +342,9 @@ def ask_reinsurance_non_proportional_by_category( if purpose == "newrisk": self.simulation.append_reinrisks(risk) elif purpose == "rollover": - return risk + risks_to_return.append(risk) + if purpose == "rollover": + return risks_to_return elif number_risks == 0 and purpose == "rollover": return None @@ -369,35 +384,25 @@ def ask_reinsurance_proportional(self): else: break - def add_reinsurance( - self, - category: int, - excess_fraction: float, - deductible_fraction: float, - contract: ReinsuranceContract, - ): + def add_reinsurance(self, contract: ReinsuranceContract): """Method called by reinsurancecontract to add the reinsurance contract to the firms counter for the given category, normally used so only one reinsurance contract is issued per category at a time. Accepts: category: Type Integer. - excess_fraction: Type Decimal. Value of excess. - deductible_fraction: Type Decimal. Value of deductible. contract: Type Class. Reinsurance contract issued to firm. No return values.""" - self.riskmodel.add_reinsurance( - category, excess_fraction, deductible_fraction, contract - ) - self.category_reinsurance[category] = contract + value = self.underwritten_risk_characterisation[contract.category][0] + self.reinsurance_profile.add(contract, value) - def delete_reinsurance(self, category: int, contract: ReinsuranceContract): + def delete_reinsurance(self, contract: ReinsuranceContract): """Method called by reinsurancecontract to delete the reinsurance contract from the firms counter for the given category, used so that another reinsurance contract can be issued for that category if needed. Accepts: category: Type Integer. contract: Type Class. Reinsurance contract issued to firm. No return values.""" - self.riskmodel.delete_reinsurance(category, contract) - self.category_reinsurance[category] = None + value = self.underwritten_risk_characterisation[contract.category][0] + self.reinsurance_profile.remove(contract, value) def issue_cat_bond( self, time: int, categ_id: int, per_value_per_period_premium: int = 0 @@ -416,7 +421,7 @@ def issue_cat_bond( avg_risk_factor, number_risks, _, - ] = self.characterize_underwritten_risks_by_category(categ_id) + ] = self.underwritten_risk_characterisation[categ_id] if number_risks > 0: # TODO: make runtime into a parameter risk = genericclasses.RiskProperties( @@ -496,13 +501,12 @@ def make_reinsurance_claims(self, time: int): contract.reincontract.explode(time, damage_extent=claims) for categ_id in range(self.simulation_no_risk_categories): - if ( - claims_this_turn[categ_id] > 0 - and self.category_reinsurance[categ_id] is not None - ): - self.category_reinsurance[categ_id].explode( - time, damage_extent=claims_this_turn[categ_id] + if claims_this_turn[categ_id] > 0: + to_explode = self.reinsurance_profile.contracts_to_explode( + damage=claims_this_turn[categ_id], category=categ_id ) + for contract in to_explode: + contract.explode(time, damage_extent=claims_this_turn[categ_id]) def get_excess_of_loss_reinsurance(self) -> MutableSequence[Mapping]: """Method to return list containing the reinsurance for each category interms of the reinsurer, value of @@ -511,47 +515,46 @@ def get_excess_of_loss_reinsurance(self) -> MutableSequence[Mapping]: Returns: reinsurance: Type list of DataDicts.""" reinsurance = [] - for categ_id in range(self.simulation_no_risk_categories): - if self.category_reinsurance[categ_id] is not None: - reinsurance_contract = {} - reinsurance_contract["reinsurer"] = self.category_reinsurance[ - categ_id - ].insurer - reinsurance_contract["value"] = self.category_reinsurance[ - categ_id - ].value - reinsurance_contract["category"] = categ_id - reinsurance.append(reinsurance_contract) + for contract in self.reinsurance_profile.all_contracts(): + reinsurance.append( + { + "reinsurer": contract.insurer, + # QUERY: value vs excess? + "value": contract.value, + "category": contract.category, + } + ) return reinsurance - def create_reinrisk( - self, time: int, categ_id: int + def refresh_reinrisk( + self, time: int, old_contract: "ReinsuranceContract" ) -> Optional[genericclasses.RiskProperties]: - """Proceed with creation of reinsurance risk only if category is not empty.""" + # TODO: Can be merged + """Takes an expiring contract and returns a renewed risk to automatically offer to the existing reinsurer. + The new risk has the same deductible and excess as the old one, but with an updated time""" [ total_value, avg_risk_factor, number_risks, periodized_total_premium, - ] = self.characterize_underwritten_risks_by_category(categ_id) - if number_risks > 0: - # TODO: make runtime into a parameter - risk = genericclasses.RiskProperties( - value=total_value, - category=categ_id, - owner=self, - insurancetype="excess-of-loss", - number_risks=number_risks, - deductible_fraction=self.np_reinsurance_deductible_fraction, - excess_fraction=self.np_reinsurance_excess_fraction, - periodized_total_premium=periodized_total_premium, - runtime=12, - expiration=time + 12, - risk_factor=avg_risk_factor, - ) - return risk - else: + ] = self.underwritten_risk_characterisation[old_contract.category] + if number_risks == 0: + # If the insurerer currently has no risks in that category it probably doesn't want reinsurance return None + risk = genericclasses.RiskProperties( + value=total_value, + category=old_contract.category, + owner=self, + insurancetype="excess-of-loss", + number_risks=number_risks, + deductible_fraction=old_contract.deductible / total_value, + excess_fraction=old_contract.excess / total_value, + periodized_total_premium=periodized_total_premium, + runtime=12, + expiration=time + 12, + risk_factor=avg_risk_factor, + ) + return risk class ReinsuranceFirm(InsuranceFirm): diff --git a/insurancesimulation.py b/insurancesimulation.py index 6eccbb5..8a04867 100644 --- a/insurancesimulation.py +++ b/insurancesimulation.py @@ -1,12 +1,8 @@ -from __future__ import annotations import math import random import copy - -import genericclasses import logger import warnings -from typing import Mapping, MutableMapping, MutableSequence, Sequence, Any, Optional import scipy.stats import numpy as np @@ -15,10 +11,20 @@ import visualization_network import insurancefirms import isleconfig -from genericclasses import GenericAgent, RiskProperties, AgentProperties, Distribution -from metainsuranceorg import MetaInsuranceOrg +from genericclasses import ( + GenericAgent, + RiskProperties, + AgentProperties, + Constant, +) import catbond +from typing import Mapping, MutableMapping, MutableSequence, Sequence, Any, Optional +from typing import TYPE_CHECKING +if TYPE_CHECKING: + from genericclasses import Distribution + from metainsuranceorg import MetaInsuranceOrg + class InsuranceSimulation(GenericAgent): """ Simulation object that is responsible for handling all aspects of the world. @@ -39,7 +45,7 @@ def __init__( simulation_parameters: MutableMapping, rc_event_schedule: MutableSequence[MutableSequence[int]], rc_event_damage: MutableSequence[MutableSequence[float]], - damage_distribution: Distribution = TruncatedDistWrapper( + damage_distribution: "Distribution" = TruncatedDistWrapper( lower_bound=0.25, upper_bound=1.0, dist=scipy.stats.pareto(b=2, loc=0, scale=0.25), @@ -70,7 +76,7 @@ def __init__( # QUERY: The distribution given is bounded by [0.25, 1.0]. Should this always be the case? "Unpacks parameters and sets distributions" - self.damage_distribution: Distribution = damage_distribution + self.damage_distribution: "Distribution" = damage_distribution self.catbonds_off: bool = simulation_parameters["catbonds_off"] self.reinsurance_off: bool = simulation_parameters["reinsurance_off"] @@ -93,9 +99,9 @@ def __init__( loc=self.risk_factor_lower_bound, scale=self.risk_factor_spread ) else: - self.risk_factor_distribution = genericclasses.Constant(loc=1.0) + self.risk_factor_distribution = Constant(loc=1.0) # self.risk_value_distribution = scipy.stats.uniform(loc=100, scale=9900) - self.risk_value_distribution = genericclasses.Constant(loc=1000) + self.risk_value_distribution = Constant(loc=1000) risk_factor_mean = self.risk_factor_distribution.mean() @@ -362,7 +368,7 @@ def add_agents( self, agent_class: type, agent_class_string: str, - agents: Sequence[GenericAgent] = None, + agents: "Sequence[GenericAgent]" = None, n: int = 1, ): """Method for building agents and adding them to the simulation. Can also add pre-made catbond agents directly @@ -857,11 +863,17 @@ def get_cat_bond_price(self, np_reinsurance_deductible_fraction: float) -> float - max_reduction * np_reinsurance_deductible_fraction ) - def append_reinrisks(self, item: RiskProperties): + def append_reinrisks(self, reinrisk: RiskProperties): """Method for appending reinrisks to simulation instance. Called from insurancefirm Accepts: item (Type: List)""" - if item: - self.reinrisks.append(item) + + # For debugging: + # for old_reinrisk in self.reinrisks: + # if reinrisk.owner is old_reinrisk.owner and reinrisk.category == old_reinrisk.category: + # pass + if reinrisk: + self.reinrisks.append(reinrisk) + def remove_reinrisks(self, risko: RiskProperties): if risko is not None: @@ -874,7 +886,7 @@ def get_reinrisks(self) -> Sequence[RiskProperties]: return self.reinrisks def solicit_insurance_requests( - self, insurer: MetaInsuranceOrg + self, insurer: "MetaInsuranceOrg" ) -> Sequence[RiskProperties]: """Method for determining which risks are to be assessed by firms based on insurer weights Accepts: @@ -895,7 +907,7 @@ def solicit_insurance_requests( return risks_to_be_sent def solicit_reinsurance_requests( - self, reinsurer: MetaInsuranceOrg + self, reinsurer: "MetaInsuranceOrg" ) -> Sequence[RiskProperties]: """Method for determining which reinsurance risks are to be assessed by firms based on reinsurer weights Accepts: @@ -1130,7 +1142,7 @@ def reset_pls(self): for cb in self.catbonds: cb.reset_pl() - def get_risk_share(self, firm: MetaInsuranceOrg) -> float: + def get_risk_share(self, firm: "MetaInsuranceOrg") -> float: """Method to determine the total percentage of risks in the market that are held by a particular firm. For insurers uses insurance risks, for reinsurers uses reinsurance risks diff --git a/isleconfig.py b/isleconfig.py index 8510659..3fd383c 100644 --- a/isleconfig.py +++ b/isleconfig.py @@ -108,4 +108,7 @@ # Determines the minimum fraction of inaccuracy that insurers can achieve - a value of 0 means the biggest insurers # can be perfectly accurate, a value of 1 disables changes in inaccuracy based on size "scale_inaccuracy": 0.3, + # The smallest number of tranches that an insurer will issue when asking for reinsurance. Note: even if this is 1, + # insurers will still end up with layered reinsurance to fill gaps + "min_tranches": 1, } diff --git a/metainsurancecontract.py b/metainsurancecontract.py index 25fddcd..abcf27b 100644 --- a/metainsurancecontract.py +++ b/metainsurancecontract.py @@ -1,13 +1,14 @@ -from __future__ import annotations -from genericclasses import RiskProperties, GenericAgent -import metainsuranceorg +from typing import TYPE_CHECKING +if TYPE_CHECKING: + from metainsuranceorg import MetaInsuranceOrg + from genericclasses import GenericAgent, RiskProperties class MetaInsuranceContract: def __init__( self, - insurer: metainsuranceorg.MetaInsuranceOrg, - risk: RiskProperties, + insurer: "MetaInsuranceOrg", + risk: "RiskProperties", time: int, premium: float, runtime: int, @@ -41,10 +42,10 @@ def __init__( # TODO: argument reinsurance seems senseless; remove? # Save parameters - self.insurer: metainsuranceorg.MetaInsuranceOrg = insurer + self.insurer: "MetaInsuranceOrg" = insurer self.risk_factor = risk.risk_factor self.category = risk.category - self.property_holder: GenericAgent = risk.owner + self.property_holder: "GenericAgent" = risk.owner self.value = risk.value self.contract = risk.contract # May be None self.risk = risk @@ -66,7 +67,7 @@ def __init__( if risk.deductible_fraction is not None else default_deductible_fraction ) - self.deductible = self.deductible_fraction * self.value + self.deductible = round(self.deductible_fraction * self.value) # set excess from argument, risk property or default value, whichever first is not None default_excess_fraction = 1.0 @@ -78,7 +79,7 @@ def __init__( else default_excess_fraction ) - self.excess = self.excess_fraction * self.value + self.excess = round(self.excess_fraction * self.value) self.reinsurance = reinsurance self.reinsurer = None diff --git a/metainsuranceorg.py b/metainsuranceorg.py index 0942734..e6f0198 100644 --- a/metainsuranceorg.py +++ b/metainsuranceorg.py @@ -1,29 +1,28 @@ -from __future__ import annotations -import copy import math import functools -from typing import ( - Optional, - Tuple, - Sequence, - Mapping, - MutableSequence, - Iterable, - Callable, - Any, -) -from itertools import cycle, islice +from itertools import cycle, islice, chain import numpy as np import scipy.stats import isleconfig -from insurancecontract import InsuranceContract -import insurancesimulation -from reinsurancecontract import ReinsuranceContract -import metainsurancecontract -from riskmodel import RiskModel -from genericclasses import GenericAgent, RiskProperties, AgentProperties, Obligation +import insurancecontract +import reinsurancecontract +import riskmodel +from genericclasses import ( + GenericAgent, + RiskProperties, + AgentProperties, + Obligation, + ReinsuranceProfile, +) + +from typing import Optional, Tuple, Sequence, Mapping, MutableSequence, Iterable, Callable, Any +from typing import TYPE_CHECKING +if TYPE_CHECKING: + from insurancesimulation import InsuranceSimulation + from metainsurancecontract import MetaInsuranceContract + from reinsurancecontract import ReinsuranceContract def roundrobin(iterables: Sequence[Iterable]) -> Iterable: @@ -75,7 +74,7 @@ def __init__( Constructor creates general instance of an insurance company which is inherited by the reinsurance and insurance firm classes. Initialises all necessary values provided by config file.""" super().__init__() - self.simulation: insurancesimulation.InsuranceSimulation = simulation_parameters[ + self.simulation: "InsuranceSimulation" = simulation_parameters[ "simulation" ] self.simulation_parameters: Mapping = simulation_parameters @@ -143,7 +142,7 @@ def __init__( 1 - isleconfig.simulation_parameters["scale_inaccuracy"] ) - self.riskmodel: RiskModel = RiskModel( + self.riskmodel: riskmodel.RiskModel = riskmodel.RiskModel( damage_distribution=rm_config["damage_distribution"], expire_immediately=rm_config["expire_immediately"], cat_separation_distribution=rm_config["cat_separation_distribution"], @@ -157,9 +156,9 @@ def __init__( inaccuracy=self.max_inaccuracy, ) - self.category_reinsurance = [ - None for _ in range(self.simulation_no_risk_categories) - ] + # Set up the reinsurance profile + self.reinsurance_profile = ReinsuranceProfile(self.riskmodel) + if self.simulation_reinsurance_type == "non-proportional": if agent_parameters.non_proportional_reinsurance_level is not None: self.np_reinsurance_deductible_fraction = ( @@ -176,9 +175,8 @@ def __init__( "default_non-proportional_reinsurance_premium_share" ] self.underwritten_contracts: MutableSequence[ - metainsurancecontract.MetaInsuranceContract + "MetaInsuranceContract" ] = [] - # self.reinsurance_contracts = [] self.is_insurer = True self.is_reinsurer = False @@ -201,6 +199,13 @@ def __init__( self.simulation_parameters["no_categories"] ) self.market_permanency_counter = 0 + # TODO: make this into a dict + self.underwritten_risk_characterisation: MutableSequence[ + Tuple[float, float, int, float] + ] = [ + (None, None, None, None) + for _ in range(self.simulation_parameters["no_categories"]) + ] # The share of all risks that this firm holds. Gets updated every timestep self.risk_share = 0 @@ -238,7 +243,7 @@ def iterate(self, time: int): """Check what proportion of the risk market we hold and then update the riskmodel accordingly""" self.update_risk_share() - self.adjust_riskmodel() + self.adjust_riskmodel_inaccuracy() """Collect and process new risks""" self.collect_process_evaluate_risks(time, contracts_dissolved) @@ -256,7 +261,10 @@ def collect_process_evaluate_risks( self, time: int, contracts_dissolved: int ) -> None: if self.operational: - + self.update_risk_characterisations() + for categ in range(len(self.counter_category)): + value = self.underwritten_risk_characterisation[categ][0] + self.reinsurance_profile.update_value(value, categ) """request risks to be considered for underwriting in the next period and collect those for this period""" new_nonproportional_risks, new_risks = self.get_newrisks_by_type() contracts_offered = len(new_risks) @@ -279,21 +287,20 @@ def collect_process_evaluate_risks( # TODO: find an efficient way to stop the loop if there are no more risks to accept or if it is # not accepting any more over several iterations. # Here we process all the new reinrisks in order to keep the portfolio as balanced as possible. - former_reinrisks_per_categ = copy.copy(reinrisks_per_categ) - [ - reinrisks_per_categ, - not_accepted_reinrisks, - ] = self.process_newrisks_reinsurer(reinrisks_per_categ, time) + has_accepted_risks, not_accepted_reinrisks = self.process_newrisks_reinsurer( + reinrisks_per_categ, time + ) - # QUERY: I moved this into the loop - was this correct? # The loop only runs once in my tests, what needs tweaking to have firms not accept risks? - self.simulation.return_reinrisks(not_accepted_reinrisks) - - if former_reinrisks_per_categ == reinrisks_per_categ: + reinrisks_per_categ = not_accepted_reinrisks + if not has_accepted_risks: # Stop condition implemented. Might solve the previous TODO. break + self.simulation.return_reinrisks( + list(chain.from_iterable(not_accepted_reinrisks)) + ) - # TODO: This takes up about 8% of processing time. Can we update the list instead of rebuilding it? + # TODO: This takes up a lot of processing time. Can we update the list instead of rebuilding it? underwritten_risks = [ RiskProperties( owner=self, @@ -324,8 +331,12 @@ def collect_process_evaluate_risks( # This is currently so because it minimizes the number of times we need to run self.riskmodel.evaluate(). # It would also be more consistent if excess capital would be updated at the end of the iteration. """handle adjusting capacity target and capacity""" + # max_var_by_categ is the max_var_by_categ = self.cash - self.excess_capital self.adjust_capacity_target(max_var_by_categ) + + self.update_risk_characterisations() + actual_capacity = self.increase_capacity(time, max_var_by_categ) # TODO: make independent of insurer/reinsurer, but change this to different deductible values @@ -353,23 +364,21 @@ def collect_process_evaluate_risks( for repetition in range(self.recursion_limit): # TODO: find an efficient way to stop the recursion if there are no more risks to accept or if it is # not accepting any more over several iterations. Done, maybe? - former_risks_per_categ = copy.copy(risks_per_categ) # Here we process all the new risks in order to keep the portfolio as balanced as possible. - risks_per_categ, not_accepted_risks = self.process_newrisks_insurer( + has_accepted_risks, not_accepted_risks = self.process_newrisks_insurer( risks_per_categ, acceptable_by_category, var_per_risk_per_categ, cash_left_by_categ, time, ) - # QUERY: As above, moved inside loop - self.simulation.return_risks(not_accepted_risks) - if ( - former_risks_per_categ == risks_per_categ - ): # Stop condition implemented. Might solve the previous TODO. + risks_per_categ = not_accepted_risks + if not has_accepted_risks: + # Stop condition implemented. Might solve the previous TODO. break - + self.simulation.return_risks(list(chain.from_iterable(not_accepted_risks))) # print(self.id, " now has ", len(self.underwritten_contracts), " & returns ", len(not_accepted_risks)) + self.update_risk_characterisations() def enter_illiquidity(self, time: int, sum_due: float): """Enter_illiquidity Method. @@ -451,9 +460,8 @@ def dissolve(self, time: int, record: str): # TODO: This seems... odd? method_to_call = getattr(self.simulation, record) method_to_call() - for category_reinsurance in self.category_reinsurance: - if category_reinsurance is not None: - category_reinsurance.dissolve(time) + for reincontract in self.reinsurance_profile.all_contracts(): + reincontract.dissolve(time) self.operational = False def pay_dividends(self, time: int): @@ -476,7 +484,7 @@ def obtain_yield(self, time: int): self.simulation.receive_obligation(amount, self, time, "yields") def mature_contracts(self, time: int) -> int: - """Method to mature contracts that have expired + """Method to mature underwritten contracts that have expired Accepts: time: Type integer Returns: @@ -511,7 +519,7 @@ def number_underwritten_contracts(self) -> int: def get_underwritten_contracts( self - ) -> Sequence[metainsurancecontract.MetaInsuranceContract]: + ) -> Sequence["MetaInsuranceContract"]: return self.underwritten_contracts def get_profitslosses(self) -> float: @@ -595,6 +603,39 @@ def adjust_capacity_target(self, time): "Method not implemented. adjust_capacity_target method should be implemented in inheriting classes" ) + def update_risk_characterisations(self): + for categ in range(self.simulation_no_risk_categories): + self.underwritten_risk_characterisation[ + categ + ] = self.characterise_underwritten_risks_by_category(categ) + + def characterise_underwritten_risks_by_category( + self, categ_id: int + ) -> Tuple[float, float, int, float]: + """Method to characterise associated risks in a given category in terms of value, number, avg risk factor, and + total premium per iteration. + Accepts: + categ_id: Type Integer. The given category for characterising risks. + Returns: + total_value: Type Decimal. Total value of all contracts in the category. + avg_risk_facotr: Type Decimal. Avg risk factor of all contracted risks in category. + number_risks: Type Integer. Total number of contracted risks in category. + periodised_total_premium: Total value per month of all contracts premium payments.""" + # TODO: Update this instead of recalculating so much + total_value = 0 + avg_risk_factor = 0 + number_risks = 0 + periodized_total_premium = 0 + for contract in self.underwritten_contracts: + if contract.category == categ_id: + total_value += contract.value + avg_risk_factor += contract.risk_factor + number_risks += 1 + periodized_total_premium += contract.periodized_premium + if number_risks > 0: + avg_risk_factor /= number_risks + return total_value, avg_risk_factor, number_risks, periodized_total_premium + def risks_reinrisks_organizer( self, new_risks: Sequence[RiskProperties] ) -> Sequence[Sequence[RiskProperties]]: @@ -650,14 +691,14 @@ def balanced_portfolio( percentage_value_at_risk = self.riskmodel.get_ppf( categ_id=risk.category, tail_size=self.riskmodel.var_tail_prob ) - expected_damage = ( + var_damage = ( percentage_value_at_risk * risk.value * risk.risk_factor * self.riskmodel.inaccuracy[risk.category] ) - expected_claim = ( - min(expected_damage, risk.value * risk.excess_fraction) + var_claim = ( + min(var_damage, risk.value * risk.excess_fraction) - risk.value * risk.deductible_fraction ) @@ -665,7 +706,7 @@ def balanced_portfolio( # Compute how the cash reserved by category would change if the new reinsurance risk was accepted cash_reserved_by_categ_store[risk.category] += ( - expected_claim * self.riskmodel.margin_of_safety + var_claim * self.riskmodel.margin_of_safety ) else: @@ -705,7 +746,8 @@ def process_newrisks_reinsurer( they should be underwritten or not. It is done in this way to maintain the portfolio as balanced as possible. For that reason we process risk[C1], risk[C2], risk[C3], risk[C4], risk[C1], risk[C2], ... and so forth. If risks are accepted then a contract is written.""" - not_accepted_reinrisks = [] + not_accepted_reinrisks = [[] for _ in range(len(reinrisks_per_categ))] + has_accepted_risks = False for risk in roundrobin(reinrisks_per_categ): # Here we take only one risk per category at a time to achieve risk[C1], risk[C2], risk[C3], # risk[C4], risk[C1], risk[C2], ... if possible. @@ -750,7 +792,7 @@ def process_newrisks_reinsurer( ) if condition: - contract = ReinsuranceContract( + contract = reinsurancecontract.ReinsuranceContract( self, risk, time, @@ -764,13 +806,14 @@ def process_newrisks_reinsurer( insurancetype=risk.insurancetype, ) # TODO: implement excess of loss for reinsurance contracts self.underwritten_contracts.append(contract) + has_accepted_risks = True self.cash_left_by_categ = cash_left_by_categ else: - not_accepted_reinrisks.append(risk) + not_accepted_reinrisks[risk.category].append(risk) else: - not_accepted_reinrisks.append(risk) + not_accepted_reinrisks[risk.category].append(risk) - return reinrisks_per_categ, not_accepted_reinrisks + return has_accepted_risks, not_accepted_reinrisks def process_newrisks_insurer( self, @@ -779,7 +822,7 @@ def process_newrisks_insurer( var_per_risk_per_categ: Sequence[float], cash_left_by_categ: Sequence[float], time: int, - ) -> Tuple[Sequence[Sequence[RiskProperties]], Sequence[RiskProperties]]: + ) -> Tuple[bool, Sequence[Sequence[RiskProperties]]]: """Method to decide if new risks are underwritten for the insurance firm. Accepts: risks_per_categ: Type List of lists containing new risks. @@ -795,7 +838,8 @@ def process_newrisks_insurer( For that reason we process risk[C1], risk[C2], risk[C3], risk[C4], risk[C1], risk[C2], ... and so forth. If risks are accepted then a contract is written.""" random_runtime = self.contract_runtime_dist.rvs() - not_accepted_risks = [] + not_accepted_risks = [[] for _ in range(len(risks_per_categ))] + has_accepted_risks = False for risk in roundrobin(risks_per_categ): assert risk if acceptable_by_category[risk.category] > 0: @@ -808,7 +852,7 @@ def process_newrisks_insurer( # Here we check whether the portfolio is balanced or not if the reinrisk (risk_to_insure) is # underwritten. Return True if it is balanced. False otherwise. if condition: - contract = ReinsuranceContract( + contract = reinsurancecontract.ReinsuranceContract( self, risk, time, @@ -820,10 +864,11 @@ def process_newrisks_insurer( ], ) self.underwritten_contracts.append(contract) + has_accepted_risks = True self.cash_left_by_categ = cash_left_by_categ acceptable_by_category[risk.category] -= 1 else: - not_accepted_risks.append(risk) + not_accepted_risks[risk.category].append(risk) else: [condition, cash_left_by_categ] = self.balanced_portfolio( @@ -834,7 +879,7 @@ def process_newrisks_insurer( # Here it is check whether the portfolio is balanced or not if the risk (risk_to_insure) is # underwritten. Return True if it is balanced. False otherwise. if condition: - contract = InsuranceContract( + contract = insurancecontract.InsuranceContract( self, risk, time, @@ -847,16 +892,17 @@ def process_newrisks_insurer( initial_var=var_per_risk_per_categ[risk.category], ) self.underwritten_contracts.append(contract) + has_accepted_risks = True self.cash_left_by_categ = cash_left_by_categ acceptable_by_category[risk.category] -= 1 else: - not_accepted_risks.append(risk) + not_accepted_risks[risk.category].append(risk) else: - not_accepted_risks.append(risk) + not_accepted_risks[risk.category].append(risk) # TODO: allow different values per risk (i.e. sum over value (and reinsurance_share) or # exposure instead of counting) # QUERY: should we only decrease this if the risk is accepted? - return risks_per_categ, not_accepted_risks + return has_accepted_risks, not_accepted_risks def market_permanency(self, time: int): """Method determining if firm stays in market. @@ -986,8 +1032,8 @@ def roll_over(self, time: int): for reincontract in maturing_next: if reincontract.property_holder.operational: reincontract.roll_over_flag = 1 - reinrisk = reincontract.property_holder.create_reinrisk( - time, reincontract.category + reinrisk = reincontract.property_holder.refresh_reinrisk( + time=time, old_contract=reincontract ) if ( next(uniform_rvs) @@ -1017,8 +1063,8 @@ def insurance_premium(self) -> float: ) return premium - def adjust_riskmodel(self): - """Adjusts the inaccuracy parameter in the risk model under use depending on the share of risks held. + def adjust_riskmodel_inaccuracy(self): + """Adjusts the inaccuracy parameter in the risk model in use depending on the share of risks held. Accepts no parameters and has no return Shrinks the risk model towards the best available risk model (as determined by "scale_inaccuracy" in isleconfig) diff --git a/reinsurancecontract.py b/reinsurancecontract.py index ca2a0b3..a4b3264 100644 --- a/reinsurancecontract.py +++ b/reinsurancecontract.py @@ -1,8 +1,14 @@ -from metainsurancecontract import MetaInsuranceContract -import insurancefirms +import metainsurancecontract +from typing import Optional +from typing import TYPE_CHECKING +if TYPE_CHECKING: + from insurancefirms import InsuranceFirm + from metainsuranceorg import MetaInsuranceOrg + from genericclasses import RiskProperties -class ReinsuranceContract(MetaInsuranceContract): + +class ReinsuranceContract(metainsurancecontract.MetaInsuranceContract): """ReinsuranceContract class. Inherits from InsuranceContract. Constructor is not currently required but may be used in the future to distinguish InsuranceContract @@ -12,18 +18,18 @@ class ReinsuranceContract(MetaInsuranceContract): def __init__( self, - insurer, - risk, - time, - premium, - runtime, - payment_period, - expire_immediately, - initial_var=0.0, - insurancetype="proportional", - deductible_fraction=None, - excess_fraction=None, - reinsurance=0, + insurer: "MetaInsuranceOrg", + risk: "RiskProperties", + time: int, + premium: float, + runtime: int, + payment_period: int, + expire_immediately: bool, + initial_var: float = 0.0, + insurancetype: str = "proportional", + deductible_fraction: "Optional[float]"=None, + excess_fraction: "Optional[float]"=None, + reinsurance: float=0, ): super().__init__( insurer, @@ -40,21 +46,15 @@ def __init__( reinsurance, ) # self.is_reinsurancecontract = True - assert type(self.property_holder) is insurancefirms.InsuranceFirm - self.property_holder: insurancefirms.InsuranceFirm + self.property_holder: "InsuranceFirm" if self.insurancetype not in ["excess-of-loss", "proportional"]: raise ValueError(f'Unrecognised insurance type "{self.insurancetype}"') if self.insurancetype == "excess-of-loss": - self.property_holder.add_reinsurance( - category=self.category, - excess_fraction=self.excess_fraction, - deductible_fraction=self.deductible_fraction, - contract=self, - ) + self.property_holder.add_reinsurance(contract=self) else: assert self.contract is not None - def explode(self, time, uniform_value=None, damage_extent=None): + def explode(self, time: int, uniform_value: None=None, damage_extent: float=None): """Explode method. Accepts arguments time: Type integer. The current time. @@ -65,7 +65,8 @@ def explode(self, time, uniform_value=None, damage_extent=None): Method marks the contract for termination. """ assert uniform_value is None - + if damage_extent is None: + raise ValueError("Damage extend should be given") # QUERY: What is the difference? Also, what happens if damage_extent = None? if damage_extent > self.deductible: # QUERY: Changed this, for the better? @@ -81,7 +82,6 @@ def explode(self, time, uniform_value=None, damage_extent=None): ) else: raise ValueError(f"Unexpected insurance type {self.insurancetype}") - # Reinsurer pays as soon as possible. # Every reinsurance claim made is immediately registered. self.insurer.register_claim(claim) @@ -93,7 +93,7 @@ def explode(self, time, uniform_value=None, damage_extent=None): self.expiration = time # self.terminating = True - def mature(self, time): + def mature(self, time: int): """Mature method. Accepts arguments time: Type integer. The current time. @@ -104,8 +104,6 @@ def mature(self, time): self.terminate_reinsurance(time) if self.insurancetype == "excess-of-loss": - self.property_holder.delete_reinsurance( - category=self.category, contract=self - ) + self.property_holder.delete_reinsurance(contract=self) else: # TODO: ? Instead: if self.insurancetype == "proportional": self.contract.unreinsure() diff --git a/requirements.txt b/requirements.txt index be2bd82..7c3d507 100644 --- a/requirements.txt +++ b/requirements.txt @@ -3,3 +3,5 @@ numpy>=1.13.3 matplotlib>=2.1.1 networkx>=2.0 argparse>=1.1 +sortedcontainers>=2.1.0 +dataclasses>=0.6 \ No newline at end of file diff --git a/riskmodel.py b/riskmodel.py index e96a967..76cb17b 100644 --- a/riskmodel.py +++ b/riskmodel.py @@ -1,20 +1,23 @@ import math -from typing import Sequence, Tuple, Union, Optional, MutableSequence +from copy import deepcopy import numpy as np import isleconfig from distributionreinsurance import ReinsuranceDistWrapper -from genericclasses import RiskProperties, Distribution -from metainsurancecontract import MetaInsuranceContract +from typing import Sequence, Tuple, Union, Optional, MutableSequence + +from typing import TYPE_CHECKING +if TYPE_CHECKING: + from genericclasses import Distribution, RiskProperties class RiskModel: def __init__( self, - damage_distribution: Distribution, + damage_distribution: "Distribution", expire_immediately: bool, - cat_separation_distribution: Distribution, + cat_separation_distribution: "Distribution", norm_premium: float, category_number: int, init_average_exposure: float, @@ -36,15 +39,10 @@ def __init__( self.margin_of_safety = margin_of_safety """damage_distribution is some scipy frozen rv distribution which is bound between 0 and 1 and indicates the share of risks suffering damage as part of any single catastrophic peril""" - self.damage_distribution: MutableSequence[Distribution] = [ + self.damage_distribution: MutableSequence["Distribution"] = [ damage_distribution for _ in range(self.category_number) ] - self.damage_distribution_stack: Sequence[MutableSequence[Distribution]] = [ - [] for _ in range(self.category_number) - ] - self.reinsurance_contract_stack: Sequence[ - MutableSequence[MetaInsuranceContract] - ] = [[] for _ in range(self.category_number)] + self.underlying_distribution = deepcopy(self.damage_distribution) # self.inaccuracy = np.random.uniform(9/10., 10/9., size=self.category_number) self.inaccuracy: Sequence[float] = inaccuracy @@ -57,8 +55,8 @@ def get_ppf(self, categ_id: int, tail_size: float) -> float: return self.damage_distribution[categ_id].ppf(1 - tail_size) def get_risks_by_categ( - self, risks: Sequence[RiskProperties] - ) -> Sequence[Sequence[RiskProperties]]: + self, risks: Sequence["RiskProperties"] + ) -> Sequence[Sequence["RiskProperties"]]: """Method splits list of risks by category Accepts: risks: Type List of DataDicts @@ -70,7 +68,7 @@ def get_risks_by_categ( return risks_by_categ def compute_expectation( - self, categ_risks: Sequence[RiskProperties], categ_id: int + self, categ_risks: Sequence["RiskProperties"], categ_id: int ) -> Tuple[float, float, float]: # TODO: more intuitive name? """Method to compute the average exposure and risk factor as well as the increase in expected profits for the @@ -87,6 +85,7 @@ def compute_expectation( runtimes = np.zeros(len(categ_risks)) for i, risk in enumerate(categ_risks): # TODO: factor in excess instead of value? + assert risk.excess is not None exposures[i] = risk.value - risk.deductible risk_factors[i] = risk.risk_factor runtimes[i] = risk.runtime @@ -131,7 +130,7 @@ def compute_expectation( return average_risk_factor, average_exposure, incr_expected_profits def evaluate_proportional( - self, risks: Sequence[RiskProperties], cash: Sequence[float] + self, risks: Sequence["RiskProperties"], cash: Sequence[float] ) -> Tuple[float, Sequence[int], Sequence[int], Sequence[float]]: """Method to evaluate proportional type risks. Accepts: @@ -184,9 +183,7 @@ def evaluate_proportional( # QUERY: Is the margin of safety appiled twice? (above and below) # record liquidity requirement and apply margin of safety for liquidity requirement - necessary_liquidity += ( - var_per_risk * self.margin_of_safety * len(categ_risks) - ) + necessary_liquidity += var_per_risk * len(categ_risks) if isleconfig.verbose: print(self.inaccuracy) print( @@ -221,16 +218,6 @@ def evaluate_proportional( else: expected_profits /= necessary_liquidity - max_cash_by_categ = max(cash_left_by_category) - floored_cash_by_categ = cash_left_by_category.copy() - floored_cash_by_categ[floored_cash_by_categ < 0] = 0 - # remaining_acceptable_by_category_old = remaining_acceptable_by_category.copy() - for categ_id in range(self.category_number): - # QUERY: Where does this come from? - remaining_acceptable_by_category[categ_id] = math.floor( - remaining_acceptable_by_category[categ_id] - * pow(floored_cash_by_categ[categ_id] / max_cash_by_categ, 5) - ) if isleconfig.verbose: print( "RISKMODEL returns: ", @@ -246,9 +233,9 @@ def evaluate_proportional( def evaluate_excess_of_loss( self, - risks: Sequence[RiskProperties], + risks: Sequence["RiskProperties"], cash: Sequence[float], - offered_risk: Optional[RiskProperties] = None, + offered_risk: Optional["RiskProperties"] = None, ) -> Tuple[Sequence[float], Sequence[float], float]: """Method to evaluate excess-of-loss type risks. Accepts: @@ -277,44 +264,44 @@ def evaluate_excess_of_loss( # TODO: allow for different risk distributions for different categories # TODO: factor in risk_factors + # QUERY: both done? percentage_value_at_risk = self.get_ppf( categ_id=categ_id, tail_size=self.var_tail_prob ) # compute liquidity requirements from existing contracts for risk in categ_risks: - expected_damage = ( + # QUERY: Expected in this context means damage at var_tail_prob rather than expectation? + var_damage = ( percentage_value_at_risk * risk.value * risk.risk_factor * self.inaccuracy[categ_id] ) - # QUERY: This doesn't look accurate to me - E(f(X)) != f(E(X)) in general - # QUERY: Isn't this wrong? - expected_claim = min(expected_damage, risk.excess) - risk.deductible + var_claim = max(min(var_damage, risk.excess) - risk.deductible, 0) # record liquidity requirement and apply margin of safety for liquidity requirement - cash_left_by_categ[categ_id] -= expected_claim * self.margin_of_safety + cash_left_by_categ[categ_id] -= var_claim * self.margin_of_safety # compute additional liquidity requirements from newly offered contract if (offered_risk is not None) and (offered_risk.category == categ_id): - expected_damage_fraction = ( + var_damage_fraction = ( percentage_value_at_risk * offered_risk.risk_factor * self.inaccuracy[categ_id] ) - expected_claim_fraction = ( - min(expected_damage_fraction, offered_risk.excess_fraction) + var_claim_fraction = ( + min(var_damage_fraction, offered_risk.excess_fraction) - offered_risk.deductible_fraction ) - expected_claim_total = expected_claim_fraction * offered_risk.value + var_claim_total = var_claim_fraction * offered_risk.value # record liquidity requirement and apply margin of safety for liquidity requirement additional_required[categ_id] += ( - expected_claim_total * self.margin_of_safety + var_claim_total * self.margin_of_safety ) - additional_var_per_categ[categ_id] += expected_claim_total + additional_var_per_categ[categ_id] += var_claim_total # Additional value at risk should only occur in one category. Assert that this is the case. assert sum(additional_var_per_categ > 0) <= 1 @@ -325,9 +312,9 @@ def evaluate_excess_of_loss( # noinspection PyUnboundLocalVariable def evaluate( self, - risks: Sequence[RiskProperties], + risks: Sequence["RiskProperties"], cash: Union[float, Sequence[float]], - offered_risk: Optional[RiskProperties] = None, + offered_risk: Optional["RiskProperties"] = None, ) -> Union[ Tuple[float, Sequence[int], Sequence[float], Sequence[float], float], Tuple[bool, Sequence[float], float, float], @@ -359,6 +346,7 @@ def evaluate( the offered_risk argument, whereas proportional risks are processed all at once leaving offered_risk = 0. This results in two sets of return values being used. These return values are what is used to determine if risks are underwritten or not.""" + # TODO: split this into two functions # ensure that any risk to be considered supplied directly as argument is non-proportional/excess-of-loss assert (offered_risk is None) or offered_risk.insurancetype == "excess-of-loss" @@ -411,45 +399,11 @@ def evaluate( min(cash_left_by_categ), ) - def add_reinsurance( - self, - categ_id: int, - excess_fraction: float, - deductible_fraction: float, - contract: MetaInsuranceContract, - ): - """Method to add any instance of EoL reinsurance to risk models list of reinsurance contracts, and add damage - distribution to stack of damage distributions per category, then replace with a new distribution. Only used in - the add_reinsurance method of insurancefirm. - Accepts: - categ_id: Type Integer. - excess_fraction: Type Decimal. - deductible_fraction: Type Decimal. - contract: Type DataDict. - No return values.""" - assert contract.insurancetype == "excess-of-loss" - self.damage_distribution_stack[categ_id].append( - self.damage_distribution[categ_id] - ) - self.reinsurance_contract_stack[categ_id].append(contract) - # QUERY: The riskmodel is based on the fractions, but these do not precisely correspond to the actual recovered - # claim if the value insured in that category grows or shrinks - self.damage_distribution[categ_id] = ReinsuranceDistWrapper( - lower_bound=deductible_fraction, - upper_bound=excess_fraction, - dist=self.damage_distribution[categ_id], - ) - - def delete_reinsurance(self, categ_id: int, contract: MetaInsuranceContract): - """Method to remove any instance of reinsurance to risk models list of reinsurance contracts, and remove its - damage distribution from the stack of damage distributions per category. Only used in the delete_reinsurance - method of insurancefirm. - Accepts: - categ_id: Type Integer. - contract: Type DataDict. - No return values.""" - assert self.reinsurance_contract_stack[categ_id][-1] == contract - self.reinsurance_contract_stack[categ_id].pop() - self.damage_distribution[categ_id] = self.damage_distribution_stack[ - categ_id - ].pop() + def set_reinsurance_coverage(self, value: float, coverage: MutableSequence[Tuple[float, float]], category: int): + """Updates the riskmodel for the category given to have the reinsurance given by coverage""" + # sometimes value==0, in which case we don't try to update the distribution + # (as the current coverage is effectively infinite) + if value > 0: + self.damage_distribution[category] = ReinsuranceDistWrapper( + self.underlying_distribution[category], coverage=coverage, value=value + ) diff --git a/start.py b/start.py index a547891..2ce6799 100644 --- a/start.py +++ b/start.py @@ -1,5 +1,5 @@ # import common packages -from __future__ import annotations + import argparse import hashlib import numpy as np From 36900c9690632bb48c2c21500f0988f365ef1378 Mon Sep 17 00:00:00 2001 From: Chris Hughes Date: Tue, 23 Jul 2019 16:05:28 +0100 Subject: [PATCH 065/125] Add provision for what should happen when paying a non-operational firm --- calibration_conditions.py | 13 ++++++++----- genericclasses.py | 17 +++++++++++++++-- insurancesimulation.py | 2 +- isleconfig.py | 5 +++-- metainsuranceorg.py | 2 ++ reinsurancecontract.py | 6 ++++-- setup_simulation.py | 5 +++-- start.py | 1 + 8 files changed, 37 insertions(+), 14 deletions(-) diff --git a/calibration_conditions.py b/calibration_conditions.py index 94a8980..3c3c4a5 100644 --- a/calibration_conditions.py +++ b/calibration_conditions.py @@ -143,11 +143,14 @@ def condition_insurance_coverage(logobj): def condition_reinsurance_coverage(logobj, minimum=0.6): """Test for reinsurance coverage close to some minimum that may be less than 100% (default 60%)""" - score = ( - logobj.history_logs["total_reincontracts"][-1] - * 1.0 - / (minimum * logobj.history_logs["total_contracts"][-1]) - ) + try: + score = ( + logobj.history_logs["total_reincontracts"][-1] + * 1.0 + / (minimum * logobj.history_logs["total_contracts"][-1]) + ) + except ZeroDivisionError: + score = 0 score = 1 if score > 1 else score return score diff --git a/genericclasses.py b/genericclasses.py index 65fd740..6b9d227 100644 --- a/genericclasses.py +++ b/genericclasses.py @@ -29,22 +29,35 @@ def __init__(self): self.obligations: MutableSequence["Obligation"] = [] self.operational: bool = True self.profits_losses: float = 0 + self.creditor = None + self.id = -1 def _pay(self, obligation: "Obligation"): """Method to _pay other class instances. Accepts: Obligation: Type DataDict No return value - Method removes value payed from the agents cash and adds it to recipient agents cash.""" + Method removes value payed from the agents cash and adds it to recipient agents cash. + If the recipient is not operational, redirect the payment to the creditor""" amount = obligation.amount recipient = obligation.recipient purpose = obligation.purpose + + if not amount >= 0: + raise ValueError("Attempting to pay an obligation for a negative ammount - something is wrong") # TODO: Think about what happens when paying non-operational firms - if self.get_operational() and recipient.get_operational(): + while not recipient.get_operational(): + if isleconfig.verbose: + print(f"Redirecting payment with purpose {purpose} due to non-operational firm {recipient.id}") + recipient = recipient.creditor + if self.get_operational(): self.cash -= amount if purpose is not "dividend": self.profits_losses -= amount recipient.receive(amount) + else: + if isleconfig.verbose: + print(f"Payment not processed as firm {self.id} is not operational") def get_operational(self) -> bool: """Method to return boolean of if agent is operational. Only used as check for payments. diff --git a/insurancesimulation.py b/insurancesimulation.py index 8a04867..5d4e839 100644 --- a/insurancesimulation.py +++ b/insurancesimulation.py @@ -182,7 +182,7 @@ def __init__( for i in range(self.simulation_parameters["no_risks"]) ] - self.risks_counter: MutableSequence[int] = [0, 0, 0, 0] + self.risks_counter: MutableSequence[int] = [0 for _ in range(self.simulation_parameters["no_categories"])] for risk in self.risks: self.risks_counter[risk.category] += 1 diff --git a/isleconfig.py b/isleconfig.py index 3fd383c..9febd3a 100644 --- a/isleconfig.py +++ b/isleconfig.py @@ -13,7 +13,7 @@ "no_categories": 4, "no_insurancefirms": 20, "no_reinsurancefirms": 4, - "no_riskmodels": 2, + "no_riskmodels": 3, # values >=1; inaccuracy higher with higher values "riskmodel_inaccuracy_parameter": 2, # values >=1; factor of additional liquidity beyond value at risk @@ -40,6 +40,7 @@ "acceptance_threshold_friction": 0.9, "insurance_firm_market_entry_probability": 0.3, # 0.02, "reinsurance_firm_market_entry_probability": 0.05, # 0.004, + # Determines the reinsurance type of the simulation. Should be "non-proportional" or "excess-of-loss" "simulation_reinsurance_type": "non-proportional", "default_non-proportional_reinsurance_deductible": 0.3, "default_non-proportional_reinsurance_excess": 1.0, @@ -110,5 +111,5 @@ "scale_inaccuracy": 0.3, # The smallest number of tranches that an insurer will issue when asking for reinsurance. Note: even if this is 1, # insurers will still end up with layered reinsurance to fill gaps - "min_tranches": 1, + "min_tranches": 5, } diff --git a/metainsuranceorg.py b/metainsuranceorg.py index e6f0198..889eac7 100644 --- a/metainsuranceorg.py +++ b/metainsuranceorg.py @@ -120,6 +120,8 @@ def __init__( "dividend_share_of_profits" ] + # If the firm goes bankrupt then by default any further payments should be made to the simulation + self.creditor = self.simulation self.owner = self.simulation # TODO: Make this into agent_parameter value? self.per_period_dividend = 0 self.cash_last_periods = list(np.zeros(4, dtype=int) * self.cash) diff --git a/reinsurancecontract.py b/reinsurancecontract.py index a4b3264..590d821 100644 --- a/reinsurancecontract.py +++ b/reinsurancecontract.py @@ -67,9 +67,11 @@ def explode(self, time: int, uniform_value: None=None, damage_extent: float=None assert uniform_value is None if damage_extent is None: raise ValueError("Damage extend should be given") - # QUERY: What is the difference? Also, what happens if damage_extent = None? if damage_extent > self.deductible: - # QUERY: Changed this, for the better? + # Proportional reinsurance is triggered by the individual reinsured contracts at the time of explosion. + # Since EoL reinsurance isn't triggered until the insurer manually makes a claim, this would mean that + # proportional reinsurance pays out a turn earlier than EoL. As such, proportional insurance claims are + # delayed for 1 turn. if self.insurancetype == "excess-of-loss": claim = min(self.excess, damage_extent) - self.deductible self.insurer.receive_obligation( diff --git a/setup_simulation.py b/setup_simulation.py index 77cb3dc..ef9811a 100644 --- a/setup_simulation.py +++ b/setup_simulation.py @@ -34,11 +34,11 @@ def __init__(self): self.no_categories = self.simulation_parameters["no_categories"] """set distribution""" # TODO: this should be a parameter - self.non_truncated = scipy.stats.pareto( + non_truncated = scipy.stats.pareto( b=2, loc=0, scale=0.25 ) # It is assumed that the damages of the catastrophes are drawn from a truncated Pareto distribution. self.damage_distribution = TruncatedDistWrapper( - lower_bound=0.25, upper_bound=1.0, dist=self.non_truncated + lower_bound=0.25, upper_bound=1.0, dist=non_truncated ) self.cat_separation_distribution = scipy.stats.expon( 0, self.simulation_parameters["event_time_mean_separation"] @@ -73,6 +73,7 @@ def schedule( total = 0 while total < self.max_time: separation_time = self.cat_separation_distribution.rvs() + # Note: the ceil of an exponential distribution is just a geometric distribution total += int(math.ceil(separation_time)) if total < self.max_time: event_schedule.append(total) diff --git a/start.py b/start.py index 2ce6799..2140092 100644 --- a/start.py +++ b/start.py @@ -62,6 +62,7 @@ def main( sim_params = d["simulation_parameters"] for key in d["isleconfig"]: isleconfig.__dict__[key] = d["isleconfig"][key] + isleconfig.simulation_parameters = sim_params for t in range(time, sim_params["max_time"]): # Main time iteration loop simulation.iterate(t) From 8cfc49c134bb1ad80aeeb23fbba34f85fe2c6dda Mon Sep 17 00:00:00 2001 From: Chris Hughes Date: Tue, 23 Jul 2019 16:08:29 +0100 Subject: [PATCH 066/125] Apply black.py --- catbond.py | 5 ++-- genericclasses.py | 50 +++++++++++++++++++++++----------------- insurancecontract.py | 1 + insurancefirms.py | 7 +++++- insurancesimulation.py | 13 ++++------- isleconfig.py | 2 +- metainsurancecontract.py | 1 + metainsuranceorg.py | 24 +++++++++++-------- reinsurancecontract.py | 11 +++++---- riskmodel.py | 12 ++++++---- 10 files changed, 74 insertions(+), 52 deletions(-) diff --git a/catbond.py b/catbond.py index 1fd4b81..f82a73a 100644 --- a/catbond.py +++ b/catbond.py @@ -4,6 +4,7 @@ from typing import MutableSequence from typing import TYPE_CHECKING + if TYPE_CHECKING: from insurancesimulation import InsuranceSimulation from metainsurancecontract import MetaInsuranceContract @@ -32,9 +33,7 @@ def __init__( This initialised the catbond class instance, inheriting methods from MetaInsuranceOrg.""" self.simulation = simulation self.id: int = 0 - self.underwritten_contracts: MutableSequence[ - "MetaInsuranceContract" - ] = [] + self.underwritten_contracts: MutableSequence["MetaInsuranceContract"] = [] self.cash: float = 0 self.profits_losses: float = 0 self.obligations: MutableSequence[Obligation] = [] diff --git a/genericclasses.py b/genericclasses.py index 6b9d227..c2fea36 100644 --- a/genericclasses.py +++ b/genericclasses.py @@ -9,6 +9,7 @@ from typing import Mapping, MutableSequence, Union, Tuple from typing import TYPE_CHECKING + if TYPE_CHECKING: from metainsurancecontract import MetaInsuranceContract from distributiontruncated import TruncatedDistWrapper @@ -17,9 +18,7 @@ from riskmodel import RiskModel Distribution = Union[ - "stats.rv_continuous", - "TruncatedDistWrapper", - "ReinsuranceDistWrapper", + "stats.rv_continuous", "TruncatedDistWrapper", "ReinsuranceDistWrapper" ] @@ -44,11 +43,15 @@ def _pay(self, obligation: "Obligation"): purpose = obligation.purpose if not amount >= 0: - raise ValueError("Attempting to pay an obligation for a negative ammount - something is wrong") + raise ValueError( + "Attempting to pay an obligation for a negative ammount - something is wrong" + ) # TODO: Think about what happens when paying non-operational firms while not recipient.get_operational(): if isleconfig.verbose: - print(f"Redirecting payment with purpose {purpose} due to non-operational firm {recipient.id}") + print( + f"Redirecting payment with purpose {purpose} due to non-operational firm {recipient.id}" + ) recipient = recipient.creditor if self.get_operational(): self.cash -= amount @@ -96,12 +99,12 @@ def enter_illiquidity(self, time: int, sum_due: float): def receive_obligation( self, amount: float, recipient: "GenericAgent", due_time: int, purpose: str ): - """Method for receiving obligations that the firm will have to _pay. + """Method for receiving obligations that the firm will have to pay. Accepts: - amount: Type integer, how much will be payed - recipient: Type Class instance, who will be payed - due_time: Type Integer, what time value they will be payed - purpose: Type string, why they are being payed + amount: Type integer, how much will be paid + recipient: Type Class instance, who will be paid + due_time: Type Integer, what time value they will be paid + purpose: Type string, why they are being paid No return value Adds obligation (Type DataDict) to list of obligations owed by the firm.""" @@ -198,7 +201,9 @@ class ReinsuranceProfile: # TODO: add, remove, explode, get uninsured regions def __init__(self, riskmodel: "RiskModel"): - self.reinsured_regions: MutableSequence[SortedList[Tuple[int, int, "ReinsuranceContract"]]] + self.reinsured_regions: MutableSequence[ + SortedList[Tuple[int, int, "ReinsuranceContract"]] + ] self.reinsured_regions = [ SortedList(key=lambda x: x[0]) @@ -208,9 +213,7 @@ def __init__(self, riskmodel: "RiskModel"): # Used for automatically updating the riskmodel when reinsurance is modified self.riskmodel = riskmodel - def add( - self, contract: "ReinsuranceContract", value: float - ) -> None: + def add(self, contract: "ReinsuranceContract", value: float) -> None: lower_bound: int = contract.deductible upper_bound: int = contract.excess category = contract.category @@ -221,24 +224,27 @@ def add( ) # Check for overlap with region to the right... - if index + 1 < len(self.reinsured_regions[category]) and self.reinsured_regions[category][index + 1][0] < upper_bound: + if ( + index + 1 < len(self.reinsured_regions[category]) + and self.reinsured_regions[category][index + 1][0] < upper_bound + ): raise ValueError( - "Attempted to add reinsurance overlapping with existing reinsurance" + "Attempted to add reinsurance overlapping with existing reinsurance \n" + f"Reinsured regions are {self.reinsured_regions[category]}" ) # ... and to the left if index != 0 and self.reinsured_regions[category][index - 1][1] > lower_bound: raise ValueError( - "Attempted to add reinsurance overlapping with existing reinsurance" + "Attempted to add reinsurance overlapping with existing reinsurance \n" + f"Reinsured regions are {self.reinsured_regions[category]}" ) self.riskmodel.set_reinsurance_coverage( value=value, coverage=self.reinsured_regions[category], category=category ) - def remove( - self, contract: "ReinsuranceContract", value: float - ) -> None: + def remove(self, contract: "ReinsuranceContract", value: float) -> None: lower_bound = contract.deductible upper_bound = contract.excess category = contract.category @@ -288,7 +294,9 @@ def update_value(self, value: float, category: int) -> None: ) @staticmethod - def split_longest(l: MutableSequence[Tuple[float, float]]) -> MutableSequence[Tuple[float, float]]: + def split_longest( + l: MutableSequence[Tuple[float, float]] + ) -> MutableSequence[Tuple[float, float]]: max_width = 0 max_width_index = None for i, region in enumerate(l): diff --git a/insurancecontract.py b/insurancecontract.py index 893d039..f7e3a21 100644 --- a/insurancecontract.py +++ b/insurancecontract.py @@ -1,6 +1,7 @@ import metainsurancecontract from typing import TYPE_CHECKING + if TYPE_CHECKING: from metainsuranceorg import MetaInsuranceOrg from insurancesimulation import InsuranceSimulation diff --git a/insurancefirms.py b/insurancefirms.py index 9d0736e..1ffcb3e 100644 --- a/insurancefirms.py +++ b/insurancefirms.py @@ -8,6 +8,7 @@ from typing import Optional, MutableSequence, Mapping from typing import TYPE_CHECKING + if TYPE_CHECKING: pass @@ -259,7 +260,11 @@ def ask_reinsurance_non_proportional(self, time: int): self.ask_reinsurance_non_proportional_by_category(time, categ_id) def ask_reinsurance_non_proportional_by_category( - self, time: int, categ_id: int, purpose: str = "newrisk", min_tranches: int = None + self, + time: int, + categ_id: int, + purpose: str = "newrisk", + min_tranches: int = None, ) -> Optional[genericclasses.RiskProperties]: """Method to create a reinsurance risk for a given category for firm that calls it. Called from increase_ capacity_by_category, ask_reinsurance_non_proportional, and roll_over in metainsuranceorg. diff --git a/insurancesimulation.py b/insurancesimulation.py index 5d4e839..8c7e717 100644 --- a/insurancesimulation.py +++ b/insurancesimulation.py @@ -11,16 +11,12 @@ import visualization_network import insurancefirms import isleconfig -from genericclasses import ( - GenericAgent, - RiskProperties, - AgentProperties, - Constant, -) +from genericclasses import GenericAgent, RiskProperties, AgentProperties, Constant import catbond from typing import Mapping, MutableMapping, MutableSequence, Sequence, Any, Optional from typing import TYPE_CHECKING + if TYPE_CHECKING: from genericclasses import Distribution from metainsuranceorg import MetaInsuranceOrg @@ -182,7 +178,9 @@ def __init__( for i in range(self.simulation_parameters["no_risks"]) ] - self.risks_counter: MutableSequence[int] = [0 for _ in range(self.simulation_parameters["no_categories"])] + self.risks_counter: MutableSequence[int] = [ + 0 for _ in range(self.simulation_parameters["no_categories"]) + ] for risk in self.risks: self.risks_counter[risk.category] += 1 @@ -874,7 +872,6 @@ def append_reinrisks(self, reinrisk: RiskProperties): if reinrisk: self.reinrisks.append(reinrisk) - def remove_reinrisks(self, risko: RiskProperties): if risko is not None: self.reinrisks.remove(risko) diff --git a/isleconfig.py b/isleconfig.py index 9febd3a..5f09547 100644 --- a/isleconfig.py +++ b/isleconfig.py @@ -40,7 +40,7 @@ "acceptance_threshold_friction": 0.9, "insurance_firm_market_entry_probability": 0.3, # 0.02, "reinsurance_firm_market_entry_probability": 0.05, # 0.004, - # Determines the reinsurance type of the simulation. Should be "non-proportional" or "excess-of-loss" + # Determines the reinsurance type of the simulation. Should be "non-proportional" or "excess-of-loss" "simulation_reinsurance_type": "non-proportional", "default_non-proportional_reinsurance_deductible": 0.3, "default_non-proportional_reinsurance_excess": 1.0, diff --git a/metainsurancecontract.py b/metainsurancecontract.py index abcf27b..6138f42 100644 --- a/metainsurancecontract.py +++ b/metainsurancecontract.py @@ -1,4 +1,5 @@ from typing import TYPE_CHECKING + if TYPE_CHECKING: from metainsuranceorg import MetaInsuranceOrg from genericclasses import GenericAgent, RiskProperties diff --git a/metainsuranceorg.py b/metainsuranceorg.py index 889eac7..50a8f85 100644 --- a/metainsuranceorg.py +++ b/metainsuranceorg.py @@ -17,8 +17,18 @@ ReinsuranceProfile, ) -from typing import Optional, Tuple, Sequence, Mapping, MutableSequence, Iterable, Callable, Any +from typing import ( + Optional, + Tuple, + Sequence, + Mapping, + MutableSequence, + Iterable, + Callable, + Any, +) from typing import TYPE_CHECKING + if TYPE_CHECKING: from insurancesimulation import InsuranceSimulation from metainsurancecontract import MetaInsuranceContract @@ -74,9 +84,7 @@ def __init__( Constructor creates general instance of an insurance company which is inherited by the reinsurance and insurance firm classes. Initialises all necessary values provided by config file.""" super().__init__() - self.simulation: "InsuranceSimulation" = simulation_parameters[ - "simulation" - ] + self.simulation: "InsuranceSimulation" = simulation_parameters["simulation"] self.simulation_parameters: Mapping = simulation_parameters self.contract_runtime_dist = scipy.stats.randint( simulation_parameters["mean_contract_runtime"] @@ -176,9 +184,7 @@ def __init__( self.np_reinsurance_premium_share = simulation_parameters[ "default_non-proportional_reinsurance_premium_share" ] - self.underwritten_contracts: MutableSequence[ - "MetaInsuranceContract" - ] = [] + self.underwritten_contracts: MutableSequence["MetaInsuranceContract"] = [] self.is_insurer = True self.is_reinsurer = False @@ -519,9 +525,7 @@ def get_excess_capital(self) -> float: def number_underwritten_contracts(self) -> int: return len(self.underwritten_contracts) - def get_underwritten_contracts( - self - ) -> Sequence["MetaInsuranceContract"]: + def get_underwritten_contracts(self) -> Sequence["MetaInsuranceContract"]: return self.underwritten_contracts def get_profitslosses(self) -> float: diff --git a/reinsurancecontract.py b/reinsurancecontract.py index 590d821..8ff727f 100644 --- a/reinsurancecontract.py +++ b/reinsurancecontract.py @@ -2,6 +2,7 @@ from typing import Optional from typing import TYPE_CHECKING + if TYPE_CHECKING: from insurancefirms import InsuranceFirm from metainsuranceorg import MetaInsuranceOrg @@ -27,9 +28,9 @@ def __init__( expire_immediately: bool, initial_var: float = 0.0, insurancetype: str = "proportional", - deductible_fraction: "Optional[float]"=None, - excess_fraction: "Optional[float]"=None, - reinsurance: float=0, + deductible_fraction: "Optional[float]" = None, + excess_fraction: "Optional[float]" = None, + reinsurance: float = 0, ): super().__init__( insurer, @@ -54,7 +55,9 @@ def __init__( else: assert self.contract is not None - def explode(self, time: int, uniform_value: None=None, damage_extent: float=None): + def explode( + self, time: int, uniform_value: None = None, damage_extent: float = None + ): """Explode method. Accepts arguments time: Type integer. The current time. diff --git a/riskmodel.py b/riskmodel.py index 76cb17b..bfc5f7a 100644 --- a/riskmodel.py +++ b/riskmodel.py @@ -8,6 +8,7 @@ from typing import Sequence, Tuple, Union, Optional, MutableSequence from typing import TYPE_CHECKING + if TYPE_CHECKING: from genericclasses import Distribution, RiskProperties @@ -298,9 +299,7 @@ def evaluate_excess_of_loss( var_claim_total = var_claim_fraction * offered_risk.value # record liquidity requirement and apply margin of safety for liquidity requirement - additional_required[categ_id] += ( - var_claim_total * self.margin_of_safety - ) + additional_required[categ_id] += var_claim_total * self.margin_of_safety additional_var_per_categ[categ_id] += var_claim_total # Additional value at risk should only occur in one category. Assert that this is the case. @@ -399,7 +398,12 @@ def evaluate( min(cash_left_by_categ), ) - def set_reinsurance_coverage(self, value: float, coverage: MutableSequence[Tuple[float, float]], category: int): + def set_reinsurance_coverage( + self, + value: float, + coverage: MutableSequence[Tuple[float, float]], + category: int, + ): """Updates the riskmodel for the category given to have the reinsurance given by coverage""" # sometimes value==0, in which case we don't try to update the distribution # (as the current coverage is effectively infinite) From 771083b70c301428817a970887c6cad7cb67a41b Mon Sep 17 00:00:00 2001 From: Chris Hughes Date: Tue, 23 Jul 2019 17:12:48 +0100 Subject: [PATCH 067/125] Change excess to limit --- genericclasses.py | 8 ++++---- insurancecontract.py | 6 +++--- insurancefirms.py | 8 ++++---- metainsurancecontract.py | 14 +++++++------- metainsuranceorg.py | 6 +++--- reinsurancecontract.py | 8 ++++---- riskmodel.py | 6 +++--- 7 files changed, 28 insertions(+), 28 deletions(-) diff --git a/genericclasses.py b/genericclasses.py index c2fea36..116fa64 100644 --- a/genericclasses.py +++ b/genericclasses.py @@ -134,11 +134,11 @@ class RiskProperties: deductible: float = None runtime: int = None expiration: int = None - excess_fraction: float = None + limit_fraction: float = None deductible_fraction: float = None reinsurance_share: float = None periodized_total_premium: float = None - excess: float = None + limit: float = None runtime_left: int = None @@ -215,7 +215,7 @@ def __init__(self, riskmodel: "RiskModel"): def add(self, contract: "ReinsuranceContract", value: float) -> None: lower_bound: int = contract.deductible - upper_bound: int = contract.excess + upper_bound: int = contract.limit category = contract.category self.reinsured_regions[category].add((lower_bound, upper_bound, contract)) @@ -246,7 +246,7 @@ def add(self, contract: "ReinsuranceContract", value: float) -> None: def remove(self, contract: "ReinsuranceContract", value: float) -> None: lower_bound = contract.deductible - upper_bound = contract.excess + upper_bound = contract.limit category = contract.category try: diff --git a/insurancecontract.py b/insurancecontract.py index f7e3a21..a9c6227 100644 --- a/insurancecontract.py +++ b/insurancecontract.py @@ -28,7 +28,7 @@ def __init__( initial_var: float = 0.0, insurancetype: str = "proportional", deductible_fraction: float = None, - excess_fraction: float = None, + limit_fraction: float = None, reinsurance: float = 0, ): super().__init__( @@ -42,7 +42,7 @@ def __init__( initial_var, insurancetype, deductible_fraction, - excess_fraction, + limit_fraction, reinsurance, ) # the property holder in an insurance contract should always be the simulation @@ -68,7 +68,7 @@ def explode(self, time, uniform_value=None, damage_extent=None): "damage_extent must be passed to InsuranceContract.explode" ) if uniform_value < self.risk_factor: - claim = min(self.excess, damage_extent * self.value) - self.deductible + claim = min(self.limit, damage_extent * self.value) - self.deductible self.insurer.register_claim( claim ) # Every insurance claim made is immediately registered. diff --git a/insurancefirms.py b/insurancefirms.py index 1ffcb3e..948399a 100644 --- a/insurancefirms.py +++ b/insurancefirms.py @@ -292,7 +292,7 @@ def ask_reinsurance_non_proportional_by_category( if number_risks > 0: tranches = self.reinsurance_profile.uncovered(categ_id) - # Don't get reinsurance above maximum excess + # Don't get reinsurance above maximum limit while tranches[-1][1] > self.np_reinsurance_excess_fraction * total_value: if tranches[-1][0] >= self.np_reinsurance_excess_fraction * total_value: tranches.pop() @@ -338,7 +338,7 @@ def ask_reinsurance_non_proportional_by_category( insurancetype="excess-of-loss", number_risks=number_risks, deductible_fraction=tranche[0] / total_value, - excess_fraction=tranche[1] / total_value, + limit_fraction=tranche[1] / total_value, periodized_total_premium=periodized_total_premium, runtime=12, expiration=time + 12, @@ -436,7 +436,7 @@ def issue_cat_bond( insurancetype="excess-of-loss", number_risks=number_risks, deductible_fraction=self.np_reinsurance_deductible_fraction, - excess_fraction=self.np_reinsurance_excess_fraction, + limit_fraction=self.np_reinsurance_excess_fraction, periodized_total_premium=0, runtime=12, expiration=time + 12, @@ -553,7 +553,7 @@ def refresh_reinrisk( insurancetype="excess-of-loss", number_risks=number_risks, deductible_fraction=old_contract.deductible / total_value, - excess_fraction=old_contract.excess / total_value, + limit_fraction=old_contract.limit / total_value, periodized_total_premium=periodized_total_premium, runtime=12, expiration=time + 12, diff --git a/metainsurancecontract.py b/metainsurancecontract.py index 6138f42..4e7194a 100644 --- a/metainsurancecontract.py +++ b/metainsurancecontract.py @@ -18,7 +18,7 @@ def __init__( initial_var: float = 0.0, insurancetype: str = "proportional", deductible_fraction: float = None, - excess_fraction: float = None, + limit_fraction: float = None, reinsurance: float = 0, ): """Constructor method. @@ -72,15 +72,15 @@ def __init__( # set excess from argument, risk property or default value, whichever first is not None default_excess_fraction = 1.0 - self.excess_fraction = ( - excess_fraction - if excess_fraction is not None - else risk.excess_fraction - if risk.excess_fraction is not None + self.limit_fraction = ( + limit_fraction + if limit_fraction is not None + else risk.limit_fraction + if risk.limit_fraction is not None else default_excess_fraction ) - self.excess = round(self.excess_fraction * self.value) + self.limit = round(self.limit_fraction * self.value) self.reinsurance = reinsurance self.reinsurer = None diff --git a/metainsuranceorg.py b/metainsuranceorg.py index 50a8f85..19f140f 100644 --- a/metainsuranceorg.py +++ b/metainsuranceorg.py @@ -316,7 +316,7 @@ def collect_process_evaluate_risks( category=contract.category, risk_factor=contract.risk_factor, deductible=contract.deductible, - excess=contract.excess, + limit=contract.limit, insurancetype=contract.insurancetype, runtime=contract.runtime, ) @@ -704,7 +704,7 @@ def balanced_portfolio( * self.riskmodel.inaccuracy[risk.category] ) var_claim = ( - min(var_damage, risk.value * risk.excess_fraction) + min(var_damage, risk.value * risk.limit_fraction) - risk.value * risk.deductible_fraction ) @@ -766,7 +766,7 @@ def process_newrisks_reinsurer( category=contract.category, risk_factor=contract.risk_factor, deductible=contract.deductible, - excess=contract.excess, + limit=contract.limit, insurancetype=contract.insurancetype, runtime_left=(contract.expiration - time), ) diff --git a/reinsurancecontract.py b/reinsurancecontract.py index 8ff727f..2e0145a 100644 --- a/reinsurancecontract.py +++ b/reinsurancecontract.py @@ -29,7 +29,7 @@ def __init__( initial_var: float = 0.0, insurancetype: str = "proportional", deductible_fraction: "Optional[float]" = None, - excess_fraction: "Optional[float]" = None, + limit_fraction: "Optional[float]" = None, reinsurance: float = 0, ): super().__init__( @@ -43,7 +43,7 @@ def __init__( initial_var, insurancetype, deductible_fraction, - excess_fraction, + limit_fraction, reinsurance, ) # self.is_reinsurancecontract = True @@ -76,12 +76,12 @@ def explode( # proportional reinsurance pays out a turn earlier than EoL. As such, proportional insurance claims are # delayed for 1 turn. if self.insurancetype == "excess-of-loss": - claim = min(self.excess, damage_extent) - self.deductible + claim = min(self.limit, damage_extent) - self.deductible self.insurer.receive_obligation( claim, self.property_holder, time, "claim" ) elif self.insurancetype == "proportional": - claim = min(self.excess, damage_extent) - self.deductible + claim = min(self.limit, damage_extent) - self.deductible self.insurer.receive_obligation( claim, self.property_holder, time + 1, "claim" ) diff --git a/riskmodel.py b/riskmodel.py index bfc5f7a..44dee17 100644 --- a/riskmodel.py +++ b/riskmodel.py @@ -86,7 +86,7 @@ def compute_expectation( runtimes = np.zeros(len(categ_risks)) for i, risk in enumerate(categ_risks): # TODO: factor in excess instead of value? - assert risk.excess is not None + assert risk.limit is not None exposures[i] = risk.value - risk.deductible risk_factors[i] = risk.risk_factor runtimes[i] = risk.runtime @@ -280,7 +280,7 @@ def evaluate_excess_of_loss( * self.inaccuracy[categ_id] ) - var_claim = max(min(var_damage, risk.excess) - risk.deductible, 0) + var_claim = max(min(var_damage, risk.limit) - risk.deductible, 0) # record liquidity requirement and apply margin of safety for liquidity requirement cash_left_by_categ[categ_id] -= var_claim * self.margin_of_safety @@ -293,7 +293,7 @@ def evaluate_excess_of_loss( * self.inaccuracy[categ_id] ) var_claim_fraction = ( - min(var_damage_fraction, offered_risk.excess_fraction) + min(var_damage_fraction, offered_risk.limit_fraction) - offered_risk.deductible_fraction ) var_claim_total = var_claim_fraction * offered_risk.value From bdbaf648f1068789c69176764f43afbd4228aa59 Mon Sep 17 00:00:00 2001 From: Chris Hughes Date: Wed, 24 Jul 2019 10:17:37 +0100 Subject: [PATCH 068/125] Little bit of cleanup --- calibration_conditions.py | 2 +- condition_aux.py | 2 +- genericclasses.py | 2 +- insurancefirms.py | 2 +- insurancesimulation.py | 41 +++++++++++++++++++++------------------ listify.py | 10 +++++----- logger.py | 8 ++++---- metainsurancecontract.py | 2 +- metainsuranceorg.py | 2 +- reinsurancecontract.py | 8 ++++---- riskmodel.py | 4 ++-- setup_simulation.py | 2 +- 12 files changed, 44 insertions(+), 41 deletions(-) diff --git a/calibration_conditions.py b/calibration_conditions.py index 3c3c4a5..de0e490 100644 --- a/calibration_conditions.py +++ b/calibration_conditions.py @@ -1,7 +1,7 @@ """Collection of calibration test conditions as functions to be imported by the CalibrationScore class. Each function accepts a Logger object as argument and runs the tests on this. Auxiliary functions are in calibration_aux.py. - + Components of Logger log, that can used for validation/calibration are: [0]: 'total_cash' diff --git a/condition_aux.py b/condition_aux.py index 206bf13..8caae30 100644 --- a/condition_aux.py +++ b/condition_aux.py @@ -131,7 +131,7 @@ def scaler( ): # TODO: find a better way to scale heavy-tailed distributions than to use standard score scaling on logs """Function to do a standard score scaling of the log of a heavy-tailed distribution. This is used to calibrate distributions where the unit is not important (distributions of sizes of firms e.g.). This would be perfectly - appropriate for lognormal distributions, but should work reasonably well for calibration of other heavy-tailed + appropriate for lognormal distributions, but should work reasonably well for calibration of other heavy-tailed distributions. An alternative would be a scaling robust towards outliers (as included in the sklearn package). Arguments: series: Type list of numeric or numpy array. The time series diff --git a/genericclasses.py b/genericclasses.py index 116fa64..7c41ec8 100644 --- a/genericclasses.py +++ b/genericclasses.py @@ -55,7 +55,7 @@ def _pay(self, obligation: "Obligation"): recipient = recipient.creditor if self.get_operational(): self.cash -= amount - if purpose is not "dividend": + if purpose != "dividend": self.profits_losses -= amount recipient.receive(amount) else: diff --git a/insurancefirms.py b/insurancefirms.py index 948399a..f94b2ed 100644 --- a/insurancefirms.py +++ b/insurancefirms.py @@ -251,7 +251,7 @@ def ask_reinsurance_non_proportional(self, time: int): """ Method for requesting excess of loss reinsurance for all underwritten contracts by category. The method calculates the combined value at risk. With a probability it then creates a combined reinsurance risk that may then be underwritten by a reinsurance firm. - Arguments: + Arguments: time: integer Returns None.""" """Evaluate by risk category""" diff --git a/insurancesimulation.py b/insurancesimulation.py index 8c7e717..cf14019 100644 --- a/insurancesimulation.py +++ b/insurancesimulation.py @@ -540,24 +540,7 @@ def iterate(self, t: int): self.simulation_parameters["no_categories"] ) - # TODO: this and the next look like they could be cleaner - for insurer in self.insurancefirms: - if insurer.operational: - for i in range(len(self.inaccuracy)): - if np.array_equal(insurer.riskmodel.inaccuracy, self.inaccuracy[i]): - self.insurance_models_counter[i] += 1 - - self.reinsurance_models_counter = np.zeros( - self.simulation_parameters["no_categories"] - ) - - for reinsurer in self.reinsurancefirms: - for i in range(len(self.inaccuracy)): - if reinsurer.operational: - if np.array_equal( - reinsurer.riskmodel.inaccuracy, self.inaccuracy[i] - ): - self.reinsurance_models_counter[i] += 1 + self._update_model_counters() network_division = 1 # How often network is updated. if isleconfig.show_network and t % network_division == 0 and t > 0: @@ -569,7 +552,7 @@ def iterate(self, t: int): self.RN.visualize() def save_data(self): - """Method to collect statistics about the current state of the simulation. Will pass these to the + """Method to collect statistics about the current state of the simulation. Will pass these to the Logger object (self.logger) to be recorded. No arguments. Returns None.""" @@ -753,6 +736,26 @@ def _reset_insurance_weights(self): s = math.floor(np.random.uniform(0, len(operational_firms), 1)) self.insurers_weights[operational_firms[s].id] += 1 + def _update_model_counters(self): + # TODO: this and the next look like they could be cleaner + for insurer in self.insurancefirms: + if insurer.operational: + for i in range(len(self.inaccuracy)): + if np.array_equal(insurer.riskmodel.inaccuracy, self.inaccuracy[i]): + self.insurance_models_counter[i] += 1 + + self.reinsurance_models_counter = np.zeros( + self.simulation_parameters["no_categories"] + ) + + for reinsurer in self.reinsurancefirms: + for i in range(len(self.inaccuracy)): + if reinsurer.operational: + if np.array_equal( + reinsurer.riskmodel.inaccuracy, self.inaccuracy[i] + ): + self.reinsurance_models_counter[i] += 1 + def _shuffle_risks(self): """Method for shuffling risks.""" np.random.shuffle(self.reinrisks) diff --git a/listify.py b/listify.py index c410c21..fe370c8 100644 --- a/listify.py +++ b/listify.py @@ -7,23 +7,23 @@ def listify(d): Arguments: d: dict - input dict Returns: - list with dict values as elements [:-1] and dict keys as + list with dict values as elements [:-1] and dict keys as last element.""" """extract keys""" keys = list(d.keys()) """create list""" - l = [d[key] for key in keys] - l.append(keys) + lst = [d[key] for key in keys] + lst.append(keys) - return l + return lst def delistify(l): """Function to convert listified dict back to dict. Arguments: - l: list - input listified dict. This must be a list of dict + l: list - input listified dict. This must be a list of dict elements as elements [:-1] and the corresponding dict keys as list in the last element. Returns: diff --git a/logger.py b/logger.py index f572760..ad5e7e9 100644 --- a/logger.py +++ b/logger.py @@ -116,12 +116,12 @@ def obtain_log(self, requested_logs=None): return listify.listify(log) def restore_logger_object(self, log): - """Method to restore logger object. A log can be restored later. It can also be restored + """Method to restore logger object. A log can be restored later. It can also be restored on a different machine. This is useful in the case of ensemble runs to move the log to the master node from the computation nodes. Arguments: - log - listified dict - The log. This must be a list of dict values plus the dict - keys in the last element. It should have been created by + log - listified dict - The log. This must be a list of dict values plus the dict + keys in the last element. It should have been created by listify.listify() Returns None.""" @@ -179,7 +179,7 @@ def single_log_prepare(self): return to_log def add_insurance_agent(self): - """Method for adding an additional insurer agent to the history log. This is necessary to keep the number + """Method for adding an additional insurer agent to the history log. This is necessary to keep the number of individual insurance firm logs constant in time. No arguments. Returns None.""" diff --git a/metainsurancecontract.py b/metainsurancecontract.py index 4e7194a..7ce8182 100644 --- a/metainsurancecontract.py +++ b/metainsurancecontract.py @@ -23,7 +23,7 @@ def __init__( ): """Constructor method. Accepts arguments - insurer: Type InsuranceFirm. + insurer: Type InsuranceFirm. risk: Type RiskProperties. time: Type integer. The current time. premium: Type float. diff --git a/metainsuranceorg.py b/metainsuranceorg.py index 19f140f..48f9436 100644 --- a/metainsuranceorg.py +++ b/metainsuranceorg.py @@ -136,7 +136,7 @@ def __init__( rm_config = agent_parameters.riskmodel_config - """Here we modify the margin of safety depending on the number of risks models available in the market. + """Here we modify the margin of safety depending on the number of risks models available in the market. When is 0 all risk models have the same margin of safety. The reason for doing this is that with more risk models the firms tend to be closer to the max capacity""" margin_of_safety_correction = ( diff --git a/reinsurancecontract.py b/reinsurancecontract.py index 2e0145a..0aed26a 100644 --- a/reinsurancecontract.py +++ b/reinsurancecontract.py @@ -62,8 +62,8 @@ def explode( Accepts arguments time: Type integer. The current time. uniform_value: Not used - damage_extent: Type float. The absolute damage in excess-of-loss reinsurance (not relative as in - proportional contracts. + damage_extent: Type float. The absolute damage in excess-of-loss reinsurance (not relative as in + proportional contracts. No return value. Method marks the contract for termination. """ @@ -99,11 +99,11 @@ def explode( # self.terminating = True def mature(self, time: int): - """Mature method. + """Mature method. Accepts arguments time: Type integer. The current time. No return value. - Removes any reinsurance functions this contract has and terminates any reinsurance contracts for this + Removes any reinsurance functions this contract has and terminates any reinsurance contracts for this contract.""" # self.terminating = True self.terminate_reinsurance(time) diff --git a/riskmodel.py b/riskmodel.py index 44dee17..efd94de 100644 --- a/riskmodel.py +++ b/riskmodel.py @@ -38,7 +38,7 @@ def __init__( self.init_average_risk_factor = init_average_risk_factor self.init_profit_estimate = init_profit_estimate self.margin_of_safety = margin_of_safety - """damage_distribution is some scipy frozen rv distribution which is bound between 0 and 1 and indicates + """damage_distribution is some scipy frozen rv distribution which is bound between 0 and 1 and indicates the share of risks suffering damage as part of any single catastrophic peril""" self.damage_distribution: MutableSequence["Distribution"] = [ damage_distribution for _ in range(self.category_number) @@ -50,7 +50,7 @@ def __init__( def get_ppf(self, categ_id: int, tail_size: float) -> float: """Method for getting quantile function of the damage distribution (value at risk) by category. Positional arguments: - categ_id integer: category + categ_id integer: category tailSize (float 0<=x<=1): quantile Returns value-at-risk.""" return self.damage_distribution[categ_id].ppf(1 - tail_size) diff --git a/setup_simulation.py b/setup_simulation.py index ef9811a..f274d05 100644 --- a/setup_simulation.py +++ b/setup_simulation.py @@ -3,7 +3,7 @@ Every event schedule dictionary has: - event_times: list of list of int - iteration periods of risk events in each category - event_damages: list of list of float (0, 1) - damage as share of possible damage for each risk event - - num_categories: int - number of risk categories + - num_categories: int - number of risk categories - np_seed: int - numpy module random seed - random_seed: int - random module random seed A simulation given event schedule dictionary d should be set up like so: From 3aca16334c81afbd43dd3b76c3ee055fd9dd51ba Mon Sep 17 00:00:00 2001 From: KloskaT Date: Wed, 24 Jul 2019 12:14:20 +0100 Subject: [PATCH 069/125] Both types of firms going bankrupt can now be bought by others. If not bought after one iteration they are dissolved. --- insurancesimulation.py | 82 ++++++++++++++++++++++++++++++++++++++++-- metainsuranceorg.py | 71 ++++++++++++++++++++++++++++++++++-- 2 files changed, 147 insertions(+), 6 deletions(-) diff --git a/insurancesimulation.py b/insurancesimulation.py index c12a0ce..bdf6d13 100644 --- a/insurancesimulation.py +++ b/insurancesimulation.py @@ -142,7 +142,9 @@ def __init__(self, override_no_riskmodels, replic_ID, simulation_parameters, rc_ self.cumulative_market_exits = 0 self.cumulative_unrecovered_claims = 0.0 self.cumulative_claims = 0.0 - + self.current_bankruptcies = [] + self.current_reinsurer_bankruptcies = [] + "Lists for logging history" self.logger = logger.Logger(no_riskmodels=simulation_parameters["no_riskmodels"], rc_event_schedule_initial=self.rc_event_schedule_initial, @@ -321,7 +323,10 @@ def iterate(self, t): # Iterate insurance firm agents for agent in self.insurancefirms: agent.iterate(t) - + + # Reset list of bankrupt insurance firms + self.reset_bankrupt_firms() + # Iterate catbonds for agent in self.catbonds: agent.iterate(t) @@ -351,7 +356,7 @@ def iterate(self, t): if isleconfig.show_network: self.RN.visualize() - if isleconfig.save_network and t == (self.simulation_parameters['max_time']-800): + if isleconfig.save_network and t == (self.simulation_parameters['max_time']-5): self.RN.save_network_data() print("Network data has been saved to data/network_data.dat") @@ -918,3 +923,74 @@ def reset_pls(self): for catbond in self.catbonds: catbond.reset_pl() + def add_bankrupt_firm(self, firm, time): + """Method to add bankrupt firm to list of those being considered to buy dependant on firm type. + Accepts: + firm: Type Class. + time: Type Integer. + No return values.""" + if firm.is_insurer: + self.current_bankruptcies.append([firm, time]) + elif firm.is_reinsurer: + self.current_reinsurer_bankruptcies.append([firm, time]) + + def get_bankrupt_firms(self, type): + """Method to get list of bankrupt firms up for selling based on type. + Accepts: + type: Type String. + Returns: + bankruptcies_sent: List of Classes. + bankruptcy_time: Type Integer.""" + if type == "insurer": + bankruptcies_sent = [firm for firm, time in self.current_bankruptcies] + bankruptcy_time = 0 + if len(self.current_bankruptcies) > 0: + bankruptcy_time = self.current_bankruptcies[0][1] + elif type == "reinsurer": + bankruptcies_sent = [firm for firm in self.current_reinsurer_bankruptcies] + bankruptcy_time = 0 + if len(self.current_reinsurer_bankruptcies) > 0: + bankruptcy_time = self.current_reinsurer_bankruptcies[0][1] + else: + print("No accepted type for bankruptcies") + return bankruptcies_sent, bankruptcy_time + + def get_total_firm_cash(self, type): + """Method to get sum of all cash of firms of a given type. Called from consider_buyout() but could be used for + setting market premium. + Accepts: + type: Type String. + Returns: + sum_capital: Type Integer.""" + if type == "insurer": + sum_capital = sum([agent.get_cash() for agent in self.insurancefirms]) + elif type == "reinsurer": + sum_capital = sum([agent.get_cash() for agent in self.reinsurancefirms]) + else: + print("No accepted type for cash") + return sum_capital + + def remove_bankrupt_firm(self, firm, time): + """Method to remove firm from list of bankrupt firms. Called when firm is bought buy another. + Accepts: + firm: Type Class. + time: Type Integer. + No return values.""" + if firm.is_insurer: + self.current_bankruptcies.remove([firm, time]) + elif firm.is_reinsurer: + self.current_reinsurer_bankruptcies.remove([firm, time]) + + def reset_bankrupt_firms(self): + """Method to reset list of bankrupt firms being sold. Called every iteration of insurance simulation. + No accepted values. + No return values. + Firms going bankrupt only considered for iteration they go bankrupt, after this not wanted so all are dissolved + and relevant list attribute is reset.""" + for firm, time in self.current_bankruptcies: + firm.dissolve(time, 'record_bankruptcy') + self.current_bankruptcies = [] + + for reinfirm, time in self.current_reinsurer_bankruptcies: + reinfirm.dissolve(time) + self.current_reinsurer_bankruptcies = [] diff --git a/metainsuranceorg.py b/metainsuranceorg.py index 15490d2..72cb64a 100644 --- a/metainsuranceorg.py +++ b/metainsuranceorg.py @@ -113,7 +113,6 @@ def iterate(self, time): For each time step this method obtains every firms interest payments, pays obligations, claim reinsurance, matures necessary contracts. Check condition for operational firms (as never removed) so only operational firms receive new risks to evaluate, pay dividends, adjust capacity.""" - """Obtain interest generated by cash""" self.simulation.bank.award_interest(self, self.cash) @@ -137,6 +136,11 @@ def iterate(self, time): [contract.check_payment_due(time) for contract in self.underwritten_contracts] if self.operational: + # Allow firms to buy those going bankrupt this iteration. + if self.is_insurer: + self.consider_buyout(type="insurer") + if self.is_reinsurer: + self.consider_buyout(type="reinsurer") """request risks to be considered for underwriting in the next period, organised by insurance type""" new_nonproportional_risks, new_risks = self.get_newrisks_by_type() @@ -222,7 +226,14 @@ def enter_bankruptcy(self, time): This method is used when a firm does not have enough cash to pay all its obligations. It is only called from the method self.enter_illiquidity() which is only called from the method self.effect_payments(). This method dissolves the firm through the method self.dissolve().""" - self.dissolve(time, 'record_bankruptcy') + if self.is_insurer and self.operational: + self.simulation.add_bankrupt_firm(self, time) + self.operational = False + elif self.is_reinsurer and self.operational: + self.simulation.add_bankrupt_firm(self, time) + self.operational = False + else: + self.dissolve(time, 'record_bankruptcy') def market_exit(self, time): """Market_exit Method. @@ -382,7 +393,7 @@ def estimated_var(self): for category in range(len(self.counter_category)): self.var_counter += self.counter_category[category] * self.riskmodel.inaccuracy[category] - self.var_sum += + self.var_category[category] + self.var_sum += self.var_category[category] if not sum(self.counter_category) == 0: self.var_counter_per_risk = self.var_counter / sum(self.counter_category) @@ -675,3 +686,57 @@ def roll_over(self, time): if reinrisk is not None: self.reinrisks_kept.append(reinrisk) + def consider_buyout(self, type="insurer"): + """Method to allow firm to decide if to buy one of the firms going bankrupt. + Accepts: + type: Type string. Used to decide if insurance or reinsurance firm. + No return values. + This method is called for both types of firms to consider buying one firm going bankrupt for only this iteration + It has a chance (based on market share) to buyout other firm if its excess capital is large enough to cover + the other firms value at risk multiplied by its margin of safety. Will call buyout() if necessary.""" + firms_to_consider, time = self.simulation.get_bankrupt_firms(type) + firms_further_considered = [] + + for firm in firms_to_consider: + total_contract_value = sum([contract.value for contract in firm.underwritten_contracts]) + firm_cost = total_contract_value + all_firms_cash = self.simulation.get_total_firm_cash(type) + + if self.excess_capital - firm_cost > self.riskmodel.margin_of_safety * firm.var_sum: + firm_likelihood = 0.25 + (1.5 * firm.cash + np.mean(firm.cash_last_periods[1:4]) + self.cash)/all_firms_cash + firm_likelihood = min(1, 2*firm_likelihood) + firms_further_considered.append([firm, firm_likelihood, firm_cost]) + + if len(firms_further_considered) > 0: + best_likelihood = 0 + for firm_data in firms_further_considered: + if firm_data[1] > best_likelihood: + best_likelihood = firm_data[1] + best_firm = firm_data[0] + best_firm_cost = firm_data[2] + random_chance = np.random.uniform(0,1) + if best_likelihood > random_chance: + self.buyout(best_firm, best_firm_cost, time) + self.simulation.remove_bankrupt_firm(best_firm, time) + + def buyout(self, firm, firm_cost, time): + """Method called to actually buyout firm. + Accepts: + firm: Type Class. Firm being bought. + firm_cost: Type Decimal. Cost of firm being bought. + time: Type Integer. Time at which bought. + No return values. + This method causes buyer to receive obligation to buy firm. Sets all the bought firms contracts as its own. Then + clears bought firms contracts and dissolves it. Only called from consider_buyout().""" + self.receive_obligation(firm_cost, self.simulation, time, 'buyout') + if self.is_insurer and firm.is_insurer: + print("Insurer %i has bought %i" % (self.id, firm.id)) + elif self.is_reinsurer and firm.is_reinsurer: + print("Reinsurer %i has bought %i" % (self.id, firm.id)) + + for contract in firm.underwritten_contracts: + contract.insurer = self + self.underwritten_contracts.append(contract) + firm.underwritten_contracts = [] + firm.dissolve(time, 'buyout') + From 8b9aa5d8d21a37150f06229b79d8181e5db98893 Mon Sep 17 00:00:00 2001 From: KloskaT Date: Wed, 24 Jul 2019 12:15:19 +0100 Subject: [PATCH 070/125] Changed event damage values so they are just a number not array (allows logging) --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index f7579e1..f9b1d62 100644 --- a/setup.py +++ b/setup.py @@ -62,7 +62,7 @@ def schedule(self, replications): #This method returns the lists of schedule ti total += int(math.ceil(separation_time)) if total < self.max_time: event_schedule.append(total) - event_damage.append(self.damage_distribution.rvs()) + event_damage.append(self.damage_distribution.rvs()[0]) rc_event_schedule.append(event_schedule) rc_event_damage.append(event_damage) From 9b17456b515064f6d71ff05df229d425f1e17114 Mon Sep 17 00:00:00 2001 From: KloskaT Date: Thu, 25 Jul 2019 14:21:26 +0100 Subject: [PATCH 071/125] Network data now saved (to separate file) using logger for both types of run (i.e. doesnt need networkx). Added condition to isleconfig for allowing buying other firms. --- ensemble.py | 30 +++++++++------- insurancesimulation.py | 75 ++++++++++++++++++++++++++++++++-------- isleconfig.py | 1 + logger.py | 45 ++++++++++++++++++++---- metainsuranceorg.py | 35 ++++++++++--------- visualization_network.py | 30 ++++++---------- 6 files changed, 145 insertions(+), 71 deletions(-) diff --git a/ensemble.py b/ensemble.py index 442d340..2974a43 100644 --- a/ensemble.py +++ b/ensemble.py @@ -1,6 +1,6 @@ -#This script allows to launch an ensemble of simulations for different number of risks models. -#It can be run locally if no argument is passed when called from the terminal. -#It can be run in the cloud if it is passed as argument the server that will be used. +"""This script allows to launch an ensemble of simulations for different number of risks models. +It can be run locally if no argument is passed when called from the terminal. +It can be run in the cloud if it is passed as argument the server that will be used.""" import sys import random import os @@ -16,7 +16,6 @@ from sandman2.api import operation, Session - @operation def agg(*outputs): # do nothing @@ -73,10 +72,12 @@ def rake(hostname): 'rc_event_schedule_initial': '_rc_event_schedule.dat', 'rc_event_damage_initial': '_rc_event_damage.dat', 'number_riskmodels': '_number_riskmodels.dat' + 'individual_contracts' '_insurance_contracts.dat' + 'reinsurance_contracts' '_reinsurance_contracts.dat' } if isleconfig.slim_log: - for name in ['insurance_firms_cash', 'reinsurance_firms_cash']: + for name in ['insurance_firms_cash', 'reinsurance_firms_cash', 'individual_contracts', 'reinsurance_contracts']: del requested_logs[name] assert "number_riskmodels" in requested_logs @@ -119,17 +120,14 @@ def rake(hostname): """Run simulation and obtain result""" result = sess.submit(job) - - - """find number of riskmodels from log""" + + + """Find number of riskmodels from log""" delistified_result = [listify.delistify(list(res)) for res in result] - #nrmidx = result[0][-1].index("number_riskmodels") - #nrm = result[0][nrmidx] nrm = delistified_result[0]["number_riskmodels"] """These are the files created to collect the results""" wfiles_dict = {} - logfile_dict = {} for name in requested_logs.keys(): @@ -155,8 +153,10 @@ def rake(hostname): """Save logs as dict (to _history_logs.dat)""" L.save_log(True) + if isleconfig.save_network: + L.save_network_data(ensemble=True) - """Save logs as indivitual files""" + """Save logs as individual files""" for name in logfile_dict: wfiles_dict[name].write(str(delistified_result[i][name]) + "\n") @@ -169,5 +169,9 @@ def rake(hostname): if __name__ == '__main__': host = None if len(sys.argv) > 1: - host = sys.argv[1] #The server is passed as an argument. + host = sys.argv[1] # The server is passed as an argument. rake(host) + +# ID = OxeChiwQ3l0vsBnkLLXKMnpSBn948ptW +# Secret = lfK_Gav8EK2B_gN0bsNXjUVQq84ua1iL2xlYs8Ef58tFyTSwzzJcWeDdSH21BnHx +# Hostname = bright-lemur.clusters.sandman.ai \ No newline at end of file diff --git a/insurancesimulation.py b/insurancesimulation.py index bdf6d13..5598161 100644 --- a/insurancesimulation.py +++ b/insurancesimulation.py @@ -311,9 +311,13 @@ def iterate(self, t): # Reset reinweights self.reset_reinsurance_weights() - # Iterate reinsurnace firm agents + # Iterate reinsurance firm agents for reinagent in self.reinsurancefirms: reinagent.iterate(t) + if isleconfig.buy_bankruptcies: + for reinagent in self.reinsurancefirms: + if reinagent.operational: + reinagent.consider_buyout(type="reinsurer") self.reinrisks = [] @@ -323,6 +327,10 @@ def iterate(self, t): # Iterate insurance firm agents for agent in self.insurancefirms: agent.iterate(t) + if isleconfig.buy_bankruptcies: + for agent in self.insurancefirms: + if agent.operational: + agent.consider_buyout(type="insurer") # Reset list of bankrupt insurance firms self.reset_bankrupt_firms() @@ -347,7 +355,7 @@ def iterate(self, t): if reinsurer.riskmodel.inaccuracy == self.inaccuracy[i]: self.reinsurance_models_counter[i] += 1 - network_division = 1 # How often network is updated. + """network_division = 1 # How often network is updated. if (isleconfig.show_network or isleconfig.save_network) and t % network_division == 0 and t > 0: if t == network_division: # Only creates once instance so only one figure. self.RN = visualization_network.ReinsuranceNetwork(self.rc_event_schedule_initial) @@ -358,7 +366,7 @@ def iterate(self, t): self.RN.visualize() if isleconfig.save_network and t == (self.simulation_parameters['max_time']-5): self.RN.save_network_data() - print("Network data has been saved to data/network_data.dat") + print("Network data has been saved to data/network_data.dat")""" def save_data(self): """Method to collect statistics about the current state of the simulation. Will pass these to the @@ -369,11 +377,11 @@ def save_data(self): """ collect data """ total_cash_no = sum([insurancefirm.cash for insurancefirm in self.insurancefirms]) total_excess_capital = sum([insurancefirm.get_excess_capital() for insurancefirm in self.insurancefirms]) - total_profitslosses = sum([insurancefirm.get_profitslosses() for insurancefirm in self.insurancefirms]) + total_profitslosses = sum([insurancefirm.get_profitslosses() for insurancefirm in self.insurancefirms]) total_contracts_no = sum([len(insurancefirm.underwritten_contracts) for insurancefirm in self.insurancefirms]) total_reincash_no = sum([reinsurancefirm.cash for reinsurancefirm in self.reinsurancefirms]) total_reinexcess_capital = sum([reinsurancefirm.get_excess_capital() for reinsurancefirm in self.reinsurancefirms]) - total_reinprofitslosses = sum([reinsurancefirm.get_profitslosses() for reinsurancefirm in self.reinsurancefirms]) + total_reinprofitslosses = sum([reinsurancefirm.get_profitslosses() for reinsurancefirm in self.reinsurancefirms]) total_reincontracts_no = sum([len(reinsurancefirm.underwritten_contracts) for reinsurancefirm in self.reinsurancefirms]) operational_no = sum([insurancefirm.operational for insurancefirm in self.insurancefirms]) reinoperational_no = sum([reinsurancefirm.operational for reinsurancefirm in self.reinsurancefirms]) @@ -401,7 +409,7 @@ def save_data(self): current_log['cumulative_bankruptcies'] = self.cumulative_bankruptcies current_log['cumulative_market_exits'] = self.cumulative_market_exits current_log['cumulative_unrecovered_claims'] = self.cumulative_unrecovered_claims - current_log['cumulative_claims'] = self.cumulative_claims #Log the cumulative claims received so far. + current_log['cumulative_claims'] = self.cumulative_claims """ add agent-level data to dict""" current_log['insurance_firms_cash'] = insurance_firms @@ -418,6 +426,15 @@ def save_data(self): for i in range(len(reinsurance_contracts_no)): current_log['reinsurance_contracts'].append(reinsurance_contracts_no[i]) + if isleconfig.save_network and not isleconfig.slim_log: + adj_list, node_labels, edge_labels, agent_numbers = self.update_network_data() + else: + adj_list = node_labels = edge_labels = agent_numbers = [] + current_log["unweighted_network_data"] = adj_list + current_log["network_node_labels"] = node_labels + current_log["network_edge_labels"] = edge_labels + current_log["number_of_agents"] = agent_numbers + """ call to Logger object """ self.logger.record_data(current_log) @@ -425,15 +442,6 @@ def obtain_log(self, requested_logs=None): """This function allows to return in a list all the data generated by the model. There is no other way to transfer it back from the cloud.""" return self.logger.obtain_log(requested_logs) - - def finalize(self, *args): - """Function to handle operations after the end of the simulation run. - Currently empty. - It may be used to handle e.g. logging by including: - self.log() - but logging has been moved to start.py and ensemble.py - """ - pass def inflict_peril(self, categ_id, damage, t): """Method that calculates percentage damage done to each underwritten risk that is affected in the category @@ -994,3 +1002,40 @@ def reset_bankrupt_firms(self): for reinfirm, time in self.current_reinsurer_bankruptcies: reinfirm.dissolve(time) self.current_reinsurer_bankruptcies = [] + + def update_network_data(self): + """Method to update the network data. + No accepted values. + No return values. + This method is called from save_data() for every iteration to get the current adjacency list so network + visualisation can be saved.""" + """obtain lists of operational entities""" + op_entities = {} + num_entities = {} + for firmtype, firmlist in [("insurers", self.insurancefirms), ("reinsurers", self.reinsurancefirms), + ("catbonds", self.catbonds)]: + op_firmtype = [firm for firm in firmlist if firm.operational] + op_entities[firmtype] = op_firmtype + num_entities[firmtype] = len(op_firmtype) + + network_size = sum(num_entities.values()) + + """Create weighted adjacency matrix and category edge labels""" + weights_matrix = np.zeros(network_size ** 2).reshape(network_size, network_size) + edge_labels = {} + node_labels = {} + for idx_to, firm in enumerate(op_entities["insurers"] + op_entities["reinsurers"]): + node_labels[idx_to] = firm.id + eolrs = firm.get_excess_of_loss_reinsurance() + for eolr in eolrs: + try: + idx_from = num_entities["insurers"] + ( + op_entities["reinsurers"] + op_entities["catbonds"]).index(eolr["reinsurer"]) + weights_matrix[idx_from][idx_to] = eolr["value"] + edge_labels[idx_to, idx_from] = eolr["category"] + except ValueError: + print("Reinsurer is not in list of reinsurance companies") + + """unweighted adjacency matrix""" + adj_matrix = np.sign(weights_matrix) + return adj_matrix.tolist(), node_labels, edge_labels, num_entities diff --git a/isleconfig.py b/isleconfig.py index 5f46e01..835c17b 100644 --- a/isleconfig.py +++ b/isleconfig.py @@ -6,6 +6,7 @@ show_network = False # Should network be visualized? This should be False by default, to be overridden by commandline arguments save_network = True slim_log = False # Should logs be small in ensemble runs (only aggregated level data)? +buy_bankruptcies = False simulation_parameters = {"no_categories": 4, "no_insurancefirms": 20, diff --git a/logger.py b/logger.py index 93ce229..69d3ad2 100644 --- a/logger.py +++ b/logger.py @@ -10,7 +10,8 @@ 'total_reincontracts total_reinoperational total_catbondsoperational market_premium ' 'market_reinpremium cumulative_bankruptcies cumulative_market_exits cumulative_unrecovered_claims ' 'cumulative_claims insurance_firms_cash reinsurance_firms_cash market_diffvar ' - 'rc_event_schedule_initial rc_event_damage_initial number_riskmodels individual_contracts reinsurance_contracts' + 'rc_event_schedule_initial rc_event_damage_initial number_riskmodels individual_contracts reinsurance_contracts ' + 'unweighted_network_data network_node_labels network_edge_labels number_of_agents' ).split(' ') class Logger(): @@ -67,7 +68,20 @@ def __init__(self, no_riskmodels=None, rc_event_schedule_initial=None, rc_event_ self.history_logs['market_premium'] = [] self.history_logs['market_reinpremium'] = [] self.history_logs['market_diffvar'] = [] - + + + "Network Data Logs to be stored in separate file" + self.network_data = {} + self.network_data["unweighted_network_data"] = [] + self.network_data["network_node_labels"] = [] + self.network_data["network_edge_labels"] = [] + self.network_data["number_of_agents"] = [] + + self.history_logs["unweighted_network_data"] = [] + self.history_logs["network_node_labels"] = [] + self.history_logs["network_edge_labels"] = [] + self.history_logs["number_of_agents"] = [] + def record_data(self, data_dict): """Method to record data for one period Arguments @@ -114,7 +128,13 @@ def restore_logger_object(self, log): """Restore dict""" log = listify.delistify(log) - + + self.network_data["unweighted_network_data"] = log["unweighted_network_data"] + self.network_data["network_node_labels"] = log["network_node_labels"] + self.network_data["network_edge_labels"] = log["network_edge_labels"] + self.network_data["number_of_agents"] = log["number_of_agents"] + del log["number_of_agents"], log["network_edge_labels"], log["network_node_labels"], log["unweighted_network_data"] + """Extract environment variables (number of risk models and risk event schedule)""" self.rc_event_schedule_initial = log["rc_event_schedule_initial"] self.rc_event_damage_initial = log["rc_event_damage_initial"] @@ -164,14 +184,27 @@ def single_log_prepare(self): to_log = [] to_log.append(("data/history_logs.dat", self.history_logs, "w")) return to_log - + + def save_network_data(self, ensemble): + if ensemble is True: + filename_prefix = {1: "one", 2: "two", 3: "three", 4: "four"} + fpf = filename_prefix[self.number_riskmodels] + network_logs = [] + network_logs.append(("data/" + fpf + "_network_data.dat", self.network_data, "a")) + + for filename, data, operation_character in network_logs: + with open(filename, operation_character) as wfile: + wfile.write(str(data) + "\n") + else: + with open("data/network_data.dat", "w") as wfile: + wfile.write(str(self.network_data) + "\n") + wfile.write(str(self.rc_event_schedule_initial) + "\n") + def add_insurance_agent(self): """Method for adding an additional insurer agent to the history log. This is necessary to keep the number of individual insurance firm logs constant in time. No arguments. Returns None.""" - # TODO: should this not also be done for self.history_logs['insurance_firms_cash'] and - # self.history_logs['reinsurance_firms_cash'] if len(self.history_logs['individual_contracts']) > 0: zeroes_to_append = list(np.zeros(len(self.history_logs['individual_contracts'][0]), dtype=int)) else: diff --git a/metainsuranceorg.py b/metainsuranceorg.py index 72cb64a..a74464c 100644 --- a/metainsuranceorg.py +++ b/metainsuranceorg.py @@ -136,12 +136,6 @@ def iterate(self, time): [contract.check_payment_due(time) for contract in self.underwritten_contracts] if self.operational: - # Allow firms to buy those going bankrupt this iteration. - if self.is_insurer: - self.consider_buyout(type="insurer") - if self.is_reinsurer: - self.consider_buyout(type="reinsurer") - """request risks to be considered for underwriting in the next period, organised by insurance type""" new_nonproportional_risks, new_risks = self.get_newrisks_by_type() contracts_offered = len(new_risks) @@ -226,12 +220,15 @@ def enter_bankruptcy(self, time): This method is used when a firm does not have enough cash to pay all its obligations. It is only called from the method self.enter_illiquidity() which is only called from the method self.effect_payments(). This method dissolves the firm through the method self.dissolve().""" - if self.is_insurer and self.operational: - self.simulation.add_bankrupt_firm(self, time) - self.operational = False - elif self.is_reinsurer and self.operational: - self.simulation.add_bankrupt_firm(self, time) - self.operational = False + if isleconfig.buy_bankruptcies: + if self.is_insurer and self.operational: + self.simulation.add_bankrupt_firm(self, time) + self.operational = False + elif self.is_reinsurer and self.operational: + self.simulation.add_bankrupt_firm(self, time) + self.operational = False + else: + self.dissolve(time, 'record_bankruptcy') else: self.dissolve(time, 'record_bankruptcy') @@ -698,14 +695,13 @@ def consider_buyout(self, type="insurer"): firms_further_considered = [] for firm in firms_to_consider: - total_contract_value = sum([contract.value for contract in firm.underwritten_contracts]) - firm_cost = total_contract_value + # total_contract_value = sum([contract.value for contract in firm.underwritten_contracts]) + # firm_cost = total_contract_value all_firms_cash = self.simulation.get_total_firm_cash(type) - - if self.excess_capital - firm_cost > self.riskmodel.margin_of_safety * firm.var_sum: + if self.excess_capital > self.riskmodel.margin_of_safety * firm.var_sum: firm_likelihood = 0.25 + (1.5 * firm.cash + np.mean(firm.cash_last_periods[1:4]) + self.cash)/all_firms_cash firm_likelihood = min(1, 2*firm_likelihood) - firms_further_considered.append([firm, firm_likelihood, firm_cost]) + firms_further_considered.append([firm, firm_likelihood, firm.var_sum]) if len(firms_further_considered) > 0: best_likelihood = 0 @@ -729,6 +725,7 @@ def buyout(self, firm, firm_cost, time): This method causes buyer to receive obligation to buy firm. Sets all the bought firms contracts as its own. Then clears bought firms contracts and dissolves it. Only called from consider_buyout().""" self.receive_obligation(firm_cost, self.simulation, time, 'buyout') + if self.is_insurer and firm.is_insurer: print("Insurer %i has bought %i" % (self.id, firm.id)) elif self.is_reinsurer and firm.is_reinsurer: @@ -737,6 +734,10 @@ def buyout(self, firm, firm_cost, time): for contract in firm.underwritten_contracts: contract.insurer = self self.underwritten_contracts.append(contract) + for obli in firm.obligations: + self.receive_obligation(obli['amount'], obli["recipient"], obli["due_time"], obli["purpose"]) + + firm.obligations = [] firm.underwritten_contracts = [] firm.dissolve(time, 'buyout') diff --git a/visualization_network.py b/visualization_network.py index 80198e3..1009c09 100644 --- a/visualization_network.py +++ b/visualization_network.py @@ -75,14 +75,9 @@ def update(self, insurancefirms, reinsurancefirms, catbonds): """unweighted adjacency matrix""" adj_matrix = np.sign(weights_matrix) - """define network""" - self.network = nx.from_numpy_array(weights_matrix, create_using=nx.DiGraph()) # weighted - self.network_unweighted = nx.from_numpy_array(adj_matrix, create_using=nx.DiGraph()) # unweighted - """Add this iteration of network data to be saved""" self.save_data['unweighted_network'].append(adj_matrix.tolist()) - self.save_data['weighted_network'].append(weights_matrix.tolist()) - self.save_data['network_edgelabels'].append(self.edge_labels) + self.save_data['network_edge_labels'].append(self.edge_labels) self.save_data['network_node_labels'].append(self.node_labels) self.save_data['number_of_agents'].append(self.num_entities) @@ -125,11 +120,6 @@ def visualize(self): self.figure.canvas.flush_events() self.figure.clear() - def save_network_data(self): - with open("data/network_data.dat", "w") as wfile: - wfile.write(str(self.save_data) + "\n") - wfile.write(str(self.event_schedule) + "\n") - class LoadNetwork: def __init__(self, network_data, num_iter): @@ -140,9 +130,8 @@ def __init__(self, network_data, num_iter): No return values. This class is given the loaded network data and then uses it to create an animated network.""" self.figure = plt.figure(num=None, figsize=(10, 8), dpi=100, facecolor='w', edgecolor='k') - self.unweighted_network_data = network_data[0]["unweighted_network"] - # self.weighted_network_data = network_data[0]["weighted_network"] # Unused for now - self.network_edge_labels = network_data[0]["network_edgelabels"] + self.unweighted_network_data = network_data[0]["unweighted_network_data"] + self.network_edge_labels = network_data[0]["network_edge_labels"] self.network_node_labels = network_data[0]["network_node_labels"] self.number_agent_type = network_data[0]["number_of_agents"] self.event_schedule = network_data[1] @@ -162,7 +151,7 @@ def update(self, i): self.figure.clear() plt.suptitle('Network Timestep %i' % i) unweighted_nx_network = nx.from_numpy_array(np.array(self.unweighted_network_data[i])) - pos = nx.shell_layout(unweighted_nx_network) + pos = nx.kamada_kawai_layout(unweighted_nx_network) # Can also use circular/shell/spring nx.draw_networkx_nodes(unweighted_nx_network, pos, list(range(self.number_agent_type[i]["insurers"])), node_color='b', node_size=50, alpha=0.9, label='Insurer') @@ -177,14 +166,14 @@ def update(self, i): node_color='g', node_size=50, alpha=0.9, label='CatBond') nx.draw_networkx_edges(unweighted_nx_network, pos, width=1.0, alpha=0.5, node_size=50) - nx.draw_networkx_edge_labels(self.unweighted_network_data[i], pos, self.network_edge_labels[i], font_size=5) - nx.draw_networkx_labels(self.unweighted_network_data[i], pos, self.network_node_labels[i], font_size=10) + nx.draw_networkx_edge_labels(self.unweighted_network_data[i], pos, self.network_edge_labels[i], font_size=3) + nx.draw_networkx_labels(self.unweighted_network_data[i], pos, self.network_node_labels[i], font_size=7) while self.all_events[0] == i: plt.title('EVENT!') self.all_events = self.all_events[1:] - plt.legend() + plt.legend(loc='upper right') plt.axis('off') def animate(self): @@ -192,7 +181,7 @@ def animate(self): No accepted values. No return values.""" self.network_ani = animation.FuncAnimation(self.figure, self.update, frames=self.num_iter, repeat=False, - interval=20, save_count=self.num_iter) + interval=50, save_count=self.num_iter) def save_network_animation(self): """Method to save animation as MP4. @@ -207,7 +196,8 @@ def save_network_animation(self): parser.add_argument("--save", action="store_true", help="Save the network as an mp4") parser.add_argument("--number_iterations", type=int, help="number of frames for animation") args = parser.parse_args() - + args.save = True + args.number_iterations = 199 if args.number_iterations: num_iter = args.number_iterations else: From 75ae851b4c790ed6181cb2c72d47c7d09a541c12 Mon Sep 17 00:00:00 2001 From: KloskaT Date: Thu, 25 Jul 2019 14:28:19 +0100 Subject: [PATCH 072/125] Deleted redundant plotter files. Visualisation prints when each requested file is saved. --- metaplotter.py | 159 --------------- metaplotter_pl_timescale.py | 176 ----------------- ...lotter_pl_timescale_additional_measures.py | 187 ------------------ plotter.py | 80 -------- plotter_pl_timescale.py | 107 ---------- visualisation.py | 38 ++-- 6 files changed, 26 insertions(+), 721 deletions(-) delete mode 100644 metaplotter.py delete mode 100644 metaplotter_pl_timescale.py delete mode 100644 metaplotter_pl_timescale_additional_measures.py delete mode 100755 plotter.py delete mode 100644 plotter_pl_timescale.py diff --git a/metaplotter.py b/metaplotter.py deleted file mode 100644 index 775b55e..0000000 --- a/metaplotter.py +++ /dev/null @@ -1,159 +0,0 @@ -import matplotlib.pyplot as plt -import numpy as np -import pdb -import os -import time -import glob - -def read_data(): - # do not overwrite old pdfs - #if os.path.exists("data/fig_one_and_two_rm_comp.pdf"): - # os.rename("data/fig_one_and_two_rm_comp.pdf", "data/fig_one_and_two_rm_comp_old_" + time.strftime('%Y_%b_%d_%H_%M') + ".pdf") - #if os.path.exists("data/fig_three_and_four_rm_comp.pdf"): - # os.rename("data/fig_three_and_four_rm_comp.pdf", "data/fig_three_and_four_rm_comp_old_" + time.strftime('%Y_%b_%d_%H_%M') + ".pdf") - - upper_bound = 75 - lower_bound = 25 - - timeseries_dict = {} - timeseries_dict["mean"] = {} - timeseries_dict["median"] = {} - timeseries_dict["quantile25"] = {} - timeseries_dict["quantile75"] = {} - - filenames_ones = glob.glob("data/one*.dat") - filenames_twos = glob.glob("data/two*.dat") - filenames_threes = glob.glob("data/three*.dat") - filenames_fours = glob.glob("data/four*.dat") - filenames_ones.sort() - filenames_twos.sort() - filenames_threes.sort() - filenames_fours.sort() - - assert len(filenames_ones) == len(filenames_twos) == len(filenames_threes) == len(filenames_fours) - all_filenames = filenames_ones + filenames_twos + filenames_threes + filenames_fours - - for filename in all_filenames: - # read files - rfile = open(filename, "r") - data = [eval(k) for k in rfile] - rfile.close() - - # compute data series - data_means = [] - data_medians = [] - data_q25 = [] - data_q75 = [] - for i in range(len(data[0])): - data_means.append(np.mean([item[i] for item in data])) - data_q25.append(np.percentile([item[i] for item in data], lower_bound)) - data_q75.append(np.percentile([item[i] for item in data], upper_bound)) - data_medians.append(np.median([item[i] for item in data])) - data_means = np.array(data_means) - data_medians = np.array(data_medians) - data_q25 = np.array(data_q25) - data_q75 = np.array(data_q75) - - # record data series - timeseries_dict["mean"][filename] = data_means - timeseries_dict["median"][filename] = data_medians - timeseries_dict["quantile25"][filename] = data_q25 - timeseries_dict["quantile75"][filename] = data_q75 - return timeseries_dict - - - -def plotting(output_label, timeseries_dict, riskmodelsetting1, riskmodelsetting2, series1, series2=None, additionalriskmodelsetting3=None, additionalriskmodelsetting4=None, plottype1="mean", plottype2="mean"): - # dictionaries - colors = {"one": "red", "two": "blue", "three": "green", "four": "yellow"} - labels = {"contracts": "Contracts (Insurers)", "cash": "Liquidity (Insurers)", "operational": "Active Insurers", "premium": "Premium", "reincash": "Liquidity (Reinsurers)", "reincontracts": "Contracts (Reinsurers)", "reinoperational": "Active Reinsurers"} - - # prepare labels, timeseries, etc. - color1 = colors[riskmodelsetting1] - color2 = colors[riskmodelsetting2] - label1 = str.upper(riskmodelsetting1[0]) + riskmodelsetting1[1:] + " riskmodels" - label2 = str.upper(riskmodelsetting2[0]) + riskmodelsetting2[1:] + " riskmodels" - plot_1_1 = "data/" + riskmodelsetting1 + "_" + series1 + ".dat" - plot_1_2 = "data/" + riskmodelsetting2 + "_" + series1 + ".dat" - if series2 is not None: - plot_2_1 = "data/" + riskmodelsetting1 + "_" + series2 + ".dat" - plot_2_2 = "data/" + riskmodelsetting2 + "_" + series2 + ".dat" - if additionalriskmodelsetting3 is not None: - color3 = colors[additionalriskmodelsetting3] - label3 = str.upper(additionalriskmodelsetting3[0]) + additionalriskmodelsetting3[1:] + " riskmodels" - plot_1_3 = "data/" + additionalriskmodelsetting3 + "_" + series1 + ".dat" - if series2 is not None: - plot_2_3 = "data/" + additionalriskmodelsetting3 + "_" + series2 + ".dat" - if additionalriskmodelsetting4 is not None: - color4 = colors[additionalriskmodelsetting4] - label4 = str.upper(additionalriskmodelsetting4[0]) + additionalriskmodelsetting4[1:] + " riskmodels" - plot_1_4 = "data/" + additionalriskmodelsetting4 + "_" + series1 + ".dat" - if series2 is not None: - plot_2_4 = "data/" + additionalriskmodelsetting4 + "_" + series2 + ".dat" - - # Backup existing figures (so as not to overwrite them) - outputfilename = "data/" + output_label + ".pdf" - backupfilename = "data/" + output_label + "_old_" + time.strftime('%Y_%b_%d_%H_%M') + ".pdf" - if os.path.exists(outputfilename): - os.rename(outputfilename, backupfilename) - - # Plot and save - fig = plt.figure() - if series2 is not None: - ax0 = fig.add_subplot(211) - else: - ax0 = fig.add_subplot(111) - if additionalriskmodelsetting3 is not None: - ax0.plot(range(len(timeseries_dict[plottype1][plot_1_3])), timeseries_dict[plottype1][plot_1_3], color=color3, label=label3) - if additionalriskmodelsetting4 is not None: - ax0.plot(range(len(timeseries_dict[plottype1][plot_1_4])), timeseries_dict[plottype1][plot_1_4], color=color4, label=label4) - ax0.plot(range(len(timeseries_dict[plottype1][plot_1_1])), timeseries_dict[plottype1][plot_1_1], color=color1, label=label1) - ax0.plot(range(len(timeseries_dict[plottype1][plot_1_2])), timeseries_dict[plottype1][plot_1_2], color=color2, label=label2) - ax0.fill_between(range(len(timeseries_dict["quantile25"][plot_1_1])), timeseries_dict["quantile25"][plot_1_1], timeseries_dict["quantile75"][plot_1_1], facecolor=color1, alpha=0.25) - ax0.fill_between(range(len(timeseries_dict["quantile25"][plot_1_1])), timeseries_dict["quantile25"][plot_1_2], timeseries_dict["quantile75"][plot_1_2], facecolor=color2, alpha=0.25) - ax0.set_ylabel(labels[series1])#"Contracts") - ax0.legend(loc='best') - if series2 is not None: - ax1 = fig.add_subplot(212) - if additionalriskmodelsetting3 is not None: - ax1.plot(range(len(timeseries_dict[plottype2][plot_2_3])), timeseries_dict[plottype2][plot_2_3], color=color3, label=label3) - if additionalriskmodelsetting4 is not None: - ax1.plot(range(len(timeseries_dict[plottype2][plot_2_4])), timeseries_dict[plottype2][plot_2_4], color=color4, label=label4) - ax1.plot(range(len(timeseries_dict[plottype2][plot_2_1])), timeseries_dict[plottype2][plot_2_1], color=color1, label=label1) - ax1.plot(range(len(timeseries_dict[plottype2][plot_2_2])), timeseries_dict[plottype2][plot_2_2], color=color2, label=label2) - ax1.fill_between(range(len(timeseries_dict["quantile25"][plot_2_1])), timeseries_dict["quantile25"][plot_2_1], timeseries_dict["quantile75"][plot_2_1], facecolor=color1, alpha=0.25) - ax1.fill_between(range(len(timeseries_dict["quantile25"][plot_2_1])), timeseries_dict["quantile25"][plot_2_2], timeseries_dict["quantile75"][plot_2_2], facecolor=color2, alpha=0.25) - ax1.set_ylabel(labels[series2]) - ax1.set_xlabel("Time") - plt.savefig(outputfilename) - plt.show() - -timeseries = read_data() - -# for just two different riskmodel settings -plotting(output_label="fig_contracts_survival_1_2", timeseries_dict=timeseries, riskmodelsetting1="one", \ - riskmodelsetting2="two", series1="contracts", series2="operational", plottype1="mean", plottype2="median") -plotting(output_label="fig_reinsurers_contracts_survival_1_2", timeseries_dict=timeseries, riskmodelsetting1="one", \ - riskmodelsetting2="two", series1="reincontracts", series2="reinoperational", plottype1="mean", plottype2="median") -plotting(output_label="fig_premium_1_2", timeseries_dict=timeseries, riskmodelsetting1="one", riskmodelsetting2="two", \ - series1="premium", series2=None, plottype1="mean", plottype2=None) - -raise SystemExit -# for four different riskmodel settings -plotting(output_label="fig_contracts_survival_1_2", timeseries_dict=timeseries, riskmodelsetting1="one", \ - riskmodelsetting2="two", series1="contracts", series2="operational", additionalriskmodelsetting3="three", \ - additionalriskmodelsetting4="four", plottype1="mean", plottype2="median") -plotting(output_label="fig_contracts_survival_3_4", timeseries_dict=timeseries, riskmodelsetting1="three", \ - riskmodelsetting2="four", series1="contracts", series2="operational", additionalriskmodelsetting3="one", \ - additionalriskmodelsetting4="two", plottype1="mean", plottype2="median") -plotting(output_label="fig_reinsurers_contracts_survival_1_2", timeseries_dict=timeseries, riskmodelsetting1="one", \ - riskmodelsetting2="two", series1="reincontracts", series2="reinoperational", \ - additionalriskmodelsetting3="three", additionalriskmodelsetting4="four", plottype1="mean", plottype2="median") -plotting(output_label="fig_reinsurers_contracts_survival_3_4", timeseries_dict=timeseries, riskmodelsetting1="three", \ - riskmodelsetting2="four", series1="reincontracts", series2="reinoperational", \ - additionalriskmodelsetting3="one", additionalriskmodelsetting4="two", plottype1="mean", plottype2="median") -plotting(output_label="fig_premium_1_2", timeseries_dict=timeseries, riskmodelsetting1="one", riskmodelsetting2="two", \ - series1="premium", series2=None, additionalriskmodelsetting3="three", additionalriskmodelsetting4="four", \ - plottype1="mean", plottype2=None) - -#pdb.set_trace() diff --git a/metaplotter_pl_timescale.py b/metaplotter_pl_timescale.py deleted file mode 100644 index d261d11..0000000 --- a/metaplotter_pl_timescale.py +++ /dev/null @@ -1,176 +0,0 @@ -import matplotlib.pyplot as plt -import numpy as np -import pdb -import os -import time -import glob - -def read_data(): - # do not overwrite old pdfs - #if os.path.exists("data/fig_one_and_two_rm_comp.pdf"): - # os.rename("data/fig_one_and_two_rm_comp.pdf", "data/fig_one_and_two_rm_comp_old_" + time.strftime('%Y_%b_%d_%H_%M') + ".pdf") - #if os.path.exists("data/fig_three_and_four_rm_comp.pdf"): - # os.rename("data/fig_three_and_four_rm_comp.pdf", "data/fig_three_and_four_rm_comp_old_" + time.strftime('%Y_%b_%d_%H_%M') + ".pdf") - - upper_bound = 75 - lower_bound = 25 - - timeseries_dict = {} - timeseries_dict["mean"] = {} - timeseries_dict["median"] = {} - timeseries_dict["quantile25"] = {} - timeseries_dict["quantile75"] = {} - - filenames_ones = glob.glob("data/one*.dat") - filenames_twos = glob.glob("data/two*.dat") - filenames_threes = glob.glob("data/three*.dat") - filenames_fours = glob.glob("data/four*.dat") - filenames_ones.sort() - filenames_twos.sort() - filenames_threes.sort() - filenames_fours.sort() - - #assert len(filenames_ones) == len(filenames_twos) == len(filenames_threes) == len(filenames_fours) - all_filenames = filenames_ones + filenames_twos + filenames_threes + filenames_fours - - for filename in all_filenames: - # read files - rfile = open(filename, "r") - data = [eval(k) for k in rfile] - rfile.close() - - # compute data series - data_means = [] - data_medians = [] - data_q25 = [] - data_q75 = [] - for i in range(len(data[0])): - data_means.append(np.mean([item[i] for item in data])) - data_q25.append(np.percentile([item[i] for item in data], lower_bound)) - data_q75.append(np.percentile([item[i] for item in data], upper_bound)) - data_medians.append(np.median([item[i] for item in data])) - data_means = np.array(data_means) - data_medians = np.array(data_medians) - data_q25 = np.array(data_q25) - data_q75 = np.array(data_q75) - - # record data series - timeseries_dict["mean"][filename] = data_means - timeseries_dict["median"][filename] = data_medians - timeseries_dict["quantile25"][filename] = data_q25 - timeseries_dict["quantile75"][filename] = data_q75 - return timeseries_dict - - - -def plotting(output_label, timeseries_dict, riskmodelsetting1, riskmodelsetting2, series1, series2=None, additionalriskmodelsetting3=None, additionalriskmodelsetting4=None, plottype1="mean", plottype2="mean"): - # dictionaries - colors = {"one": "red", "two": "blue", "three": "green", "four": "yellow"} - labels = {"profitslosses": "Profits and Losses (Insurer)", "contracts": "Contracts (Insurers)", "cash": "Liquidity (Insurers)", "operational": "Active Insurers", "premium": "Premium", "reinprofitslosses": "Profits and Losses (Reinsurer)", "reincash": "Liquidity (Reinsurers)", "reincontracts": "Contracts (Reinsurers)", "reinoperational": "Active Reinsurers"} - - # prepare labels, timeseries, etc. - color1 = colors[riskmodelsetting1] - color2 = colors[riskmodelsetting2] - label1 = str.upper(riskmodelsetting1[0]) + riskmodelsetting1[1:] + " riskmodels" - label2 = str.upper(riskmodelsetting2[0]) + riskmodelsetting2[1:] + " riskmodels" - plot_1_1 = "data/" + riskmodelsetting1 + "_" + series1 + ".dat" - plot_1_2 = "data/" + riskmodelsetting2 + "_" + series1 + ".dat" - if series2 is not None: - plot_2_1 = "data/" + riskmodelsetting1 + "_" + series2 + ".dat" - plot_2_2 = "data/" + riskmodelsetting2 + "_" + series2 + ".dat" - if additionalriskmodelsetting3 is not None: - color3 = colors[additionalriskmodelsetting3] - label3 = str.upper(additionalriskmodelsetting3[0]) + additionalriskmodelsetting3[1:] + " riskmodels" - plot_1_3 = "data/" + additionalriskmodelsetting3 + "_" + series1 + ".dat" - if series2 is not None: - plot_2_3 = "data/" + additionalriskmodelsetting3 + "_" + series2 + ".dat" - if additionalriskmodelsetting4 is not None: - color4 = colors[additionalriskmodelsetting4] - label4 = str.upper(additionalriskmodelsetting4[0]) + additionalriskmodelsetting4[1:] + " riskmodels" - plot_1_4 = "data/" + additionalriskmodelsetting4 + "_" + series1 + ".dat" - if series2 is not None: - plot_2_4 = "data/" + additionalriskmodelsetting4 + "_" + series2 + ".dat" - - # Backup existing figures (so as not to overwrite them) - outputfilename = "data/" + output_label + ".pdf" - backupfilename = "data/" + output_label + "_old_" + time.strftime('%Y_%b_%d_%H_%M') + ".pdf" - if os.path.exists(outputfilename): - os.rename(outputfilename, backupfilename) - - # Plot and save - fig = plt.figure() - if series2 is not None: - ax0 = fig.add_subplot(211) - else: - ax0 = fig.add_subplot(111) - maxlen_plots = 0 - if additionalriskmodelsetting3 is not None: - ax0.plot(range(len(timeseries_dict[plottype1][plot_1_3]))[200:], timeseries_dict[plottype1][plot_1_3][200:], color=color3, label=label3) - maxlen_plots = max(maxlen_plots, len(timeseries_dict[plottype1][plot_1_3])) - if additionalriskmodelsetting4 is not None: - ax0.plot(range(len(timeseries_dict[plottype1][plot_1_4]))[200:], timeseries_dict[plottype1][plot_1_4][200:], color=color4, label=label4) - maxlen_plots = max(maxlen_plots, len(timeseries_dict[plottype1][plot_1_4])) - ax0.plot(range(len(timeseries_dict[plottype1][plot_1_1]))[200:], timeseries_dict[plottype1][plot_1_1][200:], color=color1, label=label1) - ax0.plot(range(len(timeseries_dict[plottype1][plot_1_2]))[200:], timeseries_dict[plottype1][plot_1_2][200:], color=color2, label=label2) - ax0.fill_between(range(len(timeseries_dict["quantile25"][plot_1_1]))[200:], timeseries_dict["quantile25"][plot_1_1][200:], timeseries_dict["quantile75"][plot_1_1][200:], facecolor=color1, alpha=0.25) - ax0.fill_between(range(len(timeseries_dict["quantile25"][plot_1_1]))[200:], timeseries_dict["quantile25"][plot_1_2][200:], timeseries_dict["quantile75"][plot_1_2][200:], facecolor=color2, alpha=0.25) - ax0.set_ylabel(labels[series1])#"Contracts") - maxlen_plots = max(maxlen_plots, len(timeseries_dict[plottype1][plot_1_1]), len(timeseries_dict[plottype1][plot_1_2])) - xticks = np.arange(200, maxlen_plots, step=120) - ax0.set_xticks(xticks) - ax0.set_xticklabels(["${0:d}$".format(int((xtc-200)/12)) for xtc in xticks]); - - ax0.legend(loc='best') - if series2 is not None: - ax1 = fig.add_subplot(212) - maxlen_plots = 0 - if additionalriskmodelsetting3 is not None: - ax1.plot(range(len(timeseries_dict[plottype2][plot_2_3]))[200:], timeseries_dict[plottype2][plot_2_3][200:], color=color3, label=label3) - maxlen_plots = max(maxlen_plots, len(timeseries_dict[plottype1][plot_2_3])) - if additionalriskmodelsetting4 is not None: - ax1.plot(range(len(timeseries_dict[plottype2][plot_2_4]))[200:], timeseries_dict[plottype2][plot_2_4][200:], color=color4, label=label4) - maxlen_plots = max(maxlen_plots, len(timeseries_dict[plottype1][plot_2_4])) - ax1.plot(range(len(timeseries_dict[plottype2][plot_2_1]))[200:], timeseries_dict[plottype2][plot_2_1][200:], color=color1, label=label1) - ax1.plot(range(len(timeseries_dict[plottype2][plot_2_2]))[200:], timeseries_dict[plottype2][plot_2_2][200:], color=color2, label=label2) - ax1.fill_between(range(len(timeseries_dict["quantile25"][plot_2_1]))[200:], timeseries_dict["quantile25"][plot_2_1][200:], timeseries_dict["quantile75"][plot_2_1][200:], facecolor=color1, alpha=0.25) - ax1.fill_between(range(len(timeseries_dict["quantile25"][plot_2_1]))[200:], timeseries_dict["quantile25"][plot_2_2][200:], timeseries_dict["quantile75"][plot_2_2][200:], facecolor=color2, alpha=0.25) - maxlen_plots = max(maxlen_plots, len(timeseries_dict[plottype1][plot_2_1]), len(timeseries_dict[plottype1][plot_2_2])) - xticks = np.arange(200, maxlen_plots, step=120) - ax1.set_xticks(xticks) - ax1.set_xticklabels(["${0:d}$".format(int((xtc-200)/12)) for xtc in xticks]); - ax1.set_ylabel(labels[series2]) - ax1.set_xlabel("Years") - else: - ax0.set_xlabel("Years") - plt.savefig(outputfilename) - plt.show() - -timeseries = read_data() - -## for just two different riskmodel settings -#plotting(output_label="fig_pl_survival_1_2", timeseries_dict=timeseries, riskmodelsetting1="one", \ -# riskmodelsetting2="two", series1="profitslosses", series2="operational", plottype1="mean", plottype2="median") -#plotting(output_label="fig_reinsurers_pl_survival_1_2", timeseries_dict=timeseries, riskmodelsetting1="one", \ -# riskmodelsetting2="two", series1="reinprofitslosses", series2="reinoperational", plottype1="mean", plottype2="median") -#plotting(output_label="fig_premium_1_2", timeseries_dict=timeseries, riskmodelsetting1="one", riskmodelsetting2="two", \ -# series1="premium", series2=None, plottype1="mean", plottype2=None) -# -#raise SystemExit -# for four different riskmodel settings -plotting(output_label="fig_pl_survival_1_2", timeseries_dict=timeseries, riskmodelsetting1="one", \ - riskmodelsetting2="two", series1="profitslosses", series2="operational", additionalriskmodelsetting3="three", \ - additionalriskmodelsetting4="four", plottype1="mean", plottype2="median") -plotting(output_label="fig_pl_survival_3_4", timeseries_dict=timeseries, riskmodelsetting1="three", \ - riskmodelsetting2="four", series1="profitslosses", series2="operational", additionalriskmodelsetting3="one", \ - additionalriskmodelsetting4="two", plottype1="mean", plottype2="median") -plotting(output_label="fig_reinsurers_pl_survival_1_2", timeseries_dict=timeseries, riskmodelsetting1="one", \ - riskmodelsetting2="two", series1="reinprofitslosses", series2="reinoperational", \ - additionalriskmodelsetting3="three", additionalriskmodelsetting4="four", plottype1="mean", plottype2="median") -plotting(output_label="fig_reinsurers_pl_survival_3_4", timeseries_dict=timeseries, riskmodelsetting1="three", \ - riskmodelsetting2="four", series1="reinprofitslosses", series2="reinoperational", \ - additionalriskmodelsetting3="one", additionalriskmodelsetting4="two", plottype1="mean", plottype2="median") -plotting(output_label="fig_premium_1_2", timeseries_dict=timeseries, riskmodelsetting1="one", riskmodelsetting2="two", \ - series1="premium", series2=None, additionalriskmodelsetting3="three", additionalriskmodelsetting4="four", \ - plottype1="mean", plottype2=None) - -#pdb.set_trace() diff --git a/metaplotter_pl_timescale_additional_measures.py b/metaplotter_pl_timescale_additional_measures.py deleted file mode 100644 index 5b0c449..0000000 --- a/metaplotter_pl_timescale_additional_measures.py +++ /dev/null @@ -1,187 +0,0 @@ -import matplotlib.pyplot as plt -import numpy as np -import pdb -import os -import time -import glob - -def read_data(): - # do not overwrite old pdfs - #if os.path.exists("data/fig_one_and_two_rm_comp.pdf"): - # os.rename("data/fig_one_and_two_rm_comp.pdf", "data/fig_one_and_two_rm_comp_old_" + time.strftime('%Y_%b_%d_%H_%M') + ".pdf") - #if os.path.exists("data/fig_three_and_four_rm_comp.pdf"): - # os.rename("data/fig_three_and_four_rm_comp.pdf", "data/fig_three_and_four_rm_comp_old_" + time.strftime('%Y_%b_%d_%H_%M') + ".pdf") - - upper_bound = 75 - lower_bound = 25 - - timeseries_dict = {} - timeseries_dict["mean"] = {} - timeseries_dict["median"] = {} - timeseries_dict["quantile25"] = {} - timeseries_dict["quantile75"] = {} - - filenames_ones = glob.glob("data/one*.dat") - filenames_twos = glob.glob("data/two*.dat") - filenames_threes = glob.glob("data/three*.dat") - filenames_fours = glob.glob("data/four*.dat") - filenames_ones.sort() - filenames_twos.sort() - filenames_threes.sort() - filenames_fours.sort() - - #assert len(filenames_ones) == len(filenames_twos) == len(filenames_threes) == len(filenames_fours) - all_filenames = filenames_ones + filenames_twos + filenames_threes + filenames_fours - - for filename in all_filenames: - # read files - rfile = open(filename, "r") - data = [eval(k) for k in rfile] - rfile.close() - - # compute data series - data_means = [] - data_medians = [] - data_q25 = [] - data_q75 = [] - for i in range(len(data[0])): - data_means.append(np.mean([item[i] for item in data])) - data_q25.append(np.percentile([item[i] for item in data], lower_bound)) - data_q75.append(np.percentile([item[i] for item in data], upper_bound)) - data_medians.append(np.median([item[i] for item in data])) - data_means = np.array(data_means) - data_medians = np.array(data_medians) - data_q25 = np.array(data_q25) - data_q75 = np.array(data_q75) - - # record data series - timeseries_dict["mean"][filename] = data_means - timeseries_dict["median"][filename] = data_medians - timeseries_dict["quantile25"][filename] = data_q25 - timeseries_dict["quantile75"][filename] = data_q75 - return timeseries_dict - - - -def plotting(output_label, timeseries_dict, riskmodelsetting1, riskmodelsetting2, series1, series2=None, additionalriskmodelsetting3=None, additionalriskmodelsetting4=None, plottype1="mean", plottype2="mean"): - # dictionaries - colors = {"one": "red", "two": "blue", "three": "green", "four": "yellow"} - labels = {"reinexcess_capital": "Excess Capital (Reinsurers)", "excess_capital": "Excess Capital (Insurers)", "cumulative_unrecovered_claims": "Uncovered Claims (cumulative)", "cumulative_bankruptcies": "Bankruptcies (cumulative)", "profitslosses": "Profits and Losses (Insurer)", "contracts": "Contracts (Insurers)", "cash": "Liquidity (Insurers)", "operational": "Active Insurers", "premium": "Premium", "reinprofitslosses": "Profits and Losses (Reinsurer)", "reincash": "Liquidity (Reinsurers)", "reincontracts": "Contracts (Reinsurers)", "reinoperational": "Active Reinsurers"} - - # prepare labels, timeseries, etc. - color1 = colors[riskmodelsetting1] - color2 = colors[riskmodelsetting2] - label1 = str.upper(riskmodelsetting1[0]) + riskmodelsetting1[1:] + " riskmodels" - label2 = str.upper(riskmodelsetting2[0]) + riskmodelsetting2[1:] + " riskmodels" - plot_1_1 = "data/" + riskmodelsetting1 + "_" + series1 + ".dat" - plot_1_2 = "data/" + riskmodelsetting2 + "_" + series1 + ".dat" - if series2 is not None: - plot_2_1 = "data/" + riskmodelsetting1 + "_" + series2 + ".dat" - plot_2_2 = "data/" + riskmodelsetting2 + "_" + series2 + ".dat" - if additionalriskmodelsetting3 is not None: - color3 = colors[additionalriskmodelsetting3] - label3 = str.upper(additionalriskmodelsetting3[0]) + additionalriskmodelsetting3[1:] + " riskmodels" - plot_1_3 = "data/" + additionalriskmodelsetting3 + "_" + series1 + ".dat" - if series2 is not None: - plot_2_3 = "data/" + additionalriskmodelsetting3 + "_" + series2 + ".dat" - if additionalriskmodelsetting4 is not None: - color4 = colors[additionalriskmodelsetting4] - label4 = str.upper(additionalriskmodelsetting4[0]) + additionalriskmodelsetting4[1:] + " riskmodels" - plot_1_4 = "data/" + additionalriskmodelsetting4 + "_" + series1 + ".dat" - if series2 is not None: - plot_2_4 = "data/" + additionalriskmodelsetting4 + "_" + series2 + ".dat" - - # Backup existing figures (so as not to overwrite them) - outputfilename = "data/" + output_label + ".pdf" - backupfilename = "data/" + output_label + "_old_" + time.strftime('%Y_%b_%d_%H_%M') + ".pdf" - if os.path.exists(outputfilename): - os.rename(outputfilename, backupfilename) - - # Plot and save - fig = plt.figure() - if series2 is not None: - ax0 = fig.add_subplot(211) - else: - ax0 = fig.add_subplot(111) - maxlen_plots = 0 - if additionalriskmodelsetting3 is not None: - ax0.plot(range(len(timeseries_dict[plottype1][plot_1_3]))[200:], timeseries_dict[plottype1][plot_1_3][200:], color=color3, label=label3) - maxlen_plots = max(maxlen_plots, len(timeseries_dict[plottype1][plot_1_3])) - if additionalriskmodelsetting4 is not None: - ax0.plot(range(len(timeseries_dict[plottype1][plot_1_4]))[200:], timeseries_dict[plottype1][plot_1_4][200:], color=color4, label=label4) - maxlen_plots = max(maxlen_plots, len(timeseries_dict[plottype1][plot_1_4])) - ax0.plot(range(len(timeseries_dict[plottype1][plot_1_1]))[200:], timeseries_dict[plottype1][plot_1_1][200:], color=color1, label=label1) - ax0.plot(range(len(timeseries_dict[plottype1][plot_1_2]))[200:], timeseries_dict[plottype1][plot_1_2][200:], color=color2, label=label2) - ax0.fill_between(range(len(timeseries_dict["quantile25"][plot_1_1]))[200:], timeseries_dict["quantile25"][plot_1_1][200:], timeseries_dict["quantile75"][plot_1_1][200:], facecolor=color1, alpha=0.25) - ax0.fill_between(range(len(timeseries_dict["quantile25"][plot_1_1]))[200:], timeseries_dict["quantile25"][plot_1_2][200:], timeseries_dict["quantile75"][plot_1_2][200:], facecolor=color2, alpha=0.25) - ax0.set_ylabel(labels[series1])#"Contracts") - maxlen_plots = max(maxlen_plots, len(timeseries_dict[plottype1][plot_1_1]), len(timeseries_dict[plottype1][plot_1_2])) - xticks = np.arange(200, maxlen_plots, step=120) - ax0.set_xticks(xticks) - ax0.set_xticklabels(["${0:d}$".format(int((xtc-200)/12)) for xtc in xticks]); - - ax0.legend(loc='best') - if series2 is not None: - ax1 = fig.add_subplot(212) - maxlen_plots = 0 - if additionalriskmodelsetting3 is not None: - ax1.plot(range(len(timeseries_dict[plottype2][plot_2_3]))[200:], timeseries_dict[plottype2][plot_2_3][200:], color=color3, label=label3) - maxlen_plots = max(maxlen_plots, len(timeseries_dict[plottype1][plot_2_3])) - if additionalriskmodelsetting4 is not None: - ax1.plot(range(len(timeseries_dict[plottype2][plot_2_4]))[200:], timeseries_dict[plottype2][plot_2_4][200:], color=color4, label=label4) - maxlen_plots = max(maxlen_plots, len(timeseries_dict[plottype1][plot_2_4])) - ax1.plot(range(len(timeseries_dict[plottype2][plot_2_1]))[200:], timeseries_dict[plottype2][plot_2_1][200:], color=color1, label=label1) - ax1.plot(range(len(timeseries_dict[plottype2][plot_2_2]))[200:], timeseries_dict[plottype2][plot_2_2][200:], color=color2, label=label2) - ax1.fill_between(range(len(timeseries_dict["quantile25"][plot_2_1]))[200:], timeseries_dict["quantile25"][plot_2_1][200:], timeseries_dict["quantile75"][plot_2_1][200:], facecolor=color1, alpha=0.25) - ax1.fill_between(range(len(timeseries_dict["quantile25"][plot_2_1]))[200:], timeseries_dict["quantile25"][plot_2_2][200:], timeseries_dict["quantile75"][plot_2_2][200:], facecolor=color2, alpha=0.25) - maxlen_plots = max(maxlen_plots, len(timeseries_dict[plottype1][plot_2_1]), len(timeseries_dict[plottype1][plot_2_2])) - xticks = np.arange(200, maxlen_plots, step=120) - ax1.set_xticks(xticks) - ax1.set_xticklabels(["${0:d}$".format(int((xtc-200)/12)) for xtc in xticks]); - ax1.set_ylabel(labels[series2]) - ax1.set_xlabel("Years") - else: - ax0.set_xlabel("Years") - plt.savefig(outputfilename) - plt.show() - -timeseries = read_data() - -# for just two different riskmodel settings -#plotting(output_label="fig_pl_excap_1_2", timeseries_dict=timeseries, riskmodelsetting1="one", \ -# riskmodelsetting2="two", series1="profitslosses", series2="excess_capital", plottype1="mean", plottype2="mean") -#plotting(output_label="fig_reinsurers_pl_excap_1_2", timeseries_dict=timeseries, riskmodelsetting1="one", \ -# riskmodelsetting2="two", series1="reinprofitslosses", series2="reinexcess_capital", plottype1="mean", plottype2="mean") -#plotting(output_label="fig_bankruptcies_unrecovered_claims_1_2", timeseries_dict=timeseries, riskmodelsetting1="one", \ -# riskmodelsetting2="two", series1="cumulative_bankruptcies", series2="cumulative_unrecovered_claims", plottype1="mean", plottype2="median") -#plotting(output_label="fig_premium_1_2", timeseries_dict=timeseries, riskmodelsetting1="one", riskmodelsetting2="two", \ -# series1="premium", series2=None, plottype1="mean", plottype2=None) -# -#raise SystemExit -# for four different riskmodel settings -plotting(output_label="fig_pl_excap_1_2", timeseries_dict=timeseries, riskmodelsetting1="one", \ - riskmodelsetting2="two", series1="profitslosses", series2="excess_capital", additionalriskmodelsetting3="three", \ - additionalriskmodelsetting4="four", plottype1="mean", plottype2="mean") -plotting(output_label="fig_reinsurers_pl_excap_1_2", timeseries_dict=timeseries, riskmodelsetting1="one", \ - riskmodelsetting2="two", series1="reinprofitslosses", series2="reinexcess_capital", additionalriskmodelsetting3="three", \ - additionalriskmodelsetting4="four", plottype1="mean", plottype2="mean") -plotting(output_label="fig_bankruptcies_unrecovered_claims_1_2", timeseries_dict=timeseries, riskmodelsetting1="one", \ - riskmodelsetting2="two", series1="cumulative_bankruptcies", series2="cumulative_unrecovered_claims", additionalriskmodelsetting3="three", \ - additionalriskmodelsetting4="four", plottype1="mean", plottype2="median") -plotting(output_label="fig_premium_1_2", timeseries_dict=timeseries, riskmodelsetting1="one", riskmodelsetting2="two", \ - series1="premium", series2=None, additionalriskmodelsetting3="three", additionalriskmodelsetting4="four", plottype1="mean", plottype2=None) - -plotting(output_label="fig_pl_excap_3_4", timeseries_dict=timeseries, riskmodelsetting1="three", \ - riskmodelsetting2="four", series1="profitslosses", series2="excess_capital", additionalriskmodelsetting3="one", \ - additionalriskmodelsetting4="two", plottype1="mean", plottype2="mean") -plotting(output_label="fig_reinsurers_pl_excap_3_4", timeseries_dict=timeseries, riskmodelsetting1="three", \ - riskmodelsetting2="four", series1="reinprofitslosses", series2="reinexcess_capital", additionalriskmodelsetting3="one", \ - additionalriskmodelsetting4="two", plottype1="mean", plottype2="mean") -plotting(output_label="fig_bankruptcies_unrecovered_claims_3_4", timeseries_dict=timeseries, riskmodelsetting1="three", \ - riskmodelsetting2="four", series1="cumulative_bankruptcies", series2="cumulative_unrecovered_claims", additionalriskmodelsetting3="one", \ - additionalriskmodelsetting4="two", plottype1="mean", plottype2="median") -plotting(output_label="fig_premium_3_4", timeseries_dict=timeseries, riskmodelsetting1="three", riskmodelsetting2="four", \ - series1="premium", series2=None, additionalriskmodelsetting3="one", additionalriskmodelsetting4="two", plottype1="mean", plottype2=None) - - -#pdb.set_trace() diff --git a/plotter.py b/plotter.py deleted file mode 100755 index 593b95f..0000000 --- a/plotter.py +++ /dev/null @@ -1,80 +0,0 @@ -import matplotlib.pyplot as plt -import numpy as np - -rfile = open("data/history_logs.dat","r") - -data = [eval(k) for k in rfile] - -contracts = data[0]['total_contracts'] -op = data[0]['total_operational'] -cash = data[0]['total_cash'] -pl = data[0]['total_profitslosses'] -reincontracts = data[0]['total_reincontracts'] -reinop = data[0]['total_reinoperational'] -reincash = data[0]['total_reincash'] -reinpl = data[0]['total_reinprofitslosses'] -premium = data[0]['market_premium'] -catbop = data[0]['total_catbondsoperational'] - -rfile.close() - -cs = contracts -pls = pl -os = op -hs = cash - -cre = reincontracts -plre = reinpl -ore = reinop -hre = reincash - -ocb = catbop -ps = premium - -fig1 = plt.figure() -ax0 = fig1.add_subplot(511) -ax0.get_xaxis().set_visible(False) -ax0.plot(range(len(cs)), cs,"b") -ax0.set_ylabel("Contracts") -ax1 = fig1.add_subplot(512) -ax1.get_xaxis().set_visible(False) -ax1.plot(range(len(os)), os,"b") -ax1.set_ylabel("Active firms") -ax2 = fig1.add_subplot(513) -ax2.get_xaxis().set_visible(False) -ax2.plot(range(len(hs)), hs,"b") -ax2.set_ylabel("Cash") -ax3 = fig1.add_subplot(514) -ax3.get_xaxis().set_visible(False) -ax3.plot(range(len(pls)), pls,"b") -ax3.set_ylabel("Profits, Losses") -ax9 = fig1.add_subplot(515) -ax9.plot(range(len(ps)), ps,"k") -ax9.set_ylabel("Premium") -ax9.set_xlabel("Time") -plt.savefig("data/single_replication_pt1.pdf") - -fig2 = plt.figure() -ax4 = fig2.add_subplot(511) -ax4.get_xaxis().set_visible(False) -ax4.plot(range(len(cre)), cre,"r") -ax4.set_ylabel("Contracts") -ax5 = fig2.add_subplot(512) -ax5.get_xaxis().set_visible(False) -ax5.plot(range(len(ore)), ore,"r") -ax5.set_ylabel("Active reinfirms") -ax6 = fig2.add_subplot(513) -ax6.get_xaxis().set_visible(False) -ax6.plot(range(len(hre)), hre,"r") -ax6.set_ylabel("Cash") -ax7 = fig2.add_subplot(514) -ax7.get_xaxis().set_visible(False) -ax7.plot(range(len(plre)), plre,"r") -ax7.set_ylabel("Profits, Losses") -ax8 = fig2.add_subplot(515) -ax8.plot(range(len(ocb)), ocb,"m") -ax8.set_ylabel("Active cat bonds") -ax8.set_xlabel("Time") - -plt.savefig("data/single_replication_pt2.pdf") -plt.show() diff --git a/plotter_pl_timescale.py b/plotter_pl_timescale.py deleted file mode 100644 index 4f517ff..0000000 --- a/plotter_pl_timescale.py +++ /dev/null @@ -1,107 +0,0 @@ -import matplotlib.pyplot as plt -import numpy as np - -def get_data(name): - rfile = open(name, "r") - out = [eval(k) for k in rfile] - rfile.close() - return out - -contracts = get_data("data/contracts.dat") -op = get_data("data/operational.dat") -cash = get_data("data/cash.dat") -# pl = get_data("data/profitslosses.dat") -reincontracts = get_data("data/reincontracts.dat") -reinop = get_data("data/reinoperational.dat") -reincash = get_data("data/reincash.dat") -# reinpl = get_data("data/reinprofitslosses.dat") -premium = get_data("data/premium.dat") -catbop = get_data("data/catbonds_number.dat") - -c_s = [] - -o_s = [] - -h_s = [] - -p_s = [] - -pl_s = [] - -c_re = [] - -o_re= [] - -h_re = [] - -pl_re = [] - -o_cb = [] - -p_e = [] - -for i in range(len(contracts[0])): #for every time period i - cs = np.mean([item[i] for item in contracts]) - #pls = np.mean([item[i] for item in pl]) - os = np.median([item[i] for item in op]) - hs = np.median([item[i] for item in cash]) - c_s.append(cs) - o_s.append(os) - h_s.append(hs) - - if i>0: - pls = np.mean([item[i]-item[i-1] for item in cash]) - plre = np.mean([item[i]-item[i-1] for item in reincash]) - pl_s.append(pls) - pl_re.append(plre) - - cre = np.mean([item[i] for item in reincontracts]) - ore = np.median([item[i] for item in reinop]) - hre = np.median([item[i] for item in reincash]) - c_re.append(cre) - o_re.append(ore) - h_re.append(hre) - - ocb = np.median([item[i] for item in catbop]) - o_cb.append(ocb) - - p_s = np.median([item[i] for item in premium]) - p_e.append(p_s) - - -maxlen_plots = max(len(pl_s), len(pl_re), len(o_s), len(o_re), len(p_e)) -xticks = np.arange(200, maxlen_plots, step=120) -fig0 = plt.figure() -ax3 = fig0.add_subplot(511) -ax3.plot(range(len(pl_s))[200:], pl_s[200:],"b") -ax3.set_ylabel("Profits, Losses") -ax3.set_xticks(xticks) -ax3.set_xticklabels(["${0:d}$".format(int((xtc-200)/12)) for xtc in xticks]) -ax7 = fig0.add_subplot(512) -ax7.plot(range(len(pl_re))[200:], pl_re[200:],"r") -ax7.set_ylabel("Profits, Losses (Reins.)") -ax7.set_xticks(xticks) -ax7.set_xticklabels(["${0:d}$".format(int((xtc-200)/12)) for xtc in xticks]) -ax1 = fig0.add_subplot(513) -ax1.plot(range(len(o_s))[200:], o_s[200:],"b") -ax1.set_ylabel("Active firms") -ax1.set_xticks(xticks) -ax1.set_xticklabels(["${0:d}$".format(int((xtc-200)/12)) for xtc in xticks]) -ax5 = fig0.add_subplot(514) -ax5.plot(range(len(o_re))[200:], o_re[200:],"r") -ax5.set_ylabel("Active reins. firms") -ax5.set_xticks(xticks) -ax5.set_xticklabels(["${0:d}$".format(int((xtc-200)/12)) for xtc in xticks]) -ax9 = fig0.add_subplot(515) -ax9.plot(range(len(p_e))[200:], p_e[200:],"k") -ax9.set_ylabel("Premium") -ax9.set_xlabel("Years") -ax9.set_xticks(xticks) -ax9.set_xticklabels(["${0:d}$".format(int((xtc-200)/12)) for xtc in xticks]) - - -plt.savefig("data/single_replication_new.pdf") -plt.show() - - -raise SystemExit diff --git a/visualisation.py b/visualisation.py index 646106c..a31bd3d 100644 --- a/visualisation.py +++ b/visualisation.py @@ -418,6 +418,7 @@ def create_insurer_timeseries(self, fig=None, axlst=None, percentiles=[25,75]): colour=colour, percentiles=percentiles, title="%i Risk Model Insurer" % risk_model) self.insurer_fig.savefig("figures/"+str(risk_model)+"risk_model(s)_insurer_ensemble_timeseries.png") + print("Saved " + str(risk_model) + " risk_model(s)_insurer_ensemble_timeseries") def create_reinsurer_timeseries(self, fig=None, axlst=None, percentiles=[25,75]): """Method to create separate reinsurer time series for all numbers of risk models using visualisations' @@ -435,6 +436,7 @@ def create_reinsurer_timeseries(self, fig=None, axlst=None, percentiles=[25,75]) colour=colour, percentiles=percentiles, title="%i Risk Model Reinsurer" % risk_model) self.reinsurer_fig.savefig("figures/"+str(risk_model)+"risk_model(s)_reinsurer_ensemble_timeseries.png") + print("Saved " + str(risk_model) + " risk_model(s)_reinsurer_ensemble_timeseries") def show(self): plt.show() @@ -511,6 +513,7 @@ def generate_plot(self, xlabel=None, filename=None): self.fig.tight_layout() self.fig.savefig(filename + ".pdf") self.fig.savefig(filename + ".png", density=300) + print("Saved " + self.variable + " CDF") class Histogram_plot: @@ -578,6 +581,7 @@ def generate_plot(self, xlabel=None, filename=None, logscale=False, minmax=None, self.fig.tight_layout(pad=.1, w_pad=.1, h_pad=.1) self.fig.savefig(filename + ".pdf") self.fig.savefig(filename + ".png", density=300) + print("Saved " + self.variable + " histogram") class CDFDistribution: @@ -930,10 +934,10 @@ def plot(self, outputfile): reinsurerfig, axs = vis.reinsurer_time_series() insurerfig.savefig("figures/insurer_singlerun_timeseries.png") reinsurerfig.savefig("figures/reinsurer_singlerun_timeseries.png") - vis.show() + # vis.show() N = len(history_logs_list) - if args.timeseries_comparison or args.firmdistribution or args.bankruptcydistribution: + if args.timeseries_comparison or args.bankruptcydistribution: vis_list = [] colour_list = ['red', 'blue', 'green', 'yellow'] @@ -949,16 +953,7 @@ def plot(self, outputfile): cmp_rsk = compare_riskmodels(vis_list, colour_list) cmp_rsk.create_insurer_timeseries(percentiles=[10, 90]) cmp_rsk.create_reinsurer_timeseries(percentiles=[10, 90]) - cmp_rsk.show() - - if args.firmdistribution: - # Creates CDF for firm size using cash as measure of size. - CP = CDF_distribution_plot(vis_list, colour_list, variable="insurance_firms_cash", timestep=-1, plot_cCDF=True) - CP.generate_plot(xlabel="Firm size (capital)") - if not isleconfig.simulation_parameters["reinsurance_off"]: - CP = CDF_distribution_plot(vis_list, colour_list, variable="reinsurance_firms_cash", timestep=-1, - plot_cCDF=True) - CP.generate_plot(xlabel="Firm size (capital)") + # cmp_rsk.show() if args.bankruptcydistribution: # Creates histogram for each number of risk models for size and frequency of bankruptcies/unrecovered claims. @@ -973,6 +968,25 @@ def plot(self, outputfile): HP.generate_plot(logscale=True, xlabel="Damages not recovered", minmax=[0, 6450000], VaR005guess=0.1) # =691186.8726311699) # this is the VaR threshold for 4 risk models with reinsurance + if args.firmdistribution: + vis_list = [] + colour_list = ['red', 'blue', 'green', 'yellow'] + + # Loads all risk model history logs data (very long :'( ) and creates list of visualisation class instances. + filenames = ["./data/" + x + "_history_logs_complete.dat" for x in ["one", "two", "three", "four"]] + for filename in filenames: + with open(filename, 'r') as rfile: + history_logs_list = [eval(k) for k in rfile] # one dict on each line + vis_list.append(visualisation(history_logs_list)) + + # Creates CDF for firm size using cash as measure of size. + CP = CDF_distribution_plot(vis_list, colour_list, variable="insurance_firms_cash", timestep=-1, plot_cCDF=True) + CP.generate_plot(xlabel="Firm size (capital)") + if not isleconfig.simulation_parameters["reinsurance_off"]: + CP = CDF_distribution_plot(vis_list, colour_list, variable="reinsurance_firms_cash", timestep=-1, + plot_cCDF=True) + CP.generate_plot(xlabel="Firm size (capital)") + if args.riskmodel_comparison: # Lists of insurance and reinsurance data files to be compared (Must be same size and equivalent). data_types = ["_noninsured_risks.dat", "_excess_capital.dat", "_cash.dat", "_contracts.dat", "_premium.dat", "_operational.dat"] From a3dca48cca21b2f6136cd54a0cb522345697894a Mon Sep 17 00:00:00 2001 From: KloskaT Date: Thu, 25 Jul 2019 15:02:30 +0100 Subject: [PATCH 073/125] Added save network to start.py for single run using logger object. --- start.py | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/start.py b/start.py index 4f05902..8f933a5 100644 --- a/start.py +++ b/start.py @@ -44,7 +44,7 @@ def main(simulation_parameters, rc_event_schedule, rc_event_damage, np_seed, ran world.accept_agents("reinsurancefirm", reinsurancefirm_pointers, reinsurancefirms_group) """Time iteration""" - for t in range(simulation_parameters["max_time"]): + for t in range(simulation_parameters['max_time']): "create new agents and accepts them based on probability" # TODO: write method for this; this code block is executed almost identically 4 times if world.insurance_firm_market_entry(agent_type="InsuranceFirm"): @@ -73,9 +73,6 @@ def main(simulation_parameters, rc_event_schedule, rc_event_damage, np_seed, ran if t%50 == save_iter: save_simulation(t, simulation, simulation_parameters, rc_event_schedule, rc_event_damage, exit_now=False) - - """finish simulation, write logs""" - simulation.finalize() return simulation.obtain_log(requested_logs) # It is required to return this list to download all the data generated by a single run of the model from the cloud. @@ -154,7 +151,7 @@ def save_simulation(t, sim, sim_param, event_schedule, event_damage, exit_now=Fa if args.save_iterations: save_iter = args.save_iterations else: - save_iter = 20 + save_iter = 20000 from setup import SetupSim setup = SetupSim() #Here the setup for the simulation is done. @@ -167,6 +164,8 @@ def save_simulation(t, sim, sim_param, event_schedule, event_damage, exit_now=Fa L = logger.Logger() L.restore_logger_object(list(log)) L.save_log(is_background) + if isleconfig.save_network: + L.save_network_data(ensemble=False) """ Obtain calibration score """ CS = calibrationscore.CalibrationScore(L) From 10a8f1e1ec36018ba4a7a52a16a5a4b482622a8f Mon Sep 17 00:00:00 2001 From: KloskaT Date: Thu, 25 Jul 2019 15:09:16 +0100 Subject: [PATCH 074/125] Creates figures folder if not already existing. --- visualisation.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/visualisation.py b/visualisation.py index a31bd3d..81e670f 100644 --- a/visualisation.py +++ b/visualisation.py @@ -11,6 +11,8 @@ import time import os +if not os.path.isdir("figures"): + os.makedirs("figures") class TimeSeries(object): def __init__(self, series_list, event_schedule, damage_schedule, title="",xlabel="Time", colour='k', axlst=None, fig=None, percentiles=None, alpha=0.7): From c23aefd06c8b1b9bee76fa74e585c5425f586919 Mon Sep 17 00:00:00 2001 From: KloskaT Date: Thu, 25 Jul 2019 15:14:58 +0100 Subject: [PATCH 075/125] Checks, creates, and writes animation to figures folder --- visualization_network.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/visualization_network.py b/visualization_network.py index 1009c09..503fede 100644 --- a/visualization_network.py +++ b/visualization_network.py @@ -3,6 +3,7 @@ import matplotlib.animation as animation import numpy as np import argparse +import os class ReinsuranceNetwork: @@ -187,7 +188,9 @@ def save_network_animation(self): """Method to save animation as MP4. No accepted values. No return values.""" - self.network_ani.save("data/animated_network.mp4", writer="ffmpeg", dpi=200, fps=5) + if not os.path.isdir("figures"): + os.makedirs("figures") + self.network_ani.save("figures/animated_network.mp4", writer="ffmpeg", dpi=200, fps=5) if __name__ == "__main__": From 747b1883a6c179d9aa906423894aab901a2dda00 Mon Sep 17 00:00:00 2001 From: KloskaT Date: Mon, 29 Jul 2019 17:31:43 +0100 Subject: [PATCH 076/125] Added regulator to central bank based on solvency ii, issues warning which stops underwriting, or sells off firm. Included condition in isleconfig to enable/disable. Fixed bug causing non operational firms rolling over contracts. Simulation also keeps track of firms exiting due to regulations and being bought. Simulation also only iterates through operational firms, otherwise unnecessary looping/dissolutions. Adjusted firm pricing, might still need changing. --- centralbank.py | 66 ++++++++++++++--- insurancesimulation.py | 37 ++++++++-- isleconfig.py | 7 +- metainsuranceorg.py | 161 +++++++++++++++++++++++++---------------- 4 files changed, 188 insertions(+), 83 deletions(-) diff --git a/centralbank.py b/centralbank.py index bf903ae..944b9c7 100644 --- a/centralbank.py +++ b/centralbank.py @@ -1,8 +1,9 @@ from isleconfig import simulation_parameters +import numpy as np class CentralBank: - def __init__(self): + def __init__(self, money_supply): """Constructor Method. No accepted arguments. Constructs the CentralBank class. This class is currently only used to award interest payments.""" @@ -13,6 +14,25 @@ def __init__(self): self.twelvemonth_CPI = 0 self.feedback_counter = 0 self.prices_list = [] + self.economy_money = money_supply + self.warnings = {} + + def update_money_supply(self, amount, reduce=True): + if reduce: + self.economy_money -= amount + else: + self.economy_money += amount + assert self.economy_money > 0 + + def award_interest(self, firm, total_cash): + """Method to award interest. + Accepts: + firm: Type class, the agent that is to be awarded interest. + total_cash: Type decimal + This method takes an agents cash and awards it an interest payment on the cash.""" + interest_payment = total_cash * self.interest_rate + firm.receive(interest_payment) + self.update_money_supply(interest_payment, reduce=True) def set_interest_rate(self): """Method to set the interest rate @@ -37,15 +57,6 @@ def set_interest_rate(self): self.feedback_counter = 0 print(self.interest_rate) - def award_interest(self, firm, total_cash): - """Method to award interest. - Accepts: - firm: Type class, the agent that is to be awarded interest. - total_cash: Type decimal - This method takes an agents cash and awards it an interest payment on the cash.""" - interest_payment = total_cash * self.interest_rate - firm.receive(interest_payment) - def calculate_inflation(self, current_price, time): """Method to calculate inflation in insurance prices. Accepts: @@ -60,3 +71,38 @@ def calculate_inflation(self, current_price, time): self.onemonth_CPI = (current_price - self.prices_list[-2])/self.prices_list[-2] self.twelvemonth_CPI = (current_price - self.prices_list[-13])/self.prices_list[-13] self.actual_inflation = self.twelvemonth_CPI + + def regulate(self, firm_id, firm_cash, firm_var): + """Method to regulate firms. Checks if their average cash over the last year can cover their average VaR and to + what percent. Based on solvency 2 which has a MCR of 85% and SCR of 99.5%. + Accepts: + firm_id: Type Integer. Firms unique ID. + firm_cash: Type list of decimals. List of cash for last twelve periods. + firm_var: Type list of decimals. List of VaR for last twelve periods. + Returns: + Type String: "Good", "Warning", "LoseControl". + This method takes a firms average cash and VaR. If average cash above SCR of 99.5% of VaR then all is well, + if cash is between 85% and 99.5% then is issued a warning (limits business heavily), if under 85% then firm is + sold.""" + if firm_id not in self.warnings.keys(): + self.warnings[firm_id] = 0 + + avg_firm_cash = np.mean(firm_cash) + avg_var = np.mean(firm_var) + + if avg_firm_cash >= 0.995 * avg_var: + self.warnings[firm_id] = 0 + elif avg_firm_cash >= 0.85 * avg_var: + self.warnings[firm_id] += 1 + elif avg_firm_cash < 0.85* avg_var: + if self.warnings[firm_id] > 0: + self.warnings[firm_id] = 2 + else: + self.warnings[firm_id] += 1 + + if self.warnings[firm_id] == 0: + return "Good" + elif self.warnings[firm_id] == 1: + return "Warning" + elif self.warnings[firm_id] >= 2: + return "LoseControl" diff --git a/insurancesimulation.py b/insurancesimulation.py index 5598161..4a6a1f3 100644 --- a/insurancesimulation.py +++ b/insurancesimulation.py @@ -71,7 +71,7 @@ def __init__(self, override_no_riskmodels, replic_ID, simulation_parameters, rc_ "Set up monetary system (should instead be with the customers, if customers are modeled explicitly)" self.money_supply = self.simulation_parameters["money_supply"] self.obligations = [] - self.bank = CentralBank() + self.bank = CentralBank(self.money_supply) "Set up risk categories" self.riskcategories = list(range(self.simulation_parameters["no_categories"])) @@ -140,6 +140,8 @@ def __init__(self, override_no_riskmodels, replic_ID, simulation_parameters, rc_ "Cumulative variables for history and logging" self.cumulative_bankruptcies = 0 self.cumulative_market_exits = 0 + self.bought_firms = 0 + self.cumulative_nonregulation_firms = 0 self.cumulative_unrecovered_claims = 0.0 self.cumulative_claims = 0.0 self.current_bankruptcies = [] @@ -313,7 +315,10 @@ def iterate(self, t): # Iterate reinsurance firm agents for reinagent in self.reinsurancefirms: - reinagent.iterate(t) + if reinagent.operational is True: + reinagent.iterate(t) + if reinagent.cash < 0: + print("Reinsurer %i has negative cash" % reinagent.id) if isleconfig.buy_bankruptcies: for reinagent in self.reinsurancefirms: if reinagent.operational: @@ -323,10 +328,13 @@ def iterate(self, t): # Reset weights self.reset_insurance_weights() - + # Iterate insurance firm agents for agent in self.insurancefirms: - agent.iterate(t) + if agent.operational is True: + agent.iterate(t) + if agent.cash < 0: + print("Insurer %i has negative cash" % agent.id) if isleconfig.buy_bankruptcies: for agent in self.insurancefirms: if agent.operational: @@ -495,6 +503,7 @@ def pay(self, obligation): print("Something wrong: economy out of money", file=sys.stderr) if self.get_operational() and recipient.get_operational(): self.money_supply -= amount + self.bank.update_money_supply(amount, reduce=True) recipient.receive(amount) def receive(self, amount): @@ -503,6 +512,7 @@ def receive(self, amount): Amount due: Type Integer Returns None""" self.money_supply += amount + self.bank.update_money_supply(amount, reduce=False) def reduce_money_supply(self, amount): """Method to reduce money supply immediately and without payment recipient @@ -510,6 +520,7 @@ def reduce_money_supply(self, amount): Accepts: amount: Type Integer""" self.money_supply -= amount + self.bank.update_money_supply(amount, reduce=True) assert self.money_supply >= 0 def reset_reinsurance_weights(self): @@ -824,6 +835,12 @@ def record_market_exit(self): from the method dissolve() from the class metainsuranceorg.py after the dissolution of the firm.""" self.cumulative_market_exits += 1 + def record_bought_firm(self): + self.bought_firms += 1 + + def record_nonregulation_firm(self): + self.cumulative_nonregulation_firms += 1 + def record_unrecovered_claims(self, loss): """Method for recording unrecovered claims. If firm runs out of money it cannot pay more claims and so that money is lost and recorded using this method. @@ -955,7 +972,7 @@ def get_bankrupt_firms(self, type): if len(self.current_bankruptcies) > 0: bankruptcy_time = self.current_bankruptcies[0][1] elif type == "reinsurer": - bankruptcies_sent = [firm for firm in self.current_reinsurer_bankruptcies] + bankruptcies_sent = [firm for firm, time in self.current_reinsurer_bankruptcies] bankruptcy_time = 0 if len(self.current_reinsurer_bankruptcies) > 0: bankruptcy_time = self.current_reinsurer_bankruptcies[0][1] @@ -997,10 +1014,16 @@ def reset_bankrupt_firms(self): and relevant list attribute is reset.""" for firm, time in self.current_bankruptcies: firm.dissolve(time, 'record_bankruptcy') + for contract in firm.underwritten_contracts: + contract.mature(time) + firm.underwritten_contracts = [] self.current_bankruptcies = [] for reinfirm, time in self.current_reinsurer_bankruptcies: - reinfirm.dissolve(time) + reinfirm.dissolve(time, 'record_bankruptcy') + for contract in reinfirm.underwritten_contracts: + contract.mature(time) + reinfirm.underwritten_contracts = [] self.current_reinsurer_bankruptcies = [] def update_network_data(self): @@ -1008,7 +1031,7 @@ def update_network_data(self): No accepted values. No return values. This method is called from save_data() for every iteration to get the current adjacency list so network - visualisation can be saved.""" + visualisation can be saved. Only called if conditions save_network is True and slim logs is False.""" """obtain lists of operational entities""" op_entities = {} num_entities = {} diff --git a/isleconfig.py b/isleconfig.py index 835c17b..bb16206 100644 --- a/isleconfig.py +++ b/isleconfig.py @@ -4,10 +4,11 @@ verbose = False showprogress = True show_network = False # Should network be visualized? This should be False by default, to be overridden by commandline arguments -save_network = True +save_network = False slim_log = False # Should logs be small in ensemble runs (only aggregated level data)? -buy_bankruptcies = False - +buy_bankruptcies = True +enforce_regulations = True + simulation_parameters = {"no_categories": 4, "no_insurancefirms": 20, "no_reinsurancefirms": 4, diff --git a/metainsuranceorg.py b/metainsuranceorg.py index a74464c..b46c8d1 100644 --- a/metainsuranceorg.py +++ b/metainsuranceorg.py @@ -54,7 +54,7 @@ def init(self, simulation_parameters, agent_parameters): self.owner = self.simulation # TODO: Make this into agent_parameter value? self.per_period_dividend = 0 - self.cash_last_periods = list(np.zeros(4, dtype=int)*self.cash) + self.cash_last_periods = list(np.zeros(12, dtype=int)*self.cash) rm_config = agent_parameters['riskmodel_config'] @@ -88,6 +88,7 @@ def init(self, simulation_parameters, agent_parameters): self.profits_losses = 0 #self.reinsurance_contracts = [] self.operational = True + self.warning = False self.is_insurer = True self.is_reinsurer = False @@ -95,6 +96,7 @@ def init(self, simulation_parameters, agent_parameters): self.var_counter = 0 # sum over risk model inaccuracies for all contracts self.var_counter_per_risk = 0 # average risk model inaccuracy across contracts self.var_sum = 0 # sum over initial VaR for all contracts + self.var_sum_last_periods = list(np.zeros(12, dtype=int)) self.counter_category = np.zeros(self.simulation_no_risk_categories) # var_counter disaggregated by category self.var_category = np.zeros(self.simulation_no_risk_categories) # var_sum disaggregated by category self.naccep = [] @@ -128,42 +130,47 @@ def iterate(self, time): print("Number of underwritten contracts ", len(self.underwritten_contracts)) maturing = [contract for contract in self.underwritten_contracts if contract.expiration <= time] for contract in maturing: - self.underwritten_contracts.remove(contract) - contract.mature(time) + self.underwritten_contracts.remove(contract) + contract.mature(time) contracts_dissolved = len(maturing) - - """effect payments from contracts""" [contract.check_payment_due(time) for contract in self.underwritten_contracts] - if self.operational: - """request risks to be considered for underwriting in the next period, organised by insurance type""" - new_nonproportional_risks, new_risks = self.get_newrisks_by_type() - contracts_offered = len(new_risks) - if isleconfig.verbose and contracts_offered < 2 * contracts_dissolved: - print("Something wrong; agent {0:d} receives too few new contracts {1:d} <= {2:d}".format( - self.id, contracts_offered, 2 * contracts_dissolved)) - - """deal with non-proportional risks first as they must evaluate each request separately""" - [reinrisks_per_categ, number_reinrisks_categ] = self.risks_reinrisks_organizer(new_nonproportional_risks) - for repetition in range(self.recursion_limit): - former_reinrisks_per_categ = copy.copy(reinrisks_per_categ) - [reinrisks_per_categ, not_accepted_reinrisks] = self.process_newrisks_reinsurer(reinrisks_per_categ, number_reinrisks_categ, time) #Here we process all the new reinrisks in order to keep the portfolio as balanced as possible. - if former_reinrisks_per_categ == reinrisks_per_categ: - break - self.simulation.return_reinrisks(not_accepted_reinrisks) - - underwritten_risks = [{"value": contract.value, "category": contract.category, \ + if self.operational is True: + # Firms submit cash and var data for regulation every 12 iterations + if time % 12 == 0 and isleconfig.enforce_regulations is True: + self.submit_regulator_report(time) + if self.operational is False: # If not enough average cash then firm is closed and so no underwriting. + return + + if self.warning is False: + """request risks to be considered for underwriting in the next period, organised by insurance type""" + new_nonproportional_risks, new_risks = self.get_newrisks_by_type() + contracts_offered = len(new_risks) + if isleconfig.verbose and contracts_offered < 2 * contracts_dissolved: + print("Something wrong; agent {0:d} receives too few new contracts {1:d} <= {2:d}".format( + self.id, contracts_offered, 2 * contracts_dissolved)) + + """deal with non-proportional risks first as they must evaluate each request separately""" + [reinrisks_per_categ, number_reinrisks_categ] = self.risks_reinrisks_organizer(new_nonproportional_risks) + for repetition in range(self.recursion_limit): + former_reinrisks_per_categ = copy.copy(reinrisks_per_categ) + [reinrisks_per_categ, not_accepted_reinrisks] = self.process_newrisks_reinsurer(reinrisks_per_categ, number_reinrisks_categ, time) #Here we process all the new reinrisks in order to keep the portfolio as balanced as possible. + if former_reinrisks_per_categ == reinrisks_per_categ: + break + self.simulation.return_reinrisks(not_accepted_reinrisks) + + underwritten_risks = [{"value": contract.value, "category": contract.category, \ "risk_factor": contract.risk_factor, "deductible": contract.deductible, \ "excess": contract.excess, "insurancetype": contract.insurancetype, \ "runtime": contract.runtime} for contract in self.underwritten_contracts if contract.reinsurance_share != 1.0] - """obtain risk model evaluation (VaR) for underwriting decisions and for capacity specific decisions""" - # TODO: Enable reinsurance shares other than 0.0 and 1.0 - expected_profit, acceptable_by_category, cash_left_by_categ, var_per_risk_per_categ, self.excess_capital = self.riskmodel.evaluate(underwritten_risks, self.cash) - # TODO: resolve insurance reinsurance inconsistency (insurer underwrite after capacity decisions, reinsurers before). - # This is currently so because it minimizes the number of times we need to run self.riskmodel.evaluate(). - # It would also be more consistent if excess capital would be updated at the end of the iteration. + """obtain risk model evaluation (VaR) for underwriting decisions and for capacity specific decisions""" + # TODO: Enable reinsurance shares other than 0.0 and 1.0 + expected_profit, acceptable_by_category, cash_left_by_categ, var_per_risk_per_categ, self.excess_capital = self.riskmodel.evaluate(underwritten_risks, self.cash) + # TODO: resolve insurance reinsurance inconsistency (insurer underwrite after capacity decisions, reinsurers before). + # This is currently so because it minimizes the number of times we need to run self.riskmodel.evaluate(). + # It would also be more consistent if excess capital would be updated at the end of the iteration. """handle adjusting capacity target and capacity""" max_var_by_categ = self.cash - self.excess_capital @@ -172,34 +179,32 @@ def iterate(self, time): # TODO: make independent of insurer/reinsurer, but change this to different deductible values """handle capital market interactions: capital history, dividends""" - self.cash_last_periods = [self.cash] + self.cash_last_periods[:3] + self.cash_last_periods = np.roll(self.cash_last_periods, 1) + self.cash_last_periods[0] = self.cash self.adjust_dividends(time, actual_capacity) self.pay_dividends(time) - """make underwriting decisions, category-wise""" - growth_limit = max(50, 2 * len(self.underwritten_contracts) + contracts_dissolved) - if sum(acceptable_by_category) > growth_limit: - acceptable_by_category = np.asarray(acceptable_by_category).astype(np.double) - acceptable_by_category = acceptable_by_category * growth_limit / sum(acceptable_by_category) - acceptable_by_category = np.int64(np.round(acceptable_by_category)) - - [risks_per_categ, number_risks_categ] = self.risks_reinrisks_organizer(new_risks) - for repetition in range(self.recursion_limit): - former_risks_per_categ = copy.copy(risks_per_categ) - [risks_per_categ, not_accepted_risks] = self.process_newrisks_insurer(risks_per_categ, number_risks_categ, acceptable_by_category, - var_per_risk_per_categ, cash_left_by_categ, time) #Here we process all the new risks in order to keep the portfolio as balanced as possible. - if former_risks_per_categ == risks_per_categ: - break - self.simulation.return_risks(not_accepted_risks) - - """not implemented - adjust liquidity, borrow or invest - pass""" - - self.market_permanency(time) - - self.roll_over(time) - + if self.warning is False: + """make underwriting decisions, category-wise""" + growth_limit = max(50, 2 * len(self.underwritten_contracts) + contracts_dissolved) + if sum(acceptable_by_category) > growth_limit: + acceptable_by_category = np.asarray(acceptable_by_category).astype(np.double) + acceptable_by_category = acceptable_by_category * growth_limit / sum(acceptable_by_category) + acceptable_by_category = np.int64(np.round(acceptable_by_category)) + + [risks_per_categ, number_risks_categ] = self.risks_reinrisks_organizer(new_risks) + for repetition in range(self.recursion_limit): + former_risks_per_categ = copy.copy(risks_per_categ) + [risks_per_categ, not_accepted_risks] = self.process_newrisks_insurer(risks_per_categ, number_risks_categ, acceptable_by_category, + var_per_risk_per_categ, cash_left_by_categ, time) #Here we process all the new risks in order to keep the portfolio as balanced as possible. + if former_risks_per_categ == risks_per_categ: + break + self.simulation.return_risks(not_accepted_risks) + + self.market_permanency(time) + + self.roll_over(time) + self.estimated_var() def enter_illiquidity(self, time): @@ -244,9 +249,14 @@ def market_exit(self, time): method self.dissolve().""" due = [item for item in self.obligations] for obligation in due: + if obligation["amount"] > self.cash: + print("Not enough money to market exit") self.pay(obligation) self.obligations = [] self.dissolve(time, 'record_market_exit') + for contract in self.underwritten_contracts: + contract.mature(time) + self.underwritten_contracts = [] def dissolve(self, time, record): """Dissolve Method. @@ -391,7 +401,8 @@ def estimated_var(self): for category in range(len(self.counter_category)): self.var_counter += self.counter_category[category] * self.riskmodel.inaccuracy[category] self.var_sum += self.var_category[category] - + self.var_sum_last_periods = np.roll(self.var_sum_last_periods, 1) + self.var_sum_last_periods[0] = self.var_sum if not sum(self.counter_category) == 0: self.var_counter_per_risk = self.var_counter / sum(self.counter_category) else: @@ -664,6 +675,8 @@ def roll_over(self, time): coverage that appears, if contracts are allowed to mature and are evaluated again the next iteration.""" maturing_next = [contract for contract in self.underwritten_contracts if contract.expiration == time + 1] + if self.operational is False and len(self.underwritten_contracts)>0: + print("rolling over non operational contracts") if self.is_insurer is True: for contract in maturing_next: @@ -695,13 +708,14 @@ def consider_buyout(self, type="insurer"): firms_further_considered = [] for firm in firms_to_consider: - # total_contract_value = sum([contract.value for contract in firm.underwritten_contracts]) - # firm_cost = total_contract_value all_firms_cash = self.simulation.get_total_firm_cash(type) - if self.excess_capital > self.riskmodel.margin_of_safety * firm.var_sum: - firm_likelihood = 0.25 + (1.5 * firm.cash + np.mean(firm.cash_last_periods[1:4]) + self.cash)/all_firms_cash + all_obligations = sum([obligation["amount"] for obligation in firm.obligations]) + total_premium = sum([np.mean(contract.payment_values) for contract in firm.underwritten_contracts if len(contract.payment_values) > 0]) + if self.excess_capital > self.riskmodel.margin_of_safety * firm.var_sum + all_obligations - total_premium: + firm_likelihood = 0.25 + (1.5 * firm.cash + np.mean(firm.cash_last_periods[1:12]) + self.cash)/all_firms_cash firm_likelihood = min(1, 2*firm_likelihood) - firms_further_considered.append([firm, firm_likelihood, firm.var_sum]) + firm_price = (firm.var_sum/10) + total_premium + firm.per_period_dividend + firms_further_considered.append([firm, firm_likelihood, firm_price]) if len(firms_further_considered) > 0: best_likelihood = 0 @@ -727,9 +741,9 @@ def buyout(self, firm, firm_cost, time): self.receive_obligation(firm_cost, self.simulation, time, 'buyout') if self.is_insurer and firm.is_insurer: - print("Insurer %i has bought %i" % (self.id, firm.id)) + print("Insurer %i has bought %i for %d with total cash %d" % (self.id, firm.id, firm_cost, self.cash)) elif self.is_reinsurer and firm.is_reinsurer: - print("Reinsurer %i has bought %i" % (self.id, firm.id)) + print("Reinsurer %i has bought %i for %d with total cash %d" % (self.id, firm.id, firm_cost, self.cash)) for contract in firm.underwritten_contracts: contract.insurer = self @@ -739,5 +753,26 @@ def buyout(self, firm, firm_cost, time): firm.obligations = [] firm.underwritten_contracts = [] - firm.dissolve(time, 'buyout') + firm.dissolve(time, 'record_firm_bought') + + def submit_regulator_report(self, time): + """Method to submit cash and var data to central banks regulate(). Sets a warning or triggers selling of firm if + not complying with regulation (holding enough capital for risk). + No accepted values. + No return values.""" + condition = self.simulation.bank.regulate(self.id, self.cash_last_periods, self.var_sum_last_periods) + if condition == "Good": + self.warning = False + if condition == "Warning": + self.warning = True + if condition == "LoseControl": + print("Firm %i has lost control" % self.id) + if isleconfig.buy_bankruptcies: + self.simulation.add_bankrupt_firm(self, time) + self.operational = False + else: + self.dissolve(time, "record_nonregulation_firm") + for contract in self.underwritten_contracts: + contract.mature(time) + self.underwritten_contracts = [] From 555f54b334a0ec51934bb15520f9fb31e9a2b62f Mon Sep 17 00:00:00 2001 From: KloskaT Date: Tue, 30 Jul 2019 13:04:32 +0100 Subject: [PATCH 077/125] Added method to central bank that grants disaster relief aid to firms if large enough damage. Called from simulation and added condition in isleconfig to enable. --- centralbank.py | 26 ++++++++++++++++++++++++++ insurancesimulation.py | 15 ++++++++++++--- isleconfig.py | 5 +++-- 3 files changed, 41 insertions(+), 5 deletions(-) diff --git a/centralbank.py b/centralbank.py index 944b9c7..52db834 100644 --- a/centralbank.py +++ b/centralbank.py @@ -16,6 +16,7 @@ def __init__(self, money_supply): self.prices_list = [] self.economy_money = money_supply self.warnings = {} + self.aid_budget = 1000000 def update_money_supply(self, amount, reduce=True): if reduce: @@ -106,3 +107,28 @@ def regulate(self, firm_id, firm_cash, firm_var): return "Warning" elif self.warnings[firm_id] >= 2: return "LoseControl" + + def adjust_aid_budget(self, time): + if time % 12 == 0: + money_left = self.aid_budget + self.aid_budget = 1000000 + money_taken = self.aid_budget - money_left + + def provide_aid(self, insurance_firms, damage_fraction, time): + all_firms_aid = 0 + given_aid_dict = {} + if damage_fraction > 0.50: + for insurer in insurance_firms: + claims = sum([ob['amount'] for ob in insurer.obligations if ob["purpose"] == "claim" and ob["due_time"] == time + 2]) + aid = claims * damage_fraction + all_firms_aid += aid + given_aid_dict[insurer] = aid + for fraction in [1, 0.9, 0.8, 0.7, 0.6, 0.5, 0.4, 0.3, 0.2, 0.1, 0]: + if self.aid_budget - (all_firms_aid * fraction) > 0: + self.aid_budget -= (all_firms_aid * fraction) + for key in given_aid_dict: + given_aid_dict[key] *= fraction + print("Damage %f causes %d to be given out in aid. %d budget left." % (damage_fraction, all_firms_aid * fraction, self.aid_budget)) + return given_aid_dict + else: + return given_aid_dict diff --git a/insurancesimulation.py b/insurancesimulation.py index 4a6a1f3..68c5c40 100644 --- a/insurancesimulation.py +++ b/insurancesimulation.py @@ -300,13 +300,22 @@ def iterate(self, t): print("Something wrong; past events not deleted", file=sys.stderr) if len(self.rc_event_schedule[categ_id]) > 0 and self.rc_event_schedule[categ_id][0] == t: self.rc_event_schedule[categ_id] = self.rc_event_schedule[categ_id][1:] - damage_extent = copy.copy(self.rc_event_damage[categ_id][0]) # Schedules of catastrophes and damages must be generated at the same time. - self.inflict_peril(categ_id=categ_id, damage=damage_extent, t=t) # TODO: consider splitting the following lines from this method and running it with nb.jit + damage_extent = copy.copy(self.rc_event_damage[categ_id][0]) + self.inflict_peril(categ_id=categ_id, damage=damage_extent, t=t) self.rc_event_damage[categ_id] = self.rc_event_damage[categ_id][1:] else: if isleconfig.verbose: print("Next peril ", self.rc_event_schedule[categ_id]) - + + # Provide government aid if damage severe enough + if isleconfig.aid_relief is True: + self.bank.adjust_aid_budget(time=t) + if 'damage_extent' in locals(): + op_firms = [firm for firm in self.insurancefirms if firm.operational is True] + aid_dict = self.bank.provide_aid(op_firms, damage_extent, time=t) + for key in aid_dict.keys(): + self.receive_obligation(amount=aid_dict[key], recipient=key, due_time=t, purpose="aid") + # Shuffle risks (insurance and reinsurance risks) self.shuffle_risks() diff --git a/isleconfig.py b/isleconfig.py index bb16206..0ba76ef 100644 --- a/isleconfig.py +++ b/isleconfig.py @@ -6,8 +6,9 @@ show_network = False # Should network be visualized? This should be False by default, to be overridden by commandline arguments save_network = False slim_log = False # Should logs be small in ensemble runs (only aggregated level data)? -buy_bankruptcies = True -enforce_regulations = True +buy_bankruptcies = False +enforce_regulations = False +aid_relief = False simulation_parameters = {"no_categories": 4, "no_insurancefirms": 20, From 6c389d67b387870301ac2b2da5e019be7654e8e4 Mon Sep 17 00:00:00 2001 From: KloskaT Date: Wed, 31 Jul 2019 18:02:30 +0100 Subject: [PATCH 078/125] Added file to load different data sets and compare them by plot or test --- comparison.py | 186 ++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 186 insertions(+) create mode 100644 comparison.py diff --git a/comparison.py b/comparison.py new file mode 100644 index 0000000..8e44bcb --- /dev/null +++ b/comparison.py @@ -0,0 +1,186 @@ +import numpy as np +import scipy.stats as ss +import matplotlib.pyplot as plt + + +def OpenAndPlot(): + # Old data with bought bankruptcies and regulator + with open("data/single_history_logs_old_2019_Jul_30_16_30.dat", "r") as rfile: + history_logs_list_regulator = [eval(k) for k in rfile] + + # New data with government aid + with open("data/single_history_logs_old_2019_Jul_31_11_37.dat", "r") as rfile: + history_logs_list_aid = [eval(k) for k in rfile] + + # Data with no conditions + with open("data/single_history_logs_old_2019_Jul_31_17_42.dat", "r") as rfile: + history_logs_list_no_change = [eval(k) for k in rfile] + + new_data = history_logs_list_aid[0] + old_data = history_logs_list_regulator[0] + no_change_data = history_logs_list_no_change[0] + + for key in new_data.keys(): + if "firms_cash" in key or key == "market_diffvar" or "event" in key or "riskmodels" in key: + pass + elif key == "individual_contracts" or key == "reinsurance_contracts": + time_range = min(len(old_data[key][0]), len(new_data[key][0])) + avg_contract_per_firm_new = [] + avg_contract_per_firm_old = [] + for t in range(time_range): + total_contracts = 0 + firm_counter = 0 + for i in range(len(new_data[key])): + if new_data[key][i][t] > 0: + total_contracts += new_data[key][i][t] + firm_counter += 1 + if firm_counter > 0: + avg_contract_per_firm_new.append(total_contracts/firm_counter) + else: + avg_contract_per_firm_new.append(0) + + total_contracts = 0 + firm_counter = 0 + for i in range(len(old_data[key])): + if old_data[key][i][t] > 0: + total_contracts += old_data[key][i][t] + firm_counter += 1 + if firm_counter > 0: + avg_contract_per_firm_old.append(total_contracts / firm_counter) + else: + avg_contract_per_firm_old.append(0) + plt.plot(range(time_range), avg_contract_per_firm_new, label="New contract data", color="orange") + plt.plot(range(time_range), avg_contract_per_firm_old, label="Old contract data", color="blue") + plt.axhline(np.mean(avg_contract_per_firm_new), linestyle="--", label="Avg New Contract Data", color="orange") + plt.axhline(np.mean(avg_contract_per_firm_old), linestyle="--", label="Avg Old Contract Data", color="blue") + plt.legend() + plt.suptitle(key) + plt.xlabel("Time") + plt.show() + else: + old_values = old_data[key] + mean_old_values = np.mean(old_values) + new_values = new_data[key] + mean_new_values = np.mean(new_values) + xvalues = np.arange(1000) + plt.plot(xvalues, old_values, label='Regulator used (Old)', color="blue") + plt.plot(xvalues, new_values, label='Aid used(New)', color="red") + if "cum" not in key: + plt.axhline(mean_new_values, linestyle='--', label="New Mean", color="red") + plt.axhline(mean_old_values, linestyle='--', label="Old Mean", color="blue") + plt.legend() + plt.xlabel("Time") + plt.ylabel(key) + # plt.show() + + +class CompareData: + def __init__(self, original_filename, new_filename, extra_filename=None): + """Initialises the CompareData class. Is provided with two or three filenames and unpacks them, also creating + dictionaries of the average values in case of ensemble/replication runs. + Accepts: + original_filename: Type String. + new_filename: Type String. + extra_filename: Type String. Defaults None but there in case of extra file to be compared.""" + with open(original_filename, "r") as rfile: + self.original_data = [eval(k) for k in rfile] + with open(new_filename, "r") as rfile: + self.new_data = [eval(k) for k in rfile] + if extra_filename is not None: + with open(extra_filename, "r") as rfile: + self.extra_data = [eval(k) for k in rfile] + self.extra = True + else: + self.extra = False + self.extra_data = {} + + self.original_averages = {} + self.new_averages = {} + self.extra_averages = {} + dicts = [self.original_averages, self.new_averages, self.extra_averages] + datas = [self.original_data, self.new_data, self.extra_data] + for i in range(len(datas)): + if self.extra is False and i == 2: + pass + else: + self.init_averages(dicts[i], datas[i]) + + def init_averages(self, avg_dict, data_dict): + """Method that initliases the average value dictionaries for the files. Takes a complete data dict and adds the + average values to a different dict provided. + Accepts: + avg_dict: Type Dict. Initially should be empty. + data_dict: Type List of data dict. Each element is a data dict containing data from that replication. + No return values.""" + for data in data_dict: + for key in data.keys(): + if "firms_cash" in key or key == "market_diffvar" or "event" in key or "riskmodels" in key: + pass + elif key == "individual_contracts" or key == "reinsurance_contracts": + avg_contract_per_firm = [] + for t in range(len(data[key][0])): + total_contracts = 0 + for i in range(len(data[key])): + if data[key][i][t] > 0: + total_contracts += data[key][i][t] + if "re" in key: + firm_count = data["total_reinoperational"][t] + else: + firm_count = data["total_operational"][t] + if firm_count > 0: + avg_contract_per_firm.append(total_contracts / firm_count) + else: + avg_contract_per_firm.append(0) + if key not in avg_dict.keys(): + avg_dict[key] = avg_contract_per_firm + else: + avg_dict[key] = [list1 + list2 for list1, list2 in zip(avg_dict[key], avg_contract_per_firm)] + else: + if key not in avg_dict.keys(): + avg_dict[key] = data[key] + else: + avg_dict[key] = [list1 + list2 for list1, list2 in zip(avg_dict[key], data[key])] + for key in avg_dict.keys(): + avg_dict[key] = [value/len(data_dict) for value in avg_dict[key]] + + def plot(self): + """Method to plot same type of data for different files on a plot. + No accepted values. + No return values.""" + for key in self.original_averages.keys(): + plt.figure() + original_values = self.original_averages[key] + mean_original_values = np.mean(original_values) + new_values = self.new_averages[key] + mean_new_values = np.mean(new_values) + xvalues = np.arange(1000) + plt.plot(xvalues[:500], original_values, label='Original Values', color="blue") + plt.plot(xvalues, new_values, label='New Values', color="red") + if self.extra: + extra_values = self.extra_averages[key] + mean_extra_values = np.mean(extra_values) + plt.plot(xvalues, extra_values, label="Extra Values", color="yellow") + if "cum" not in key: + plt.axhline(mean_new_values, linestyle='--', label="New Mean", color="red") + plt.axhline(mean_original_values, linestyle='--', label="Original Mean", color="blue") + if self.extra: + plt.axhline(mean_extra_values, linestyle='--', label='Extra Mean', color='yellow') + plt.legend() + plt.xlabel("Time") + plt.ylabel(key) + plt.show() + + def test(self): + """Method to perform ks test on two sets of file data. Returns the D statistic and p-value. + No accepted values. + No return values.""" + for key in self.original_averages.keys(): + original_values = self.original_averages[key] + new_values = self.new_averages[key][:500] + D, p = ss.ks_2samp(original_values, new_values) + print("%s has p value: %f and D: %f" % (key, p, D)) + + +CD = CompareData("data/single_history_logs.dat", "data/single_history_logs_old_2019_Jul_30_16_30.dat", "data/single_history_logs_old_2019_Jul_31_11_37.dat") +CD.test() +CD.plot() From d1756a21a676b3c2c3e2bab517b1f953e4560236 Mon Sep 17 00:00:00 2001 From: KloskaT Date: Thu, 1 Aug 2019 11:18:36 +0100 Subject: [PATCH 079/125] Removed redundant method --- comparison.py | 71 --------------------------------------------------- 1 file changed, 71 deletions(-) diff --git a/comparison.py b/comparison.py index 8e44bcb..bbc0c43 100644 --- a/comparison.py +++ b/comparison.py @@ -3,77 +3,6 @@ import matplotlib.pyplot as plt -def OpenAndPlot(): - # Old data with bought bankruptcies and regulator - with open("data/single_history_logs_old_2019_Jul_30_16_30.dat", "r") as rfile: - history_logs_list_regulator = [eval(k) for k in rfile] - - # New data with government aid - with open("data/single_history_logs_old_2019_Jul_31_11_37.dat", "r") as rfile: - history_logs_list_aid = [eval(k) for k in rfile] - - # Data with no conditions - with open("data/single_history_logs_old_2019_Jul_31_17_42.dat", "r") as rfile: - history_logs_list_no_change = [eval(k) for k in rfile] - - new_data = history_logs_list_aid[0] - old_data = history_logs_list_regulator[0] - no_change_data = history_logs_list_no_change[0] - - for key in new_data.keys(): - if "firms_cash" in key or key == "market_diffvar" or "event" in key or "riskmodels" in key: - pass - elif key == "individual_contracts" or key == "reinsurance_contracts": - time_range = min(len(old_data[key][0]), len(new_data[key][0])) - avg_contract_per_firm_new = [] - avg_contract_per_firm_old = [] - for t in range(time_range): - total_contracts = 0 - firm_counter = 0 - for i in range(len(new_data[key])): - if new_data[key][i][t] > 0: - total_contracts += new_data[key][i][t] - firm_counter += 1 - if firm_counter > 0: - avg_contract_per_firm_new.append(total_contracts/firm_counter) - else: - avg_contract_per_firm_new.append(0) - - total_contracts = 0 - firm_counter = 0 - for i in range(len(old_data[key])): - if old_data[key][i][t] > 0: - total_contracts += old_data[key][i][t] - firm_counter += 1 - if firm_counter > 0: - avg_contract_per_firm_old.append(total_contracts / firm_counter) - else: - avg_contract_per_firm_old.append(0) - plt.plot(range(time_range), avg_contract_per_firm_new, label="New contract data", color="orange") - plt.plot(range(time_range), avg_contract_per_firm_old, label="Old contract data", color="blue") - plt.axhline(np.mean(avg_contract_per_firm_new), linestyle="--", label="Avg New Contract Data", color="orange") - plt.axhline(np.mean(avg_contract_per_firm_old), linestyle="--", label="Avg Old Contract Data", color="blue") - plt.legend() - plt.suptitle(key) - plt.xlabel("Time") - plt.show() - else: - old_values = old_data[key] - mean_old_values = np.mean(old_values) - new_values = new_data[key] - mean_new_values = np.mean(new_values) - xvalues = np.arange(1000) - plt.plot(xvalues, old_values, label='Regulator used (Old)', color="blue") - plt.plot(xvalues, new_values, label='Aid used(New)', color="red") - if "cum" not in key: - plt.axhline(mean_new_values, linestyle='--', label="New Mean", color="red") - plt.axhline(mean_old_values, linestyle='--', label="Old Mean", color="blue") - plt.legend() - plt.xlabel("Time") - plt.ylabel(key) - # plt.show() - - class CompareData: def __init__(self, original_filename, new_filename, extra_filename=None): """Initialises the CompareData class. Is provided with two or three filenames and unpacks them, also creating From 05739aa0905f5ead209754cf8574dbfc67590c6b Mon Sep 17 00:00:00 2001 From: KloskaT Date: Thu, 1 Aug 2019 18:33:32 +0100 Subject: [PATCH 080/125] Regulator changed such that it includes reinsurance contract values and allows a 24 month grace period for new firms. Aid budget is now simulation parameter. Changed method of selling bankrupt firms in simulation, to just selling firms (helps logging). Now properly records and saves cumulative values in logger/simulation/ensemble(e.g. bankruptcies, bought firms). Network data should now properly save for ensemble runs. --- centralbank.py | 72 ++++++++++++++++++++------ ensemble.py | 66 ++++++++++++++---------- insurancesimulation.py | 114 +++++++++++++++++++++-------------------- isleconfig.py | 7 +-- logger.py | 22 ++++++-- metainsuranceorg.py | 87 +++++++++++++++++++------------ 6 files changed, 228 insertions(+), 140 deletions(-) diff --git a/centralbank.py b/centralbank.py index 52db834..45bd21e 100644 --- a/centralbank.py +++ b/centralbank.py @@ -16,9 +16,14 @@ def __init__(self, money_supply): self.prices_list = [] self.economy_money = money_supply self.warnings = {} - self.aid_budget = 1000000 + self.aid_budget = self.aid_budget_reset = simulation_parameters['aid_budget'] def update_money_supply(self, amount, reduce=True): + """Method to update the current supply of money in the insurance simulation economy. Only used to monitor + supply, all handling of money (e.g obligations) is done by simulation. + Accepts: + amount: Type Integer. + reduce: Type Boolean.""" if reduce: self.economy_money -= amount else: @@ -73,33 +78,53 @@ def calculate_inflation(self, current_price, time): self.twelvemonth_CPI = (current_price - self.prices_list[-13])/self.prices_list[-13] self.actual_inflation = self.twelvemonth_CPI - def regulate(self, firm_id, firm_cash, firm_var): - """Method to regulate firms. Checks if their average cash over the last year can cover their average VaR and to - what percent. Based on solvency 2 which has a MCR of 85% and SCR of 99.5%. + def regulate(self, firm_id, firm_cash, firm_var, reinsurance, age): + """Method to regulate firms Accepts: firm_id: Type Integer. Firms unique ID. firm_cash: Type list of decimals. List of cash for last twelve periods. firm_var: Type list of decimals. List of VaR for last twelve periods. + reinsurance: Type List of Lists of Lists. Contains deductible and excess values for each reinsurance + contract in each category for each iteration. + age: Type Integer. Returns: Type String: "Good", "Warning", "LoseControl". - This method takes a firms average cash and VaR. If average cash above SCR of 99.5% of VaR then all is well, - if cash is between 85% and 99.5% then is issued a warning (limits business heavily), if under 85% then firm is - sold.""" + This method calculates how much each reinsurance contract would pay out if all VaR in respective category was + claimed, adds to cash for that iteration and calculated fraction of capital to total VaR. If average fraction + over all iterations is above SCR (from solvency ii) of 99.5% of VaR then all is well, if cash is between 85% and + 99.5% then is issued a warning (limits business heavily), if under 85% then firm is sold. Each firm is given + initial 24 iteration period that it cannot lose control otherwise all firm immediately bankrupt.""" if firm_id not in self.warnings.keys(): self.warnings[firm_id] = 0 - avg_firm_cash = np.mean(firm_cash) - avg_var = np.mean(firm_var) + # Calculates reinsurance that covers VaR for each category in each iteration and adds to cash. + cash_fractions = [] + for iter in range(len(reinsurance)): + reinsurance_capital = 0 + for categ in range(len(reinsurance[iter])): + if firm_var[iter][categ] >= reinsurance[iter][categ][0]: # Check VaR greater than deductible + if firm_var[iter][categ] >= reinsurance[iter][categ][1]: # Check VaR greater than excess + reinsurance_capital += (reinsurance[iter][categ][1] - reinsurance[iter][categ][0]) + else: + reinsurance_capital += (firm_var[iter][categ] - reinsurance[iter][categ][0]) + else: + reinsurance_capital += 0 # If below deductible no reinsurance + if sum(firm_var[iter]) > 0: + cash_fractions.append((firm_cash[iter]+reinsurance_capital)/sum(firm_var[iter])) + else: + cash_fractions.append(1) + + avg_var_coverage = np.mean(cash_fractions) - if avg_firm_cash >= 0.995 * avg_var: + if avg_var_coverage >= 0.995: self.warnings[firm_id] = 0 - elif avg_firm_cash >= 0.85 * avg_var: + elif avg_var_coverage >= 0.85: self.warnings[firm_id] += 1 - elif avg_firm_cash < 0.85* avg_var: - if self.warnings[firm_id] > 0: - self.warnings[firm_id] = 2 - else: + elif avg_var_coverage < 0.85: + if age < 24: self.warnings[firm_id] += 1 + else: + self.warnings[firm_id] = 2 if self.warnings[firm_id] == 0: return "Good" @@ -109,12 +134,26 @@ def regulate(self, firm_id, firm_cash, firm_var): return "LoseControl" def adjust_aid_budget(self, time): + """Method to reset the aid budget every 12 iterations (i.e. a year) + Accepts: + time: type Integer. + No return values.""" if time % 12 == 0: money_left = self.aid_budget - self.aid_budget = 1000000 + self.aid_budget = self.aid_budget_reset money_taken = self.aid_budget - money_left def provide_aid(self, insurance_firms, damage_fraction, time): + """Method to provide aid to firms if enough damage. + Accepts: + insurance_firms: Type List of Classes. + damage_fraction: Type Decimal. + time: Type Integer. + Returns: + given_aid_dict: Type DataDict. Each key is an insurance firm with the value as the aid provided. + If damage is above a given threshold then firms are given a percentage of total claims as aid (as cannot provide + actual policyholders with cash) based on damage fraction and how much budget is left. Each firm given equal + proportion. Returns data dict of values so simulation instance can pay.""" all_firms_aid = 0 given_aid_dict = {} if damage_fraction > 0.50: @@ -123,6 +162,7 @@ def provide_aid(self, insurance_firms, damage_fraction, time): aid = claims * damage_fraction all_firms_aid += aid given_aid_dict[insurer] = aid + # Give each firm an equal fraction of claims for fraction in [1, 0.9, 0.8, 0.7, 0.6, 0.5, 0.4, 0.3, 0.2, 0.1, 0]: if self.aid_budget - (all_firms_aid * fraction) > 0: self.aid_budget -= (all_firms_aid * fraction) diff --git a/ensemble.py b/ensemble.py index 2974a43..8c5d269 100644 --- a/ensemble.py +++ b/ensemble.py @@ -50,41 +50,51 @@ def rake(hostname): """Configure the return values and corresponding file suffixes where they should be saved""" requested_logs = {'total_cash': '_cash.dat', - 'total_excess_capital': '_excess_capital.dat', - 'total_profitslosses': '_profitslosses.dat', - 'total_contracts': '_contracts.dat', - 'total_operational': '_operational.dat', - 'total_reincash': '_reincash.dat', - 'total_reinexcess_capital': '_reinexcess_capital.dat', - 'total_reinprofitslosses': '_reinprofitslosses.dat', - 'total_reincontracts': '_reincontracts.dat', - 'total_reinoperational': '_reinoperational.dat', - 'total_catbondsoperational': '_total_catbondsoperational.dat', - 'market_premium': '_premium.dat', - 'market_reinpremium': '_reinpremium.dat', - 'cumulative_bankruptcies': '_cumulative_bankruptcies.dat', - 'cumulative_market_exits': '_cumulative_market_exits', # TODO: correct filename - 'cumulative_unrecovered_claims': '_cumulative_unrecovered_claims.dat', - 'cumulative_claims': '_cumulative_claims.dat', - 'insurance_firms_cash': '_insurance_firms_cash.dat', - 'reinsurance_firms_cash': '_reinsurance_firms_cash.dat', - 'market_diffvar': '_market_diffvar.dat', - 'rc_event_schedule_initial': '_rc_event_schedule.dat', - 'rc_event_damage_initial': '_rc_event_damage.dat', - 'number_riskmodels': '_number_riskmodels.dat' - 'individual_contracts' '_insurance_contracts.dat' - 'reinsurance_contracts' '_reinsurance_contracts.dat' - } + 'total_excess_capital': '_excess_capital.dat', + 'total_profitslosses': '_profitslosses.dat', + 'total_contracts': '_contracts.dat', + 'total_operational': '_operational.dat', + 'total_reincash': '_reincash.dat', + 'total_reinexcess_capital': '_reinexcess_capital.dat', + 'total_reinprofitslosses': '_reinprofitslosses.dat', + 'total_reincontracts': '_reincontracts.dat', + 'total_reinoperational': '_reinoperational.dat', + 'total_catbondsoperational': '_total_catbondsoperational.dat', + 'market_premium': '_premium.dat', + 'market_reinpremium': '_reinpremium.dat', + 'cumulative_bankruptcies': '_cumulative_bankruptcies.dat', + 'cumulative_market_exits': '_cumulative_market_exits', # TODO: correct filename + 'cumulative_unrecovered_claims': '_cumulative_unrecovered_claims.dat', + 'cumulative_claims': '_cumulative_claims.dat', + 'cumulative_bought_firms': '_cumulative_bought_firms.dat', + 'cumulative_nonregulation_firms':'_cumulative_nonregulation_firms.dat', + 'insurance_firms_cash': '_insurance_firms_cash.dat', + 'reinsurance_firms_cash': '_reinsurance_firms_cash.dat', + 'market_diffvar': '_market_diffvar.dat', + 'rc_event_schedule_initial': '_rc_event_schedule.dat', + 'rc_event_damage_initial': '_rc_event_damage.dat', + 'number_riskmodels': '_number_riskmodels.dat', + 'individual_contracts': '_insurance_contracts.dat', + 'reinsurance_contracts': '_reinsurance_contracts.dat', + 'unweighted_network_data': '_unweighted_network_data.dat', + 'network_node_labels': '_network_node_labels.dat', + 'network_edge_labels': '_network_edge_labels.dat', + 'number_of_agents': '_number_of_agents'} if isleconfig.slim_log: - for name in ['insurance_firms_cash', 'reinsurance_firms_cash', 'individual_contracts', 'reinsurance_contracts']: + for name in ['insurance_firms_cash', 'reinsurance_firms_cash', 'individual_contracts', 'reinsurance_contracts' + 'unweighted_network_data', 'network_node_labels', 'network_edge_labels', 'number_of_agents']: del requested_logs[name] - + + if not isleconfig.save_network: + for name in ['unweighted_network_data', 'network_node_labels', 'network_edge_labels', 'number_of_agents']: + del requested_logs[name] + assert "number_riskmodels" in requested_logs """Configure log directory and ensure that the directory exists""" - dir_prefix = "/data/" + dir_prefix = "/new_data/" directory = os.getcwd() + dir_prefix try: # Here it is checked whether the directory to collect the results exists or not. If not it is created. os.stat(directory) diff --git a/insurancesimulation.py b/insurancesimulation.py index 68c5c40..ebf2307 100644 --- a/insurancesimulation.py +++ b/insurancesimulation.py @@ -140,12 +140,14 @@ def __init__(self, override_no_riskmodels, replic_ID, simulation_parameters, rc_ "Cumulative variables for history and logging" self.cumulative_bankruptcies = 0 self.cumulative_market_exits = 0 - self.bought_firms = 0 + self.cumulative_bought_firms = 0 self.cumulative_nonregulation_firms = 0 self.cumulative_unrecovered_claims = 0.0 self.cumulative_claims = 0.0 - self.current_bankruptcies = [] - self.current_reinsurer_bankruptcies = [] + + "Lists for firms that are to be sold." + self.selling_insurance_firms = [] + self.selling_reinsurance_firms = [] "Lists for logging history" self.logger = logger.Logger(no_riskmodels=simulation_parameters["no_riskmodels"], @@ -350,7 +352,7 @@ def iterate(self, t): agent.consider_buyout(type="insurer") # Reset list of bankrupt insurance firms - self.reset_bankrupt_firms() + self.reset_selling_firms() # Iterate catbonds for agent in self.catbonds: @@ -427,11 +429,12 @@ def save_data(self): current_log['cumulative_market_exits'] = self.cumulative_market_exits current_log['cumulative_unrecovered_claims'] = self.cumulative_unrecovered_claims current_log['cumulative_claims'] = self.cumulative_claims + current_log["cumulative_bought_firms"] = self.cumulative_bought_firms + current_log["cumulative_nonregulation_firms"] = self.cumulative_nonregulation_firms """ add agent-level data to dict""" current_log['insurance_firms_cash'] = insurance_firms current_log['reinsurance_firms_cash'] = reinsurance_firms - current_log['market_diffvar'] = self.compute_market_diffvar() current_log['individual_contracts'] = [] individual_contracts_no = [len(insurancefirm.underwritten_contracts) for insurancefirm in self.insurancefirms] @@ -845,9 +848,15 @@ def record_market_exit(self): self.cumulative_market_exits += 1 def record_bought_firm(self): - self.bought_firms += 1 + """Method to record number of firms bought when buy_bankruptcies is enabled. + No accepted values. + No return values.""" + self.cumulative_bought_firms += 1 def record_nonregulation_firm(self): + """Method to record number of firms shut down by regulator (when enabled). + No accepted values. + No return values.""" self.cumulative_nonregulation_firms += 1 def record_unrecovered_claims(self, loss): @@ -957,38 +966,6 @@ def reset_pls(self): for catbond in self.catbonds: catbond.reset_pl() - def add_bankrupt_firm(self, firm, time): - """Method to add bankrupt firm to list of those being considered to buy dependant on firm type. - Accepts: - firm: Type Class. - time: Type Integer. - No return values.""" - if firm.is_insurer: - self.current_bankruptcies.append([firm, time]) - elif firm.is_reinsurer: - self.current_reinsurer_bankruptcies.append([firm, time]) - - def get_bankrupt_firms(self, type): - """Method to get list of bankrupt firms up for selling based on type. - Accepts: - type: Type String. - Returns: - bankruptcies_sent: List of Classes. - bankruptcy_time: Type Integer.""" - if type == "insurer": - bankruptcies_sent = [firm for firm, time in self.current_bankruptcies] - bankruptcy_time = 0 - if len(self.current_bankruptcies) > 0: - bankruptcy_time = self.current_bankruptcies[0][1] - elif type == "reinsurer": - bankruptcies_sent = [firm for firm, time in self.current_reinsurer_bankruptcies] - bankruptcy_time = 0 - if len(self.current_reinsurer_bankruptcies) > 0: - bankruptcy_time = self.current_reinsurer_bankruptcies[0][1] - else: - print("No accepted type for bankruptcies") - return bankruptcies_sent, bankruptcy_time - def get_total_firm_cash(self, type): """Method to get sum of all cash of firms of a given type. Called from consider_buyout() but could be used for setting market premium. @@ -1004,36 +981,63 @@ def get_total_firm_cash(self, type): print("No accepted type for cash") return sum_capital - def remove_bankrupt_firm(self, firm, time): - """Method to remove firm from list of bankrupt firms. Called when firm is bought buy another. + def add_firm_to_be_sold(self, firm, time, reason): + """Method to add firm to list of those being considered to buy dependant on firm type. Accepts: firm: Type Class. time: Type Integer. + reason: Type String. Used in case of dissolution for logging. No return values.""" if firm.is_insurer: - self.current_bankruptcies.remove([firm, time]) + self.selling_insurance_firms.append([firm, time, reason]) elif firm.is_reinsurer: - self.current_reinsurer_bankruptcies.remove([firm, time]) - - def reset_bankrupt_firms(self): - """Method to reset list of bankrupt firms being sold. Called every iteration of insurance simulation. - No accepted values. - No return values. - Firms going bankrupt only considered for iteration they go bankrupt, after this not wanted so all are dissolved - and relevant list attribute is reset.""" - for firm, time in self.current_bankruptcies: - firm.dissolve(time, 'record_bankruptcy') + self.selling_reinsurance_firms.append([firm, time, reason]) + + def get_firms_to_sell(self, type): + """Method to get list of firms that are up for selling based on type. + Accepts: + type: Type String. + Returns: + firms_info_sent: Type List of Lists. Contains firm, type and reason.""" + if type == "insurer": + firms_info_sent = [(firm, time, reason) for firm, time, reason in self.selling_insurance_firms] + elif type == "reinsurer": + firms_info_sent = [(firm, time, reason) for firm, time, reason in self.selling_reinsurance_firms] + else: + print("No accepted type for selling") + return firms_info_sent + + def remove_sold_firm(self, firm, time, reason): + """Method to remove firm from list of firms being sold. Called when firm is bought buy another. + Accepts: + firm: Type Class. + time: Type Integer. + reason: Type String. + No return values.""" + if firm.is_insurer: + self.selling_insurance_firms.remove([firm, time, reason]) + elif firm.is_reinsurer: + self.selling_reinsurance_firms.remove([firm, time, reason]) + + def reset_selling_firms(self): + """Method to reset list of firms being offered to sell. Called every iteration of insurance simulation. + No accepted values. + No return values. + Firms being sold only considered for iteration they are added for given reason, after this not wanted so all + are dissolved and relevant list attribute is reset.""" + for firm, time, reason in self.selling_insurance_firms: + firm.dissolve(time, reason) for contract in firm.underwritten_contracts: contract.mature(time) firm.underwritten_contracts = [] - self.current_bankruptcies = [] + self.selling_insurance_firms = [] - for reinfirm, time in self.current_reinsurer_bankruptcies: - reinfirm.dissolve(time, 'record_bankruptcy') + for reinfirm, time, reason in self.selling_reinsurance_firms: + reinfirm.dissolve(time, reason) for contract in reinfirm.underwritten_contracts: contract.mature(time) reinfirm.underwritten_contracts = [] - self.current_reinsurer_bankruptcies = [] + self.selling_reinsurance_firms = [] def update_network_data(self): """Method to update the network data. diff --git a/isleconfig.py b/isleconfig.py index 0ba76ef..8a570ea 100644 --- a/isleconfig.py +++ b/isleconfig.py @@ -7,7 +7,7 @@ save_network = False slim_log = False # Should logs be small in ensemble runs (only aggregated level data)? buy_bankruptcies = False -enforce_regulations = False +enforce_regulations = True aid_relief = False simulation_parameters = {"no_categories": 4, @@ -40,7 +40,7 @@ "default_non-proportional_reinsurance_excess": 1.0, "default_non-proportional_reinsurance_premium_share": 0.3, "static_non-proportional_reinsurance_levels": False, - "catbonds_off": False, + "catbonds_off": True, "reinsurance_off": False, "capacity_target_decrement_threshold": 1.8, "capacity_target_increment_threshold": 1.2, @@ -77,5 +77,6 @@ "reinsurance_limit": 0.1, "upper_price_limit": 1.2, "lower_price_limit": 0.85, - "no_risks": 20000} + "no_risks": 20000, + "aid_budget": 1000000} diff --git a/logger.py b/logger.py index 69d3ad2..469b547 100644 --- a/logger.py +++ b/logger.py @@ -3,6 +3,8 @@ import numpy as np import pdb import listify +import os +import time LOG_DEFAULT = ( 'total_cash total_excess_capital total_profitslosses total_contracts ' @@ -11,7 +13,8 @@ 'market_reinpremium cumulative_bankruptcies cumulative_market_exits cumulative_unrecovered_claims ' 'cumulative_claims insurance_firms_cash reinsurance_firms_cash market_diffvar ' 'rc_event_schedule_initial rc_event_damage_initial number_riskmodels individual_contracts reinsurance_contracts ' - 'unweighted_network_data network_node_labels network_edge_labels number_of_agents' + 'unweighted_network_data network_node_labels network_edge_labels number_of_agents ' + 'cumulative_bought_firms cumulative_nonregulation_firms' ).split(' ') class Logger(): @@ -34,6 +37,7 @@ def __init__(self, no_riskmodels=None, rc_event_schedule_initial=None, rc_event_ """Prepare history log dict""" self.history_logs = {} + self.history_logs_to_save = [] """Variables pertaining to insurance sector""" # TODO: should we not have `cumulative_bankruptcies` and @@ -42,7 +46,8 @@ def __init__(self, no_riskmodels=None, rc_event_schedule_initial=None, rc_event_ # by the whole insurance sector until a certain time. insurance_sector = ('total_cash total_excess_capital total_profitslosses ' 'total_contracts total_operational cumulative_bankruptcies ' - 'cumulative_market_exits cumulative_claims cumulative_unrecovered_claims').split(' ') + 'cumulative_market_exits cumulative_claims cumulative_unrecovered_claims ' + 'cumulative_bought_firms cumulative_nonregulation_firms').split(' ') for _v in insurance_sector: self.history_logs[_v] = [] @@ -141,7 +146,7 @@ def restore_logger_object(self, log): self.number_riskmodels = log["number_riskmodels"] """Restore history log""" - self.history_logs = log + self.history_logs_to_save.append(log) def save_log(self, background_run): """Method to save log to disk of local machine. Distinguishes single and ensemble runs. @@ -182,10 +187,19 @@ def single_log_prepare(self): Element 2: data structure to save Element 3: operation parameter (w-write or a-append).""" to_log = [] - to_log.append(("data/history_logs.dat", self.history_logs, "w")) + filename = "data/single_history_logs.dat" + backupfilename = "data/single_history_logs_old_" + time.strftime('%Y_%b_%d_%H_%M') + ".dat" + if os.path.exists(filename): + os.rename(filename, backupfilename) + for data in self.history_logs_to_save: + to_log.append((filename, data, "a")) return to_log def save_network_data(self, ensemble): + """Method to save network data to its own file. + Accepts: + ensemble: Type Boolean. Saves to files based on number risk models. + No return values.""" if ensemble is True: filename_prefix = {1: "one", 2: "two", 3: "three", 4: "four"} fpf = filename_prefix[self.number_riskmodels] diff --git a/metainsuranceorg.py b/metainsuranceorg.py index b46c8d1..500d959 100644 --- a/metainsuranceorg.py +++ b/metainsuranceorg.py @@ -89,6 +89,7 @@ def init(self, simulation_parameters, agent_parameters): #self.reinsurance_contracts = [] self.operational = True self.warning = False + self.age = 0 self.is_insurer = True self.is_reinsurer = False @@ -96,7 +97,8 @@ def init(self, simulation_parameters, agent_parameters): self.var_counter = 0 # sum over risk model inaccuracies for all contracts self.var_counter_per_risk = 0 # average risk model inaccuracy across contracts self.var_sum = 0 # sum over initial VaR for all contracts - self.var_sum_last_periods = list(np.zeros(12, dtype=int)) + self.var_sum_last_periods = list(np.zeros((12, 4), dtype=int)) + self.reinsurance_history = list(np.zeros((12, 4, 2), dtype=int)) self.counter_category = np.zeros(self.simulation_no_risk_categories) # var_counter disaggregated by category self.var_category = np.zeros(self.simulation_no_risk_categories) # var_sum disaggregated by category self.naccep = [] @@ -117,6 +119,7 @@ def iterate(self, time): firms receive new risks to evaluate, pay dividends, adjust capacity.""" """Obtain interest generated by cash""" self.simulation.bank.award_interest(self, self.cash) + self.age += 1 """realize due payments""" self.effect_payments(time) @@ -227,10 +230,10 @@ def enter_bankruptcy(self, time): dissolves the firm through the method self.dissolve().""" if isleconfig.buy_bankruptcies: if self.is_insurer and self.operational: - self.simulation.add_bankrupt_firm(self, time) + self.simulation.add_firm_to_be_sold(self, time, "record_bankruptcy") self.operational = False elif self.is_reinsurer and self.operational: - self.simulation.add_bankrupt_firm(self, time) + self.simulation.add_firm_to_be_sold(self, time, "record_bankruptcy") self.operational = False else: self.dissolve(time, 'record_bankruptcy') @@ -279,9 +282,9 @@ def dissolve(self, time, record): self.pay(obligation) # This MUST be the last obligation before the dissolution of the firm. self.excess_capital = 0 # Excess of capital is 0 after bankruptcy or market exit. self.profits_losses = 0 # Profits and losses are 0 after bankruptcy or market exit. - if self.operational: - method_to_call = getattr(self.simulation, record) - method_to_call() + method_to_call = getattr(self.simulation, record) + method_to_call() + for category_reinsurance in self.category_reinsurance: if category_reinsurance is not None: category_reinsurance.dissolve(time) @@ -384,29 +387,43 @@ def estimated_var(self): No Accepted arguments. No return values Calculates value at risk per category and overall, based on underwritten contracts initial value at risk. - Assigns it to agent instance. Called at the end of each agents iteration cycle.""" + Assigns it to agent instance. Called at the end of each agents iteration cycle. Also records the VaR and + reinsurance contract info for the last 12 iterations, used for regulation.""" self.counter_category = np.zeros(self.simulation_no_risk_categories) self.var_category = np.zeros(self.simulation_no_risk_categories) - self.var_counter = 0 self.var_counter_per_risk = 0 self.var_sum = 0 - - if self.operational: - - for contract in self.underwritten_contracts: - self.counter_category[contract.category] += 1 - self.var_category[contract.category] += contract.initial_VaR - - for category in range(len(self.counter_category)): - self.var_counter += self.counter_category[category] * self.riskmodel.inaccuracy[category] - self.var_sum += self.var_category[category] - self.var_sum_last_periods = np.roll(self.var_sum_last_periods, 1) - self.var_sum_last_periods[0] = self.var_sum - if not sum(self.counter_category) == 0: - self.var_counter_per_risk = self.var_counter / sum(self.counter_category) + current_reinsurance_info = [] + + # Extract initial VaR per category + for contract in self.underwritten_contracts: + self.counter_category[contract.category] += 1 + self.var_category[contract.category] += contract.initial_VaR + + # Caclulate risks per category and sum of all VaR + for category in range(len(self.counter_category)): + self.var_counter += self.counter_category[category] * self.riskmodel.inaccuracy[category] + self.var_sum += self.var_category[category] + + # Record reinsurance info + for reinsurance in self.category_reinsurance: + if reinsurance is not None: + current_reinsurance_info.append([reinsurance.deductible, reinsurance.excess]) else: - self.var_counter_per_risk = 0 + current_reinsurance_info.append([0, 0]) + + # Rotate lists and replace for up-to-date list for 12 iterations + self.var_sum_last_periods = np.roll(self.var_sum_last_periods, 4) + self.var_sum_last_periods[0] = self.var_category + self.reinsurance_history = np.roll(self.reinsurance_history, 8) + self.reinsurance_history[0] = current_reinsurance_info + + # Calculate average no. risks per category + if not sum(self.counter_category) == 0: + self.var_counter_per_risk = self.var_counter / sum(self.counter_category) + else: + self.var_counter_per_risk = 0 def get_newrisks_by_type(self): """Method for soliciting new risks from insurance simulation then organising them based if non-proportional @@ -704,10 +721,10 @@ def consider_buyout(self, type="insurer"): This method is called for both types of firms to consider buying one firm going bankrupt for only this iteration It has a chance (based on market share) to buyout other firm if its excess capital is large enough to cover the other firms value at risk multiplied by its margin of safety. Will call buyout() if necessary.""" - firms_to_consider, time = self.simulation.get_bankrupt_firms(type) + firms_to_consider = self.simulation.get_firms_to_sell(type) firms_further_considered = [] - for firm in firms_to_consider: + for firm, time, reason in firms_to_consider: all_firms_cash = self.simulation.get_total_firm_cash(type) all_obligations = sum([obligation["amount"] for obligation in firm.obligations]) total_premium = sum([np.mean(contract.payment_values) for contract in firm.underwritten_contracts if len(contract.payment_values) > 0]) @@ -715,7 +732,8 @@ def consider_buyout(self, type="insurer"): firm_likelihood = 0.25 + (1.5 * firm.cash + np.mean(firm.cash_last_periods[1:12]) + self.cash)/all_firms_cash firm_likelihood = min(1, 2*firm_likelihood) firm_price = (firm.var_sum/10) + total_premium + firm.per_period_dividend - firms_further_considered.append([firm, firm_likelihood, firm_price]) + firm_sell_reason = reason + firms_further_considered.append([firm, firm_likelihood, firm_price, firm_sell_reason]) if len(firms_further_considered) > 0: best_likelihood = 0 @@ -724,10 +742,11 @@ def consider_buyout(self, type="insurer"): best_likelihood = firm_data[1] best_firm = firm_data[0] best_firm_cost = firm_data[2] - random_chance = np.random.uniform(0,1) + best_firm_sell_reason = firm_data[3] + random_chance = np.random.uniform(0, 1) if best_likelihood > random_chance: self.buyout(best_firm, best_firm_cost, time) - self.simulation.remove_bankrupt_firm(best_firm, time) + self.simulation.remove_sold_firm(best_firm, time, best_firm_sell_reason) def buyout(self, firm, firm_cost, time): """Method called to actually buyout firm. @@ -753,22 +772,22 @@ def buyout(self, firm, firm_cost, time): firm.obligations = [] firm.underwritten_contracts = [] - firm.dissolve(time, 'record_firm_bought') + firm.dissolve(time, 'record_bought_firm') def submit_regulator_report(self, time): - """Method to submit cash and var data to central banks regulate(). Sets a warning or triggers selling of firm if - not complying with regulation (holding enough capital for risk). + """Method to submit cash, VaR, and reinsurance data to central banks regulate(). Sets a warning or triggers + selling of firm if not complying with regulation (holding enough effective capital for risk). No accepted values. No return values.""" - condition = self.simulation.bank.regulate(self.id, self.cash_last_periods, self.var_sum_last_periods) + condition = self.simulation.bank.regulate(self.id, self.cash_last_periods, self.var_sum_last_periods, + self.reinsurance_history, self.age) if condition == "Good": self.warning = False if condition == "Warning": self.warning = True if condition == "LoseControl": - print("Firm %i has lost control" % self.id) if isleconfig.buy_bankruptcies: - self.simulation.add_bankrupt_firm(self, time) + self.simulation.add_firm_to_be_sold(self, time, "record_nonregulation_firm") self.operational = False else: self.dissolve(time, "record_nonregulation_firm") From 44050e24df87458ccf5e0c6aafd86a87630c60da Mon Sep 17 00:00:00 2001 From: Chris Hughes Date: Fri, 2 Aug 2019 12:20:44 +0100 Subject: [PATCH 081/125] black --- calibration_conditions.py | 134 +++-- calibrationscore.py | 33 +- catbond.py | 40 +- centralbank.py | 49 +- compute_profits_losses_from_cash.py | 8 +- condition_aux.py | 131 ++++- distribution_wrapper_test.py | 16 +- distributionreinsurance.py | 64 ++- distributiontruncated.py | 50 +- ensemble.py | 214 ++++--- genericagent.py | 9 +- genericagentabce.py | 1 + insurancecontract.py | 45 +- insurancefirm.py | 278 ++++++--- insurancesimulation.py | 686 ++++++++++++++++------- isleconfig.py | 147 ++--- listify.py | 16 +- logger.py | 147 +++-- metainsurancecontract.py | 95 +++- metainsuranceorg.py | 676 +++++++++++++++------- reinsurancecontract.py | 86 ++- reinsurancefirm.py | 4 +- resume.py | 127 +++-- riskmodel.py | 211 +++++-- setup.py | 90 ++- start.py | 218 ++++++-- visualisation.py | 837 +++++++++++++++++++++------- visualization_network.py | 258 +++++++-- 28 files changed, 3344 insertions(+), 1326 deletions(-) diff --git a/calibration_conditions.py b/calibration_conditions.py index 33bc6f7..94a8980 100644 --- a/calibration_conditions.py +++ b/calibration_conditions.py @@ -31,100 +31,158 @@ import condition_aux import isleconfig + def condition_stationary_state_cash(logobj): """Stationarity test for total cash""" - return condition_aux.condition_stationary_state(logobj.history_logs['total_cash']) - + return condition_aux.condition_stationary_state(logobj.history_logs["total_cash"]) + + def condition_stationary_state_excess_capital(logobj): """Stationarity test for total excess capital""" - return condition_aux.condition_stationary_state(logobj.history_logs['total_excess_capital']) + return condition_aux.condition_stationary_state( + logobj.history_logs["total_excess_capital"] + ) + def condition_stationary_state_profits_losses(logobj): """Stationarity test for total profits and losses""" - return condition_aux.condition_stationary_state(logobj.history_logs['total_profitslosses']) + return condition_aux.condition_stationary_state( + logobj.history_logs["total_profitslosses"] + ) + def condition_stationary_state_contracts(logobj): """Stationarity test for total number of contracts""" - return condition_aux.condition_stationary_state(logobj.history_logs['total_contracts']) + return condition_aux.condition_stationary_state( + logobj.history_logs["total_contracts"] + ) + def condition_stationary_state_rein_cash(logobj): """Stationarity test for total cash (reinsurers)""" - return condition_aux.condition_stationary_state(logobj.history_logs['total_reincash']) + return condition_aux.condition_stationary_state( + logobj.history_logs["total_reincash"] + ) + def condition_stationary_state_rein_excess_capital(logobj): """Stationarity test for total excess capital (reinsurers)""" - return condition_aux.condition_stationary_state(logobj.history_logs['total_reinexcess_capital']) + return condition_aux.condition_stationary_state( + logobj.history_logs["total_reinexcess_capital"] + ) + def condition_stationary_state_rein_profits_losses(logobj): """Stationarity test for total profits and losses (reinsurers)""" - return condition_aux.condition_stationary_state(logobj.history_logs['total_reinprofitslosses']) + return condition_aux.condition_stationary_state( + logobj.history_logs["total_reinprofitslosses"] + ) + def condition_stationary_state_rein_contracts(logobj): """Stationarity test for total number of reinsured contracts""" - return condition_aux.condition_stationary_state(logobj.history_logs['total_reincontracts']) + return condition_aux.condition_stationary_state( + logobj.history_logs["total_reincontracts"] + ) + def condition_stationary_state_market_premium(logobj): """Stationarity test for insurance market premium""" - return condition_aux.condition_stationary_state(logobj.history_logs['market_premium']) + return condition_aux.condition_stationary_state( + logobj.history_logs["market_premium"] + ) + def condition_stationary_state_rein_market_premium(logobj): """Stationarity test for reinsurance market premium""" - return condition_aux.condition_stationary_state(logobj.history_logs['market_reinpremium']) + return condition_aux.condition_stationary_state( + logobj.history_logs["market_reinpremium"] + ) -def condition_defaults_insurance(logobj): # TODO: develop this into a non-binary measure + +def condition_defaults_insurance( + logobj +): # TODO: develop this into a non-binary measure """Test for number of insurance bankruptcies (non zero, not all insurers)""" - #series = logobj.history_logs['total_operational'] - #if series[-1] != 0 and any(series[i]-series[i-1] < 0 for i in range(1,len(series))): - opseries = [logobj.history_logs["insurance_firms_cash"][-1][i][2] for i in \ - range(len(logobj.history_logs["insurance_firms_cash"][-1]))] + # series = logobj.history_logs['total_operational'] + # if series[-1] != 0 and any(series[i]-series[i-1] < 0 for i in range(1,len(series))): + opseries = [ + logobj.history_logs["insurance_firms_cash"][-1][i][2] + for i in range(len(logobj.history_logs["insurance_firms_cash"][-1])) + ] if any(opseries) and not all(opseries): return 1 else: return 0 -def condition_defaults_reinsurance(logobj): # TODO: develop this into a non-binary measure + +def condition_defaults_reinsurance( + logobj +): # TODO: develop this into a non-binary measure """Test for number of reinsurance bankruptcies (non zero, not all reinsurers)""" - #series = logobj.history_logs['total_reinoperational'] - #if series[-1] != 0 and any(series[i]-series[i-1] < 0 for i in range(1,len(series))): - opseries = [logobj.history_logs["reinsurance_firms_cash"][-1][i][2] for i in \ - range(len(logobj.history_logs["reinsurance_firms_cash"][-1]))] + # series = logobj.history_logs['total_reinoperational'] + # if series[-1] != 0 and any(series[i]-series[i-1] < 0 for i in range(1,len(series))): + opseries = [ + logobj.history_logs["reinsurance_firms_cash"][-1][i][2] + for i in range(len(logobj.history_logs["reinsurance_firms_cash"][-1])) + ] if any(opseries) and not all(opseries): return 1 else: return 0 + def condition_insurance_coverage(logobj): """Test for insurance coverage close to 100%""" - return logobj.history_logs['total_contracts'][-1] * 1. / isleconfig.simulation_parameters["no_risks"] + return ( + logobj.history_logs["total_contracts"][-1] + * 1.0 + / isleconfig.simulation_parameters["no_risks"] + ) + def condition_reinsurance_coverage(logobj, minimum=0.6): """Test for reinsurance coverage close to some minimum that may be less than 100% (default 60%)""" - score = logobj.history_logs['total_reincontracts'][-1] * 1. / (minimum * logobj.history_logs['total_contracts'][-1]) - score = 1 if score>1 else score + score = ( + logobj.history_logs["total_reincontracts"][-1] + * 1.0 + / (minimum * logobj.history_logs["total_contracts"][-1]) + ) + score = 1 if score > 1 else score return score -def condition_insurance_firm_dist(logobj): + +def condition_insurance_firm_dist(logobj): """Empirical calibration test for insurance firm size (total assets; cash)""" """filter operational firms""" - #dist = [logobj.history_logs["insurance_firms_cash"][-1][i][0] for i in range(len(logobj.history_logs["insurance_firms_cash"])) if \ + # dist = [logobj.history_logs["insurance_firms_cash"][-1][i][0] for i in range(len(logobj.history_logs["insurance_firms_cash"])) if \ # logobj.history_logs["insurance_firms_cash"][-1][i][0] > isleconfig.simulation_parameters["cash_permanency_limit"]] - dist = [logobj.history_logs["insurance_firms_cash"][-1][i][0] for i in \ - range(len(logobj.history_logs["insurance_firms_cash"][-1])) if \ - logobj.history_logs["insurance_firms_cash"][-1][i][2]] + dist = [ + logobj.history_logs["insurance_firms_cash"][-1][i][0] + for i in range(len(logobj.history_logs["insurance_firms_cash"][-1])) + if logobj.history_logs["insurance_firms_cash"][-1][i][2] + ] """run two-sided KS test""" - KS_statistic, p_value = stats.ks_2samp(condition_aux.scaler(condition_aux.insurance_firm_sizes_empirical_2017), - condition_aux.scaler(dist)) + KS_statistic, p_value = stats.ks_2samp( + condition_aux.scaler(condition_aux.insurance_firm_sizes_empirical_2017), + condition_aux.scaler(dist), + ) return p_value -def condition_reinsurance_firm_dist(logobj): + +def condition_reinsurance_firm_dist(logobj): """Empirical calibration test for reinsurance firm size (total assets; cash)""" """filter operational firms""" - #dist = [logobj.history_logs["reinsurance_firms_cash"][-1][i][0] for i in range(len(logobj.history_logs["reinsurance_firms_cash"])) if \ + # dist = [logobj.history_logs["reinsurance_firms_cash"][-1][i][0] for i in range(len(logobj.history_logs["reinsurance_firms_cash"])) if \ # logobj.history_logs["reinsurance_firms_cash"][-1][i][0] > isleconfig.simulation_parameters["cash_permanency_limit"]] - dist = [logobj.history_logs["reinsurance_firms_cash"][-1][i][0] for i in \ - range(len(logobj.history_logs["reinsurance_firms_cash"][-1])) if - logobj.history_logs["reinsurance_firms_cash"][-1][i][2]] + dist = [ + logobj.history_logs["reinsurance_firms_cash"][-1][i][0] + for i in range(len(logobj.history_logs["reinsurance_firms_cash"][-1])) + if logobj.history_logs["reinsurance_firms_cash"][-1][i][2] + ] """run two-sided KS test""" - KS_statistic, p_value = stats.ks_2samp(condition_aux.scaler(condition_aux.reinsurance_firm_sizes_empirical_2017), - condition_aux.scaler(dist)) + KS_statistic, p_value = stats.ks_2samp( + condition_aux.scaler(condition_aux.reinsurance_firm_sizes_empirical_2017), + condition_aux.scaler(dist), + ) return p_value diff --git a/calibrationscore.py b/calibrationscore.py index 32d870c..b0a86a6 100644 --- a/calibrationscore.py +++ b/calibrationscore.py @@ -5,9 +5,10 @@ from inspect import getmembers, isfunction import numpy as np -import calibration_conditions # Test functions +import calibration_conditions # Test functions -class CalibrationScore(): + +class CalibrationScore: def __init__(self, L): """Constructor method. Arguments: @@ -17,31 +18,41 @@ def __init__(self, L): """Assert sanity of log and save log.""" assert isinstance(L, logger.Logger) self.logger = L - + """Prepare list of calibration tests from calibration_conditions.py""" - self.conditions = [f for f in getmembers(calibration_conditions) if isfunction(f[1])] - + self.conditions = [ + f for f in getmembers(calibration_conditions) if isfunction(f[1]) + ] + """Prepare calibration score variable.""" self.calibration_score = None - + def test_all(self): """Method to test all calibration tests. No arguments. Returns combined calibration score as float \in [0,1].""" - + """Compute score components""" - scores = {condition[0]: condition[1](self.logger) for condition in self.conditions} + scores = { + condition[0]: condition[1](self.logger) for condition in self.conditions + } """Print components""" print("\n") for cond_name, score in scores.items(): print("{0:47s}: {1:8f}".format(cond_name, score)) """Compute combined score""" - self.calibration_score = self.combine_scores(np.array([*scores.values()], dtype=object)) + self.calibration_score = self.combine_scores( + np.array([*scores.values()], dtype=object) + ) """Print combined score""" - print("\n Total calibration score: {0:8f}".format(self.calibration_score)) + print( + "\n Total calibration score: {0:8f}".format( + self.calibration_score + ) + ) """Return""" return self.calibration_score - + def combine_scores(self, slist): """Method to combine calibration score components. Combination is additive (mean). Change the function for other combination methods (multiplicative or minimum). diff --git a/catbond.py b/catbond.py index 62e02f5..65281dd 100644 --- a/catbond.py +++ b/catbond.py @@ -1,4 +1,3 @@ - import isleconfig import numpy as np import scipy.stats @@ -38,23 +37,34 @@ def iterate(self, time): self.simulation.bank.award_interest(self, self.cash) self.effect_payments(time) if isleconfig.verbose: - print(time, ":", self.id, len(self.underwritten_contracts), self.cash, self.operational) - + print( + time, + ":", + self.id, + len(self.underwritten_contracts), + self.cash, + self.operational, + ) + """mature contracts""" print("Number of underwritten contracts ", len(self.underwritten_contracts)) - maturing = [contract for contract in self.underwritten_contracts if contract.expiration <= time] + maturing = [ + contract + for contract in self.underwritten_contracts + if contract.expiration <= time + ] for contract in maturing: self.underwritten_contracts.remove(contract) contract.mature(time) [contract.check_payment_due(time) for contract in self.underwritten_contracts] - + if self.underwritten_contracts == []: self.mature_bond() - else: #TODO: dividend should only be payed according to pre-arranged schedule, and only if no risk events have materialized so far + else: # TODO: dividend should only be payed according to pre-arranged schedule, and only if no risk events have materialized so far if self.operational: self.pay_dividends(time) - + def set_owner(self, owner): """Method to set owner of the Cat Bond. Accepts: @@ -63,7 +73,7 @@ def set_owner(self, owner): self.owner = owner if isleconfig.verbose: print("SOLD") - + def set_contract(self, contract): """Method to record new instances of CatBonds. Accepts: @@ -71,7 +81,7 @@ def set_contract(self, contract): No return values Only one contract is ever added to the list of underwritten contracts as each CatBond is a contract itself.""" self.underwritten_contracts.append(contract) - + def mature_bond(self): """Method to mature CatBond. No accepted values @@ -79,10 +89,14 @@ def mature_bond(self): When the catbond contract matures this is called which pays the value of the catbond to the simulation, and is then deleted from the list of agents.""" if self.operational: - obligation = {"amount": self.cash, "recipient": self.simulation, "due_time": 1, "purpose": 'mature'} + obligation = { + "amount": self.cash, + "recipient": self.simulation, + "due_time": 1, + "purpose": "mature", + } self.pay(obligation) self.simulation.delete_agents("catbond", [self]) self.operational = False - else: print('CatBond is not operational so cannot mature') - - + else: + print("CatBond is not operational so cannot mature") diff --git a/centralbank.py b/centralbank.py index 45bd21e..ff9ac75 100644 --- a/centralbank.py +++ b/centralbank.py @@ -7,7 +7,7 @@ def __init__(self, money_supply): """Constructor Method. No accepted arguments. Constructs the CentralBank class. This class is currently only used to award interest payments.""" - self.interest_rate = simulation_parameters['interest_rate'] + self.interest_rate = simulation_parameters["interest_rate"] self.inflation_target = 0.02 self.actual_inflation = 0 self.onemonth_CPI = 0 @@ -16,7 +16,7 @@ def __init__(self, money_supply): self.prices_list = [] self.economy_money = money_supply self.warnings = {} - self.aid_budget = self.aid_budget_reset = simulation_parameters['aid_budget'] + self.aid_budget = self.aid_budget_reset = simulation_parameters["aid_budget"] def update_money_supply(self, amount, reduce=True): """Method to update the current supply of money in the insurance simulation economy. Only used to monitor @@ -74,8 +74,12 @@ def calculate_inflation(self, current_price, time): if time < 13: self.actual_inflation = self.inflation_target else: - self.onemonth_CPI = (current_price - self.prices_list[-2])/self.prices_list[-2] - self.twelvemonth_CPI = (current_price - self.prices_list[-13])/self.prices_list[-13] + self.onemonth_CPI = ( + current_price - self.prices_list[-2] + ) / self.prices_list[-2] + self.twelvemonth_CPI = ( + current_price - self.prices_list[-13] + ) / self.prices_list[-13] self.actual_inflation = self.twelvemonth_CPI def regulate(self, firm_id, firm_cash, firm_var, reinsurance, age): @@ -102,15 +106,25 @@ def regulate(self, firm_id, firm_cash, firm_var, reinsurance, age): for iter in range(len(reinsurance)): reinsurance_capital = 0 for categ in range(len(reinsurance[iter])): - if firm_var[iter][categ] >= reinsurance[iter][categ][0]: # Check VaR greater than deductible - if firm_var[iter][categ] >= reinsurance[iter][categ][1]: # Check VaR greater than excess - reinsurance_capital += (reinsurance[iter][categ][1] - reinsurance[iter][categ][0]) + if ( + firm_var[iter][categ] >= reinsurance[iter][categ][0] + ): # Check VaR greater than deductible + if ( + firm_var[iter][categ] >= reinsurance[iter][categ][1] + ): # Check VaR greater than excess + reinsurance_capital += ( + reinsurance[iter][categ][1] - reinsurance[iter][categ][0] + ) else: - reinsurance_capital += (firm_var[iter][categ] - reinsurance[iter][categ][0]) + reinsurance_capital += ( + firm_var[iter][categ] - reinsurance[iter][categ][0] + ) else: - reinsurance_capital += 0 # If below deductible no reinsurance + reinsurance_capital += 0 # If below deductible no reinsurance if sum(firm_var[iter]) > 0: - cash_fractions.append((firm_cash[iter]+reinsurance_capital)/sum(firm_var[iter])) + cash_fractions.append( + (firm_cash[iter] + reinsurance_capital) / sum(firm_var[iter]) + ) else: cash_fractions.append(1) @@ -158,17 +172,26 @@ def provide_aid(self, insurance_firms, damage_fraction, time): given_aid_dict = {} if damage_fraction > 0.50: for insurer in insurance_firms: - claims = sum([ob['amount'] for ob in insurer.obligations if ob["purpose"] == "claim" and ob["due_time"] == time + 2]) + claims = sum( + [ + ob["amount"] + for ob in insurer.obligations + if ob["purpose"] == "claim" and ob["due_time"] == time + 2 + ] + ) aid = claims * damage_fraction all_firms_aid += aid given_aid_dict[insurer] = aid # Give each firm an equal fraction of claims for fraction in [1, 0.9, 0.8, 0.7, 0.6, 0.5, 0.4, 0.3, 0.2, 0.1, 0]: if self.aid_budget - (all_firms_aid * fraction) > 0: - self.aid_budget -= (all_firms_aid * fraction) + self.aid_budget -= all_firms_aid * fraction for key in given_aid_dict: given_aid_dict[key] *= fraction - print("Damage %f causes %d to be given out in aid. %d budget left." % (damage_fraction, all_firms_aid * fraction, self.aid_budget)) + print( + "Damage %f causes %d to be given out in aid. %d budget left." + % (damage_fraction, all_firms_aid * fraction, self.aid_budget) + ) return given_aid_dict else: return given_aid_dict diff --git a/compute_profits_losses_from_cash.py b/compute_profits_losses_from_cash.py index ed6691a..865f749 100644 --- a/compute_profits_losses_from_cash.py +++ b/compute_profits_losses_from_cash.py @@ -9,11 +9,9 @@ infile.close() filename = "data/" + r + "_" + ft + "profitslosses.dat" outfile = open(filename, "w") - + for series in data: - outputdata = [series[i]-series[i-1] for i in range(1, len(series))] + outputdata = [series[i] - series[i - 1] for i in range(1, len(series))] outfile.write(str(outputdata) + "\n") - - outfile.close() - + outfile.close() diff --git a/condition_aux.py b/condition_aux.py index 9ffab5b..eefc0f5 100644 --- a/condition_aux.py +++ b/condition_aux.py @@ -6,20 +6,98 @@ """Data""" """Bloomberg size data for US firms""" -insurance_firm_sizes_empirical_2017 = [42.4701, 108.0418, 110.2641, 114.437, 130.2988, 133.674, 146.438, 152.3354, - 239.032, 337.689, 375.914, 376.988, 395.859, 436.191, 482.503, 585.824, 667.849, - 842.264, 894.848, 896.227, 904.873, 1231.126, 1357.016, 1454.999, 1518.236, - 1665.859, 1681.94, 1737.9198, 1771.21, 1807.279, 1989.742, 2059.921, 2385.485, - 2756.695, 2947.244, 3014.3, 3659.2, 3840.1, 4183.431, 4929.197, 5101.323, - 5224.622, 5900.881, 7686.431, 8376.2, 8439.743, 8764.0, 9095.0, 11198.34, - 14433.0, 15469.6, 19403.5, 21843.0, 23192.374, 24299.917, 25218.63, 31843.0, - 32051.658, 32805.016, 38701.2, 56567.0, 60658.0, 79586.0, 103483.0, 112422.0, - 167022.0, 225260.0, 498301.0, 702095.0] -reinsurance_firm_sizes_empirical_2017 = [396.898, 627.808, 6644.189, 15226.131, 25384.317, 23591.792, 3357.393, - 13606.422, 4671.794, 614.121, 60514.818, 24760.177, 2001.669, 182.2, 12906.4] +insurance_firm_sizes_empirical_2017 = [ + 42.4701, + 108.0418, + 110.2641, + 114.437, + 130.2988, + 133.674, + 146.438, + 152.3354, + 239.032, + 337.689, + 375.914, + 376.988, + 395.859, + 436.191, + 482.503, + 585.824, + 667.849, + 842.264, + 894.848, + 896.227, + 904.873, + 1231.126, + 1357.016, + 1454.999, + 1518.236, + 1665.859, + 1681.94, + 1737.9198, + 1771.21, + 1807.279, + 1989.742, + 2059.921, + 2385.485, + 2756.695, + 2947.244, + 3014.3, + 3659.2, + 3840.1, + 4183.431, + 4929.197, + 5101.323, + 5224.622, + 5900.881, + 7686.431, + 8376.2, + 8439.743, + 8764.0, + 9095.0, + 11198.34, + 14433.0, + 15469.6, + 19403.5, + 21843.0, + 23192.374, + 24299.917, + 25218.63, + 31843.0, + 32051.658, + 32805.016, + 38701.2, + 56567.0, + 60658.0, + 79586.0, + 103483.0, + 112422.0, + 167022.0, + 225260.0, + 498301.0, + 702095.0, +] +reinsurance_firm_sizes_empirical_2017 = [ + 396.898, + 627.808, + 6644.189, + 15226.131, + 25384.317, + 23591.792, + 3357.393, + 13606.422, + 4671.794, + 614.121, + 60514.818, + 24760.177, + 2001.669, + 182.2, + 12906.4, +] """Functions""" + def condition_stationary_state(series): """Stationarity test function for time series. Tests if the mean of the last 25% of the time series is within 1-2 standard deviation of the mean of the middle section (between 25% and 75% of the time series). The first @@ -29,24 +107,29 @@ def condition_stationary_state(series): Returns: Calibration score between 0 and 1. Is 1 if last 25% are within one standard deviation, between 0 and 1 if they are between 1 and 2 standard deviations, 0 otherwise.""" - + """Compute means and standard deviation""" - mean_reference = np.mean(series[int(len(series)*.25):int(len(series)*.75)]) - std_reference = np.std(series[int(len(series)*.25):int(len(series)*.75)]) - mean_test = np.mean(series[int(len(series)*.75):int(len(series)*1.)]) - + mean_reference = np.mean(series[int(len(series) * 0.25) : int(len(series) * 0.75)]) + std_reference = np.std(series[int(len(series) * 0.25) : int(len(series) * 0.75)]) + mean_test = np.mean(series[int(len(series) * 0.75) : int(len(series) * 1.0)]) + """Compute score""" score = 1 + (np.abs(mean_test - mean_reference) - std_reference) / std_reference - score = 1 if score>1 else score - score = 0 if score<0 else score - + score = 1 if score > 1 else score + score = 0 if score < 0 else score + """Set score to one if standard deviation is zero""" - if score == np.nan and np.std(series[int(len(series)*.25):int(len(series)*.75)]) == 0: + if ( + score == np.nan + and np.std(series[int(len(series) * 0.25) : int(len(series) * 0.75)]) == 0 + ): score = 1 return score - -def scaler(series): # TODO: find a better way to scale heavy-tailed distributions than to use standard score scaling on logs + +def scaler( + series +): # TODO: find a better way to scale heavy-tailed distributions than to use standard score scaling on logs """Function to do a standard score scaling of the log of a heavy-tailed distribution. This is used to calibrate distributions where the unit is not important (distributions of sizes of firms e.g.). This would be perfectly appropriate for lognormal distributions, but should work reasonably well for calibration of other heavy-tailed @@ -56,10 +139,10 @@ def scaler(series): # TODO: find a better way to scale heavy-tailed distribution Returns: Calibratied series.""" series = np.asarray(series) - assert (series>1).all() + assert (series > 1).all() logseries = np.log(series) mean = np.mean(logseries) std = np.std(logseries) - z = (logseries - mean)/std + z = (logseries - mean) / std newseries = np.exp(z) return newseries diff --git a/distribution_wrapper_test.py b/distribution_wrapper_test.py index b4bfa97..81734f9 100644 --- a/distribution_wrapper_test.py +++ b/distribution_wrapper_test.py @@ -5,14 +5,20 @@ import pdb non_truncated_dist = scipy.stats.pareto(b=2, loc=0, scale=0.5) -truncated_dist = TruncatedDistWrapper(lower_bound=0.6, upper_bound=1., dist=non_truncated_dist) -reinsurance_dist = ReinsuranceDistWrapper(lower_bound=0.85, upper_bound=0.95, dist=truncated_dist) +truncated_dist = TruncatedDistWrapper( + lower_bound=0.6, upper_bound=1.0, dist=non_truncated_dist +) +reinsurance_dist = ReinsuranceDistWrapper( + lower_bound=0.85, upper_bound=0.95, dist=truncated_dist +) x1 = np.linspace(non_truncated_dist.ppf(0.01), non_truncated_dist.ppf(0.99), 100) -x2 = np.linspace(truncated_dist.ppf(0.01), truncated_dist.ppf(1.), 100) -x3 = np.linspace(reinsurance_dist.ppf(0.01), reinsurance_dist.ppf(1.), 100) +x2 = np.linspace(truncated_dist.ppf(0.01), truncated_dist.ppf(1.0), 100) +x3 = np.linspace(reinsurance_dist.ppf(0.01), reinsurance_dist.ppf(1.0), 100) x_val_1 = reinsurance_dist.lower_bound -x_val_2 = truncated_dist.upper_bound - (reinsurance_dist.upper_bound - reinsurance_dist.lower_bound) +x_val_2 = truncated_dist.upper_bound - ( + reinsurance_dist.upper_bound - reinsurance_dist.lower_bound +) x_val_3 = reinsurance_dist.upper_bound x_val_4 = truncated_dist.upper_bound diff --git a/distributionreinsurance.py b/distributionreinsurance.py index fd85eb3..8d9dcb2 100644 --- a/distributionreinsurance.py +++ b/distributionreinsurance.py @@ -4,7 +4,8 @@ import scipy import pdb -class ReinsuranceDistWrapper(): + +class ReinsuranceDistWrapper: def __init__(self, dist, lower_bound=None, upper_bound=None): assert lower_bound is not None or upper_bound is not None self.dist = dist @@ -17,57 +18,72 @@ def __init__(self, dist, lower_bound=None, upper_bound=None): assert self.upper_bound > self.lower_bound self.redistributed_share = dist.cdf(upper_bound) - dist.cdf(lower_bound) - def pdf(self, x): x = np.array(x, ndmin=1) - r = map(lambda Y: self.dist.pdf(Y) if Y < self.lower_bound \ - else np.inf if Y==self.lower_bound \ - else self.dist.pdf(Y + self.upper_bound - self.lower_bound), x) + r = map( + lambda Y: self.dist.pdf(Y) + if Y < self.lower_bound + else np.inf + if Y == self.lower_bound + else self.dist.pdf(Y + self.upper_bound - self.lower_bound), + x, + ) r = np.array(list(r)) if len(r.flatten()) == 1: r = float(r) return r - + def cdf(self, x): x = np.array(x, ndmin=1) - r = map(lambda Y: self.dist.cdf(Y) if Y < self.lower_bound \ - else self.dist.cdf(Y + self.upper_bound - self.lower_bound), x) + r = map( + lambda Y: self.dist.cdf(Y) + if Y < self.lower_bound + else self.dist.cdf(Y + self.upper_bound - self.lower_bound), + x, + ) r = np.array(list(r)) if len(r.flatten()) == 1: r = float(r) return r - + def ppf(self, x): x = np.array(x, ndmin=1) assert (x >= 0).all() and (x <= 1).all() - r = map(lambda Y: self.dist.ppf(Y) if Y <= self.dist.cdf(self.lower_bound) \ - else self.dist.ppf(self.dist.cdf(self.lower_bound)) if Y <= self.dist.cdf(self.upper_bound) \ - else self.dist.ppf(Y) - self.upper_bound + self.lower_bound, x) + r = map( + lambda Y: self.dist.ppf(Y) + if Y <= self.dist.cdf(self.lower_bound) + else self.dist.ppf(self.dist.cdf(self.lower_bound)) + if Y <= self.dist.cdf(self.upper_bound) + else self.dist.ppf(Y) - self.upper_bound + self.lower_bound, + x, + ) r = np.array(list(r)) if len(r.flatten()) == 1: r = float(r) return r - + def rvs(self, size=1): sample = self.dist.rvs(size=size) - sample1 = sample[sample<=self.lower_bound] - sample2 = sample[sample>self.lower_bound] - sample3 = sample2[sample2>=self.upper_bound] - sample2 = sample2[sample2 self.lower_bound] + sample3 = sample2[sample2 >= self.upper_bound] + sample2 = sample2[sample2 < self.upper_bound] + sample2 = np.ones(len(sample2)) * self.lower_bound - sample3 = sample3 -self.upper_bound + self.lower_bound - - sample = np.append(np.append(sample1,sample2),sample3) + sample3 = sample3 - self.upper_bound + self.lower_bound + + sample = np.append(np.append(sample1, sample2), sample3) return sample[:size] if __name__ == "__main__": non_truncated = scipy.stats.pareto(b=2, loc=0, scale=0.5) - #truncated = ReinsuranceDistWrapper(lower_bound=0, upper_bound=1, dist=non_truncated) - truncated = ReinsuranceDistWrapper(lower_bound=0.9, upper_bound=1.1, dist=non_truncated) + # truncated = ReinsuranceDistWrapper(lower_bound=0, upper_bound=1, dist=non_truncated) + truncated = ReinsuranceDistWrapper( + lower_bound=0.9, upper_bound=1.1, dist=non_truncated + ) x = np.linspace(non_truncated.ppf(0.01), non_truncated.ppf(0.99), 100) x2 = np.linspace(truncated.ppf(0.01), truncated.ppf(0.99), 100) - #pdb.set_trace() + # pdb.set_trace() diff --git a/distributiontruncated.py b/distributiontruncated.py index f0c36f3..18b8552 100644 --- a/distributiontruncated.py +++ b/distributiontruncated.py @@ -4,55 +4,73 @@ import scipy.integrate -class TruncatedDistWrapper(): +class TruncatedDistWrapper: def __init__(self, dist, lower_bound=0, upper_bound=1): self.dist = dist self.normalizing_factor = dist.cdf(upper_bound) - dist.cdf(lower_bound) self.lower_bound = lower_bound self.upper_bound = upper_bound assert self.upper_bound > self.lower_bound - + def pdf(self, x): x = np.array(x, ndmin=1) - r = map(lambda Y: self.dist.pdf(Y) / self.normalizing_factor \ - if (Y >= self.lower_bound and Y <= self.upper_bound) else 0, x) + r = map( + lambda Y: self.dist.pdf(Y) / self.normalizing_factor + if (Y >= self.lower_bound and Y <= self.upper_bound) + else 0, + x, + ) r = np.array(list(r)) if len(r.flatten()) == 1: r = float(r) return r - + def cdf(self, x): x = np.array(x, ndmin=1) - r = map(lambda Y: 0 if Y < self.lower_bound else 1 if Y > self.upper_bound \ - else (self.dist.cdf(Y) - self.dist.cdf(self.lower_bound))/ self.normalizing_factor, x) + r = map( + lambda Y: 0 + if Y < self.lower_bound + else 1 + if Y > self.upper_bound + else (self.dist.cdf(Y) - self.dist.cdf(self.lower_bound)) + / self.normalizing_factor, + x, + ) r = np.array(list(r)) if len(r.flatten()) == 1: r = float(r) return r - + def ppf(self, x): x = np.array(x, ndmin=1) assert (x >= 0).all() and (x <= 1).all() - return self.dist.ppf(x * self.normalizing_factor + self.dist.cdf(self.lower_bound)) + return self.dist.ppf( + x * self.normalizing_factor + self.dist.cdf(self.lower_bound) + ) def rvs(self, size=1): init_sample_size = int(ceil(size / self.normalizing_factor * 1.1)) sample = self.dist.rvs(size=init_sample_size) - sample = sample[sample>=self.lower_bound] - sample = sample[sample<=self.upper_bound] + sample = sample[sample >= self.lower_bound] + sample = sample[sample <= self.upper_bound] while len(sample) < size: sample = np.append(sample, self.rvs(size - len(sample))) - return sample[:size] - + return sample[:size] + def mean(self): - mean_estimate, mean_error = scipy.integrate.quad(lambda Y: Y*self.pdf(Y), self.lower_bound, self.upper_bound) + mean_estimate, mean_error = scipy.integrate.quad( + lambda Y: Y * self.pdf(Y), self.lower_bound, self.upper_bound + ) return mean_estimate + if __name__ == "__main__": non_truncated = scipy.stats.pareto(b=2, loc=0, scale=0.5) - truncated = TruncatedDistWrapper(lower_bound=0.55, upper_bound=1., dist=non_truncated) + truncated = TruncatedDistWrapper( + lower_bound=0.55, upper_bound=1.0, dist=non_truncated + ) x = np.linspace(non_truncated.ppf(0.01), non_truncated.ppf(0.99), 100) x2 = np.linspace(truncated.ppf(0.01), truncated.ppf(0.99), 100) - + print(truncated.mean()) diff --git a/ensemble.py b/ensemble.py index 8c5d269..2c47b7b 100644 --- a/ensemble.py +++ b/ensemble.py @@ -19,7 +19,7 @@ @operation def agg(*outputs): # do nothing - return outputs + return outputs def rake(hostname): @@ -28,71 +28,88 @@ def rake(hostname): """Configuration of the ensemble""" - replications = 70 # Number of replications to be carried out for each configuration. Usually one risk model, two risk models, three risk models, four risk models. + replications = ( + 70 + ) # Number of replications to be carried out for each configuration. Usually one risk model, two risk models, three risk models, four risk models. model = start.main - m = operation(model, include_modules = True) + m = operation(model, include_modules=True) - riskmodels = [1,2,3,4] # The number of risk models that will be used. + riskmodels = [1, 2, 3, 4] # The number of risk models that will be used. parameters = isleconfig.simulation_parameters - nums = {'1': 'one', - '2': 'two', - '3': 'three', - '4': 'four', - '5': 'five', - '6': 'six', - '7': 'seven', - '8': 'eight', - '9': 'nine'} + nums = { + "1": "one", + "2": "two", + "3": "three", + "4": "four", + "5": "five", + "6": "six", + "7": "seven", + "8": "eight", + "9": "nine", + } """Configure the return values and corresponding file suffixes where they should be saved""" - requested_logs = {'total_cash': '_cash.dat', - 'total_excess_capital': '_excess_capital.dat', - 'total_profitslosses': '_profitslosses.dat', - 'total_contracts': '_contracts.dat', - 'total_operational': '_operational.dat', - 'total_reincash': '_reincash.dat', - 'total_reinexcess_capital': '_reinexcess_capital.dat', - 'total_reinprofitslosses': '_reinprofitslosses.dat', - 'total_reincontracts': '_reincontracts.dat', - 'total_reinoperational': '_reinoperational.dat', - 'total_catbondsoperational': '_total_catbondsoperational.dat', - 'market_premium': '_premium.dat', - 'market_reinpremium': '_reinpremium.dat', - 'cumulative_bankruptcies': '_cumulative_bankruptcies.dat', - 'cumulative_market_exits': '_cumulative_market_exits', # TODO: correct filename - 'cumulative_unrecovered_claims': '_cumulative_unrecovered_claims.dat', - 'cumulative_claims': '_cumulative_claims.dat', - 'cumulative_bought_firms': '_cumulative_bought_firms.dat', - 'cumulative_nonregulation_firms':'_cumulative_nonregulation_firms.dat', - 'insurance_firms_cash': '_insurance_firms_cash.dat', - 'reinsurance_firms_cash': '_reinsurance_firms_cash.dat', - 'market_diffvar': '_market_diffvar.dat', - 'rc_event_schedule_initial': '_rc_event_schedule.dat', - 'rc_event_damage_initial': '_rc_event_damage.dat', - 'number_riskmodels': '_number_riskmodels.dat', - 'individual_contracts': '_insurance_contracts.dat', - 'reinsurance_contracts': '_reinsurance_contracts.dat', - 'unweighted_network_data': '_unweighted_network_data.dat', - 'network_node_labels': '_network_node_labels.dat', - 'network_edge_labels': '_network_edge_labels.dat', - 'number_of_agents': '_number_of_agents'} - + requested_logs = { + "total_cash": "_cash.dat", + "total_excess_capital": "_excess_capital.dat", + "total_profitslosses": "_profitslosses.dat", + "total_contracts": "_contracts.dat", + "total_operational": "_operational.dat", + "total_reincash": "_reincash.dat", + "total_reinexcess_capital": "_reinexcess_capital.dat", + "total_reinprofitslosses": "_reinprofitslosses.dat", + "total_reincontracts": "_reincontracts.dat", + "total_reinoperational": "_reinoperational.dat", + "total_catbondsoperational": "_total_catbondsoperational.dat", + "market_premium": "_premium.dat", + "market_reinpremium": "_reinpremium.dat", + "cumulative_bankruptcies": "_cumulative_bankruptcies.dat", + "cumulative_market_exits": "_cumulative_market_exits", # TODO: correct filename + "cumulative_unrecovered_claims": "_cumulative_unrecovered_claims.dat", + "cumulative_claims": "_cumulative_claims.dat", + "cumulative_bought_firms": "_cumulative_bought_firms.dat", + "cumulative_nonregulation_firms": "_cumulative_nonregulation_firms.dat", + "insurance_firms_cash": "_insurance_firms_cash.dat", + "reinsurance_firms_cash": "_reinsurance_firms_cash.dat", + "market_diffvar": "_market_diffvar.dat", + "rc_event_schedule_initial": "_rc_event_schedule.dat", + "rc_event_damage_initial": "_rc_event_damage.dat", + "number_riskmodels": "_number_riskmodels.dat", + "individual_contracts": "_insurance_contracts.dat", + "reinsurance_contracts": "_reinsurance_contracts.dat", + "unweighted_network_data": "_unweighted_network_data.dat", + "network_node_labels": "_network_node_labels.dat", + "network_edge_labels": "_network_edge_labels.dat", + "number_of_agents": "_number_of_agents", + } + if isleconfig.slim_log: - for name in ['insurance_firms_cash', 'reinsurance_firms_cash', 'individual_contracts', 'reinsurance_contracts' - 'unweighted_network_data', 'network_node_labels', 'network_edge_labels', 'number_of_agents']: + for name in [ + "insurance_firms_cash", + "reinsurance_firms_cash", + "individual_contracts", + "reinsurance_contracts" "unweighted_network_data", + "network_node_labels", + "network_edge_labels", + "number_of_agents", + ]: del requested_logs[name] if not isleconfig.save_network: - for name in ['unweighted_network_data', 'network_node_labels', 'network_edge_labels', 'number_of_agents']: + for name in [ + "unweighted_network_data", + "network_node_labels", + "network_edge_labels", + "number_of_agents", + ]: del requested_logs[name] assert "number_riskmodels" in requested_logs - """Configure log directory and ensure that the directory exists""" dir_prefix = "/new_data/" directory = os.getcwd() + dir_prefix @@ -107,31 +124,60 @@ def rake(hostname): if os.path.exists(filename): os.remove(filename) - """Setup of the simulations""" - setup = SetupSim() # Here the setup for the simulation is done. - [general_rc_event_schedule, general_rc_event_damage, np_seeds, random_seeds] = setup.obtain_ensemble(replications) #Since this script is used to carry out simulations in the cloud will usually have more than 1 replication.. - save_iter = isleconfig.simulation_parameters["max_time"] + 2 # never save simulation state in ensemble runs (resuming is impossible anyway) - - for i in riskmodels: #In this loop the parameters, schedules and random seeds for every run are prepared. Different risk models will be run with the same schedule, damage size and random seed for a fair comparison. - - simulation_parameters = copy.copy(parameters) #Here the parameters used for the simulation are loaded. Clone is needed otherwise all the runs will be carried out with the last number of thee loop. - simulation_parameters["no_riskmodels"] = i #Since we want to obtain ensembles for different number of risk models, we vary here the number of risks models. - job = [m(simulation_parameters, general_rc_event_schedule[x], general_rc_event_damage[x], np_seeds[x], random_seeds[x], save_iter, list(requested_logs.keys())) for x in range(replications)] #Here is assembled each job with the corresponding: simulation parameters, time events, damage events, seeds, simulation state save interval (never, i.e. longer than max_time), and list of requested logs. - jobs.append(job) # All jobs are collected in the jobs list. - + setup = SetupSim() # Here the setup for the simulation is done. + [ + general_rc_event_schedule, + general_rc_event_damage, + np_seeds, + random_seeds, + ] = setup.obtain_ensemble( + replications + ) # Since this script is used to carry out simulations in the cloud will usually have more than 1 replication.. + save_iter = ( + isleconfig.simulation_parameters["max_time"] + 2 + ) # never save simulation state in ensemble runs (resuming is impossible anyway) + + for ( + i + ) in ( + riskmodels + ): # In this loop the parameters, schedules and random seeds for every run are prepared. Different risk models will be run with the same schedule, damage size and random seed for a fair comparison. + + simulation_parameters = copy.copy( + parameters + ) # Here the parameters used for the simulation are loaded. Clone is needed otherwise all the runs will be carried out with the last number of thee loop. + simulation_parameters[ + "no_riskmodels" + ] = ( + i + ) # Since we want to obtain ensembles for different number of risk models, we vary here the number of risks models. + job = [ + m( + simulation_parameters, + general_rc_event_schedule[x], + general_rc_event_damage[x], + np_seeds[x], + random_seeds[x], + save_iter, + list(requested_logs.keys()), + ) + for x in range(replications) + ] # Here is assembled each job with the corresponding: simulation parameters, time events, damage events, seeds, simulation state save interval (never, i.e. longer than max_time), and list of requested logs. + jobs.append(job) # All jobs are collected in the jobs list. """Here the jobs are submitted""" with Session(host=hostname, default_cb_to_stdout=True) as sess: - for job in jobs: #If there are 4 risk models jobs will be a list with 4 elements. - + for ( + job + ) in jobs: # If there are 4 risk models jobs will be a list with 4 elements. + """Run simulation and obtain result""" result = sess.submit(job) - """Find number of riskmodels from log""" delistified_result = [listify.delistify(list(res)) for res in result] nrm = delistified_result[0]["number_riskmodels"] @@ -139,49 +185,65 @@ def rake(hostname): """These are the files created to collect the results""" wfiles_dict = {} logfile_dict = {} - + for name in requested_logs.keys(): if "rc_event" in name or "number_riskmodels" in name: - logfile_dict[name] = os.getcwd() + dir_prefix + "check_" + str(nums[str(nrm)]) + requested_logs[name] + logfile_dict[name] = ( + os.getcwd() + + dir_prefix + + "check_" + + str(nums[str(nrm)]) + + requested_logs[name] + ) elif "firms_cash" in name: - logfile_dict[name] = os.getcwd() + dir_prefix + "record_" + str(nums[str(nrm)]) + requested_logs[name] + logfile_dict[name] = ( + os.getcwd() + + dir_prefix + + "record_" + + str(nums[str(nrm)]) + + requested_logs[name] + ) else: - logfile_dict[name] = os.getcwd() + dir_prefix + str(nums[str(nrm)]) + requested_logs[name] + logfile_dict[name] = ( + os.getcwd() + + dir_prefix + + str(nums[str(nrm)]) + + requested_logs[name] + ) for name in logfile_dict: wfiles_dict[name] = open(logfile_dict[name], "w") - """Recreate logger object locally and save logs""" - + """Create local object""" L = logger.Logger() for i in range(len(job)): """Populate logger object with logs obtained from remote simulation run""" L.restore_logger_object(list(result[i])) - + """Save logs as dict (to _history_logs.dat)""" L.save_log(True) if isleconfig.save_network: L.save_network_data(ensemble=True) - + """Save logs as individual files""" for name in logfile_dict: wfiles_dict[name].write(str(delistified_result[i][name]) + "\n") - + """Once the data is stored in disk the files are closed""" for name in logfile_dict: wfiles_dict[name].close() del wfiles_dict[name] -if __name__ == '__main__': +if __name__ == "__main__": host = None if len(sys.argv) > 1: - host = sys.argv[1] # The server is passed as an argument. + host = sys.argv[1] # The server is passed as an argument. rake(host) # ID = OxeChiwQ3l0vsBnkLLXKMnpSBn948ptW # Secret = lfK_Gav8EK2B_gN0bsNXjUVQq84ua1iL2xlYs8Ef58tFyTSwzzJcWeDdSH21BnHx -# Hostname = bright-lemur.clusters.sandman.ai \ No newline at end of file +# Hostname = bright-lemur.clusters.sandman.ai diff --git a/genericagent.py b/genericagent.py index 8e58efe..4985372 100644 --- a/genericagent.py +++ b/genericagent.py @@ -1,7 +1,8 @@ - -class GenericAgent(): +class GenericAgent: def __init__(self, *args, **kwargs): self.init(*args, **kwargs) - + def init(*args, **kwargs): - assert False, "Error: GenericAgent init method should have been overridden but was not." + assert ( + False + ), "Error: GenericAgent init method should have been overridden but was not." diff --git a/genericagentabce.py b/genericagentabce.py index c0eb884..28d9531 100644 --- a/genericagentabce.py +++ b/genericagentabce.py @@ -1,4 +1,5 @@ import abce + class GenericAgent(abce.Agent): pass diff --git a/insurancecontract.py b/insurancecontract.py index d331a8d..55a8fc3 100644 --- a/insurancecontract.py +++ b/insurancecontract.py @@ -11,11 +11,35 @@ class InsuranceContract(MetaInsuranceContract): The signature of this class' constructor is the same as that of the InsuranceContract constructor. The class has two methods (explode, mature) that overwrite methods in InsuranceContract.""" - def __init__(self, insurer, properties, time, premium, runtime, payment_period, expire_immediately, initial_VaR=0.,\ - insurancetype="proportional", deductible_fraction=None, excess_fraction=None, reinsurance=0): - super(InsuranceContract, self).__init__(insurer, properties, time, premium, runtime, payment_period, \ - expire_immediately, initial_VaR, insurancetype, deductible_fraction, - excess_fraction, reinsurance) + def __init__( + self, + insurer, + properties, + time, + premium, + runtime, + payment_period, + expire_immediately, + initial_VaR=0.0, + insurancetype="proportional", + deductible_fraction=None, + excess_fraction=None, + reinsurance=0, + ): + super(InsuranceContract, self).__init__( + insurer, + properties, + time, + premium, + runtime, + payment_period, + expire_immediately, + initial_VaR, + insurancetype, + deductible_fraction, + excess_fraction, + reinsurance, + ) self.risk_data = properties @@ -34,10 +58,14 @@ def explode(self, time, uniform_value, damage_extent): if uniform_value < self.risk_factor: # if True: claim = min(self.excess, damage_extent * self.value) - self.deductible - self.insurer.register_claim(claim) #Every insurance claim made is immediately registered. + self.insurer.register_claim( + claim + ) # Every insurance claim made is immediately registered. self.current_claim += claim - self.insurer.receive_obligation(claim, self.property_holder, time + 2, 'claim') + self.insurer.receive_obligation( + claim, self.property_holder, time + 2, "claim" + ) # Insurer pays one time step after reinsurer to avoid bankruptcy. # TODO: Is this realistic? Change this? if self.expire_immediately: @@ -51,10 +79,9 @@ def mature(self, time): No return value. Returns risk to simulation as contract terminates. Calls terminate_reinsurance to dissolve any reinsurance contracts.""" - #self.terminating = True + # self.terminating = True self.terminate_reinsurance(time) if not self.roll_over_flag: self.property_holder.return_risks([self.risk_data]) - diff --git a/insurancefirm.py b/insurancefirm.py index da75298..723b9a7 100644 --- a/insurancefirm.py +++ b/insurancefirm.py @@ -8,6 +8,7 @@ class InsuranceFirm(MetaInsuranceOrg): """ReinsuranceFirm class. Inherits from InsuranceFirm.""" + def init(self, simulation_parameters, agent_parameters): """Constructor method. Accepts arguments @@ -26,10 +27,14 @@ def adjust_dividends(self, time, actual_capacity): No return values. Method is called from MetaInsuranceOrg iterate method between evaluating reinsurance and insurance risks to calculate dividend to be payed if the firm has made profit and has achieved capital targets.""" - #TODO: Implement algorithm from flowchart + # TODO: Implement algorithm from flowchart profits = self.profits_losses - self.per_period_dividend = max(0, self.dividend_share_of_profits * profits) # max function ensures that no negative dividends are paid - if actual_capacity < self.capacity_target: # no dividends if firm misses capital target + self.per_period_dividend = max( + 0, self.dividend_share_of_profits * profits + ) # max function ensures that no negative dividends are paid + if ( + actual_capacity < self.capacity_target + ): # no dividends if firm misses capital target self.per_period_dividend = 0 def get_reinsurance_VaR_estimate(self, max_var): @@ -40,13 +45,20 @@ def get_reinsurance_VaR_estimate(self, max_var): reinsurance_VaR_estimate: Type Decimal. This method takes the max VaR and mulitiplies it by a factor that estimates the VaR if another reinsurance contract was to be taken. Called by the adjust_target_capacity and get_capacity methods.""" - reinsurance_factor_estimate = (sum([ 1 for categ_id in range(self.simulation_no_risk_categories) \ - if (self.category_reinsurance[categ_id] is None)]) \ - * 1. / self.simulation_no_risk_categories) \ - * (1. - self.np_reinsurance_deductible_fraction) - reinsurance_VaR_estimate = max_var * (1. + reinsurance_factor_estimate) + reinsurance_factor_estimate = ( + sum( + [ + 1 + for categ_id in range(self.simulation_no_risk_categories) + if (self.category_reinsurance[categ_id] is None) + ] + ) + * 1.0 + / self.simulation_no_risk_categories + ) * (1.0 - self.np_reinsurance_deductible_fraction) + reinsurance_VaR_estimate = max_var * (1.0 + reinsurance_factor_estimate) return reinsurance_VaR_estimate - + def adjust_capacity_target(self, max_var): """Method to adjust capacity target. Accepts: @@ -55,12 +67,22 @@ def adjust_capacity_target(self, max_var): This method decides to increase/decrease the capacity target dependant on if the ratio of capacity target to max VaR is above/below a predetermined limit.""" reinsurance_VaR_estimate = self.get_reinsurance_VaR_estimate(max_var) - capacity_target_var_ratio_estimate = (self.capacity_target + reinsurance_VaR_estimate) * 1. / (max_var + reinsurance_VaR_estimate) - if capacity_target_var_ratio_estimate > self.capacity_target_increment_threshold: + capacity_target_var_ratio_estimate = ( + (self.capacity_target + reinsurance_VaR_estimate) + * 1.0 + / (max_var + reinsurance_VaR_estimate) + ) + if ( + capacity_target_var_ratio_estimate + > self.capacity_target_increment_threshold + ): self.capacity_target *= self.capacity_target_increment_factor - elif capacity_target_var_ratio_estimate < self.capacity_target_decrement_threshold: + elif ( + capacity_target_var_ratio_estimate + < self.capacity_target_decrement_threshold + ): self.capacity_target *= self.capacity_target_decrement_factor - return + return def get_capacity(self, max_var): """Method to get capacity of firm. @@ -71,10 +93,12 @@ def get_capacity(self, max_var): This method is called by increase_capacity to get the real capacity of the firm. If the firm has enough money to cover its max value at risk then its capacity is its cash + the reinsurance VaR estimate, otherwise the firm is recovering from some losses and so capacity is just cash.""" - if max_var < self.cash: # ensure presence of sufficiently much cash to cover VaR + if ( + max_var < self.cash + ): # ensure presence of sufficiently much cash to cover VaR reinsurance_VaR_estimate = self.get_reinsurance_VaR_estimate(max_var) return self.cash + reinsurance_VaR_estimate - return self.cash # Ensure insurer recovers complete coverage. + return self.cash # Ensure insurer recovers complete coverage. def increase_capacity(self, time, max_var): """Method to increase the capacity of the firm. @@ -89,28 +113,50 @@ def increase_capacity(self, time, max_var): market premium is above its average premium, otherwise firm is 'forced' to get a catbond or reinsurance. Only implemented for non-proportional(excess of loss) reinsurance. Only issues one reinsurance or catbond per iteration unless not enough capacity to meet target.""" - assert self.simulation_reinsurance_type == 'non-proportional' - reinsurance_price = self.simulation.get_reinsurance_premium(self.np_reinsurance_deductible_fraction) - cat_bond_price = self.simulation.get_cat_bond_price(self.np_reinsurance_deductible_fraction) + assert self.simulation_reinsurance_type == "non-proportional" + reinsurance_price = self.simulation.get_reinsurance_premium( + self.np_reinsurance_deductible_fraction + ) + cat_bond_price = self.simulation.get_cat_bond_price( + self.np_reinsurance_deductible_fraction + ) capacity = None - if not reinsurance_price == cat_bond_price == float('inf'): - categ_ids = [categ_id for categ_id in range(self.simulation_no_risk_categories) if (self.category_reinsurance[categ_id] is None)] + if not reinsurance_price == cat_bond_price == float("inf"): + categ_ids = [ + categ_id + for categ_id in range(self.simulation_no_risk_categories) + if (self.category_reinsurance[categ_id] is None) + ] if len(categ_ids) > 1: np.random.shuffle(categ_ids) - while len(categ_ids) >= 1: + while len(categ_ids) >= 1: categ_id = categ_ids.pop() capacity = self.get_capacity(max_var) if self.capacity_target < capacity: - if self.increase_capacity_by_category(time, categ_id, reinsurance_price=reinsurance_price, cat_bond_price=cat_bond_price, force=False): + if self.increase_capacity_by_category( + time, + categ_id, + reinsurance_price=reinsurance_price, + cat_bond_price=cat_bond_price, + force=False, + ): categ_ids = [] else: - self.increase_capacity_by_category(time, categ_id, reinsurance_price=reinsurance_price, cat_bond_price=cat_bond_price, force=True) + self.increase_capacity_by_category( + time, + categ_id, + reinsurance_price=reinsurance_price, + cat_bond_price=cat_bond_price, + force=True, + ) # capacity is returned in order not to recompute more often than necessary - if capacity is None: + if capacity is None: capacity = self.get_capacity(max_var) - return capacity + return capacity - def increase_capacity_by_category(self, time, categ_id, reinsurance_price, cat_bond_price, force=False): + def increase_capacity_by_category( + self, time, categ_id, reinsurance_price, cat_bond_price, force=False + ): """Method to increase capacity. Only called by increase_capacity. Accepts: time: Type Integer> @@ -123,20 +169,26 @@ def increase_capacity_by_category(self, time, categ_id, reinsurance_price, cat_b firm for the given category. This is forced if firm does not have enough capacity to meet target otherwise will only issue if market premium is greater than firms average premium.""" if isleconfig.verbose: - print("IF {0:d} increasing capacity in period {1:d}, cat bond price: {2:f}, reinsurance premium {3:f}".format(self.id, time, cat_bond_price, reinsurance_price)) + print( + "IF {0:d} increasing capacity in period {1:d}, cat bond price: {2:f}, reinsurance premium {3:f}".format( + self.id, time, cat_bond_price, reinsurance_price + ) + ) if not force: actual_premium = self.get_average_premium(categ_id) possible_premium = self.simulation.get_market_premium() if actual_premium >= possible_premium: return False - '''on the basis of prices decide for obtaining reinsurance or for issuing cat bond''' + """on the basis of prices decide for obtaining reinsurance or for issuing cat bond""" if reinsurance_price > cat_bond_price: if isleconfig.verbose: print("IF {0:d} issuing Cat bond in period {1:d}".format(self.id, time)) self.issue_cat_bond(time, categ_id) else: if isleconfig.verbose: - print("IF {0:d} getting reinsurance in period {1:d}".format(self.id, time)) + print( + "IF {0:d} getting reinsurance in period {1:d}".format(self.id, time) + ) self.ask_reinsurance_non_proportional_by_category(time, categ_id) return True @@ -154,18 +206,18 @@ def get_average_premium(self, categ_id): contract_premium = contract.periodized_premium * contract.runtime weighted_premium_sum += contract_premium if total_weight == 0: - return 0 # will prevent any attempt to reinsure empty categories + return 0 # will prevent any attempt to reinsure empty categories return weighted_premium_sum * 1.0 / total_weight - + def ask_reinsurance(self, time): """Method called specifically to call relevant reinsurance function for simulations reinsurance type. Only non-proportional type is used as this is the one mainly used in reality. Accepts: time: Type Integer. No return values.""" - if self.simulation_reinsurance_type == 'proportional': + if self.simulation_reinsurance_type == "proportional": self.ask_reinsurance_proportional() - elif self.simulation_reinsurance_type == 'non-proportional': + elif self.simulation_reinsurance_type == "non-proportional": self.ask_reinsurance_non_proportional(time) else: assert False, "Undefined reinsurance type" @@ -180,7 +232,7 @@ def ask_reinsurance_non_proportional(self, time): """Evaluate by risk category""" for categ_id in range(self.simulation_no_risk_categories): """Seek reinsurance only with probability 10% if not already reinsured""" # TODO: find a more generic way to decide whether to request reinsurance for category in this period - if (self.category_reinsurance[categ_id] is None): + if self.category_reinsurance[categ_id] is None: self.ask_reinsurance_non_proportional_by_category(time, categ_id) def characterize_underwritten_risks_by_category(self, time, categ_id): @@ -204,11 +256,13 @@ def characterize_underwritten_risks_by_category(self, time, categ_id): avg_risk_factor += contract.risk_factor number_risks += 1 periodized_total_premium += contract.periodized_premium - if number_risks > 0: + if number_risks > 0: avg_risk_factor /= number_risks return total_value, avg_risk_factor, number_risks, periodized_total_premium - def ask_reinsurance_non_proportional_by_category(self, time, categ_id, purpose='newrisk'): + def ask_reinsurance_non_proportional_by_category( + self, time, categ_id, purpose="newrisk" + ): """Method to create a reinsurance risk for a given category for firm that calls it. Called from increase_ capacity_by_category, ask_reinsurance_non_proportional, and roll_over in metainsuranceorg. Accepts: @@ -221,20 +275,29 @@ def ask_reinsurance_non_proportional_by_category(self, time, categ_id, purpose=' and, assuming firms has underwritten risks in category, creates new reinsurance risk with values based on firms existing underwritten risks. If the method was called to create a new risks then it is appended to list of 'reinrisks', otherwise used for creating the risk when a reinsurance contract rolls over.""" - total_value, avg_risk_factor, number_risks, periodized_total_premium = self.characterize_underwritten_risks_by_category(time, categ_id) - if number_risks > 0: - risk = {"value": total_value, "category": categ_id, "owner": self, - #"identifier": uuid.uuid1(), - "insurancetype": 'excess-of-loss', "number_risks": number_risks, - "deductible_fraction": self.np_reinsurance_deductible_fraction, - "excess_fraction": self.np_reinsurance_excess_fraction, - "periodized_total_premium": periodized_total_premium, "runtime": 12, - "expiration": time + 12, "risk_factor": avg_risk_factor} # TODO: make runtime into a parameter - if purpose == 'newrisk': + total_value, avg_risk_factor, number_risks, periodized_total_premium = self.characterize_underwritten_risks_by_category( + time, categ_id + ) + if number_risks > 0: + risk = { + "value": total_value, + "category": categ_id, + "owner": self, + # "identifier": uuid.uuid1(), + "insurancetype": "excess-of-loss", + "number_risks": number_risks, + "deductible_fraction": self.np_reinsurance_deductible_fraction, + "excess_fraction": self.np_reinsurance_excess_fraction, + "periodized_total_premium": periodized_total_premium, + "runtime": 12, + "expiration": time + 12, + "risk_factor": avg_risk_factor, + } # TODO: make runtime into a parameter + if purpose == "newrisk": self.simulation.append_reinrisks(risk) - elif purpose == 'rollover': + elif purpose == "rollover": return risk - elif number_risks == 0 and purpose == 'rollover': + elif number_risks == 0 and purpose == "rollover": return None def ask_reinsurance_proportional(self): @@ -248,16 +311,25 @@ def ask_reinsurance_proportional(self): nonreinsured.reverse() - if len(nonreinsured) >= (1 - self.reinsurance_limit) * len(self.underwritten_contracts): + if len(nonreinsured) >= (1 - self.reinsurance_limit) * len( + self.underwritten_contracts + ): counter = 0 - limitrein = len(nonreinsured) - (1 - self.reinsurance_limit) * len(self.underwritten_contracts) + limitrein = len(nonreinsured) - (1 - self.reinsurance_limit) * len( + self.underwritten_contracts + ) for contract in nonreinsured: if counter < limitrein: - risk = {"value": contract.value, "category": contract.category, "owner": self, - #"identifier": uuid.uuid1(), - "reinsurance_share": 1., - "expiration": contract.expiration, "contract": contract, - "risk_factor": contract.risk_factor} + risk = { + "value": contract.value, + "category": contract.category, + "owner": self, + # "identifier": uuid.uuid1(), + "reinsurance_share": 1.0, + "expiration": contract.expiration, + "contract": contract, + "risk_factor": contract.risk_factor, + } self.simulation.append_reinrisks(risk) counter += 1 @@ -273,10 +345,14 @@ def add_reinsurance(self, category, excess_fraction, deductible_fraction, contra deductible_fraction: Type Decimal. Value of deductible. contract: Type Class. Reinsurance contract issued to firm. No return values.""" - self.riskmodel.add_reinsurance(category, excess_fraction, deductible_fraction, contract) + self.riskmodel.add_reinsurance( + category, excess_fraction, deductible_fraction, contract + ) self.category_reinsurance[category] = contract - def delete_reinsurance(self, category, excess_fraction, deductible_fraction, contract): + def delete_reinsurance( + self, category, excess_fraction, deductible_fraction, contract + ): """Method called by reinsurancecontract to delete the reinsurance contract from the firms counter for the given category, used so that another reinsurance contract can be issued for that category if needed. Accepts: @@ -285,9 +361,11 @@ def delete_reinsurance(self, category, excess_fraction, deductible_fraction, con deductible_fraction: Type Decimal. Value of deductible. contract: Type Class. Reinsurance contract issued to firm. No return values.""" - self.riskmodel.delete_reinsurance(category, excess_fraction, deductible_fraction, contract) + self.riskmodel.delete_reinsurance( + category, excess_fraction, deductible_fraction, contract + ) self.category_reinsurance[category] = None - + def issue_cat_bond(self, time, categ_id, per_value_per_period_premium=0): """Method to issue cat bond to given firm for given category. Accepts: @@ -298,29 +376,55 @@ def issue_cat_bond(self, time, categ_id, per_value_per_period_premium=0): Method is only called by increase_capacity_by_category method when CatBond prices are lower than reinsurance. It then creates the CatBond as a quasi-reinsurance contract that is paid for immediately (by simulation) with no premium payments.""" - total_value, avg_risk_factor, number_risks, periodized_total_premium = self.characterize_underwritten_risks_by_category(time, categ_id) + total_value, avg_risk_factor, number_risks, periodized_total_premium = self.characterize_underwritten_risks_by_category( + time, categ_id + ) if number_risks > 0: - risk = {"value": total_value, "category": categ_id, "owner": self, - #"identifier": uuid.uuid1(), - "insurancetype": 'excess-of-loss', "number_risks": number_risks, - "deductible_fraction": self.np_reinsurance_deductible_fraction, - "excess_fraction": self.np_reinsurance_excess_fraction, - "periodized_total_premium": 0, "runtime": 12, - "expiration": time + 12, "risk_factor": avg_risk_factor} # TODO: make runtime into a parameter + risk = { + "value": total_value, + "category": categ_id, + "owner": self, + # "identifier": uuid.uuid1(), + "insurancetype": "excess-of-loss", + "number_risks": number_risks, + "deductible_fraction": self.np_reinsurance_deductible_fraction, + "excess_fraction": self.np_reinsurance_excess_fraction, + "periodized_total_premium": 0, + "runtime": 12, + "expiration": time + 12, + "risk_factor": avg_risk_factor, + } # TODO: make runtime into a parameter _, _, var_this_risk, _ = self.riskmodel.evaluate([], self.cash, risk) per_period_premium = per_value_per_period_premium * risk["value"] - total_premium = sum([per_period_premium * ((1/(1+self.interest_rate))**i) for i in range(risk["runtime"])]) + total_premium = sum( + [ + per_period_premium * ((1 / (1 + self.interest_rate)) ** i) + for i in range(risk["runtime"]) + ] + ) catbond = CatBond(self.simulation, per_period_premium, self.simulation) - contract = ReinsuranceContract(catbond, risk, time, 0, risk["runtime"], - self.default_contract_payment_period, \ - expire_immediately=self.simulation_parameters["expire_immediately"], \ - initial_VaR=var_this_risk, insurancetype=risk["insurancetype"]) + contract = ReinsuranceContract( + catbond, + risk, + time, + 0, + risk["runtime"], + self.default_contract_payment_period, + expire_immediately=self.simulation_parameters["expire_immediately"], + initial_VaR=var_this_risk, + insurancetype=risk["insurancetype"], + ) catbond.set_contract(contract) - self.simulation.receive_obligation(var_this_risk, self, time, 'bond') + self.simulation.receive_obligation(var_this_risk, self, time, "bond") """hand cash over to cat bond such that var_this_risk is covered""" - obligation = {"amount": var_this_risk + total_premium, "recipient": catbond, "due_time": time, "purpose": 'bond'} + obligation = { + "amount": var_this_risk + total_premium, + "recipient": catbond, + "due_time": time, + "purpose": "bond", + } self.pay(obligation) self.simulation.accept_agents("catbond", [catbond], time=time) @@ -339,12 +443,17 @@ def make_reinsurance_claims(self, time): categ_id, claims, is_proportional = contract.get_and_reset_current_claim() if is_proportional: claims_this_turn[categ_id] += claims - if (contract.reincontract != None): + if contract.reincontract != None: contract.reincontract.explode(time, claims) for categ_id in range(self.simulation_no_risk_categories): - if claims_this_turn[categ_id] > 0 and self.category_reinsurance[categ_id] is not None: - self.category_reinsurance[categ_id].explode(time, claims_this_turn[categ_id]) + if ( + claims_this_turn[categ_id] > 0 + and self.category_reinsurance[categ_id] is not None + ): + self.category_reinsurance[categ_id].explode( + time, claims_this_turn[categ_id] + ) def get_excess_of_loss_reinsurance(self): """Method to return list containing the reinsurance for each category interms of the reinsurer, value of @@ -355,10 +464,13 @@ def get_excess_of_loss_reinsurance(self): reinsurance = [] for categ_id in range(self.simulation_no_risk_categories): if self.category_reinsurance[categ_id] is not None: - reinsurance_contract = {} - reinsurance_contract["reinsurer"] = self.category_reinsurance[categ_id].insurer - reinsurance_contract["value"] = self.category_reinsurance[categ_id].value - reinsurance_contract["category"] = categ_id - reinsurance.append(reinsurance_contract) + reinsurance_contract = {} + reinsurance_contract["reinsurer"] = self.category_reinsurance[ + categ_id + ].insurer + reinsurance_contract["value"] = self.category_reinsurance[ + categ_id + ].value + reinsurance_contract["category"] = categ_id + reinsurance.append(reinsurance_contract) return reinsurance - diff --git a/insurancesimulation.py b/insurancesimulation.py index ebf2307..3754a35 100644 --- a/insurancesimulation.py +++ b/insurancesimulation.py @@ -15,8 +15,15 @@ import visualization_network -class InsuranceSimulation(): - def __init__(self, override_no_riskmodels, replic_ID, simulation_parameters, rc_event_schedule, rc_event_damage): +class InsuranceSimulation: + def __init__( + self, + override_no_riskmodels, + replic_ID, + simulation_parameters, + rc_event_schedule, + rc_event_damage, + ): """Initialises the simulation (Called from start.py) Accepts: override_no_riskmodels: Boolean determining if number of risk models should be overwritten @@ -29,43 +36,63 @@ def __init__(self, override_no_riskmodels, replic_ID, simulation_parameters, rc_ if override_no_riskmodels: simulation_parameters["no_riskmodels"] = override_no_riskmodels self.number_riskmodels = simulation_parameters["no_riskmodels"] - + "Save parameters, sets parameters of sim according to isleconfig.py" if (replic_ID is None) or isleconfig.force_foreground: - self.background_run = False + self.background_run = False else: self.background_run = True self.replic_ID = replic_ID self.simulation_parameters = simulation_parameters - + "Unpacks parameters and sets distributions" self.catbonds_off = simulation_parameters["catbonds_off"] self.reinsurance_off = simulation_parameters["reinsurance_off"] self.total_no_risks = simulation_parameters["no_risks"] self.risk_factor_lower_bound = simulation_parameters["risk_factor_lower_bound"] - self.cat_separation_distribution = scipy.stats.expon(0, simulation_parameters["event_time_mean_separation"]) - - self.risk_factor_spread = simulation_parameters["risk_factor_upper_bound"] - simulation_parameters["risk_factor_lower_bound"] - self.risk_factor_distribution = scipy.stats.uniform(loc=self.risk_factor_lower_bound, scale=self.risk_factor_spread) + self.cat_separation_distribution = scipy.stats.expon( + 0, simulation_parameters["event_time_mean_separation"] + ) + + self.risk_factor_spread = ( + simulation_parameters["risk_factor_upper_bound"] + - simulation_parameters["risk_factor_lower_bound"] + ) + self.risk_factor_distribution = scipy.stats.uniform( + loc=self.risk_factor_lower_bound, scale=self.risk_factor_spread + ) if not simulation_parameters["risk_factors_present"]: self.risk_factor_distribution = scipy.stats.uniform(loc=1.0, scale=0) self.risk_value_distribution = scipy.stats.uniform(loc=1000, scale=0) risk_factor_mean = self.risk_factor_distribution.mean() - if np.isnan(risk_factor_mean): # unfortunately scipy.stats.mean is not well-defined if scale = 0 + if np.isnan( + risk_factor_mean + ): # unfortunately scipy.stats.mean is not well-defined if scale = 0 risk_factor_mean = self.risk_factor_distribution.rvs() non_truncated = scipy.stats.pareto(b=2, loc=0, scale=0.25) - self.damage_distribution = TruncatedDistWrapper(lower_bound=0.25, upper_bound=1., dist=non_truncated) + self.damage_distribution = TruncatedDistWrapper( + lower_bound=0.25, upper_bound=1.0, dist=non_truncated + ) "set initial market price (normalized, i.e. must be multiplied by value or excess-deductible)" if self.simulation_parameters["expire_immediately"]: assert self.cat_separation_distribution.dist.name == "expon" - expected_damage_frequency = 1 - scipy.stats.poisson(1 / self.simulation_parameters["event_time_mean_separation"] * \ - self.simulation_parameters["mean_contract_runtime"]).pmf(0) + expected_damage_frequency = 1 - scipy.stats.poisson( + 1 + / self.simulation_parameters["event_time_mean_separation"] + * self.simulation_parameters["mean_contract_runtime"] + ).pmf(0) else: - expected_damage_frequency = self.simulation_parameters["mean_contract_runtime"] / \ - self.cat_separation_distribution.mean() - self.norm_premium = expected_damage_frequency * self.damage_distribution.mean() * \ - risk_factor_mean * (1 + self.simulation_parameters["norm_profit_markup"]) + expected_damage_frequency = ( + self.simulation_parameters["mean_contract_runtime"] + / self.cat_separation_distribution.mean() + ) + self.norm_premium = ( + expected_damage_frequency + * self.damage_distribution.mean() + * risk_factor_mean + * (1 + self.simulation_parameters["norm_profit_markup"]) + ) self.reinsurance_market_premium = self.market_premium = self.norm_premium "Set up monetary system (should instead be with the customers, if customers are modeled explicitly)" @@ -77,58 +104,100 @@ def __init__(self, override_no_riskmodels, replic_ID, simulation_parameters, rc_ self.riskcategories = list(range(self.simulation_parameters["no_categories"])) self.rc_event_schedule = [] self.rc_event_damage = [] - self.rc_event_schedule_initial = [] # For debugging (cloud debugging) purposes is good to store the initial schedule of catastrophes - self.rc_event_damage_initial = [] # and damages that will be use in a single run of the model. - - if rc_event_schedule is not None and rc_event_damage is not None: # If we have schedules pass as arguments we used them. + self.rc_event_schedule_initial = ( + [] + ) # For debugging (cloud debugging) purposes is good to store the initial schedule of catastrophes + self.rc_event_damage_initial = ( + [] + ) # and damages that will be use in a single run of the model. + + if ( + rc_event_schedule is not None and rc_event_damage is not None + ): # If we have schedules pass as arguments we used them. self.rc_event_schedule = copy.copy(rc_event_schedule) self.rc_event_schedule_initial = copy.copy(rc_event_schedule) self.rc_event_damage = copy.copy(rc_event_damage) self.rc_event_damage_initial = copy.copy(rc_event_damage) - else: # Otherwise the schedules and damages are generated. + else: # Otherwise the schedules and damages are generated. self.setup_risk_categories_caller() "Set up risks" risk_value_mean = self.risk_value_distribution.mean() - if np.isnan(risk_value_mean): # unfortunately scipy.stats.mean is not well-defined if scale = 0 + if np.isnan( + risk_value_mean + ): # unfortunately scipy.stats.mean is not well-defined if scale = 0 risk_value_mean = self.risk_value_distribution.rvs() - rrisk_factors = self.risk_factor_distribution.rvs(size=self.simulation_parameters["no_risks"]) - rvalues = self.risk_value_distribution.rvs(size=self.simulation_parameters["no_risks"]) - rcategories = np.random.randint(0, self.simulation_parameters["no_categories"], size=self.simulation_parameters["no_risks"]) - self.risks = [{"risk_factor": rrisk_factors[i], "value": rvalues[i], "category": rcategories[i], "owner": self} for i in range(self.simulation_parameters["no_risks"])] - self.risks_counter = [0,0,0,0] + rrisk_factors = self.risk_factor_distribution.rvs( + size=self.simulation_parameters["no_risks"] + ) + rvalues = self.risk_value_distribution.rvs( + size=self.simulation_parameters["no_risks"] + ) + rcategories = np.random.randint( + 0, + self.simulation_parameters["no_categories"], + size=self.simulation_parameters["no_risks"], + ) + self.risks = [ + { + "risk_factor": rrisk_factors[i], + "value": rvalues[i], + "category": rcategories[i], + "owner": self, + } + for i in range(self.simulation_parameters["no_risks"]) + ] + self.risks_counter = [0, 0, 0, 0] for item in self.risks: - self.risks_counter[item["category"]] = self.risks_counter[item["category"]] + 1 - - self.inaccuracy = self.get_all_riskmodel_combinations(self.simulation_parameters["riskmodel_inaccuracy_parameter"]) - - self.inaccuracy = random.sample(self.inaccuracy, self.simulation_parameters["no_riskmodels"]) - - risk_model_configurations = [{"damage_distribution": self.damage_distribution, - "expire_immediately": self.simulation_parameters["expire_immediately"], - "cat_separation_distribution": self.cat_separation_distribution, - "norm_premium": self.norm_premium, - "no_categories": self.simulation_parameters["no_categories"], - "risk_value_mean": risk_value_mean, - "risk_factor_mean": risk_factor_mean, - "norm_profit_markup": self.simulation_parameters["norm_profit_markup"], - "margin_of_safety": self.simulation_parameters["riskmodel_margin_of_safety"], - "var_tail_prob": self.simulation_parameters["value_at_risk_tail_probability"], - "inaccuracy_by_categ": self.inaccuracy[i]} \ - for i in range(self.simulation_parameters["no_riskmodels"])] - + self.risks_counter[item["category"]] = ( + self.risks_counter[item["category"]] + 1 + ) + + self.inaccuracy = self.get_all_riskmodel_combinations( + self.simulation_parameters["riskmodel_inaccuracy_parameter"] + ) + + self.inaccuracy = random.sample( + self.inaccuracy, self.simulation_parameters["no_riskmodels"] + ) + + risk_model_configurations = [ + { + "damage_distribution": self.damage_distribution, + "expire_immediately": self.simulation_parameters["expire_immediately"], + "cat_separation_distribution": self.cat_separation_distribution, + "norm_premium": self.norm_premium, + "no_categories": self.simulation_parameters["no_categories"], + "risk_value_mean": risk_value_mean, + "risk_factor_mean": risk_factor_mean, + "norm_profit_markup": self.simulation_parameters["norm_profit_markup"], + "margin_of_safety": self.simulation_parameters[ + "riskmodel_margin_of_safety" + ], + "var_tail_prob": self.simulation_parameters[ + "value_at_risk_tail_probability" + ], + "inaccuracy_by_categ": self.inaccuracy[i], + } + for i in range(self.simulation_parameters["no_riskmodels"]) + ] + "Setting up agents (to be done from start.py)" self.agent_parameters = {"insurancefirm": [], "reinsurancefirm": []} - self.initialize_agent_parameters("insurancefirm", simulation_parameters, risk_model_configurations) - self.initialize_agent_parameters("reinsurancefirm", simulation_parameters, risk_model_configurations) + self.initialize_agent_parameters( + "insurancefirm", simulation_parameters, risk_model_configurations + ) + self.initialize_agent_parameters( + "reinsurancefirm", simulation_parameters, risk_model_configurations + ) "Agent lists" self.reinsurancefirms = [] self.insurancefirms = [] self.catbonds = [] - + "Lists of agent weights" self.insurers_weights = {} self.reinsurers_weights = {} @@ -136,7 +205,7 @@ def __init__(self, override_no_riskmodels, replic_ID, simulation_parameters, rc_ "List of reinsurance risks offered for underwriting" self.reinrisks = [] self.not_accepted_reinrisks = [] - + "Cumulative variables for history and logging" self.cumulative_bankruptcies = 0 self.cumulative_market_exits = 0 @@ -150,14 +219,22 @@ def __init__(self, override_no_riskmodels, replic_ID, simulation_parameters, rc_ self.selling_reinsurance_firms = [] "Lists for logging history" - self.logger = logger.Logger(no_riskmodels=simulation_parameters["no_riskmodels"], - rc_event_schedule_initial=self.rc_event_schedule_initial, - rc_event_damage_initial=self.rc_event_damage_initial) - - self.insurance_models_counter = np.zeros(self.simulation_parameters["no_categories"]) - self.reinsurance_models_counter = np.zeros(self.simulation_parameters["no_categories"]) - - def initialize_agent_parameters(self, firmtype, simulation_parameters, risk_model_configurations): + self.logger = logger.Logger( + no_riskmodels=simulation_parameters["no_riskmodels"], + rc_event_schedule_initial=self.rc_event_schedule_initial, + rc_event_damage_initial=self.rc_event_damage_initial, + ) + + self.insurance_models_counter = np.zeros( + self.simulation_parameters["no_categories"] + ) + self.reinsurance_models_counter = np.zeros( + self.simulation_parameters["no_categories"] + ) + + def initialize_agent_parameters( + self, firmtype, simulation_parameters, risk_model_configurations + ): """General function for initialising the agent parameters Takes the firm type as argument, also needing sim params and risk configs Creates the agent parameters of both firm types for the initial number specified in isleconfig.py @@ -166,15 +243,23 @@ def initialize_agent_parameters(self, firmtype, simulation_parameters, risk_mode self.insurer_id_counter = 0 no_firms = simulation_parameters["no_insurancefirms"] initial_cash = "initial_agent_cash" - reinsurance_level_lowerbound = simulation_parameters["insurance_reinsurance_levels_lower_bound"] - reinsurance_level_upperbound = simulation_parameters["insurance_reinsurance_levels_upper_bound"] + reinsurance_level_lowerbound = simulation_parameters[ + "insurance_reinsurance_levels_lower_bound" + ] + reinsurance_level_upperbound = simulation_parameters[ + "insurance_reinsurance_levels_upper_bound" + ] elif firmtype == "reinsurancefirm": self.reinsurer_id_counter = 0 no_firms = simulation_parameters["no_reinsurancefirms"] initial_cash = "initial_reinagent_cash" - reinsurance_level_lowerbound = simulation_parameters["reinsurance_reinsurance_levels_lower_bound"] - reinsurance_level_upperbound = simulation_parameters["reinsurance_reinsurance_levels_upper_bound"] + reinsurance_level_lowerbound = simulation_parameters[ + "reinsurance_reinsurance_levels_lower_bound" + ] + reinsurance_level_upperbound = simulation_parameters[ + "reinsurance_reinsurance_levels_upper_bound" + ] for i in range(no_firms): if firmtype == "insurancefirm": @@ -182,26 +267,52 @@ def initialize_agent_parameters(self, firmtype, simulation_parameters, risk_mode elif firmtype == "reinsurancefirm": unique_id = self.get_unique_reinsurer_id() - if simulation_parameters['static_non-proportional_reinsurance_levels']: - reinsurance_level = simulation_parameters["default_non-proportional_reinsurance_deductible"] + if simulation_parameters["static_non-proportional_reinsurance_levels"]: + reinsurance_level = simulation_parameters[ + "default_non-proportional_reinsurance_deductible" + ] else: - reinsurance_level = np.random.uniform(reinsurance_level_lowerbound, reinsurance_level_upperbound) - - riskmodel_config = risk_model_configurations[i % len(risk_model_configurations)] - self.agent_parameters[firmtype].append({'id': unique_id, 'initial_cash': simulation_parameters[initial_cash], - 'riskmodel_config': riskmodel_config, 'norm_premium': self.norm_premium, - 'profit_target': simulation_parameters["norm_profit_markup"], - 'initial_acceptance_threshold': simulation_parameters["initial_acceptance_threshold"], - 'acceptance_threshold_friction': simulation_parameters["acceptance_threshold_friction"], - 'reinsurance_limit': simulation_parameters["reinsurance_limit"], - 'non-proportional_reinsurance_level': reinsurance_level, - 'capacity_target_decrement_threshold': simulation_parameters['capacity_target_decrement_threshold'], - 'capacity_target_increment_threshold': simulation_parameters['capacity_target_increment_threshold'], - 'capacity_target_decrement_factor': simulation_parameters['capacity_target_decrement_factor'], - 'capacity_target_increment_factor': simulation_parameters['capacity_target_increment_factor'], - 'interest_rate': simulation_parameters["interest_rate"]}) - - def build_agents(self, agent_class, agent_class_string, parameters, agent_parameters): + reinsurance_level = np.random.uniform( + reinsurance_level_lowerbound, reinsurance_level_upperbound + ) + + riskmodel_config = risk_model_configurations[ + i % len(risk_model_configurations) + ] + self.agent_parameters[firmtype].append( + { + "id": unique_id, + "initial_cash": simulation_parameters[initial_cash], + "riskmodel_config": riskmodel_config, + "norm_premium": self.norm_premium, + "profit_target": simulation_parameters["norm_profit_markup"], + "initial_acceptance_threshold": simulation_parameters[ + "initial_acceptance_threshold" + ], + "acceptance_threshold_friction": simulation_parameters[ + "acceptance_threshold_friction" + ], + "reinsurance_limit": simulation_parameters["reinsurance_limit"], + "non-proportional_reinsurance_level": reinsurance_level, + "capacity_target_decrement_threshold": simulation_parameters[ + "capacity_target_decrement_threshold" + ], + "capacity_target_increment_threshold": simulation_parameters[ + "capacity_target_increment_threshold" + ], + "capacity_target_decrement_factor": simulation_parameters[ + "capacity_target_decrement_factor" + ], + "capacity_target_increment_factor": simulation_parameters[ + "capacity_target_increment_factor" + ], + "interest_rate": simulation_parameters["interest_rate"], + } + ) + + def build_agents( + self, agent_class, agent_class_string, parameters, agent_parameters + ): """Method for building new agents, only used for re/insurance firms. Loops through the agent parameters for each initialised agent to create an instance of them using re/insurancefirm. Accepts: @@ -215,7 +326,7 @@ def build_agents(self, agent_class, agent_class_string, parameters, agent_parame for ap in agent_parameters: agents.append(agent_class(parameters, ap)) return agents - + def accept_agents(self, agent_class_string, agents, agent_group=None, time=0): """Method to 'accept' agents in that it adds agent to relevant list of agents kept by simulation instance, also adds agent to logger. Also takes created agents initial cash out of economy. @@ -255,9 +366,11 @@ def accept_agents(self, agent_class_string, agents, agent_group=None, time=0): self.catbonds += agents except: print(sys.exc_info()) - pdb.set_trace() + pdb.set_trace() else: - assert False, "Error: Unexpected agent class used {0:s}".format(agent_class_string) + assert False, "Error: Unexpected agent class used {0:s}".format( + agent_class_string + ) def delete_agents(self, agent_class_string, agents): """Method for deleting catbonds as it is only agent that is allowed to be removed @@ -267,8 +380,10 @@ def delete_agents(self, agent_class_string, agents): for agent in agents: self.catbonds.remove(agent) else: - assert False, "Trying to remove unremovable agent, type: {0:s}".format(agent_class_string) - + assert False, "Trying to remove unremovable agent, type: {0:s}".format( + agent_class_string + ) + def iterate(self, t): """Function that is called from start.py for each iteration that settles obligations, capital then reselects risks for the insurance and reinsurance companies to evaluate. Firms are then iterated through to accept @@ -292,7 +407,7 @@ def iterate(self, t): # Pay obligations self.effect_payments(t) - + # Identify perils and effect claims for categ_id in range(len(self.rc_event_schedule)): try: @@ -300,7 +415,10 @@ def iterate(self, t): assert self.rc_event_schedule[categ_id][0] >= t except: print("Something wrong; past events not deleted", file=sys.stderr) - if len(self.rc_event_schedule[categ_id]) > 0 and self.rc_event_schedule[categ_id][0] == t: + if ( + len(self.rc_event_schedule[categ_id]) > 0 + and self.rc_event_schedule[categ_id][0] == t + ): self.rc_event_schedule[categ_id] = self.rc_event_schedule[categ_id][1:] damage_extent = copy.copy(self.rc_event_damage[categ_id][0]) self.inflict_peril(categ_id=categ_id, damage=damage_extent, t=t) @@ -312,18 +430,22 @@ def iterate(self, t): # Provide government aid if damage severe enough if isleconfig.aid_relief is True: self.bank.adjust_aid_budget(time=t) - if 'damage_extent' in locals(): - op_firms = [firm for firm in self.insurancefirms if firm.operational is True] + if "damage_extent" in locals(): + op_firms = [ + firm for firm in self.insurancefirms if firm.operational is True + ] aid_dict = self.bank.provide_aid(op_firms, damage_extent, time=t) for key in aid_dict.keys(): - self.receive_obligation(amount=aid_dict[key], recipient=key, due_time=t, purpose="aid") + self.receive_obligation( + amount=aid_dict[key], recipient=key, due_time=t, purpose="aid" + ) # Shuffle risks (insurance and reinsurance risks) self.shuffle_risks() # Reset reinweights self.reset_reinsurance_weights() - + # Iterate reinsurance firm agents for reinagent in self.reinsurancefirms: if reinagent.operational is True: @@ -358,7 +480,9 @@ def iterate(self, t): for agent in self.catbonds: agent.iterate(t) - self.insurance_models_counter = np.zeros(self.simulation_parameters["no_categories"]) + self.insurance_models_counter = np.zeros( + self.simulation_parameters["no_categories"] + ) for insurer in self.insurancefirms: for i in range(len(self.inaccuracy)): @@ -366,7 +490,9 @@ def iterate(self, t): if insurer.riskmodel.inaccuracy == self.inaccuracy[i]: self.insurance_models_counter[i] += 1 - self.reinsurance_models_counter = np.zeros(self.simulation_parameters["no_categories"]) + self.reinsurance_models_counter = np.zeros( + self.simulation_parameters["no_categories"] + ) for reinsurer in self.reinsurancefirms: for i in range(len(self.inaccuracy)): @@ -392,62 +518,115 @@ def save_data(self): Logger object (self.logger) to be recorded. No arguments. Returns None.""" - + """ collect data """ - total_cash_no = sum([insurancefirm.cash for insurancefirm in self.insurancefirms]) - total_excess_capital = sum([insurancefirm.get_excess_capital() for insurancefirm in self.insurancefirms]) - total_profitslosses = sum([insurancefirm.get_profitslosses() for insurancefirm in self.insurancefirms]) - total_contracts_no = sum([len(insurancefirm.underwritten_contracts) for insurancefirm in self.insurancefirms]) - total_reincash_no = sum([reinsurancefirm.cash for reinsurancefirm in self.reinsurancefirms]) - total_reinexcess_capital = sum([reinsurancefirm.get_excess_capital() for reinsurancefirm in self.reinsurancefirms]) - total_reinprofitslosses = sum([reinsurancefirm.get_profitslosses() for reinsurancefirm in self.reinsurancefirms]) - total_reincontracts_no = sum([len(reinsurancefirm.underwritten_contracts) for reinsurancefirm in self.reinsurancefirms]) - operational_no = sum([insurancefirm.operational for insurancefirm in self.insurancefirms]) - reinoperational_no = sum([reinsurancefirm.operational for reinsurancefirm in self.reinsurancefirms]) + total_cash_no = sum( + [insurancefirm.cash for insurancefirm in self.insurancefirms] + ) + total_excess_capital = sum( + [ + insurancefirm.get_excess_capital() + for insurancefirm in self.insurancefirms + ] + ) + total_profitslosses = sum( + [insurancefirm.get_profitslosses() for insurancefirm in self.insurancefirms] + ) + total_contracts_no = sum( + [ + len(insurancefirm.underwritten_contracts) + for insurancefirm in self.insurancefirms + ] + ) + total_reincash_no = sum( + [reinsurancefirm.cash for reinsurancefirm in self.reinsurancefirms] + ) + total_reinexcess_capital = sum( + [ + reinsurancefirm.get_excess_capital() + for reinsurancefirm in self.reinsurancefirms + ] + ) + total_reinprofitslosses = sum( + [ + reinsurancefirm.get_profitslosses() + for reinsurancefirm in self.reinsurancefirms + ] + ) + total_reincontracts_no = sum( + [ + len(reinsurancefirm.underwritten_contracts) + for reinsurancefirm in self.reinsurancefirms + ] + ) + operational_no = sum( + [insurancefirm.operational for insurancefirm in self.insurancefirms] + ) + reinoperational_no = sum( + [reinsurancefirm.operational for reinsurancefirm in self.reinsurancefirms] + ) catbondsoperational_no = sum([catbond.operational for catbond in self.catbonds]) - + """ collect agent-level data """ - insurance_firms = [(insurancefirm.cash,insurancefirm.id,insurancefirm.operational) for insurancefirm in self.insurancefirms] - reinsurance_firms = [(reinsurancefirm.cash,reinsurancefirm.id,reinsurancefirm.operational) for reinsurancefirm in self.reinsurancefirms] - + insurance_firms = [ + (insurancefirm.cash, insurancefirm.id, insurancefirm.operational) + for insurancefirm in self.insurancefirms + ] + reinsurance_firms = [ + (reinsurancefirm.cash, reinsurancefirm.id, reinsurancefirm.operational) + for reinsurancefirm in self.reinsurancefirms + ] + """ prepare dict """ current_log = {} - current_log['total_cash'] = total_cash_no - current_log['total_excess_capital'] = total_excess_capital - current_log['total_profitslosses'] = total_profitslosses - current_log['total_contracts'] = total_contracts_no - current_log['total_operational'] = operational_no - current_log['total_reincash'] = total_reincash_no - current_log['total_reinexcess_capital'] = total_reinexcess_capital - current_log['total_reinprofitslosses'] = total_reinprofitslosses - current_log['total_reincontracts'] = total_reincontracts_no - current_log['total_reinoperational'] = reinoperational_no - current_log['total_catbondsoperational'] = catbondsoperational_no - current_log['market_premium'] = self.market_premium - current_log['market_reinpremium'] = self.reinsurance_market_premium - current_log['cumulative_bankruptcies'] = self.cumulative_bankruptcies - current_log['cumulative_market_exits'] = self.cumulative_market_exits - current_log['cumulative_unrecovered_claims'] = self.cumulative_unrecovered_claims - current_log['cumulative_claims'] = self.cumulative_claims + current_log["total_cash"] = total_cash_no + current_log["total_excess_capital"] = total_excess_capital + current_log["total_profitslosses"] = total_profitslosses + current_log["total_contracts"] = total_contracts_no + current_log["total_operational"] = operational_no + current_log["total_reincash"] = total_reincash_no + current_log["total_reinexcess_capital"] = total_reinexcess_capital + current_log["total_reinprofitslosses"] = total_reinprofitslosses + current_log["total_reincontracts"] = total_reincontracts_no + current_log["total_reinoperational"] = reinoperational_no + current_log["total_catbondsoperational"] = catbondsoperational_no + current_log["market_premium"] = self.market_premium + current_log["market_reinpremium"] = self.reinsurance_market_premium + current_log["cumulative_bankruptcies"] = self.cumulative_bankruptcies + current_log["cumulative_market_exits"] = self.cumulative_market_exits + current_log[ + "cumulative_unrecovered_claims" + ] = self.cumulative_unrecovered_claims + current_log["cumulative_claims"] = self.cumulative_claims current_log["cumulative_bought_firms"] = self.cumulative_bought_firms - current_log["cumulative_nonregulation_firms"] = self.cumulative_nonregulation_firms - - """ add agent-level data to dict""" - current_log['insurance_firms_cash'] = insurance_firms - current_log['reinsurance_firms_cash'] = reinsurance_firms - - current_log['individual_contracts'] = [] - individual_contracts_no = [len(insurancefirm.underwritten_contracts) for insurancefirm in self.insurancefirms] + current_log[ + "cumulative_nonregulation_firms" + ] = self.cumulative_nonregulation_firms + + """ add agent-level data to dict""" + current_log["insurance_firms_cash"] = insurance_firms + current_log["reinsurance_firms_cash"] = reinsurance_firms + + current_log["individual_contracts"] = [] + individual_contracts_no = [ + len(insurancefirm.underwritten_contracts) + for insurancefirm in self.insurancefirms + ] for i in range(len(individual_contracts_no)): - current_log['individual_contracts'].append(individual_contracts_no[i]) + current_log["individual_contracts"].append(individual_contracts_no[i]) - current_log['reinsurance_contracts'] = [] - reinsurance_contracts_no = [len(reinsurancefirm.underwritten_contracts) for reinsurancefirm in self.reinsurancefirms] + current_log["reinsurance_contracts"] = [] + reinsurance_contracts_no = [ + len(reinsurancefirm.underwritten_contracts) + for reinsurancefirm in self.reinsurancefirms + ] for i in range(len(reinsurance_contracts_no)): - current_log['reinsurance_contracts'].append(reinsurance_contracts_no[i]) + current_log["reinsurance_contracts"].append(reinsurance_contracts_no[i]) if isleconfig.save_network and not isleconfig.slim_log: - adj_list, node_labels, edge_labels, agent_numbers = self.update_network_data() + adj_list, node_labels, edge_labels, agent_numbers = ( + self.update_network_data() + ) else: adj_list = node_labels = edge_labels = agent_numbers = [] current_log["unweighted_network_data"] = adj_list @@ -457,7 +636,7 @@ def save_data(self): """ call to Logger object """ self.logger.record_data(current_log) - + def obtain_log(self, requested_logs=None): """This function allows to return in a list all the data generated by the model. There is no other way to transfer it back from the cloud.""" @@ -471,13 +650,23 @@ def inflict_peril(self, categ_id, damage, t): Given severity of damage from pareto distribution Time iteration No return value""" - affected_contracts = [contract for insurer in self.insurancefirms for contract in insurer.underwritten_contracts if contract.category == categ_id] + affected_contracts = [ + contract + for insurer in self.insurancefirms + for contract in insurer.underwritten_contracts + if contract.category == categ_id + ] if isleconfig.verbose: print("**** PERIL ", damage) - damagevalues = np.random.beta(1, 1./damage -1, size=self.risks_counter[categ_id]) + damagevalues = np.random.beta( + 1, 1.0 / damage - 1, size=self.risks_counter[categ_id] + ) uniformvalues = np.random.uniform(0, 1, size=self.risks_counter[categ_id]) - [contract.explode(t, uniformvalues[i], damagevalues[i]) for i, contract in enumerate(affected_contracts)] - + [ + contract.explode(t, uniformvalues[i], damagevalues[i]) + for i, contract in enumerate(affected_contracts) + ] + def receive_obligation(self, amount, recipient, due_time, purpose): """Method for adding obligation to list that is resolved at the start if each iteration of simulation. Only called by metainsuranceorg for adding interest to cash. @@ -487,7 +676,12 @@ def receive_obligation(self, amount, recipient, due_time, purpose): Due Time Purpose: Reason for obligation (Interest due) Returns None""" - obligation = {"amount": amount, "recipient": recipient, "due_time": due_time, "purpose": purpose} + obligation = { + "amount": amount, + "recipient": recipient, + "due_time": due_time, + "purpose": purpose, + } self.obligations.append(obligation) def effect_payments(self, time): @@ -495,8 +689,10 @@ def effect_payments(self, time): Arguments Current time to allow check if due Returns None""" - due = [item for item in self.obligations if item["due_time"]<=time] - self.obligations = [item for item in self.obligations if item["due_time"]>time] + due = [item for item in self.obligations if item["due_time"] <= time] + self.obligations = [ + item for item in self.obligations if item["due_time"] > time + ] sum_due = sum([item["amount"] for item in due]) for obligation in due: self.pay(obligation) @@ -540,7 +736,11 @@ def reset_reinsurance_weights(self): how many offered reinsurance risks there are.""" self.not_accepted_reinrisks = [] - operational_reinfirms = [reinsurancefirm for reinsurancefirm in self.reinsurancefirms if reinsurancefirm.operational] + operational_reinfirms = [ + reinsurancefirm + for reinsurancefirm in self.reinsurancefirms + if reinsurancefirm.operational + ] operational_no = len(operational_reinfirms) @@ -553,8 +753,8 @@ def reset_reinsurance_weights(self): if operational_no > 0: - if reinrisks_no/operational_no > 1: - weights = reinrisks_no/operational_no + if reinrisks_no / operational_no > 1: + weights = reinrisks_no / operational_no for reinsurer in self.reinsurancefirms: self.reinsurers_weights[reinsurer.id] = math.floor(weights) else: @@ -568,9 +768,15 @@ def reset_insurance_weights(self): """Method for clearing and setting insurance weights dependant on how many insurance companies exist and how many insurance risks are offered. This determined which risks are sent to metainsuranceorg iteration.""" - operational_no = sum([insurancefirm.operational for insurancefirm in self.insurancefirms]) + operational_no = sum( + [insurancefirm.operational for insurancefirm in self.insurancefirms] + ) - operational_firms = [insurancefirm for insurancefirm in self.insurancefirms if insurancefirm.operational] + operational_firms = [ + insurancefirm + for insurancefirm in self.insurancefirms + if insurancefirm.operational + ] risks_no = len(self.risks) @@ -581,8 +787,8 @@ def reset_insurance_weights(self): if operational_no > 0: - if risks_no/operational_no > 1: - weights = risks_no/operational_no + if risks_no / operational_no > 1: + weights = risks_no / operational_no for insurer in self.insurancefirms: self.insurers_weights[insurer.id] = math.floor(weights) else: @@ -604,10 +810,24 @@ def adjust_market_premium(self, capital): with the capital available in the insurance market and viceversa. The premium reduces until it reaches a minimum below which no insurer is willing to reduce further the price. This method is only called in the self.iterate() method of this class.""" - self.market_premium = self.norm_premium * (self.simulation_parameters["upper_price_limit"] - self.simulation_parameters["premium_sensitivity"] * capital / (self.simulation_parameters["initial_agent_cash"] * self.damage_distribution.mean() * self.simulation_parameters["no_risks"])) - if self.market_premium < self.norm_premium * self.simulation_parameters["lower_price_limit"]: - self.market_premium = self.norm_premium * self.simulation_parameters["lower_price_limit"] - + self.market_premium = self.norm_premium * ( + self.simulation_parameters["upper_price_limit"] + - self.simulation_parameters["premium_sensitivity"] + * capital + / ( + self.simulation_parameters["initial_agent_cash"] + * self.damage_distribution.mean() + * self.simulation_parameters["no_risks"] + ) + ) + if ( + self.market_premium + < self.norm_premium * self.simulation_parameters["lower_price_limit"] + ): + self.market_premium = ( + self.norm_premium * self.simulation_parameters["lower_price_limit"] + ) + def adjust_reinsurance_market_premium(self, capital): """Adjust_market_premium Method. Accepts arguments @@ -617,9 +837,23 @@ def adjust_reinsurance_market_premium(self, capital): with the capital available in the reinsurance market and viceversa. The premium reduces until it reaches a minimum below which no reinsurer is willing to reduce further the price. This method is only called in the self.iterate() method of this class.""" - self.reinsurance_market_premium = self.norm_premium * (self.simulation_parameters["upper_price_limit"] - self.simulation_parameters["reinpremium_sensitivity"] * capital / (self.simulation_parameters["initial_agent_cash"] * self.damage_distribution.mean() * self.simulation_parameters["no_risks"])) - if self.reinsurance_market_premium < self.norm_premium * self.simulation_parameters["lower_price_limit"]: - self.reinsurance_market_premium = self.norm_premium * self.simulation_parameters["lower_price_limit"] + self.reinsurance_market_premium = self.norm_premium * ( + self.simulation_parameters["upper_price_limit"] + - self.simulation_parameters["reinpremium_sensitivity"] + * capital + / ( + self.simulation_parameters["initial_agent_cash"] + * self.damage_distribution.mean() + * self.simulation_parameters["no_risks"] + ) + ) + if ( + self.reinsurance_market_premium + < self.norm_premium * self.simulation_parameters["lower_price_limit"] + ): + self.reinsurance_market_premium = ( + self.norm_premium * self.simulation_parameters["lower_price_limit"] + ) def get_market_premium(self): """Get_market_premium Method. @@ -645,10 +879,12 @@ def get_reinsurance_premium(self, np_reinsurance_deductible_fraction): # TODO: cut this out of the insurance market premium -> OBSOLETE?? # TODO: make max_reduction into simulation_parameter ? if self.reinsurance_off: - return float('inf') + return float("inf") max_reduction = 0.1 - return self.reinsurance_market_premium * (1. - max_reduction * np_reinsurance_deductible_fraction) - + return self.reinsurance_market_premium * ( + 1.0 - max_reduction * np_reinsurance_deductible_fraction + ) + def get_cat_bond_price(self, np_reinsurance_deductible_fraction): """Method to calculate and return catbond price. If catbonds are not desired will return infinity so no catbonds will be issued. Otherwise calculates based on reinsurance market premium, catbond premium, deductible fraction. @@ -659,11 +895,13 @@ def get_cat_bond_price(self, np_reinsurance_deductible_fraction): # TODO: implement function dependent on total capital in cat bonds and on deductible () # TODO: make max_reduction and max_CB_surcharge into simulation_parameters ? if self.catbonds_off: - return float('inf') + return float("inf") max_reduction = 0.9 max_CB_surcharge = 0.5 - return self.reinsurance_market_premium * (1. + max_CB_surcharge - max_reduction * np_reinsurance_deductible_fraction) - + return self.reinsurance_market_premium * ( + 1.0 + max_CB_surcharge - max_reduction * np_reinsurance_deductible_fraction + ) + def append_reinrisks(self, item): """Method for appending reinrisks to simulation instance. Called from insurancefirm Accepts: item (Type: List)""" @@ -684,8 +922,8 @@ def solicit_insurance_requests(self, id, cash, insurer): insurer: Type firm metainsuranceorg instance Returns: risks_to_be_sent: Type List""" - risks_to_be_sent = self.risks[:int(self.insurers_weights[insurer.id])] - self.risks = self.risks[int(self.insurers_weights[insurer.id]):] + risks_to_be_sent = self.risks[: int(self.insurers_weights[insurer.id])] + self.risks = self.risks[int(self.insurers_weights[insurer.id]) :] for risk in insurer.risks_kept: risks_to_be_sent.append(risk) @@ -703,8 +941,10 @@ def solicit_reinsurance_requests(self, id, cash, reinsurer): reinsurer: Type firm metainsuranceorg instance Returns: reinrisks_to_be_sent: Type List""" - reinrisks_to_be_sent = self.reinrisks[:int(self.reinsurers_weights[reinsurer.id])] - self.reinrisks = self.reinrisks[int(self.reinsurers_weights[reinsurer.id]):] + reinrisks_to_be_sent = self.reinrisks[ + : int(self.reinsurers_weights[reinsurer.id]) + ] + self.reinrisks = self.reinrisks[int(self.reinsurers_weights[reinsurer.id]) :] for reinrisk in reinsurer.reinrisks_kept: reinrisks_to_be_sent.append(reinrisk) @@ -740,8 +980,10 @@ def get_all_riskmodel_combinations(self, rm_factor): riskmodels: Type list""" riskmodels = [] for i in range(self.simulation_parameters["no_categories"]): - riskmodel_combination = rm_factor * np.ones(self.simulation_parameters["no_categories"]) - riskmodel_combination[i] = 1/rm_factor + riskmodel_combination = rm_factor * np.ones( + self.simulation_parameters["no_categories"] + ) + riskmodel_combination[i] = 1 / rm_factor riskmodels.append(riskmodel_combination.tolist()) return riskmodels @@ -759,12 +1001,18 @@ def setup_risk_categories(self): total += int(math.ceil(separation_time)) if total < self.simulation_parameters["max_time"]: event_schedule.append(total) - event_damage.append(self.damage_distribution.rvs()) #Schedules of catastrophes and damages must me generated at the same time. Reason: replication across different risk models. + event_damage.append( + self.damage_distribution.rvs() + ) # Schedules of catastrophes and damages must me generated at the same time. Reason: replication across different risk models. self.rc_event_schedule.append(event_schedule) self.rc_event_damage.append(event_damage) - self.rc_event_schedule_initial = copy.copy(self.rc_event_damage) #For debugging (cloud debugging) purposes is good to store the initial schedule of catastrophes - self.rc_event_damage_initial = copy.copy(self.rc_event_damage) #and damages that will be use in a single run of the model. + self.rc_event_schedule_initial = copy.copy( + self.rc_event_damage + ) # For debugging (cloud debugging) purposes is good to store the initial schedule of catastrophes + self.rc_event_damage_initial = copy.copy( + self.rc_event_damage + ) # and damages that will be use in a single run of the model. def setup_risk_categories_caller(self): """Method for calling setup_risk_categories. If conditions are set such that the system is replicating it is @@ -781,35 +1029,47 @@ def setup_risk_categories_caller(self): def save_state_and_risk_categories(self): """Method to save numpy Mersenne Twister state and event schedule to allow for replication and continuation.""" mersennetwoster_randomseed = str(np.random.get_state()) - mersennetwoster_randomseed = mersennetwoster_randomseed.replace("\n","").replace("array", "np.array").replace("uint32", "np.uint32") - wfile = open("data/replication_randomseed.dat","a") - wfile.write(mersennetwoster_randomseed+"\n") + mersennetwoster_randomseed = ( + mersennetwoster_randomseed.replace("\n", "") + .replace("array", "np.array") + .replace("uint32", "np.uint32") + ) + wfile = open("data/replication_randomseed.dat", "a") + wfile.write(mersennetwoster_randomseed + "\n") wfile.close() - wfile = open("data/replication_rc_event_schedule.dat","a") - wfile.write(str(self.rc_event_schedule)+"\n") + wfile = open("data/replication_rc_event_schedule.dat", "a") + wfile.write(str(self.rc_event_schedule) + "\n") wfile.close() - + def restore_state_and_risk_categories(self): """Method to access saved event schedule, seed, and Mersenne twister state to allow for continuation.""" - rfile = open("data/replication_rc_event_schedule.dat","r") + rfile = open("data/replication_rc_event_schedule.dat", "r") found = False for i, line in enumerate(rfile): if i == self.replic_ID: self.rc_event_schedule = eval(line) found = True rfile.close() - assert found, "rc event schedule for current replication ID number {0:d} not found in data file. Exiting.".format(self.replic_ID) - rfile = open("data/replication_randomseed.dat","r") + assert ( + found + ), "rc event schedule for current replication ID number {0:d} not found in data file. Exiting.".format( + self.replic_ID + ) + rfile = open("data/replication_randomseed.dat", "r") found = False for i, line in enumerate(rfile): - #print(i, self.replic_ID) + # print(i, self.replic_ID) if i == self.replic_ID: mersennetwister_randomseed = eval(line) found = True rfile.close() np.random.set_state(mersennetwister_randomseed) - assert found, "mersennetwister randomseed for current replication ID number {0:d} not found in data file. Exiting.".format(self.replic_ID) + assert ( + found + ), "mersennetwister randomseed for current replication ID number {0:d} not found in data file. Exiting.".format( + self.replic_ID + ) def insurance_firm_market_entry(self, agent_type="InsuranceFirm"): """Method to determine if re/insurance firm enters the market based on set entry probabilities and a random @@ -823,9 +1083,15 @@ def insurance_firm_market_entry(self, agent_type="InsuranceFirm"): if agent_type == "InsuranceFirm": prob = self.simulation_parameters["insurance_firm_market_entry_probability"] elif agent_type == "ReinsuranceFirm": - prob = self.simulation_parameters["reinsurance_firm_market_entry_probability"] + prob = self.simulation_parameters[ + "reinsurance_firm_market_entry_probability" + ] else: - assert False, "Unknown agent type. Simulation requested to create agent of type {0:s}".format(agent_type) + assert ( + False + ), "Unknown agent type. Simulation requested to create agent of type {0:s}".format( + agent_type + ) if np.random.random() < prob: return True else: @@ -871,7 +1137,7 @@ def record_claims(self, claims): """This method records every claim made to insurers and reinsurers. It is called from both insurers and reinsurers (metainsuranceorg.py).""" self.cumulative_claims += claims - + def log(self): """Method to save the data of the simulation. No accepted values @@ -880,7 +1146,7 @@ def log(self): not for replicating instances. This depends on parameters force_foreground and if the run is replicating or not.""" self.logger.save_log(self.background_run) - + def compute_market_diffvar(self): """Method for calculating difference between number of all firms and the total value at risk. Used only in save data when adding to the logger data dict.""" @@ -909,9 +1175,9 @@ def compute_market_diffvar(self): totalreal = totalreal + sum(varsreinfirms) totaldiff = totalina - totalreal - + return totaldiff - #self.history_logs['market_diffvar'].append(totaldiff) + # self.history_logs['market_diffvar'].append(totaldiff) def get_unique_insurer_id(self): """Method for getting unique id for insurer. Used in initialising agents in start.py and insurancesimulation. @@ -936,14 +1202,18 @@ def insurance_entry_index(self): that is taken from the list of already created parameters. Returns: Indices of the type of riskmodel that the least firms are using.""" - return self.insurance_models_counter[0:self.simulation_parameters["no_riskmodels"]].argmin() + return self.insurance_models_counter[ + 0 : self.simulation_parameters["no_riskmodels"] + ].argmin() def reinsurance_entry_index(self): """Method that returns the entry index for reinsurance firms, i.e. the index for the initial agent parameters that is taken from the list of already created parameters. Returns: Indices of the type of riskmodel that the least reinsurance firms are using.""" - return self.reinsurance_models_counter[0:self.simulation_parameters["no_riskmodels"]].argmin() + return self.reinsurance_models_counter[ + 0 : self.simulation_parameters["no_riskmodels"] + ].argmin() def get_operational(self): """Method to return if simulation is operational. Always true. Used only in pay methods above and @@ -1000,9 +1270,15 @@ def get_firms_to_sell(self, type): Returns: firms_info_sent: Type List of Lists. Contains firm, type and reason.""" if type == "insurer": - firms_info_sent = [(firm, time, reason) for firm, time, reason in self.selling_insurance_firms] + firms_info_sent = [ + (firm, time, reason) + for firm, time, reason in self.selling_insurance_firms + ] elif type == "reinsurer": - firms_info_sent = [(firm, time, reason) for firm, time, reason in self.selling_reinsurance_firms] + firms_info_sent = [ + (firm, time, reason) + for firm, time, reason in self.selling_reinsurance_firms + ] else: print("No accepted type for selling") return firms_info_sent @@ -1048,8 +1324,11 @@ def update_network_data(self): """obtain lists of operational entities""" op_entities = {} num_entities = {} - for firmtype, firmlist in [("insurers", self.insurancefirms), ("reinsurers", self.reinsurancefirms), - ("catbonds", self.catbonds)]: + for firmtype, firmlist in [ + ("insurers", self.insurancefirms), + ("reinsurers", self.reinsurancefirms), + ("catbonds", self.catbonds), + ]: op_firmtype = [firm for firm in firmlist if firm.operational] op_entities[firmtype] = op_firmtype num_entities[firmtype] = len(op_firmtype) @@ -1060,13 +1339,16 @@ def update_network_data(self): weights_matrix = np.zeros(network_size ** 2).reshape(network_size, network_size) edge_labels = {} node_labels = {} - for idx_to, firm in enumerate(op_entities["insurers"] + op_entities["reinsurers"]): + for idx_to, firm in enumerate( + op_entities["insurers"] + op_entities["reinsurers"] + ): node_labels[idx_to] = firm.id eolrs = firm.get_excess_of_loss_reinsurance() for eolr in eolrs: try: idx_from = num_entities["insurers"] + ( - op_entities["reinsurers"] + op_entities["catbonds"]).index(eolr["reinsurer"]) + op_entities["reinsurers"] + op_entities["catbonds"] + ).index(eolr["reinsurer"]) weights_matrix[idx_from][idx_to] = eolr["value"] edge_labels[idx_to, idx_from] = eolr["category"] except ValueError: diff --git a/isleconfig.py b/isleconfig.py index 8a570ea..3383451 100644 --- a/isleconfig.py +++ b/isleconfig.py @@ -3,80 +3,83 @@ force_foreground = False verbose = False showprogress = True -show_network = False # Should network be visualized? This should be False by default, to be overridden by commandline arguments +show_network = ( + False +) # Should network be visualized? This should be False by default, to be overridden by commandline arguments save_network = False -slim_log = False # Should logs be small in ensemble runs (only aggregated level data)? +slim_log = False # Should logs be small in ensemble runs (only aggregated level data)? buy_bankruptcies = False enforce_regulations = True aid_relief = False -simulation_parameters = {"no_categories": 4, - "no_insurancefirms": 20, - "no_reinsurancefirms": 4, - "no_riskmodels": 2, - "riskmodel_inaccuracy_parameter": 2, # values >=1; inaccuracy higher with higher values - "riskmodel_margin_of_safety": 2, # values >=1; factor of additional liquidity beyond value at risk - "margin_increase": 0, # This parameter modifies the margin of safety depending on the number of risks models available in the market. When is 0 all risk models have the same margin of safety. - "value_at_risk_tail_probability": 0.005, # values <1, >0, usually close to 0; tail probability at which the value at risk is taken by the risk models - "norm_profit_markup": 0.15, - "rein_norm_profit_markup": 0.15, - "dividend_share_of_profits": 0.4, - "mean_contract_runtime": 12, - "contract_runtime_halfspread": 2, - "default_contract_payment_period": 3, - "max_time": 1000, - "money_supply": 2000000000, - "event_time_mean_separation": 100 / 3., - "expire_immediately": False, - "risk_factors_present": False, - "risk_factor_lower_bound": 0.4, - "risk_factor_upper_bound": 0.6, - "initial_acceptance_threshold": 0.5, - "acceptance_threshold_friction": 0.9, - "insurance_firm_market_entry_probability": 0.3, #0.02, - "reinsurance_firm_market_entry_probability": 0.05, #0.004, - "simulation_reinsurance_type": 'non-proportional', - "default_non-proportional_reinsurance_deductible": 0.3, - "default_non-proportional_reinsurance_excess": 1.0, - "default_non-proportional_reinsurance_premium_share": 0.3, - "static_non-proportional_reinsurance_levels": False, - "catbonds_off": True, - "reinsurance_off": False, - "capacity_target_decrement_threshold": 1.8, - "capacity_target_increment_threshold": 1.2, - "capacity_target_decrement_factor": 24/25., - "capacity_target_increment_factor": 25/24., - # Retention parameters - "insurance_retention": 0.85, # Ratio of insurance contracts retained every iteration. - "reinsurance_retention": 1, # Ratio of reinsurance contracts retained every iteration. - # Premium sensitivity parameters - "premium_sensitivity": 5, # This parameter represents how sensitive is the variation of the insurance premium with respect of the capital of the market. Higher means more sensitive. - "reinpremium_sensitivity": 6, # This parameter represents how sensitive is the variation of the reinsurance premium with respect of the capital of the market. Higher means more sensitive. - # Balanced portfolio parameters - "insurers_balance_ratio": 0.1, # This ratio represents how low we want to keep the standard deviation of the cash reserved below the mean for insurers. Lower means more balanced. - "reinsurers_balance_ratio": 20, # This ratio represents how low we want to keep the standard deviation of the cash reserved below the mean for reinsurers. Lower means more balanced. (Deactivated for the moment) - "insurers_recursion_limit": 50, # Intensity of the recursion algorithm to balance the portfolio of risks for insurers. - "reinsurers_recursion_limit": 10, # Intensity of the recursion algorithm to balance the portfolio of risks for reinsurers. - # Market permanency parameters - "market_permanency_off": False, # This parameter activates (deactivates) the following market permanency constraints. - "cash_permanency_limit": 100, # This parameter enforces the limit under which the firms leave the market because they cannot underwrite anything. - "insurance_permanency_contracts_limit": 4, # If insurers stay for too long under this limit of contracts they decide to leave the market. - "insurance_permanency_ratio_limit": 0.6, # If insurers stay for too long under this limit they decide to leave the market because they have too much capital. - "insurance_permanency_time_constraint": 24, # This parameter defines the period that the insurers wait if they have few capital or few contract before leaving the market. - "reinsurance_permanency_contracts_limit": 2, # If reinsurers stay for too long under this limit of contracts they decide to leave the market. - "reinsurance_permanency_ratio_limit": 0.8, # If reinsurers stay for too long under this limit they decide to leave the market because they have too much capital. - "reinsurance_permanency_time_constraint": 48, # This parameter defines the period that the reinsurers wait if they have few capital or few contract before leaving the market. - # Insurance and Reinsurance deductibles - "insurance_reinsurance_levels_lower_bound": 0.25, - "insurance_reinsurance_levels_upper_bound": 0.30, - "reinsurance_reinsurance_levels_lower_bound": 0.5, - "reinsurance_reinsurance_levels_upper_bound": 0.95, - "initial_agent_cash": 80000, - "initial_reinagent_cash": 2000000, - "interest_rate": 0.001, - "reinsurance_limit": 0.1, - "upper_price_limit": 1.2, - "lower_price_limit": 0.85, - "no_risks": 20000, - "aid_budget": 1000000} - +simulation_parameters = { + "no_categories": 4, + "no_insurancefirms": 20, + "no_reinsurancefirms": 4, + "no_riskmodels": 2, + "riskmodel_inaccuracy_parameter": 2, # values >=1; inaccuracy higher with higher values + "riskmodel_margin_of_safety": 2, # values >=1; factor of additional liquidity beyond value at risk + "margin_increase": 0, # This parameter modifies the margin of safety depending on the number of risks models available in the market. When is 0 all risk models have the same margin of safety. + "value_at_risk_tail_probability": 0.005, # values <1, >0, usually close to 0; tail probability at which the value at risk is taken by the risk models + "norm_profit_markup": 0.15, + "rein_norm_profit_markup": 0.15, + "dividend_share_of_profits": 0.4, + "mean_contract_runtime": 12, + "contract_runtime_halfspread": 2, + "default_contract_payment_period": 3, + "max_time": 1000, + "money_supply": 2000000000, + "event_time_mean_separation": 100 / 3.0, + "expire_immediately": False, + "risk_factors_present": False, + "risk_factor_lower_bound": 0.4, + "risk_factor_upper_bound": 0.6, + "initial_acceptance_threshold": 0.5, + "acceptance_threshold_friction": 0.9, + "insurance_firm_market_entry_probability": 0.3, # 0.02, + "reinsurance_firm_market_entry_probability": 0.05, # 0.004, + "simulation_reinsurance_type": "non-proportional", + "default_non-proportional_reinsurance_deductible": 0.3, + "default_non-proportional_reinsurance_excess": 1.0, + "default_non-proportional_reinsurance_premium_share": 0.3, + "static_non-proportional_reinsurance_levels": False, + "catbonds_off": True, + "reinsurance_off": False, + "capacity_target_decrement_threshold": 1.8, + "capacity_target_increment_threshold": 1.2, + "capacity_target_decrement_factor": 24 / 25.0, + "capacity_target_increment_factor": 25 / 24.0, + # Retention parameters + "insurance_retention": 0.85, # Ratio of insurance contracts retained every iteration. + "reinsurance_retention": 1, # Ratio of reinsurance contracts retained every iteration. + # Premium sensitivity parameters + "premium_sensitivity": 5, # This parameter represents how sensitive is the variation of the insurance premium with respect of the capital of the market. Higher means more sensitive. + "reinpremium_sensitivity": 6, # This parameter represents how sensitive is the variation of the reinsurance premium with respect of the capital of the market. Higher means more sensitive. + # Balanced portfolio parameters + "insurers_balance_ratio": 0.1, # This ratio represents how low we want to keep the standard deviation of the cash reserved below the mean for insurers. Lower means more balanced. + "reinsurers_balance_ratio": 20, # This ratio represents how low we want to keep the standard deviation of the cash reserved below the mean for reinsurers. Lower means more balanced. (Deactivated for the moment) + "insurers_recursion_limit": 50, # Intensity of the recursion algorithm to balance the portfolio of risks for insurers. + "reinsurers_recursion_limit": 10, # Intensity of the recursion algorithm to balance the portfolio of risks for reinsurers. + # Market permanency parameters + "market_permanency_off": False, # This parameter activates (deactivates) the following market permanency constraints. + "cash_permanency_limit": 100, # This parameter enforces the limit under which the firms leave the market because they cannot underwrite anything. + "insurance_permanency_contracts_limit": 4, # If insurers stay for too long under this limit of contracts they decide to leave the market. + "insurance_permanency_ratio_limit": 0.6, # If insurers stay for too long under this limit they decide to leave the market because they have too much capital. + "insurance_permanency_time_constraint": 24, # This parameter defines the period that the insurers wait if they have few capital or few contract before leaving the market. + "reinsurance_permanency_contracts_limit": 2, # If reinsurers stay for too long under this limit of contracts they decide to leave the market. + "reinsurance_permanency_ratio_limit": 0.8, # If reinsurers stay for too long under this limit they decide to leave the market because they have too much capital. + "reinsurance_permanency_time_constraint": 48, # This parameter defines the period that the reinsurers wait if they have few capital or few contract before leaving the market. + # Insurance and Reinsurance deductibles + "insurance_reinsurance_levels_lower_bound": 0.25, + "insurance_reinsurance_levels_upper_bound": 0.30, + "reinsurance_reinsurance_levels_lower_bound": 0.5, + "reinsurance_reinsurance_levels_upper_bound": 0.95, + "initial_agent_cash": 80000, + "initial_reinagent_cash": 2000000, + "interest_rate": 0.001, + "reinsurance_limit": 0.1, + "upper_price_limit": 1.2, + "lower_price_limit": 0.85, + "no_risks": 20000, + "aid_budget": 1000000, +} diff --git a/listify.py b/listify.py index 591e626..c410c21 100644 --- a/listify.py +++ b/listify.py @@ -1,6 +1,7 @@ """Auxiliary function to transform dicts into lists and back for transfer from cloud (sandman2) to local.""" + def listify(d): """Function to convert dict to list with keys in last list element. Arguments: @@ -8,16 +9,17 @@ def listify(d): Returns: list with dict values as elements [:-1] and dict keys as last element.""" - + """extract keys""" keys = list(d.keys()) - + """create list""" l = [d[key] for key in keys] l.append(keys) - + return l + def delistify(l): """Function to convert listified dict back to dict. Arguments: @@ -26,12 +28,12 @@ def delistify(l): dict keys as list in the last element. Returns: dict - The restored dict.""" - + """extract keys""" keys = l.pop() assert len(keys) == len(l) - + """create dict""" - d = {key: l[i] for i,key in enumerate(keys)} - + d = {key: l[i] for i, key in enumerate(keys)} + return d diff --git a/logger.py b/logger.py index 469b547..7b32baa 100644 --- a/logger.py +++ b/logger.py @@ -7,18 +7,24 @@ import time LOG_DEFAULT = ( - 'total_cash total_excess_capital total_profitslosses total_contracts ' - 'total_operational total_reincash total_reinexcess_capital total_reinprofitslosses ' - 'total_reincontracts total_reinoperational total_catbondsoperational market_premium ' - 'market_reinpremium cumulative_bankruptcies cumulative_market_exits cumulative_unrecovered_claims ' - 'cumulative_claims insurance_firms_cash reinsurance_firms_cash market_diffvar ' - 'rc_event_schedule_initial rc_event_damage_initial number_riskmodels individual_contracts reinsurance_contracts ' - 'unweighted_network_data network_node_labels network_edge_labels number_of_agents ' - 'cumulative_bought_firms cumulative_nonregulation_firms' -).split(' ') - -class Logger(): - def __init__(self, no_riskmodels=None, rc_event_schedule_initial=None, rc_event_damage_initial=None): + "total_cash total_excess_capital total_profitslosses total_contracts " + "total_operational total_reincash total_reinexcess_capital total_reinprofitslosses " + "total_reincontracts total_reinoperational total_catbondsoperational market_premium " + "market_reinpremium cumulative_bankruptcies cumulative_market_exits cumulative_unrecovered_claims " + "cumulative_claims insurance_firms_cash reinsurance_firms_cash market_diffvar " + "rc_event_schedule_initial rc_event_damage_initial number_riskmodels individual_contracts reinsurance_contracts " + "unweighted_network_data network_node_labels network_edge_labels number_of_agents " + "cumulative_bought_firms cumulative_nonregulation_firms" +).split(" ") + + +class Logger: + def __init__( + self, + no_riskmodels=None, + rc_event_schedule_initial=None, + rc_event_damage_initial=None, + ): """Constructor. Prepares history_logs atribute as dict for the logs. Records initial event schedule of simulation run. Arguments @@ -26,11 +32,11 @@ def __init__(self, no_riskmodels=None, rc_event_schedule_initial=None, rc_event_ rc_event_schedule_initial: list of lists of int. Times of risk events by category rc_event_damage_initial: list of arrays (or lists) of float. Damage by peril for each category as share of total possible damage (maximum insured or excess). - Returns class instance.""" - + Returns class instance.""" + """Record number of riskmodels""" self.number_riskmodels = no_riskmodels - + """Record initial event schedule""" self.rc_event_schedule_initial = rc_event_schedule_initial self.rc_event_damage_initial = rc_event_damage_initial @@ -38,42 +44,43 @@ def __init__(self, no_riskmodels=None, rc_event_schedule_initial=None, rc_event_ """Prepare history log dict""" self.history_logs = {} self.history_logs_to_save = [] - + """Variables pertaining to insurance sector""" # TODO: should we not have `cumulative_bankruptcies` and # `cumulative_market_exits` for both insurance firms and reinsurance firms? # `cumulative_claims`: Here are stored the total cumulative claims received # by the whole insurance sector until a certain time. - insurance_sector = ('total_cash total_excess_capital total_profitslosses ' - 'total_contracts total_operational cumulative_bankruptcies ' - 'cumulative_market_exits cumulative_claims cumulative_unrecovered_claims ' - 'cumulative_bought_firms cumulative_nonregulation_firms').split(' ') + insurance_sector = ( + "total_cash total_excess_capital total_profitslosses " + "total_contracts total_operational cumulative_bankruptcies " + "cumulative_market_exits cumulative_claims cumulative_unrecovered_claims " + "cumulative_bought_firms cumulative_nonregulation_firms" + ).split(" ") for _v in insurance_sector: self.history_logs[_v] = [] - + """Variables pertaining to individual insurance firms""" - self.history_logs['individual_contracts'] = [] - self.history_logs['insurance_firms_cash'] = [] - + self.history_logs["individual_contracts"] = [] + self.history_logs["insurance_firms_cash"] = [] + """Variables pertaining to reinsurance sector""" - self.history_logs['total_reincash'] = [] - self.history_logs['total_reinexcess_capital'] = [] - self.history_logs['total_reinprofitslosses'] = [] - self.history_logs['total_reincontracts'] = [] - self.history_logs['total_reinoperational'] = [] + self.history_logs["total_reincash"] = [] + self.history_logs["total_reinexcess_capital"] = [] + self.history_logs["total_reinprofitslosses"] = [] + self.history_logs["total_reincontracts"] = [] + self.history_logs["total_reinoperational"] = [] """Variables pertaining to individual reinsurance firms""" - self.history_logs['reinsurance_firms_cash'] = [] - self.history_logs['reinsurance_contracts'] = [] + self.history_logs["reinsurance_firms_cash"] = [] + self.history_logs["reinsurance_contracts"] = [] """Variables pertaining to cat bonds""" - self.history_logs['total_catbondsoperational'] = [] + self.history_logs["total_catbondsoperational"] = [] """Variables pertaining to premiums""" - self.history_logs['market_premium'] = [] - self.history_logs['market_reinpremium'] = [] - self.history_logs['market_diffvar'] = [] - + self.history_logs["market_premium"] = [] + self.history_logs["market_reinpremium"] = [] + self.history_logs["market_diffvar"] = [] "Network Data Logs to be stored in separate file" self.network_data = {} @@ -91,36 +98,42 @@ def record_data(self, data_dict): """Method to record data for one period Arguments data_dict: Type dict. Data with the same keys as are used in self.history_log(). - Returns None.""" + Returns None.""" for key in data_dict.keys(): - if key != "individual_contracts" and key != 'reinsurance_contracts': + if key != "individual_contracts" and key != "reinsurance_contracts": self.history_logs[key].append(data_dict[key]) if key == "individual_contracts": for i in range(len(data_dict["individual_contracts"])): - self.history_logs['individual_contracts'][i].append(data_dict["individual_contracts"][i]) + self.history_logs["individual_contracts"][i].append( + data_dict["individual_contracts"][i] + ) if key == "reinsurance_contracts": for i in range(len(data_dict["reinsurance_contracts"])): - self.history_logs["reinsurance_contracts"][i].append(data_dict["reinsurance_contracts"][i]) + self.history_logs["reinsurance_contracts"][i].append( + data_dict["reinsurance_contracts"][i] + ) - def obtain_log(self, requested_logs=LOG_DEFAULT): #This function allows to return in a list all the data generated by the model. There is no other way to transfer it back from the cloud. + def obtain_log( + self, requested_logs=LOG_DEFAULT + ): # This function allows to return in a list all the data generated by the model. There is no other way to transfer it back from the cloud. """Method to transfer entire log (self.history_log as well as risk event schedule). This is used to transfer the log to master core from work cores in ensemble runs in the cloud. No arguments. Returns list (listified dict).""" - + """Include environment variables (number of risk models and risk event schedule)""" self.history_logs["number_riskmodels"] = self.number_riskmodels self.history_logs["rc_event_damage_initial"] = self.rc_event_damage_initial self.history_logs["rc_event_schedule_initial"] = self.rc_event_schedule_initial - + """Parse logs to be returned""" if requested_logs == None: requested_logs = LOG_DEFAULT log = {name: self.history_logs[name] for name in requested_logs} - + """Convert to list and return""" return listify.listify(log) - + def restore_logger_object(self, log): """Method to restore logger object. A log can be restored later. It can also be restored on a different machine. This is useful in the case of ensemble runs to move the log to @@ -138,13 +151,18 @@ def restore_logger_object(self, log): self.network_data["network_node_labels"] = log["network_node_labels"] self.network_data["network_edge_labels"] = log["network_edge_labels"] self.network_data["number_of_agents"] = log["number_of_agents"] - del log["number_of_agents"], log["network_edge_labels"], log["network_node_labels"], log["unweighted_network_data"] + del ( + log["number_of_agents"], + log["network_edge_labels"], + log["network_node_labels"], + log["unweighted_network_data"], + ) """Extract environment variables (number of risk models and risk event schedule)""" self.rc_event_schedule_initial = log["rc_event_schedule_initial"] self.rc_event_damage_initial = log["rc_event_damage_initial"] self.number_riskmodels = log["number_riskmodels"] - + """Restore history log""" self.history_logs_to_save.append(log) @@ -154,18 +172,18 @@ def save_log(self, background_run): Arguments: background_run: Type bool. Is this an ensemble run (true) or not (false). Returns None.""" - + """Prepare writing tasks""" if background_run: to_log = self.replication_log_prepare() else: to_log = self.single_log_prepare() - + """Write to disk""" for filename, data, operation_character in to_log: with open(filename, operation_character) as wfile: wfile.write(str(data) + "\n") - + def replication_log_prepare(self): """Method to prepare writing tasks for ensemble run saving. No arguments @@ -178,7 +196,7 @@ def replication_log_prepare(self): to_log = [] to_log.append(("data/" + fpf + "_history_logs.dat", self.history_logs, "a")) return to_log - + def single_log_prepare(self): """Method to prepare writing tasks for single run saving. No arguments @@ -188,7 +206,9 @@ def single_log_prepare(self): Element 3: operation parameter (w-write or a-append).""" to_log = [] filename = "data/single_history_logs.dat" - backupfilename = "data/single_history_logs_old_" + time.strftime('%Y_%b_%d_%H_%M') + ".dat" + backupfilename = ( + "data/single_history_logs_old_" + time.strftime("%Y_%b_%d_%H_%M") + ".dat" + ) if os.path.exists(filename): os.rename(filename, backupfilename) for data in self.history_logs_to_save: @@ -204,7 +224,9 @@ def save_network_data(self, ensemble): filename_prefix = {1: "one", 2: "two", 3: "three", 4: "four"} fpf = filename_prefix[self.number_riskmodels] network_logs = [] - network_logs.append(("data/" + fpf + "_network_data.dat", self.network_data, "a")) + network_logs.append( + ("data/" + fpf + "_network_data.dat", self.network_data, "a") + ) for filename, data, operation_character in network_logs: with open(filename, operation_character) as wfile: @@ -214,25 +236,28 @@ def save_network_data(self, ensemble): wfile.write(str(self.network_data) + "\n") wfile.write(str(self.rc_event_schedule_initial) + "\n") - def add_insurance_agent(self): + def add_insurance_agent(self): """Method for adding an additional insurer agent to the history log. This is necessary to keep the number of individual insurance firm logs constant in time. No arguments. Returns None.""" - if len(self.history_logs['individual_contracts']) > 0: - zeroes_to_append = list(np.zeros(len(self.history_logs['individual_contracts'][0]), dtype=int)) + if len(self.history_logs["individual_contracts"]) > 0: + zeroes_to_append = list( + np.zeros(len(self.history_logs["individual_contracts"][0]), dtype=int) + ) else: zeroes_to_append = [] - self.history_logs['individual_contracts'].append(zeroes_to_append) + self.history_logs["individual_contracts"].append(zeroes_to_append) def add_reinsurance_agent(self): """Method for adding an additional insurer agent to the history log. This is necessary to keep the number of individual insurance firm logs constant in time. No arguments. Returns None.""" - if len(self.history_logs['reinsurance_contracts']) > 0: - zeroes_to_append = list(np.zeros(len(self.history_logs['reinsurance_contracts'][0]), dtype=int)) + if len(self.history_logs["reinsurance_contracts"]) > 0: + zeroes_to_append = list( + np.zeros(len(self.history_logs["reinsurance_contracts"][0]), dtype=int) + ) else: zeroes_to_append = [] - self.history_logs['reinsurance_contracts'].append(zeroes_to_append) - + self.history_logs["reinsurance_contracts"].append(zeroes_to_append) diff --git a/metainsurancecontract.py b/metainsurancecontract.py index c464e33..551a677 100644 --- a/metainsurancecontract.py +++ b/metainsurancecontract.py @@ -1,9 +1,23 @@ import numpy as np import sys, pdb -class MetaInsuranceContract(): - def __init__(self, insurer, properties, time, premium, runtime, payment_period, expire_immediately, initial_VaR=0., \ - insurancetype="proportional", deductible_fraction=None, excess_fraction=None, reinsurance=0): + +class MetaInsuranceContract: + def __init__( + self, + insurer, + properties, + time, + premium, + runtime, + payment_period, + expire_immediately, + initial_VaR=0.0, + insurancetype="proportional", + deductible_fraction=None, + excess_fraction=None, + reinsurance=0, + ): """Constructor method. Accepts arguments insurer: Type InsuranceFirm. @@ -24,15 +38,19 @@ def __init__(self, insurer, properties, time, premium, runtime, payment_period, Creates InsuranceContract, saves parameters. Creates obligation for premium payment. Includes contract in reinsurance network if applicable (e.g. if this is a ReinsuranceContract).""" # TODO: argument reinsurance seems senseless; remove? - + # Save parameters self.insurer = insurer self.risk_factor = properties["risk_factor"] self.category = properties["category"] self.property_holder = properties["owner"] self.value = properties["value"] - self.contract = properties.get("contract") # will assign None if key does not exist - self.insurancetype = properties.get("insurancetype") if insurancetype is None else insurancetype + self.contract = properties.get( + "contract" + ) # will assign None if key does not exist + self.insurancetype = ( + properties.get("insurancetype") if insurancetype is None else insurancetype + ) self.runtime = runtime self.starttime = time self.expiration = runtime + time @@ -40,35 +58,56 @@ def __init__(self, insurer, properties, time, premium, runtime, payment_period, self.terminating = False self.current_claim = 0 self.initial_VaR = initial_VaR - - # set deductible from argument, risk property or default value, whichever first is not None + + # set deductible from argument, risk property or default value, whichever first is not None default_deductible_fraction = 0.0 - deductible_fraction_generator = (item for item in [deductible_fraction, properties.get("deductible_fraction"), \ - default_deductible_fraction] if item is not None) + deductible_fraction_generator = ( + item + for item in [ + deductible_fraction, + properties.get("deductible_fraction"), + default_deductible_fraction, + ] + if item is not None + ) self.deductible_fraction = next(deductible_fraction_generator) self.deductible = self.deductible_fraction * self.value - - # set excess from argument, risk property or default value, whichever first is not None + + # set excess from argument, risk property or default value, whichever first is not None default_excess_fraction = 1.0 - excess_fraction_generator = (item for item in [excess_fraction, properties.get("excess_fraction"), \ - default_excess_fraction] if item is not None) + excess_fraction_generator = ( + item + for item in [ + excess_fraction, + properties.get("excess_fraction"), + default_excess_fraction, + ] + if item is not None + ) self.excess_fraction = next(excess_fraction_generator) self.excess = self.excess_fraction * self.value - + self.reinsurance = reinsurance self.reinsurer = None self.reincontract = None self.reinsurance_share = None # setup payment schedule - total_premium = premium * self.value + total_premium = premium * self.value self.periodized_premium = total_premium / self.runtime - self.payment_times = [time + i for i in range(runtime) if i % payment_period == 0] - self.payment_values = total_premium * (np.ones(len(self.payment_times)) / len(self.payment_times)) + self.payment_times = [ + time + i for i in range(runtime) if i % payment_period == 0 + ] + self.payment_values = total_premium * ( + np.ones(len(self.payment_times)) / len(self.payment_times) + ) if self.contract is not None: - self.contract.reinsure(reinsurer=self.insurer, reinsurance_share=properties["reinsurance_share"], \ - reincontract=self) + self.contract.reinsure( + reinsurer=self.insurer, + reinsurance_share=properties["reinsurance_share"], + reincontract=self, + ) # This flag is set to 1, when the contract is about to expire and there is an attempt to roll it over. self.roll_over_flag = 0 @@ -81,12 +120,14 @@ def check_payment_due(self, time): This method checks if a scheduled premium payment is due, pays it to the insurer, and removes from schedule.""" if len(self.payment_times) > 0 and time >= self.payment_times[0]: # Create obligation for premium payment - self.property_holder.receive_obligation(self.payment_values[0], self.insurer, time, 'premium') - + self.property_holder.receive_obligation( + self.payment_values[0], self.insurer, time, "premium" + ) + # Remove current payment from payment schedule self.payment_times = self.payment_times[1:] self.payment_values = self.payment_values[1:] - + def get_and_reset_current_claim(self): """Method to return and reset claim. No accepted values @@ -107,7 +148,7 @@ def terminate_reinsurance(self, time): Causes any reinsurance contracts to be dissolved as the present contract terminates.""" if self.reincontract is not None: self.reincontract.dissolve(time) - + def dissolve(self, time): """Dissolve method. Accepts arguments @@ -128,9 +169,9 @@ def reinsure(self, reinsurer, reinsurance_share, reincontract): self.reinsurance = self.value * reinsurance_share self.reinsurance_share = reinsurance_share self.reincontract = reincontract - assert self.reinsurance_share in [None, 0.0, 1.0] - - def unreinsure(self): + assert self.reinsurance_share in [None, 0.0, 1.0] + + def unreinsure(self): """Unreinsurance Method. Accepts no arguments: No return value. diff --git a/metainsuranceorg.py b/metainsuranceorg.py index 500d959..75afb03 100644 --- a/metainsuranceorg.py +++ b/metainsuranceorg.py @@ -28,85 +28,129 @@ def init(self, simulation_parameters, agent_parameters): agent_parameters: Type DataDict Constructor creates general instance of an insurance company which is inherited by the reinsurance and insurance firm classes. Initialises all necessary values provided by config file.""" - self.simulation = simulation_parameters['simulation'] + self.simulation = simulation_parameters["simulation"] self.simulation_parameters = simulation_parameters - self.contract_runtime_dist = scipy.stats.randint(simulation_parameters["mean_contract_runtime"] - \ - simulation_parameters["contract_runtime_halfspread"], simulation_parameters["mean_contract_runtime"] \ - + simulation_parameters["contract_runtime_halfspread"] + 1) - self.default_contract_payment_period = simulation_parameters["default_contract_payment_period"] - self.id = agent_parameters['id'] - self.cash = agent_parameters['initial_cash'] + self.contract_runtime_dist = scipy.stats.randint( + simulation_parameters["mean_contract_runtime"] + - simulation_parameters["contract_runtime_halfspread"], + simulation_parameters["mean_contract_runtime"] + + simulation_parameters["contract_runtime_halfspread"] + + 1, + ) + self.default_contract_payment_period = simulation_parameters[ + "default_contract_payment_period" + ] + self.id = agent_parameters["id"] + self.cash = agent_parameters["initial_cash"] self.capacity_target = self.cash * 0.9 - self.capacity_target_decrement_threshold = agent_parameters['capacity_target_decrement_threshold'] - self.capacity_target_increment_threshold = agent_parameters['capacity_target_increment_threshold'] - self.capacity_target_decrement_factor = agent_parameters['capacity_target_decrement_factor'] - self.capacity_target_increment_factor = agent_parameters['capacity_target_increment_factor'] + self.capacity_target_decrement_threshold = agent_parameters[ + "capacity_target_decrement_threshold" + ] + self.capacity_target_increment_threshold = agent_parameters[ + "capacity_target_increment_threshold" + ] + self.capacity_target_decrement_factor = agent_parameters[ + "capacity_target_decrement_factor" + ] + self.capacity_target_increment_factor = agent_parameters[ + "capacity_target_increment_factor" + ] self.excess_capital = self.cash self.premium = agent_parameters["norm_premium"] - self.profit_target = agent_parameters['profit_target'] - self.acceptance_threshold = agent_parameters['initial_acceptance_threshold'] # 0.5 - self.acceptance_threshold_friction = agent_parameters['acceptance_threshold_friction'] # 0.9 #1.0 to switch off + self.profit_target = agent_parameters["profit_target"] + self.acceptance_threshold = agent_parameters[ + "initial_acceptance_threshold" + ] # 0.5 + self.acceptance_threshold_friction = agent_parameters[ + "acceptance_threshold_friction" + ] # 0.9 #1.0 to switch off self.interest_rate = agent_parameters["interest_rate"] self.reinsurance_limit = agent_parameters["reinsurance_limit"] self.simulation_no_risk_categories = simulation_parameters["no_categories"] - self.simulation_reinsurance_type = simulation_parameters["simulation_reinsurance_type"] - self.dividend_share_of_profits = simulation_parameters["dividend_share_of_profits"] - + self.simulation_reinsurance_type = simulation_parameters[ + "simulation_reinsurance_type" + ] + self.dividend_share_of_profits = simulation_parameters[ + "dividend_share_of_profits" + ] + self.owner = self.simulation # TODO: Make this into agent_parameter value? self.per_period_dividend = 0 - self.cash_last_periods = list(np.zeros(12, dtype=int)*self.cash) - - rm_config = agent_parameters['riskmodel_config'] + self.cash_last_periods = list(np.zeros(12, dtype=int) * self.cash) + + rm_config = agent_parameters["riskmodel_config"] """Here we modify the margin of safety depending on the number of risks models available in the market. When is 0 all risk models have the same margin of safety. The reason for doing this is that with more risk models the firms tend to be closer to the max capacity""" - margin_of_safety_correction = (rm_config["margin_of_safety"] + (simulation_parameters["no_riskmodels"] - 1) * simulation_parameters["margin_increase"]) - - self.riskmodel = RiskModel(damage_distribution=rm_config["damage_distribution"], \ - expire_immediately=rm_config["expire_immediately"], \ - cat_separation_distribution=rm_config["cat_separation_distribution"], \ - norm_premium=rm_config["norm_premium"], \ - category_number=rm_config["no_categories"], \ - init_average_exposure=rm_config["risk_value_mean"], \ - init_average_risk_factor=rm_config["risk_factor_mean"], \ - init_profit_estimate=rm_config["norm_profit_markup"], \ - margin_of_safety=margin_of_safety_correction, \ - var_tail_prob=rm_config["var_tail_prob"], \ - inaccuracy=rm_config["inaccuracy_by_categ"]) - - self.category_reinsurance = [None for i in range(self.simulation_no_risk_categories)] - if self.simulation_reinsurance_type == 'non-proportional': - if agent_parameters['non-proportional_reinsurance_level'] is not None: - self.np_reinsurance_deductible_fraction = agent_parameters['non-proportional_reinsurance_level'] + margin_of_safety_correction = ( + rm_config["margin_of_safety"] + + (simulation_parameters["no_riskmodels"] - 1) + * simulation_parameters["margin_increase"] + ) + + self.riskmodel = RiskModel( + damage_distribution=rm_config["damage_distribution"], + expire_immediately=rm_config["expire_immediately"], + cat_separation_distribution=rm_config["cat_separation_distribution"], + norm_premium=rm_config["norm_premium"], + category_number=rm_config["no_categories"], + init_average_exposure=rm_config["risk_value_mean"], + init_average_risk_factor=rm_config["risk_factor_mean"], + init_profit_estimate=rm_config["norm_profit_markup"], + margin_of_safety=margin_of_safety_correction, + var_tail_prob=rm_config["var_tail_prob"], + inaccuracy=rm_config["inaccuracy_by_categ"], + ) + + self.category_reinsurance = [ + None for i in range(self.simulation_no_risk_categories) + ] + if self.simulation_reinsurance_type == "non-proportional": + if agent_parameters["non-proportional_reinsurance_level"] is not None: + self.np_reinsurance_deductible_fraction = agent_parameters[ + "non-proportional_reinsurance_level" + ] else: - self.np_reinsurance_deductible_fraction = simulation_parameters["default_non-proportional_reinsurance_deductible"] - self.np_reinsurance_excess_fraction = simulation_parameters["default_non-proportional_reinsurance_excess"] - self.np_reinsurance_premium_share = simulation_parameters["default_non-proportional_reinsurance_premium_share"] + self.np_reinsurance_deductible_fraction = simulation_parameters[ + "default_non-proportional_reinsurance_deductible" + ] + self.np_reinsurance_excess_fraction = simulation_parameters[ + "default_non-proportional_reinsurance_excess" + ] + self.np_reinsurance_premium_share = simulation_parameters[ + "default_non-proportional_reinsurance_premium_share" + ] self.obligations = [] self.underwritten_contracts = [] self.profits_losses = 0 - #self.reinsurance_contracts = [] + # self.reinsurance_contracts = [] self.operational = True self.warning = False self.age = 0 self.is_insurer = True self.is_reinsurer = False - + """set up risk value estimate variables""" - self.var_counter = 0 # sum over risk model inaccuracies for all contracts - self.var_counter_per_risk = 0 # average risk model inaccuracy across contracts - self.var_sum = 0 # sum over initial VaR for all contracts + self.var_counter = 0 # sum over risk model inaccuracies for all contracts + self.var_counter_per_risk = 0 # average risk model inaccuracy across contracts + self.var_sum = 0 # sum over initial VaR for all contracts self.var_sum_last_periods = list(np.zeros((12, 4), dtype=int)) self.reinsurance_history = list(np.zeros((12, 4, 2), dtype=int)) - self.counter_category = np.zeros(self.simulation_no_risk_categories) # var_counter disaggregated by category - self.var_category = np.zeros(self.simulation_no_risk_categories) # var_sum disaggregated by category + self.counter_category = np.zeros( + self.simulation_no_risk_categories + ) # var_counter disaggregated by category + self.var_category = np.zeros( + self.simulation_no_risk_categories + ) # var_sum disaggregated by category self.naccep = [] self.risks_kept = [] self.reinrisks_kept = [] - self.balance_ratio = simulation_parameters['insurers_balance_ratio'] - self.recursion_limit = simulation_parameters['insurers_recursion_limit'] - self.cash_left_by_categ = [self.cash for i in range(self.simulation_parameters["no_categories"])] + self.balance_ratio = simulation_parameters["insurers_balance_ratio"] + self.recursion_limit = simulation_parameters["insurers_recursion_limit"] + self.cash_left_by_categ = [ + self.cash for i in range(self.simulation_parameters["no_categories"]) + ] self.market_permanency_counter = 0 def iterate(self, time): @@ -124,14 +168,25 @@ def iterate(self, time): """realize due payments""" self.effect_payments(time) if isleconfig.verbose: - print(time, ":", self.id, len(self.underwritten_contracts), self.cash, self.operational) + print( + time, + ":", + self.id, + len(self.underwritten_contracts), + self.cash, + self.operational, + ) self.make_reinsurance_claims(time) """mature contracts""" if isleconfig.verbose: print("Number of underwritten contracts ", len(self.underwritten_contracts)) - maturing = [contract for contract in self.underwritten_contracts if contract.expiration <= time] + maturing = [ + contract + for contract in self.underwritten_contracts + if contract.expiration <= time + ] for contract in maturing: self.underwritten_contracts.remove(contract) contract.mature(time) @@ -142,7 +197,9 @@ def iterate(self, time): # Firms submit cash and var data for regulation every 12 iterations if time % 12 == 0 and isleconfig.enforce_regulations is True: self.submit_regulator_report(time) - if self.operational is False: # If not enough average cash then firm is closed and so no underwriting. + if ( + self.operational is False + ): # If not enough average cash then firm is closed and so no underwriting. return if self.warning is False: @@ -150,27 +207,48 @@ def iterate(self, time): new_nonproportional_risks, new_risks = self.get_newrisks_by_type() contracts_offered = len(new_risks) if isleconfig.verbose and contracts_offered < 2 * contracts_dissolved: - print("Something wrong; agent {0:d} receives too few new contracts {1:d} <= {2:d}".format( - self.id, contracts_offered, 2 * contracts_dissolved)) + print( + "Something wrong; agent {0:d} receives too few new contracts {1:d} <= {2:d}".format( + self.id, contracts_offered, 2 * contracts_dissolved + ) + ) """deal with non-proportional risks first as they must evaluate each request separately""" - [reinrisks_per_categ, number_reinrisks_categ] = self.risks_reinrisks_organizer(new_nonproportional_risks) + [ + reinrisks_per_categ, + number_reinrisks_categ, + ] = self.risks_reinrisks_organizer(new_nonproportional_risks) for repetition in range(self.recursion_limit): former_reinrisks_per_categ = copy.copy(reinrisks_per_categ) - [reinrisks_per_categ, not_accepted_reinrisks] = self.process_newrisks_reinsurer(reinrisks_per_categ, number_reinrisks_categ, time) #Here we process all the new reinrisks in order to keep the portfolio as balanced as possible. + [ + reinrisks_per_categ, + not_accepted_reinrisks, + ] = self.process_newrisks_reinsurer( + reinrisks_per_categ, number_reinrisks_categ, time + ) # Here we process all the new reinrisks in order to keep the portfolio as balanced as possible. if former_reinrisks_per_categ == reinrisks_per_categ: break self.simulation.return_reinrisks(not_accepted_reinrisks) - underwritten_risks = [{"value": contract.value, "category": contract.category, \ - "risk_factor": contract.risk_factor, "deductible": contract.deductible, \ - "excess": contract.excess, "insurancetype": contract.insurancetype, \ - "runtime": contract.runtime} for contract in self.underwritten_contracts if - contract.reinsurance_share != 1.0] + underwritten_risks = [ + { + "value": contract.value, + "category": contract.category, + "risk_factor": contract.risk_factor, + "deductible": contract.deductible, + "excess": contract.excess, + "insurancetype": contract.insurancetype, + "runtime": contract.runtime, + } + for contract in self.underwritten_contracts + if contract.reinsurance_share != 1.0 + ] """obtain risk model evaluation (VaR) for underwriting decisions and for capacity specific decisions""" # TODO: Enable reinsurance shares other than 0.0 and 1.0 - expected_profit, acceptable_by_category, cash_left_by_categ, var_per_risk_per_categ, self.excess_capital = self.riskmodel.evaluate(underwritten_risks, self.cash) + expected_profit, acceptable_by_category, cash_left_by_categ, var_per_risk_per_categ, self.excess_capital = self.riskmodel.evaluate( + underwritten_risks, self.cash + ) # TODO: resolve insurance reinsurance inconsistency (insurer underwrite after capacity decisions, reinsurers before). # This is currently so because it minimizes the number of times we need to run self.riskmodel.evaluate(). # It would also be more consistent if excess capital would be updated at the end of the iteration. @@ -189,17 +267,36 @@ def iterate(self, time): if self.warning is False: """make underwriting decisions, category-wise""" - growth_limit = max(50, 2 * len(self.underwritten_contracts) + contracts_dissolved) + growth_limit = max( + 50, 2 * len(self.underwritten_contracts) + contracts_dissolved + ) if sum(acceptable_by_category) > growth_limit: - acceptable_by_category = np.asarray(acceptable_by_category).astype(np.double) - acceptable_by_category = acceptable_by_category * growth_limit / sum(acceptable_by_category) + acceptable_by_category = np.asarray(acceptable_by_category).astype( + np.double + ) + acceptable_by_category = ( + acceptable_by_category + * growth_limit + / sum(acceptable_by_category) + ) acceptable_by_category = np.int64(np.round(acceptable_by_category)) - [risks_per_categ, number_risks_categ] = self.risks_reinrisks_organizer(new_risks) + [risks_per_categ, number_risks_categ] = self.risks_reinrisks_organizer( + new_risks + ) for repetition in range(self.recursion_limit): former_risks_per_categ = copy.copy(risks_per_categ) - [risks_per_categ, not_accepted_risks] = self.process_newrisks_insurer(risks_per_categ, number_risks_categ, acceptable_by_category, - var_per_risk_per_categ, cash_left_by_categ, time) #Here we process all the new risks in order to keep the portfolio as balanced as possible. + [ + risks_per_categ, + not_accepted_risks, + ] = self.process_newrisks_insurer( + risks_per_categ, + number_risks_categ, + acceptable_by_category, + var_per_risk_per_categ, + cash_left_by_categ, + time, + ) # Here we process all the new risks in order to keep the portfolio as balanced as possible. if former_risks_per_categ == risks_per_categ: break self.simulation.return_risks(not_accepted_risks) @@ -236,9 +333,9 @@ def enter_bankruptcy(self, time): self.simulation.add_firm_to_be_sold(self, time, "record_bankruptcy") self.operational = False else: - self.dissolve(time, 'record_bankruptcy') + self.dissolve(time, "record_bankruptcy") else: - self.dissolve(time, 'record_bankruptcy') + self.dissolve(time, "record_bankruptcy") def market_exit(self, time): """Market_exit Method. @@ -256,7 +353,7 @@ def market_exit(self, time): print("Not enough money to market exit") self.pay(obligation) self.obligations = [] - self.dissolve(time, 'record_market_exit') + self.dissolve(time, "record_market_exit") for contract in self.underwritten_contracts: contract.mature(time) self.underwritten_contracts = [] @@ -274,14 +371,27 @@ def dissolve(self, time, record): next iteration. Finally the type of dissolution is recorded and the operational state is set to false. Different class variables are reset during the process: self.risks_kept, self.reinrisks_kept, self.excess_capital and self.profits_losses.""" - [contract.dissolve(time) for contract in self.underwritten_contracts] # removing (dissolving) all risks immediately after bankruptcy (may not be realistic, they might instead be bought by another company) + [ + contract.dissolve(time) for contract in self.underwritten_contracts + ] # removing (dissolving) all risks immediately after bankruptcy (may not be realistic, they might instead be bought by another company) self.simulation.return_risks(self.risks_kept) self.risks_kept = [] self.reinrisks_kept = [] - obligation = {"amount": self.cash, "recipient": self.simulation, "due_time": time, "purpose": "Dissolution"} - self.pay(obligation) # This MUST be the last obligation before the dissolution of the firm. - self.excess_capital = 0 # Excess of capital is 0 after bankruptcy or market exit. - self.profits_losses = 0 # Profits and losses are 0 after bankruptcy or market exit. + obligation = { + "amount": self.cash, + "recipient": self.simulation, + "due_time": time, + "purpose": "Dissolution", + } + self.pay( + obligation + ) # This MUST be the last obligation before the dissolution of the firm. + self.excess_capital = ( + 0 + ) # Excess of capital is 0 after bankruptcy or market exit. + self.profits_losses = ( + 0 + ) # Profits and losses are 0 after bankruptcy or market exit. method_to_call = getattr(self.simulation, record) method_to_call() @@ -299,7 +409,12 @@ def receive_obligation(self, amount, recipient, due_time, purpose): purpose: Type string, why they are being payed No return value Adds obligation (Type DataDict) to list of obligations owed by the firm.""" - obligation = {"amount": amount, "recipient": recipient, "due_time": due_time, "purpose": purpose} + obligation = { + "amount": amount, + "recipient": recipient, + "due_time": due_time, + "purpose": purpose, + } self.obligations.append(obligation) def effect_payments(self, time): @@ -309,8 +424,10 @@ def effect_payments(self, time): No return value Method checks firms list of obligations to see if ay are due for this time, then pays them. If the firm does not have enough cash then it enters illiquity, leaves the market, and matures all contracts.""" - due = [item for item in self.obligations if item["due_time"]<=time] - self.obligations = [item for item in self.obligations if item["due_time"]>time] + due = [item for item in self.obligations if item["due_time"] <= time] + self.obligations = [ + item for item in self.obligations if item["due_time"] > time + ] sum_due = sum([item["amount"] for item in due]) if sum_due > self.cash: self.obligations += due @@ -333,7 +450,7 @@ def pay(self, obligation): purpose = obligation["purpose"] if self.get_operational() and recipient.get_operational(): self.cash -= amount - if purpose is not 'dividend': + if purpose is not "dividend": self.profits_losses -= amount recipient.receive(amount) @@ -351,8 +468,8 @@ def pay_dividends(self, time): time: Type integer No return value If firm has positive profits will pay percentage of them as dividends. Currently pays to simulation.""" - self.receive_obligation(self.per_period_dividend, self.owner, time, 'dividend') - + self.receive_obligation(self.per_period_dividend, self.owner, time, "dividend") + def get_cash(self): """Method to return agents cash. Only used to calculate total sum of capital to recalculate market premium each iteration. @@ -377,7 +494,7 @@ def get_operational(self): No accepted values Returns Boolean""" return self.operational - + def get_pointer(self): """Method to get pointer. Returns self so renduant? Called only by resume.py""" return self @@ -403,13 +520,17 @@ def estimated_var(self): # Caclulate risks per category and sum of all VaR for category in range(len(self.counter_category)): - self.var_counter += self.counter_category[category] * self.riskmodel.inaccuracy[category] + self.var_counter += ( + self.counter_category[category] * self.riskmodel.inaccuracy[category] + ) self.var_sum += self.var_category[category] # Record reinsurance info for reinsurance in self.category_reinsurance: if reinsurance is not None: - current_reinsurance_info.append([reinsurance.deductible, reinsurance.excess]) + current_reinsurance_info.append( + [reinsurance.deductible, reinsurance.excess] + ) else: current_reinsurance_info.append([0, 0]) @@ -434,14 +555,26 @@ def get_newrisks_by_type(self): new_risks: Type list of DataDicts.""" new_risks = [] if self.is_insurer: - new_risks += self.simulation.solicit_insurance_requests(self.id, self.cash, self) + new_risks += self.simulation.solicit_insurance_requests( + self.id, self.cash, self + ) if self.is_reinsurer: - new_risks += self.simulation.solicit_reinsurance_requests(self.id, self.cash, self) - - new_nonproportional_risks = [risk for risk in new_risks if - risk.get("insurancetype") == 'excess-of-loss' and risk["owner"] is not self] - new_risks = [risk for risk in new_risks if - risk.get("insurancetype") in ['proportional', None] and risk["owner"] is not self] + new_risks += self.simulation.solicit_reinsurance_requests( + self.id, self.cash, self + ) + + new_nonproportional_risks = [ + risk + for risk in new_risks + if risk.get("insurancetype") == "excess-of-loss" + and risk["owner"] is not self + ] + new_risks = [ + risk + for risk in new_risks + if risk.get("insurancetype") in ["proportional", None] + and risk["owner"] is not self + ] return new_nonproportional_risks, new_risks def risks_reinrisks_organizer(self, new_risks): # @@ -453,11 +586,17 @@ def risks_reinrisks_organizer(self, new_risks): # number_risks_categ: Type list, elements are integers of total risks in each category """ - risks_per_categ = [[] for x in range(self.simulation_parameters["no_categories"])] - number_risks_categ = [[] for x in range(self.simulation_parameters["no_categories"])] + risks_per_categ = [ + [] for x in range(self.simulation_parameters["no_categories"]) + ] + number_risks_categ = [ + [] for x in range(self.simulation_parameters["no_categories"]) + ] for categ_id in range(self.simulation_parameters["no_categories"]): - risks_per_categ[categ_id] = [risk for risk in new_risks if risk["category"] == categ_id] + risks_per_categ[categ_id] = [ + risk for risk in new_risks if risk["category"] == categ_id + ] number_risks_categ[categ_id] = len(risks_per_categ[categ_id]) return risks_per_categ, number_risks_categ @@ -474,31 +613,52 @@ def balanced_portfolio(self, risk, cash_left_by_categ, var_per_risk): Boolean cash_left_by_categ: Type list of integers""" - cash_reserved_by_categ = self.cash - cash_left_by_categ # Calculates the cash already reserved by category + cash_reserved_by_categ = ( + self.cash - cash_left_by_categ + ) # Calculates the cash already reserved by category _, std_pre = get_mean_std(cash_reserved_by_categ) cash_reserved_by_categ_store = np.copy(cash_reserved_by_categ) - if risk.get("insurancetype") == 'excess-of-loss': - percentage_value_at_risk = self.riskmodel.getPPF(categ_id=risk["category"], tailSize=self.riskmodel.var_tail_prob) - expected_damage = percentage_value_at_risk * risk["value"] * risk["risk_factor"] \ - * self.riskmodel.inaccuracy[risk["category"]] - expected_claim = min(expected_damage, risk["value"] * risk["excess_fraction"]) - risk["value"] * risk["deductible_fraction"] + if risk.get("insurancetype") == "excess-of-loss": + percentage_value_at_risk = self.riskmodel.getPPF( + categ_id=risk["category"], tailSize=self.riskmodel.var_tail_prob + ) + expected_damage = ( + percentage_value_at_risk + * risk["value"] + * risk["risk_factor"] + * self.riskmodel.inaccuracy[risk["category"]] + ) + expected_claim = ( + min(expected_damage, risk["value"] * risk["excess_fraction"]) + - risk["value"] * risk["deductible_fraction"] + ) # record liquidity requirement and apply margin of safety for liquidity requirement - cash_reserved_by_categ_store[risk["category"]] += expected_claim * self.riskmodel.margin_of_safety #Here it is computed how the cash reserved by category would change if the new reinsurance risk was accepted + cash_reserved_by_categ_store[risk["category"]] += ( + expected_claim * self.riskmodel.margin_of_safety + ) # Here it is computed how the cash reserved by category would change if the new reinsurance risk was accepted else: - cash_reserved_by_categ_store[risk["category"]] += var_per_risk[risk["category"]] #Here it is computed how the cash reserved by category would change if the new insurance risk was accepted + cash_reserved_by_categ_store[risk["category"]] += var_per_risk[ + risk["category"] + ] # Here it is computed how the cash reserved by category would change if the new insurance risk was accepted - mean, std_post = get_mean_std(cash_reserved_by_categ_store) #Here it is computed the mean, std of the cash reserved by category after the new risk of reinrisk is accepted + mean, std_post = get_mean_std( + cash_reserved_by_categ_store + ) # Here it is computed the mean, std of the cash reserved by category after the new risk of reinrisk is accepted total_cash_reserved_by_categ_post = sum(cash_reserved_by_categ_store) - if (std_post * total_cash_reserved_by_categ_post/self.cash) <= (self.balance_ratio * mean) or std_post < std_pre: #The new risk is accepted is the standard deviation is reduced or the cash reserved by category is very well balanced. (std_post) <= (self.balance_ratio * mean) - for i in range(len(cash_left_by_categ)): #The balance condition is not taken into account if the cash reserve is far away from the limit. (total_cash_employed_by_categ_post/self.cash <<< 1) + if (std_post * total_cash_reserved_by_categ_post / self.cash) <= ( + self.balance_ratio * mean + ) or std_post < std_pre: # The new risk is accepted is the standard deviation is reduced or the cash reserved by category is very well balanced. (std_post) <= (self.balance_ratio * mean) + for i in range( + len(cash_left_by_categ) + ): # The balance condition is not taken into account if the cash reserve is far away from the limit. (total_cash_employed_by_categ_post/self.cash <<< 1) cash_left_by_categ[i] = self.cash - cash_reserved_by_categ_store[i] return True, cash_left_by_categ @@ -508,7 +668,9 @@ def balanced_portfolio(self, risk, cash_left_by_categ, var_per_risk): return False, cash_left_by_categ - def process_newrisks_reinsurer(self, reinrisks_per_categ, number_reinrisks_categ, time): + def process_newrisks_reinsurer( + self, reinrisks_per_categ, number_reinrisks_categ, time + ): """Method to decide if new risks are underwritten for the reinsurance firm. Accepts: reinrisks_per_categ: Type List of lists containing new reinsurance risks. @@ -522,31 +684,55 @@ def process_newrisks_reinsurer(self, reinrisks_per_categ, number_reinrisks_categ for iterion in range(max(number_reinrisks_categ)): for categ_id in range(self.simulation_parameters["no_categories"]): - if iterion < number_reinrisks_categ[categ_id] and reinrisks_per_categ[categ_id][iterion] is not None: + if ( + iterion < number_reinrisks_categ[categ_id] + and reinrisks_per_categ[categ_id][iterion] is not None + ): risk_to_insure = reinrisks_per_categ[categ_id][iterion] - underwritten_risks = [{"value": contract.value, "category": contract.category, \ - "risk_factor": contract.risk_factor, - "deductible": contract.deductible, \ - "excess": contract.excess, "insurancetype": contract.insurancetype, \ - "runtime_left": (contract.expiration - time)} for contract in - self.underwritten_contracts if contract.insurancetype == "excess-of-loss"] + underwritten_risks = [ + { + "value": contract.value, + "category": contract.category, + "risk_factor": contract.risk_factor, + "deductible": contract.deductible, + "excess": contract.excess, + "insurancetype": contract.insurancetype, + "runtime_left": (contract.expiration - time), + } + for contract in self.underwritten_contracts + if contract.insurancetype == "excess-of-loss" + ] accept, cash_left_by_categ, var_this_risk, self.excess_capital = self.riskmodel.evaluate( - underwritten_risks, self.cash, - risk_to_insure) + underwritten_risks, self.cash, risk_to_insure + ) if accept: - per_value_reinsurance_premium = self.np_reinsurance_premium_share * risk_to_insure[ - "periodized_total_premium"] * risk_to_insure["runtime"] * (self.simulation.get_market_reinpremium()/self.simulation.get_market_premium()) / risk_to_insure[ - "value"] # TODO: rename this to per_value_premium in insurancecontract.py to avoid confusion - [condition, cash_left_by_categ] = self.balanced_portfolio(risk_to_insure, cash_left_by_categ, None) #Here it is check whether the portfolio is balanced or not if the reinrisk (risk_to_insure) is underwritten. Return True if it is balanced. False otherwise. + per_value_reinsurance_premium = ( + self.np_reinsurance_premium_share + * risk_to_insure["periodized_total_premium"] + * risk_to_insure["runtime"] + * ( + self.simulation.get_market_reinpremium() + / self.simulation.get_market_premium() + ) + / risk_to_insure["value"] + ) # TODO: rename this to per_value_premium in insurancecontract.py to avoid confusion + [condition, cash_left_by_categ] = self.balanced_portfolio( + risk_to_insure, cash_left_by_categ, None + ) # Here it is check whether the portfolio is balanced or not if the reinrisk (risk_to_insure) is underwritten. Return True if it is balanced. False otherwise. if condition: - contract = ReinsuranceContract(self, risk_to_insure, time, per_value_reinsurance_premium, - risk_to_insure["runtime"], \ - self.default_contract_payment_period, \ - expire_immediately=self.simulation_parameters[ - "expire_immediately"], \ - initial_VaR=var_this_risk, \ - insurancetype=risk_to_insure[ - "insurancetype"]) # TODO: implement excess of loss for reinsurance contracts + contract = ReinsuranceContract( + self, + risk_to_insure, + time, + per_value_reinsurance_premium, + risk_to_insure["runtime"], + self.default_contract_payment_period, + expire_immediately=self.simulation_parameters[ + "expire_immediately" + ], + initial_VaR=var_this_risk, + insurancetype=risk_to_insure["insurancetype"], + ) # TODO: implement excess of loss for reinsurance contracts self.underwritten_contracts.append(contract) self.cash_left_by_categ = cash_left_by_categ reinrisks_per_categ[categ_id][iterion] = None @@ -557,11 +743,17 @@ def process_newrisks_reinsurer(self, reinrisks_per_categ, number_reinrisks_categ if reinrisk is not None: not_accepted_reinrisks.append(reinrisk) - - return reinrisks_per_categ, not_accepted_reinrisks - def process_newrisks_insurer(self, risks_per_categ, number_risks_categ, acceptable_by_category, var_per_risk_per_categ, cash_left_by_categ, time): + def process_newrisks_insurer( + self, + risks_per_categ, + number_risks_categ, + acceptable_by_category, + var_per_risk_per_categ, + cash_left_by_categ, + time, + ): """Method to decide if new risks are underwritten for the insurance firm. Accepts: risks_per_categ: Type List of lists containing new risks. @@ -581,36 +773,59 @@ def process_newrisks_insurer(self, risks_per_categ, number_risks_categ, acceptab _cached_rvs = self.contract_runtime_dist.rvs() for iter in range(max(number_risks_categ)): for categ_id in range(len(acceptable_by_category)): - if iter < number_risks_categ[categ_id] and acceptable_by_category[categ_id] > 0 and \ - risks_per_categ[categ_id][iter] is not None: + if ( + iter < number_risks_categ[categ_id] + and acceptable_by_category[categ_id] > 0 + and risks_per_categ[categ_id][iter] is not None + ): risk_to_insure = risks_per_categ[categ_id][iter] - if risk_to_insure.get("contract") is not None and risk_to_insure[ - "contract"].expiration > time: # required to rule out contracts that have exploded in the meantime - [condition, cash_left_by_categ] = self.balanced_portfolio(risk_to_insure, cash_left_by_categ, None) #Here it is check whether the portfolio is balanced or not if the reinrisk (risk_to_insure) is underwritten. Return True if it is balanced. False otherwise. + if ( + risk_to_insure.get("contract") is not None + and risk_to_insure["contract"].expiration > time + ): # required to rule out contracts that have exploded in the meantime + [condition, cash_left_by_categ] = self.balanced_portfolio( + risk_to_insure, cash_left_by_categ, None + ) # Here it is check whether the portfolio is balanced or not if the reinrisk (risk_to_insure) is underwritten. Return True if it is balanced. False otherwise. if condition: - contract = ReinsuranceContract(self, risk_to_insure, time, \ - self.simulation.get_reinsurance_market_premium(), - risk_to_insure["expiration"] - time, \ - self.default_contract_payment_period, \ - expire_immediately=self.simulation_parameters[ - "expire_immediately"], ) + contract = ReinsuranceContract( + self, + risk_to_insure, + time, + self.simulation.get_reinsurance_market_premium(), + risk_to_insure["expiration"] - time, + self.default_contract_payment_period, + expire_immediately=self.simulation_parameters[ + "expire_immediately" + ], + ) self.underwritten_contracts.append(contract) self.cash_left_by_categ = cash_left_by_categ risks_per_categ[categ_id][iter] = None else: - [condition, cash_left_by_categ] = self.balanced_portfolio(risk_to_insure, cash_left_by_categ, - var_per_risk_per_categ) #Here it is check whether the portfolio is balanced or not if the risk (risk_to_insure) is underwritten. Return True if it is balanced. False otherwise. + [condition, cash_left_by_categ] = self.balanced_portfolio( + risk_to_insure, cash_left_by_categ, var_per_risk_per_categ + ) # Here it is check whether the portfolio is balanced or not if the risk (risk_to_insure) is underwritten. Return True if it is balanced. False otherwise. if condition: - contract = InsuranceContract(self, risk_to_insure, time, self.simulation.get_market_premium(), \ - _cached_rvs, \ - self.default_contract_payment_period, \ - expire_immediately=self.simulation_parameters[ - "expire_immediately"], \ - initial_VaR=var_per_risk_per_categ[categ_id]) + contract = InsuranceContract( + self, + risk_to_insure, + time, + self.simulation.get_market_premium(), + _cached_rvs, + self.default_contract_payment_period, + expire_immediately=self.simulation_parameters[ + "expire_immediately" + ], + initial_VaR=var_per_risk_per_categ[categ_id], + ) self.underwritten_contracts.append(contract) self.cash_left_by_categ = cash_left_by_categ risks_per_categ[categ_id][iter] = None - acceptable_by_category[categ_id] -= 1 # TODO: allow different values per risk (i.e. sum over value (and reinsurance_share) or exposure instead of counting) + acceptable_by_category[ + categ_id + ] -= ( + 1 + ) # TODO: allow different values per risk (i.e. sum over value (and reinsurance_share) or exposure instead of counting) not_accepted_risks = [] for categ_id in range(len(acceptable_by_category)): @@ -627,7 +842,7 @@ def market_permanency(self, time): No return values. This method determines whether an insurer or reinsurer stays in the market. If it has very few risks underwritten or too much cash left for TOO LONG it eventually leaves the market. If it has very few risks - underwritten it cannot balance the portfolio so it makes sense to leave the market."""# + underwritten it cannot balance the portfolio so it makes sense to leave the market.""" # if not self.simulation_parameters["market_permanency_off"]: @@ -635,30 +850,62 @@ def market_permanency(self, time): avg_cash_left = get_mean(cash_left_by_categ) - if self.cash < self.simulation_parameters["cash_permanency_limit"]: #If their level of cash is so low that they cannot underwrite anything they also leave the market. + if ( + self.cash < self.simulation_parameters["cash_permanency_limit"] + ): # If their level of cash is so low that they cannot underwrite anything they also leave the market. self.market_exit(time) else: if self.is_insurer: - if len(self.underwritten_contracts) < self.simulation_parameters["insurance_permanency_contracts_limit"] or avg_cash_left / self.cash > self.simulation_parameters["insurance_permanency_ratio_limit"]: - #Insurers leave the market if they have contracts under the limit or an excess capital over the limit for too long. + if ( + len(self.underwritten_contracts) + < self.simulation_parameters[ + "insurance_permanency_contracts_limit" + ] + or avg_cash_left / self.cash + > self.simulation_parameters["insurance_permanency_ratio_limit"] + ): + # Insurers leave the market if they have contracts under the limit or an excess capital over the limit for too long. self.market_permanency_counter += 1 else: - self.market_permanency_counter = 0 #All these limits maybe should be parameters in isleconfig.py - - if self.market_permanency_counter >= self.simulation_parameters["insurance_permanency_time_constraint"]: # Here we determine how much is too long. + self.market_permanency_counter = ( + 0 + ) # All these limits maybe should be parameters in isleconfig.py + + if ( + self.market_permanency_counter + >= self.simulation_parameters[ + "insurance_permanency_time_constraint" + ] + ): # Here we determine how much is too long. self.market_exit(time) if self.is_reinsurer: - if len(self.underwritten_contracts) < self.simulation_parameters["reinsurance_permanency_contracts_limit"] or avg_cash_left / self.cash > self.simulation_parameters["reinsurance_permanency_ratio_limit"]: - #Reinsurers leave the market if they have contracts under the limit or an excess capital over the limit for too long. - - self.market_permanency_counter += 1 #Insurers and reinsurers potentially have different reasons to leave the market. That's why the code is duplicated here. + if ( + len(self.underwritten_contracts) + < self.simulation_parameters[ + "reinsurance_permanency_contracts_limit" + ] + or avg_cash_left / self.cash + > self.simulation_parameters[ + "reinsurance_permanency_ratio_limit" + ] + ): + # Reinsurers leave the market if they have contracts under the limit or an excess capital over the limit for too long. + + self.market_permanency_counter += ( + 1 + ) # Insurers and reinsurers potentially have different reasons to leave the market. That's why the code is duplicated here. else: self.market_permanency_counter = 0 - if self.market_permanency_counter >= self.simulation_parameters["reinsurance_permanency_time_constraint"]: # Here we determine how much is too long. + if ( + self.market_permanency_counter + >= self.simulation_parameters[ + "reinsurance_permanency_time_constraint" + ] + ): # Here we determine how much is too long. self.market_exit(time) def register_claim(self, claim): @@ -691,15 +938,24 @@ def roll_over(self, time): are created and destroyed every iteration. The main reason to implemented this method is to avoid a lack of coverage that appears, if contracts are allowed to mature and are evaluated again the next iteration.""" - maturing_next = [contract for contract in self.underwritten_contracts if contract.expiration == time + 1] - if self.operational is False and len(self.underwritten_contracts)>0: + maturing_next = [ + contract + for contract in self.underwritten_contracts + if contract.expiration == time + 1 + ] + if self.operational is False and len(self.underwritten_contracts) > 0: print("rolling over non operational contracts") if self.is_insurer is True: for contract in maturing_next: contract.roll_over_flag = 1 - if np.random.uniform(0,1,1) > self.simulation_parameters["insurance_retention"]: - self.simulation.return_risks([contract.risk_data]) # TODO: This is not a retention, so the roll_over_flag might be confusing in this case + if ( + np.random.uniform(0, 1, 1) + > self.simulation_parameters["insurance_retention"] + ): + self.simulation.return_risks( + [contract.risk_data] + ) # TODO: This is not a retention, so the roll_over_flag might be confusing in this case else: self.risks_kept.append(contract.risk_data) @@ -707,9 +963,14 @@ def roll_over(self, time): for reincontract in maturing_next: if reincontract.property_holder.operational: reincontract.roll_over_flag = 1 - #reinrisk = reincontract.property_holder.create_reinrisk(time, reincontract.category) - reinrisk = reincontract.property_holder.ask_reinsurance_non_proportional_by_category(time, reincontract.category, purpose='rollover') - if np.random.uniform(0,1,1) < self.simulation_parameters["reinsurance_retention"]: + # reinrisk = reincontract.property_holder.create_reinrisk(time, reincontract.category) + reinrisk = reincontract.property_holder.ask_reinsurance_non_proportional_by_category( + time, reincontract.category, purpose="rollover" + ) + if ( + np.random.uniform(0, 1, 1) + < self.simulation_parameters["reinsurance_retention"] + ): if reinrisk is not None: self.reinrisks_kept.append(reinrisk) @@ -726,14 +987,39 @@ def consider_buyout(self, type="insurer"): for firm, time, reason in firms_to_consider: all_firms_cash = self.simulation.get_total_firm_cash(type) - all_obligations = sum([obligation["amount"] for obligation in firm.obligations]) - total_premium = sum([np.mean(contract.payment_values) for contract in firm.underwritten_contracts if len(contract.payment_values) > 0]) - if self.excess_capital > self.riskmodel.margin_of_safety * firm.var_sum + all_obligations - total_premium: - firm_likelihood = 0.25 + (1.5 * firm.cash + np.mean(firm.cash_last_periods[1:12]) + self.cash)/all_firms_cash - firm_likelihood = min(1, 2*firm_likelihood) - firm_price = (firm.var_sum/10) + total_premium + firm.per_period_dividend + all_obligations = sum( + [obligation["amount"] for obligation in firm.obligations] + ) + total_premium = sum( + [ + np.mean(contract.payment_values) + for contract in firm.underwritten_contracts + if len(contract.payment_values) > 0 + ] + ) + if ( + self.excess_capital + > self.riskmodel.margin_of_safety * firm.var_sum + + all_obligations + - total_premium + ): + firm_likelihood = ( + 0.25 + + ( + 1.5 * firm.cash + + np.mean(firm.cash_last_periods[1:12]) + + self.cash + ) + / all_firms_cash + ) + firm_likelihood = min(1, 2 * firm_likelihood) + firm_price = ( + (firm.var_sum / 10) + total_premium + firm.per_period_dividend + ) firm_sell_reason = reason - firms_further_considered.append([firm, firm_likelihood, firm_price, firm_sell_reason]) + firms_further_considered.append( + [firm, firm_likelihood, firm_price, firm_sell_reason] + ) if len(firms_further_considered) > 0: best_likelihood = 0 @@ -757,41 +1043,55 @@ def buyout(self, firm, firm_cost, time): No return values. This method causes buyer to receive obligation to buy firm. Sets all the bought firms contracts as its own. Then clears bought firms contracts and dissolves it. Only called from consider_buyout().""" - self.receive_obligation(firm_cost, self.simulation, time, 'buyout') + self.receive_obligation(firm_cost, self.simulation, time, "buyout") if self.is_insurer and firm.is_insurer: - print("Insurer %i has bought %i for %d with total cash %d" % (self.id, firm.id, firm_cost, self.cash)) + print( + "Insurer %i has bought %i for %d with total cash %d" + % (self.id, firm.id, firm_cost, self.cash) + ) elif self.is_reinsurer and firm.is_reinsurer: - print("Reinsurer %i has bought %i for %d with total cash %d" % (self.id, firm.id, firm_cost, self.cash)) + print( + "Reinsurer %i has bought %i for %d with total cash %d" + % (self.id, firm.id, firm_cost, self.cash) + ) for contract in firm.underwritten_contracts: contract.insurer = self self.underwritten_contracts.append(contract) for obli in firm.obligations: - self.receive_obligation(obli['amount'], obli["recipient"], obli["due_time"], obli["purpose"]) + self.receive_obligation( + obli["amount"], obli["recipient"], obli["due_time"], obli["purpose"] + ) firm.obligations = [] firm.underwritten_contracts = [] - firm.dissolve(time, 'record_bought_firm') + firm.dissolve(time, "record_bought_firm") def submit_regulator_report(self, time): """Method to submit cash, VaR, and reinsurance data to central banks regulate(). Sets a warning or triggers selling of firm if not complying with regulation (holding enough effective capital for risk). No accepted values. No return values.""" - condition = self.simulation.bank.regulate(self.id, self.cash_last_periods, self.var_sum_last_periods, - self.reinsurance_history, self.age) + condition = self.simulation.bank.regulate( + self.id, + self.cash_last_periods, + self.var_sum_last_periods, + self.reinsurance_history, + self.age, + ) if condition == "Good": self.warning = False if condition == "Warning": self.warning = True if condition == "LoseControl": if isleconfig.buy_bankruptcies: - self.simulation.add_firm_to_be_sold(self, time, "record_nonregulation_firm") + self.simulation.add_firm_to_be_sold( + self, time, "record_nonregulation_firm" + ) self.operational = False else: self.dissolve(time, "record_nonregulation_firm") for contract in self.underwritten_contracts: contract.mature(time) self.underwritten_contracts = [] - diff --git a/reinsurancecontract.py b/reinsurancecontract.py index 22fbf10..ed868de 100644 --- a/reinsurancecontract.py +++ b/reinsurancecontract.py @@ -1,6 +1,7 @@ import numpy as np -from metainsurancecontract import MetaInsuranceContract +from metainsurancecontract import MetaInsuranceContract + class ReinsuranceContract(MetaInsuranceContract): """ReinsuranceContract class. @@ -9,18 +10,48 @@ class ReinsuranceContract(MetaInsuranceContract): and ReinsuranceContract objects. The signature of this class' constructor is the same as that of the InsuranceContract constructor. The class has two methods (explode, mature) that overwrite methods in InsuranceContract.""" - def __init__(self, insurer, properties, time, premium, runtime, payment_period, expire_immediately, initial_VaR=0.,\ - insurancetype="proportional", deductible_fraction=None, excess_fraction=None, reinsurance=0): - super(ReinsuranceContract, self).__init__(insurer, properties, time, premium, runtime, payment_period, \ - expire_immediately, initial_VaR, insurancetype, deductible_fraction, excess_fraction, reinsurance) - #self.is_reinsurancecontract = True - + + def __init__( + self, + insurer, + properties, + time, + premium, + runtime, + payment_period, + expire_immediately, + initial_VaR=0.0, + insurancetype="proportional", + deductible_fraction=None, + excess_fraction=None, + reinsurance=0, + ): + super(ReinsuranceContract, self).__init__( + insurer, + properties, + time, + premium, + runtime, + payment_period, + expire_immediately, + initial_VaR, + insurancetype, + deductible_fraction, + excess_fraction, + reinsurance, + ) + # self.is_reinsurancecontract = True + if self.insurancetype == "excess-of-loss": - self.property_holder.add_reinsurance(category=self.category, excess_fraction=self.excess_fraction, \ - deductible_fraction=self.deductible_fraction, contract=self) + self.property_holder.add_reinsurance( + category=self.category, + excess_fraction=self.excess_fraction, + deductible_fraction=self.deductible_fraction, + contract=self, + ) else: assert self.contract is not None - + def explode(self, time, damage_extent=None): """Explode method. Accepts arguments @@ -34,19 +65,25 @@ def explode(self, time, damage_extent=None): if self.insurancetype == "excess-of-loss" and damage_extent > self.deductible: claim = min(self.excess, damage_extent) - self.deductible - self.insurer.receive_obligation(claim, self.property_holder, time, 'claim') + self.insurer.receive_obligation(claim, self.property_holder, time, "claim") else: claim = min(self.excess, damage_extent) - self.deductible - self.insurer.receive_obligation(claim, self.property_holder, time + 1, 'claim') + self.insurer.receive_obligation( + claim, self.property_holder, time + 1, "claim" + ) # Reinsurer pays as soon as possible. - self.insurer.register_claim(claim) #Every reinsurance claim made is immediately registered. + self.insurer.register_claim( + claim + ) # Every reinsurance claim made is immediately registered. if self.expire_immediately: - self.current_claim += self.contract.claim # TODO: should proportional reinsurance claims be subject to excess_of_loss retrocession? If so, reorganize more straightforwardly - + self.current_claim += ( + self.contract.claim + ) # TODO: should proportional reinsurance claims be subject to excess_of_loss retrocession? If so, reorganize more straightforwardly + self.expiration = time - #self.terminating = True - + # self.terminating = True + def mature(self, time): """Mature method. Accepts arguments @@ -54,12 +91,15 @@ def mature(self, time): No return value. Removes any reinsurance functions this contract has and terminates any reinsurance contracts for this contract.""" - #self.terminating = True + # self.terminating = True self.terminate_reinsurance(time) - + if self.insurancetype == "excess-of-loss": - self.property_holder.delete_reinsurance(category=self.category, excess_fraction=self.excess_fraction, \ - deductible_fraction=self.deductible_fraction, contract=self) - else: #TODO: ? Instead: if self.insurancetype == "proportional": + self.property_holder.delete_reinsurance( + category=self.category, + excess_fraction=self.excess_fraction, + deductible_fraction=self.deductible_fraction, + contract=self, + ) + else: # TODO: ? Instead: if self.insurancetype == "proportional": self.contract.unreinsure() - diff --git a/reinsurancefirm.py b/reinsurancefirm.py index 1c1558b..ac2564f 100644 --- a/reinsurancefirm.py +++ b/reinsurancefirm.py @@ -1,9 +1,11 @@ -#from metainsuranceorg import MetaInsuranceOrg +# from metainsuranceorg import MetaInsuranceOrg from insurancefirm import InsuranceFirm + class ReinsuranceFirm(InsuranceFirm): """ReinsuranceFirm class. Inherits from InsuranceFirm.""" + def init(self, simulation_parameters, agent_parameters): """Constructor method. Accepts arguments diff --git a/resume.py b/resume.py index 49f374f..33ce1ec 100644 --- a/resume.py +++ b/resume.py @@ -6,7 +6,7 @@ import argparse import pickle import hashlib -import random +import random # import config file and apply configuration import isleconfig @@ -16,17 +16,46 @@ override_no_riskmodels = False # use argparse to handle command line arguments -parser = argparse.ArgumentParser(description='Model the Insurance sector') -parser.add_argument("--oneriskmodel", action="store_true", help="allow overriding the number of riskmodels from the standard config (with 1)") -parser.add_argument("--riskmodels", type=int, choices=[1,2,3,4], help="allow overriding the number of riskmodels from standard config (with 1 or other numbers)") -parser.add_argument("--replicid", type=int, help="if replication ID is given, pass this to the simulation so that the risk profile can be restored") -parser.add_argument("--replicating", action="store_true", help="if this is a simulation run designed to replicate another, override the config file parameter") -parser.add_argument("--randomseed", type=float, help="allow setting of numpy random seed") -parser.add_argument("--foreground", action="store_true", help="force foreground runs even if replication ID is given (which defaults to background runs)") -parser.add_argument("--shownetwork", action="store_true", help="show reinsurance relations as network") +parser = argparse.ArgumentParser(description="Model the Insurance sector") +parser.add_argument( + "--oneriskmodel", + action="store_true", + help="allow overriding the number of riskmodels from the standard config (with 1)", +) +parser.add_argument( + "--riskmodels", + type=int, + choices=[1, 2, 3, 4], + help="allow overriding the number of riskmodels from standard config (with 1 or other numbers)", +) +parser.add_argument( + "--replicid", + type=int, + help="if replication ID is given, pass this to the simulation so that the risk profile can be restored", +) +parser.add_argument( + "--replicating", + action="store_true", + help="if this is a simulation run designed to replicate another, override the config file parameter", +) +parser.add_argument( + "--randomseed", type=float, help="allow setting of numpy random seed" +) +parser.add_argument( + "--foreground", + action="store_true", + help="force foreground runs even if replication ID is given (which defaults to background runs)", +) +parser.add_argument( + "--shownetwork", action="store_true", help="show reinsurance relations as network" +) parser.add_argument("-p", "--showprogress", action="store_true", help="show timesteps") parser.add_argument("-v", "--verbose", action="store_true", help="more detailed output") -parser.add_argument("--save_iterations", type=int, help="number of iterations to iterate before saving world state") +parser.add_argument( + "--save_iterations", + type=int, + help="number of iterations to iterate before saving world state", +) args = parser.parse_args() if args.oneriskmodel: @@ -38,7 +67,9 @@ replic_ID = args.replicid if args.replicating: isleconfig.replicating = True - assert replic_ID is not None, "Error: Replication requires a replication ID to identify run to be replicated" + assert ( + replic_ID is not None + ), "Error: Replication requires a replication ID to identify run to be replicated" if args.randomseed: randomseed = args.randomseed seed = int(randomseed) @@ -64,7 +95,7 @@ # main function def main(): - + with open("data/simulation_save.pkl", "br") as rfile: d = pickle.load(rfile) simulation = d["simulation"] @@ -79,8 +110,12 @@ def main(): event_schedule_trunc = [] event_damage_trunc = [] for categ in range(simulation_parameters["no_categories"]): - event_schedule_trunc.append([event for event in event_schedule[categ] if event >= time]) - event_damage_trunc.append(event_damage[categ][-len(event_schedule_trunc[categ]):]) + event_schedule_trunc.append( + [event for event in event_schedule[categ] if event >= time] + ) + event_damage_trunc.append( + event_damage[categ][-len(event_schedule_trunc[categ]) :] + ) insurancefirms_group = list(simulation.insurancefirms) reinsurancefirms_group = list(simulation.reinsurancefirms) @@ -90,40 +125,58 @@ def main(): random.setstate(random_seed) for t in range(time, simulation_parameters["max_time"]): - + # create new agents # TODO: write method for this; this code block is executed almost identically 4 times if world.insurance_firm_market_entry(agent_type="InsuranceFirm"): parameters = [np.random.choice(world.agent_parameters["insurancefirm"])] parameters[0]["id"] = world.get_unique_insurer_id() - new_insurance_firm = simulation.build_agents(InsuranceFirm, - 'insurancefirm', - parameters=simulation_parameters, - agent_parameters=parameters) + new_insurance_firm = simulation.build_agents( + InsuranceFirm, + "insurancefirm", + parameters=simulation_parameters, + agent_parameters=parameters, + ) insurancefirms_group += new_insurance_firm new_insurancefirm_pointer = new_insurance_firm - world.accept_agents("insurancefirm", new_insurancefirm_pointer, new_insurance_firm, time=t) - + world.accept_agents( + "insurancefirm", new_insurancefirm_pointer, new_insurance_firm, time=t + ) + if world.insurance_firm_market_entry(agent_type="ReinsuranceFirm"): parameters = [np.random.choice(world.agent_parameters["reinsurancefirm"])] parameters[0]["id"] = world.get_unique_reinsurer_id() - new_reinsurance_firm = simulation.build_agents(ReinsuranceFirm, - 'reinsurancefirm', - parameters=simulation_parameters, - agent_parameters=parameters) + new_reinsurance_firm = simulation.build_agents( + ReinsuranceFirm, + "reinsurancefirm", + parameters=simulation_parameters, + agent_parameters=parameters, + ) reinsurancefirms_group += new_reinsurance_firm new_reinsurancefirm_pointer = new_reinsurance_firm - world.accept_agents("reinsurancefirm", new_reinsurancefirm_pointer, new_reinsurance_firm, time=t) - + world.accept_agents( + "reinsurancefirm", + new_reinsurancefirm_pointer, + new_reinsurance_firm, + time=t, + ) + # iterate simulation world.iterate(t) - + # log data world.save_data() - - if t > 0 and t//50 == t/50: - save_simulation(t, simulation, simulation_parameters, event_schedule, event_damage, exit_now=False) - #print("here") - + + if t > 0 and t // 50 == t / 50: + save_simulation( + t, + simulation, + simulation_parameters, + event_schedule, + event_damage, + exit_now=False, + ) + # print("here") + # finish simulation, write logs simulation.finalize() @@ -142,9 +195,11 @@ def save_simulation(t, sim, sim_param, event_schedule, event_damage, exit_now=Fa pickle.dump(d, wfile, protocol=pickle.HIGHEST_PROTOCOL) with open("data/simulation_resave.pkl", "br") as rfile: file_contents = rfile.read() - #print("\nSimulation hashes: ", hashlib.sha512(str(d).encode()).hexdigest(), "; ", hashlib.sha512(str(file_contents).encode()).hexdigest()) - # note that the hash over the dict is for some reason not identical between runs. The hash over the state saved to the file is. - print("\nSimulation hash: ", hashlib.sha512(str(file_contents).encode()).hexdigest()) + # print("\nSimulation hashes: ", hashlib.sha512(str(d).encode()).hexdigest(), "; ", hashlib.sha512(str(file_contents).encode()).hexdigest()) + # note that the hash over the dict is for some reason not identical between runs. The hash over the state saved to the file is. + print( + "\nSimulation hash: ", hashlib.sha512(str(file_contents).encode()).hexdigest() + ) if exit_now: exit(0) diff --git a/riskmodel.py b/riskmodel.py index ed259c0..d437c06 100644 --- a/riskmodel.py +++ b/riskmodel.py @@ -7,9 +7,20 @@ class RiskModel: - def __init__(self, damage_distribution, expire_immediately, cat_separation_distribution, norm_premium, \ - category_number, init_average_exposure, init_average_risk_factor, init_profit_estimate, \ - margin_of_safety, var_tail_prob, inaccuracy): + def __init__( + self, + damage_distribution, + expire_immediately, + cat_separation_distribution, + norm_premium, + category_number, + init_average_exposure, + init_average_risk_factor, + init_profit_estimate, + margin_of_safety, + var_tail_prob, + inaccuracy, + ): """Initialising method for RiskModel class. All accepted arguments are initialised in the __init__ method of insurancesimulation, and are mostly from isleconfig.py. """ self.cat_separation_distribution = cat_separation_distribution @@ -23,18 +34,20 @@ def __init__(self, damage_distribution, expire_immediately, cat_separation_distr self.margin_of_safety = margin_of_safety """damage_distribution is some scipy frozen rv distribution which is bound between 0 and 1 and indicates the share of risks suffering damage as part of any single catastrophic peril""" - self.damage_distribution = [damage_distribution for _ in range(self.category_number)] - self.damage_distribution_stack = [[] for _ in range(self.category_number)] + self.damage_distribution = [ + damage_distribution for _ in range(self.category_number) + ] + self.damage_distribution_stack = [[] for _ in range(self.category_number)] self.reinsurance_contract_stack = [[] for _ in range(self.category_number)] self.inaccuracy = inaccuracy - + def getPPF(self, categ_id, tailSize): """Method for getting quantile function of the damage distribution (value at risk) by category. Positional arguments: categ_id integer: category tailSize (float 1=>x=>0): quantile Returns value-at-risk.""" - return self.damage_distribution[categ_id].ppf(1-tailSize) + return self.damage_distribution[categ_id].ppf(1 - tailSize) def get_categ_risks(self, risks, categ_id): """Method takes list of all risks and only returns a list of all the risks belonging to the given category. @@ -46,7 +59,7 @@ def get_categ_risks(self, risks, categ_id): categ_risks = [risk for risk in risks if risk["category"] == categ_id] return categ_risks - def compute_expectation(self, categ_risks, categ_id): #TODO: more intuitive name? + def compute_expectation(self, categ_risks, categ_id): # TODO: more intuitive name? """Method to compute the average exposure and risk factor as well as the increase in expected profits for the risks in a given category. Accepts: @@ -61,25 +74,25 @@ def compute_expectation(self, categ_risks, categ_id): #TODO: more intuitive runtimes = [] for risk in categ_risks: # TODO: factor in excess instead of value? - exposures.append(risk["value"]-risk["deductible"]) + exposures.append(risk["value"] - risk["deductible"]) risk_factors.append(risk["risk_factor"]) runtimes.append(risk["runtime"]) average_exposure = np.mean(exposures) average_risk_factor = self.inaccuracy[categ_id] * np.mean(risk_factors) mean_runtime = np.mean(runtimes) - + if self.expire_immediately: incr_expected_profits = -1 # TODO: fix the norm_premium estimation - #incr_expected_profits = (self.norm_premium - (1 - scipy.stats.poisson(1 / self.cat_separation_distribution.mean() * \ + # incr_expected_profits = (self.norm_premium - (1 - scipy.stats.poisson(1 / self.cat_separation_distribution.mean() * \ # mean_runtime).pmf(0)) * self.damage_distribution[categ_id].mean() * average_risk_factor) * average_exposure * len(categ_risks) else: incr_expected_profits = -1 # TODO: expected profits should only be returned once the expire_immediately == False case is fixed - #incr_expected_profits = (self.norm_premium - mean_runtime / self.cat_separation_distribution[categ_id].mean() * self.damage_distribution.mean() * average_risk_factor) * average_exposure * len(categ_risks) - + # incr_expected_profits = (self.norm_premium - mean_runtime / self.cat_separation_distribution[categ_id].mean() * self.damage_distribution.mean() * average_risk_factor) * average_exposure * len(categ_risks) + return average_risk_factor, average_exposure, incr_expected_profits - + def evaluate_proportional(self, risks, cash): """Method to evaluate proportional type risks. Accepts: @@ -101,16 +114,18 @@ def evaluate_proportional(self, risks, cash): cash_left_by_category = np.copy(cash) expected_profits = 0 necessary_liquidity = 0 - + var_per_risk_per_categ = np.zeros(self.category_number) - + # compute acceptable risks by category for categ_id in range(self.category_number): # compute number of acceptable risks of this category categ_risks = self.get_categ_risks(risks=risks, categ_id=categ_id) - + if len(categ_risks) > 0: - average_risk_factor, average_exposure, incr_expected_profits = self.compute_expectation(categ_risks=categ_risks, categ_id=categ_id) + average_risk_factor, average_exposure, incr_expected_profits = self.compute_expectation( + categ_risks=categ_risks, categ_id=categ_id + ) else: average_risk_factor = self.init_average_risk_factor average_exposure = self.init_average_exposure @@ -118,15 +133,33 @@ def evaluate_proportional(self, risks, cash): # TODO: expected profits should only be returned once the expire_immediately == False case is fixed expected_profits += incr_expected_profits - + # compute value at risk - var_per_risk = self.getPPF(categ_id=categ_id, tailSize=self.var_tail_prob) * average_risk_factor * average_exposure * self.margin_of_safety - + var_per_risk = ( + self.getPPF(categ_id=categ_id, tailSize=self.var_tail_prob) + * average_risk_factor + * average_exposure + * self.margin_of_safety + ) + # record liquidity requirement and apply margin of safety for liquidity requirement - necessary_liquidity += var_per_risk * self.margin_of_safety * len(categ_risks) + necessary_liquidity += ( + var_per_risk * self.margin_of_safety * len(categ_risks) + ) if isleconfig.verbose: print(self.inaccuracy) - print("RISKMODEL: ", var_per_risk, " = PPF(0.02) * ", average_risk_factor, " * ", average_exposure, " vs. cash: ", cash[categ_id], "TOTAL_RISK_IN_CATEG: ", var_per_risk * len(categ_risks)) + print( + "RISKMODEL: ", + var_per_risk, + " = PPF(0.02) * ", + average_risk_factor, + " * ", + average_exposure, + " vs. cash: ", + cash[categ_id], + "TOTAL_RISK_IN_CATEG: ", + var_per_risk * len(categ_risks), + ) try: acceptable = int(math.floor(cash[categ_id] / var_per_risk)) remaining = acceptable - len(categ_risks) @@ -148,17 +181,28 @@ def evaluate_proportional(self, risks, cash): expected_profits = self.init_profit_estimate * cash[0] else: expected_profits /= necessary_liquidity - + max_cash_by_categ = max(cash_left_by_category) floored_cash_by_categ = cash_left_by_category.copy() floored_cash_by_categ[floored_cash_by_categ < 0] = 0 for categ_id in range(self.category_number): - remaining_acceptable_by_category[categ_id] = math.floor(remaining_acceptable_by_category[categ_id] * pow( - floored_cash_by_categ[categ_id] / max_cash_by_categ, 5)) + remaining_acceptable_by_category[categ_id] = math.floor( + remaining_acceptable_by_category[categ_id] + * pow(floored_cash_by_categ[categ_id] / max_cash_by_categ, 5) + ) if isleconfig.verbose: - print("RISKMODEL returns: ", expected_profits, remaining_acceptable_by_category) + print( + "RISKMODEL returns: ", + expected_profits, + remaining_acceptable_by_category, + ) - return expected_profits, remaining_acceptable_by_category, cash_left_by_category, var_per_risk_per_categ + return ( + expected_profits, + remaining_acceptable_by_category, + cash_left_by_category, + var_per_risk_per_categ, + ) def evaluate_excess_of_loss(self, risks, cash, offered_risk=None): """Method to evaluate excess-of-loss type risks. @@ -176,7 +220,7 @@ def evaluate_excess_of_loss(self, risks, cash, offered_risk=None): offered risk (if applicable) is then calculated (should only be one).""" cash_left_by_categ = np.copy(cash) assert len(cash_left_by_categ) == self.category_number - + # prepare variables additional_required = np.zeros(self.category_number) additional_var_per_categ = np.zeros(self.category_number) @@ -184,32 +228,52 @@ def evaluate_excess_of_loss(self, risks, cash, offered_risk=None): # values at risk and liquidity requirements by category for categ_id in range(self.category_number): categ_risks = self.get_categ_risks(risks=risks, categ_id=categ_id) - + # TODO: allow for different risk distributions for different categories - percentage_value_at_risk = self.getPPF(categ_id=categ_id, tailSize=self.var_tail_prob) - + percentage_value_at_risk = self.getPPF( + categ_id=categ_id, tailSize=self.var_tail_prob + ) + # compute liquidity requirements from existing contracts for risk in categ_risks: - expected_damage = percentage_value_at_risk * risk["value"] * risk["risk_factor"] * self.inaccuracy[categ_id] - expected_claim = min(expected_damage, risk["excess"]) - risk["deductible"] - + expected_damage = ( + percentage_value_at_risk + * risk["value"] + * risk["risk_factor"] + * self.inaccuracy[categ_id] + ) + expected_claim = ( + min(expected_damage, risk["excess"]) - risk["deductible"] + ) + # record liquidity requirement and apply margin of safety for liquidity requirement cash_left_by_categ[categ_id] -= expected_claim * self.margin_of_safety - + # compute additional liquidity requirements from newly offered contract - if (offered_risk is not None) and (offered_risk.get("category") == categ_id): - expected_damage_fraction = percentage_value_at_risk * offered_risk["risk_factor"] * self.inaccuracy[categ_id] - expected_claim_fraction = min(expected_damage_fraction, offered_risk["excess_fraction"]) - offered_risk["deductible_fraction"] + if (offered_risk is not None) and ( + offered_risk.get("category") == categ_id + ): + expected_damage_fraction = ( + percentage_value_at_risk + * offered_risk["risk_factor"] + * self.inaccuracy[categ_id] + ) + expected_claim_fraction = ( + min(expected_damage_fraction, offered_risk["excess_fraction"]) + - offered_risk["deductible_fraction"] + ) expected_claim_total = expected_claim_fraction * offered_risk["value"] - + # record liquidity requirement and apply margin of safety for liquidity requirement - additional_required[categ_id] += expected_claim_total * self.margin_of_safety + additional_required[categ_id] += ( + expected_claim_total * self.margin_of_safety + ) additional_var_per_categ[categ_id] += expected_claim_total - + # Additional value at risk should only occur in one category. Assert that this is the case. - assert sum(additional_var_per_categ > 0) <= 1 + assert sum(additional_var_per_categ > 0) <= 1 var_this_risk = max(additional_var_per_categ) - + return cash_left_by_categ, additional_required, var_this_risk def evaluate(self, risks, cash, offered_risk=None): @@ -237,7 +301,9 @@ def evaluate(self, risks, cash, offered_risk=None): results in two sets of return values being used. These return values are what is used to determine if risks are underwritten or not.""" # ensure that any risk to be considered supplied directly as argument is non-proportional/excess-of-loss - assert (offered_risk is None) or offered_risk.get("insurancetype") == "excess-of-loss" + assert (offered_risk is None) or offered_risk.get( + "insurancetype" + ) == "excess-of-loss" # construct cash_left_by_categ as a sequence, defining remaining liquidity by category if not isinstance(cash, (np.ndarray, list)): @@ -247,24 +313,44 @@ def evaluate(self, risks, cash, offered_risk=None): assert len(cash_left_by_categ) == self.category_number # sort current contracts - el_risks = [risk for risk in risks if risk["insurancetype"] == 'excess-of-loss'] - risks = [risk for risk in risks if risk["insurancetype"] == 'proportional'] + el_risks = [risk for risk in risks if risk["insurancetype"] == "excess-of-loss"] + risks = [risk for risk in risks if risk["insurancetype"] == "proportional"] # compute liquidity requirements and acceptable risks from existing contract if (offered_risk is not None) or (len(el_risks) > 0): - cash_left_by_categ, additional_required, var_this_risk = self.evaluate_excess_of_loss(el_risks, cash_left_by_categ, offered_risk) + cash_left_by_categ, additional_required, var_this_risk = self.evaluate_excess_of_loss( + el_risks, cash_left_by_categ, offered_risk + ) if (offered_risk is None) or (len(risks) > 0): - expected_profits_proportional, remaining_acceptable_by_categ, cash_left_by_categ, var_per_risk_per_categ = self.evaluate_proportional(risks, cash_left_by_categ) + expected_profits_proportional, remaining_acceptable_by_categ, cash_left_by_categ, var_per_risk_per_categ = self.evaluate_proportional( + risks, cash_left_by_categ + ) if offered_risk is None: # return numbers of remaining acceptable risks by category - return expected_profits_proportional, remaining_acceptable_by_categ, cash_left_by_categ, var_per_risk_per_categ, min(cash_left_by_categ) + return ( + expected_profits_proportional, + remaining_acceptable_by_categ, + cash_left_by_categ, + var_per_risk_per_categ, + min(cash_left_by_categ), + ) else: # return boolean value whether the offered excess_of_loss risk can be accepted if isleconfig.verbose: - print("REINSURANCE RISKMODEL", cash, cash_left_by_categ,(cash_left_by_categ - additional_required > 0).all()) + print( + "REINSURANCE RISKMODEL", + cash, + cash_left_by_categ, + (cash_left_by_categ - additional_required > 0).all(), + ) # if not (cash_left_by_categ - additional_required > 0).all(): # pdb.set_trace() - return (cash_left_by_categ - additional_required > 0).all(), cash_left_by_categ, var_this_risk, min(cash_left_by_categ) + return ( + (cash_left_by_categ - additional_required > 0).all(), + cash_left_by_categ, + var_this_risk, + min(cash_left_by_categ), + ) def add_reinsurance(self, categ_id, excess_fraction, deductible_fraction, contract): """Method to add any instance of reinsurance to risk models list of reinsurance contracts, and add damage @@ -276,13 +362,19 @@ def add_reinsurance(self, categ_id, excess_fraction, deductible_fraction, contra deductible_fraction: Type Decimal. contract: Type DataDict. No return values.""" - self.damage_distribution_stack[categ_id].append(self.damage_distribution[categ_id]) + self.damage_distribution_stack[categ_id].append( + self.damage_distribution[categ_id] + ) self.reinsurance_contract_stack[categ_id].append(contract) - self.damage_distribution[categ_id] = ReinsuranceDistWrapper(lower_bound=deductible_fraction, \ - upper_bound=excess_fraction, \ - dist=self.damage_distribution[categ_id]) + self.damage_distribution[categ_id] = ReinsuranceDistWrapper( + lower_bound=deductible_fraction, + upper_bound=excess_fraction, + dist=self.damage_distribution[categ_id], + ) - def delete_reinsurance(self, categ_id, excess_fraction, deductible_fraction, contract): + def delete_reinsurance( + self, categ_id, excess_fraction, deductible_fraction, contract + ): """Method to remove any instance of reinsurance to risk models list of reinsurance contracts, and remove its damage distribution from the stack of damage distributions per category. Only used in the delete_reinsurance method of insurancefirm. @@ -294,5 +386,6 @@ def delete_reinsurance(self, categ_id, excess_fraction, deductible_fraction, con No return values.""" assert self.reinsurance_contract_stack[categ_id][-1] == contract self.reinsurance_contract_stack[categ_id].pop() - self.damage_distribution[categ_id] = self.damage_distribution_stack[categ_id].pop() - + self.damage_distribution[categ_id] = self.damage_distribution_stack[ + categ_id + ].pop() diff --git a/setup.py b/setup.py index f9b1d62..9fe6680 100644 --- a/setup.py +++ b/setup.py @@ -23,8 +23,7 @@ from distributiontruncated import TruncatedDistWrapper -class SetupSim(): - +class SetupSim: def __init__(self): self.simulation_parameters = isleconfig.simulation_parameters @@ -33,11 +32,16 @@ def __init__(self): self.max_time = self.simulation_parameters["max_time"] self.no_categories = self.simulation_parameters["no_categories"] - """set distribution""" - self.non_truncated = scipy.stats.pareto(b=2, loc=0, scale=0.25) #It is assumed that the damages of the catastrophes are drawn from a truncated Pareto distribution. - self.damage_distribution = TruncatedDistWrapper(lower_bound=0.25, upper_bound=1., dist=self.non_truncated) - self.cat_separation_distribution = scipy.stats.expon(0, self.simulation_parameters["event_time_mean_separation"]) #It is assumed that the time between catastrophes is exponentially distributed. + self.non_truncated = scipy.stats.pareto( + b=2, loc=0, scale=0.25 + ) # It is assumed that the damages of the catastrophes are drawn from a truncated Pareto distribution. + self.damage_distribution = TruncatedDistWrapper( + lower_bound=0.25, upper_bound=1.0, dist=self.non_truncated + ) + self.cat_separation_distribution = scipy.stats.expon( + 0, self.simulation_parameters["event_time_mean_separation"] + ) # It is assumed that the time between catastrophes is exponentially distributed. """"random seeds""" self.np_seed = [] @@ -45,19 +49,33 @@ def __init__(self): self.general_rc_event_schedule = [] self.general_rc_event_damage = [] - def schedule(self, replications): #This method returns the lists of schedule times and damages for an ensemble of replications of the model. The argument (replications) is the number of replications. + def schedule( + self, replications + ): # This method returns the lists of schedule times and damages for an ensemble of replications of the model. The argument (replications) is the number of replications. - general_rc_event_schedule = [] #In this list will be stored the lists of schedule times of catastrophes for an ensemble of simulations of the model. ([[Schedule 1], [Schedule 2], [Schedule 3],...,[Schedule N]]) - general_rc_event_damage = [] #In this list will be stored the lists of schedule damages of catastrophes for an ensemble of simulations of the model. ([[Schedule 1], [Schedule 2], [Schedule 3],...,[Schedule N]]) + general_rc_event_schedule = ( + [] + ) # In this list will be stored the lists of schedule times of catastrophes for an ensemble of simulations of the model. ([[Schedule 1], [Schedule 2], [Schedule 3],...,[Schedule N]]) + general_rc_event_damage = ( + [] + ) # In this list will be stored the lists of schedule damages of catastrophes for an ensemble of simulations of the model. ([[Schedule 1], [Schedule 2], [Schedule 3],...,[Schedule N]]) for i in range(replications): - rc_event_schedule = [] #In this list will be stored the lists of times when there will be catastrophes for every category of the model during a single run. ([[times for C1],[times for C2],[times for C3],[times for C4]]) - rc_event_damage = [] #In this list will be stored the lists of catastrophe damages for every category of the model during a single run. ([[damages for C1],[damages for C2],[damages for C3],[damages for C4]]) + rc_event_schedule = ( + [] + ) # In this list will be stored the lists of times when there will be catastrophes for every category of the model during a single run. ([[times for C1],[times for C2],[times for C3],[times for C4]]) + rc_event_damage = ( + [] + ) # In this list will be stored the lists of catastrophe damages for every category of the model during a single run. ([[damages for C1],[damages for C2],[damages for C3],[damages for C4]]) for j in range(self.no_categories): - event_schedule = [] #In this list will be stored the times when there will be a catastrophe related to a particular category. - event_damage = [] #In this list will be stored the damages of a catastrophe related to a particular category. + event_schedule = ( + [] + ) # In this list will be stored the times when there will be a catastrophe related to a particular category. + event_damage = ( + [] + ) # In this list will be stored the damages of a catastrophe related to a particular category. total = 0 - while (total < self.max_time): + while total < self.max_time: separation_time = self.cat_separation_distribution.rvs() total += int(math.ceil(separation_time)) if total < self.max_time: @@ -71,17 +89,21 @@ def schedule(self, replications): #This method returns the lists of schedule ti return self.general_rc_event_schedule, self.general_rc_event_damage - def seeds(self, replications): #This method returns the seeds required for an ensemble of replications of the model. The argument (replications) is the number of replications. + def seeds( + self, replications + ): # This method returns the seeds required for an ensemble of replications of the model. The argument (replications) is the number of replications. """draw random variates for random seeds""" for i in range(replications): - np_seed, random_seed = scipy.stats.randint.rvs(0, 2**16 - 1, size=2) + np_seed, random_seed = scipy.stats.randint.rvs(0, 2 ** 16 - 1, size=2) self.np_seed.append(np_seed) self.random_seed.append(random_seed) return self.np_seed, self.random_seed - def store(self, replications): #This method stores in a file the the schedules and random seeds required for an ensemble of replications of the model. The argument (replications) is the number of replications. - #With the information stored it is possible to replicate the entire behavior of the ensemble at a later time. + def store( + self, replications + ): # This method stores in a file the the schedules and random seeds required for an ensemble of replications of the model. The argument (replications) is the number of replications. + # With the information stored it is possible to replicate the entire behavior of the ensemble at a later time. event_schedules = [] for i in range(replications): @@ -97,7 +119,9 @@ def store(self, replications): #This method stores in a file the the schedules """ ensure that logging directory exists""" if not os.path.isdir("data"): - assert not os.path.exists("data"), "./data exists as regular file. This filename is required for the logging and event schedule directory" + assert not os.path.exists( + "data" + ), "./data exists as regular file. This filename is required for the logging and event schedule directory" os.makedirs("data") """Save as both pickle and txt""" @@ -106,15 +130,29 @@ def store(self, replications): #This method stores in a file the the schedules with open("./data/risk_event_schedules.txt", "w") as wfile: for rep_schedule in event_schedules: - wfile.write(str(rep_schedule).replace("\n","").replace("array", "np.array").replace("uint32", "np.uint32") + "\n") - - def obtain_ensemble(self, replications): #This method returns all the information (schedules and seeds) required to run an ensemble of simulations of the model. Since it also stores the information in a file it will be possible to replicate the ensemble at a later time. The argument (replications) is the number of replications. - #This method will be called either form ensemble.py or start.py - [general_rc_event_schedule, general_rc_event_damage] = self.schedule(replications) + wfile.write( + str(rep_schedule) + .replace("\n", "") + .replace("array", "np.array") + .replace("uint32", "np.uint32") + + "\n" + ) + + def obtain_ensemble( + self, replications + ): # This method returns all the information (schedules and seeds) required to run an ensemble of simulations of the model. Since it also stores the information in a file it will be possible to replicate the ensemble at a later time. The argument (replications) is the number of replications. + # This method will be called either form ensemble.py or start.py + [general_rc_event_schedule, general_rc_event_damage] = self.schedule( + replications + ) [np_seeds, random_seeds] = self.seeds(replications) self.store(replications) - return general_rc_event_schedule, general_rc_event_damage, np_seeds, random_seeds - + return ( + general_rc_event_schedule, + general_rc_event_damage, + np_seeds, + random_seeds, + ) diff --git a/start.py b/start.py index 8f933a5..b75b000 100644 --- a/start.py +++ b/start.py @@ -19,60 +19,116 @@ """Creates data file for logs if does not exist""" if not os.path.isdir("data"): - assert not os.path.exists("data"), "./data exists as regular file. This filename is required for the logging directory" + assert not os.path.exists( + "data" + ), "./data exists as regular file. This filename is required for the logging directory" os.makedirs("data") -def main(simulation_parameters, rc_event_schedule, rc_event_damage, np_seed, random_seed, save_iter, requested_logs=None): +def main( + simulation_parameters, + rc_event_schedule, + rc_event_damage, + np_seed, + random_seed, + save_iter, + requested_logs=None, +): np.random.seed(np_seed) random.seed(random_seed) """create simulation and world objects""" - simulation_parameters['simulation'] = world = InsuranceSimulation(override_no_riskmodels, replic_ID, simulation_parameters, rc_event_schedule, rc_event_damage) + simulation_parameters["simulation"] = world = InsuranceSimulation( + override_no_riskmodels, + replic_ID, + simulation_parameters, + rc_event_schedule, + rc_event_damage, + ) simulation = world - + """create agents: insurance firms according to number in isleconfig.py, adds them all to the simulation instance""" - insurancefirms_group = simulation.build_agents(InsuranceFirm, 'insurancefirm', parameters=simulation_parameters, - agent_parameters=world.agent_parameters["insurancefirm"]) + insurancefirms_group = simulation.build_agents( + InsuranceFirm, + "insurancefirm", + parameters=simulation_parameters, + agent_parameters=world.agent_parameters["insurancefirm"], + ) insurancefirm_pointers = insurancefirms_group world.accept_agents("insurancefirm", insurancefirm_pointers, insurancefirms_group) """create agents: reinsurance firms according to number in isleconfig.py, adds them all to simulation instance""" - reinsurancefirms_group = simulation.build_agents(ReinsuranceFirm, 'reinsurancefirm', parameters=simulation_parameters, - agent_parameters=world.agent_parameters["reinsurancefirm"]) + reinsurancefirms_group = simulation.build_agents( + ReinsuranceFirm, + "reinsurancefirm", + parameters=simulation_parameters, + agent_parameters=world.agent_parameters["reinsurancefirm"], + ) reinsurancefirm_pointers = reinsurancefirms_group - world.accept_agents("reinsurancefirm", reinsurancefirm_pointers, reinsurancefirms_group) - + world.accept_agents( + "reinsurancefirm", reinsurancefirm_pointers, reinsurancefirms_group + ) + """Time iteration""" - for t in range(simulation_parameters['max_time']): - - "create new agents and accepts them based on probability" # TODO: write method for this; this code block is executed almost identically 4 times + for t in range(simulation_parameters["max_time"]): + + "create new agents and accepts them based on probability" # TODO: write method for this; this code block is executed almost identically 4 times if world.insurance_firm_market_entry(agent_type="InsuranceFirm"): - parameters = [world.agent_parameters["insurancefirm"][simulation.insurance_entry_index()]] + parameters = [ + world.agent_parameters["insurancefirm"][ + simulation.insurance_entry_index() + ] + ] parameters[0]["id"] = world.get_unique_insurer_id() - new_insurance_firm = simulation.build_agents(InsuranceFirm, 'insurancefirm', parameters=simulation_parameters, - agent_parameters=parameters) + new_insurance_firm = simulation.build_agents( + InsuranceFirm, + "insurancefirm", + parameters=simulation_parameters, + agent_parameters=parameters, + ) insurancefirms_group += new_insurance_firm new_insurancefirm_pointer = new_insurance_firm - world.accept_agents("insurancefirm", new_insurancefirm_pointer, new_insurance_firm, time=t) - + world.accept_agents( + "insurancefirm", new_insurancefirm_pointer, new_insurance_firm, time=t + ) + if world.insurance_firm_market_entry(agent_type="ReinsuranceFirm"): - parameters = [world.agent_parameters["reinsurancefirm"][simulation.reinsurance_entry_index()]] + parameters = [ + world.agent_parameters["reinsurancefirm"][ + simulation.reinsurance_entry_index() + ] + ] parameters[0]["id"] = world.get_unique_reinsurer_id() - new_reinsurance_firm = simulation.build_agents(ReinsuranceFirm, 'reinsurancefirm', parameters=simulation_parameters, - agent_parameters=parameters) + new_reinsurance_firm = simulation.build_agents( + ReinsuranceFirm, + "reinsurancefirm", + parameters=simulation_parameters, + agent_parameters=parameters, + ) reinsurancefirms_group += new_reinsurance_firm new_reinsurancefirm_pointer = new_reinsurance_firm - world.accept_agents("reinsurancefirm", new_reinsurancefirm_pointer, new_reinsurance_firm, time=t) - + world.accept_agents( + "reinsurancefirm", + new_reinsurancefirm_pointer, + new_reinsurance_firm, + time=t, + ) + "iterate simulation" world.iterate(t) - + "log data" world.save_data() - - if t%50 == save_iter: - save_simulation(t, simulation, simulation_parameters, rc_event_schedule, rc_event_damage, exit_now=False) + + if t % 50 == save_iter: + save_simulation( + t, + simulation, + simulation_parameters, + rc_event_schedule, + rc_event_damage, + exit_now=False, + ) return simulation.obtain_log(requested_logs) # It is required to return this list to download all the data generated by a single run of the model from the cloud. @@ -91,9 +147,11 @@ def save_simulation(t, sim, sim_param, event_schedule, event_damage, exit_now=Fa pickle.dump(d, wfile, protocol=pickle.HIGHEST_PROTOCOL) with open("data/simulation_save.pkl", "br") as rfile: file_contents = rfile.read() - #print("\nSimulation hashes: ", hashlib.sha512(str(d).encode()).hexdigest(), "; ", hashlib.sha512(str(file_contents).encode()).hexdigest()) + # print("\nSimulation hashes: ", hashlib.sha512(str(d).encode()).hexdigest(), "; ", hashlib.sha512(str(file_contents).encode()).hexdigest()) # note that the hash over the dict is for some reason not identical between runs. The hash over the state saved to the file is. - print("\nSimulation hash: ", hashlib.sha512(str(file_contents).encode()).hexdigest()) + print( + "\nSimulation hash: ", hashlib.sha512(str(file_contents).encode()).hexdigest() + ) if exit_now: exit(0) @@ -101,22 +159,52 @@ def save_simulation(t, sim, sim_param, event_schedule, event_damage, exit_now=Fa """main entry point""" if __name__ == "__main__": """ use argparse to handle command line arguments""" - parser = argparse.ArgumentParser(description='Model the Insurance sector') - parser.add_argument("--oneriskmodel", action="store_true", - help="allow overriding the number of riskmodels from the standard config (with 1)") - parser.add_argument("--riskmodels", type=int, choices=[1, 2, 3, 4], - help="allow overriding the number of riskmodels from standard config (with 1 or other numbers)") - parser.add_argument("--replicid", type=int, - help="if replication ID is given, pass this to the simulation so that the risk profile can be restored") - parser.add_argument("--replicating", action="store_true", - help="if this is a simulation run designed to replicate another, override the config file parameter") - parser.add_argument("--randomseed", type=float, help="allow setting of numpy random seed") - parser.add_argument("--foreground", action="store_true", - help="force foreground runs even if replication ID is given (which defaults to background runs)") - parser.add_argument("--shownetwork", action="store_true", help="show reinsurance relations as network") - parser.add_argument("-p", "--showprogress", action="store_true", help="show timesteps") - parser.add_argument("-v", "--verbose", action="store_true", help="more detailed output") - parser.add_argument("--save_iterations", type=int, help="number of iterations to iterate before saving world state") + parser = argparse.ArgumentParser(description="Model the Insurance sector") + parser.add_argument( + "--oneriskmodel", + action="store_true", + help="allow overriding the number of riskmodels from the standard config (with 1)", + ) + parser.add_argument( + "--riskmodels", + type=int, + choices=[1, 2, 3, 4], + help="allow overriding the number of riskmodels from standard config (with 1 or other numbers)", + ) + parser.add_argument( + "--replicid", + type=int, + help="if replication ID is given, pass this to the simulation so that the risk profile can be restored", + ) + parser.add_argument( + "--replicating", + action="store_true", + help="if this is a simulation run designed to replicate another, override the config file parameter", + ) + parser.add_argument( + "--randomseed", type=float, help="allow setting of numpy random seed" + ) + parser.add_argument( + "--foreground", + action="store_true", + help="force foreground runs even if replication ID is given (which defaults to background runs)", + ) + parser.add_argument( + "--shownetwork", + action="store_true", + help="show reinsurance relations as network", + ) + parser.add_argument( + "-p", "--showprogress", action="store_true", help="show timesteps" + ) + parser.add_argument( + "-v", "--verbose", action="store_true", help="more detailed output" + ) + parser.add_argument( + "--save_iterations", + type=int, + help="number of iterations to iterate before saving world state", + ) args = parser.parse_args() if args.oneriskmodel: @@ -124,11 +212,13 @@ def save_simulation(t, sim, sim_param, event_schedule, event_damage, exit_now=Fa override_no_riskmodels = 1 if args.riskmodels: override_no_riskmodels = args.riskmodels - if args.replicid is not None: # TODO: this is broken, must be fixed or removed + if args.replicid is not None: # TODO: this is broken, must be fixed or removed replic_ID = args.replicid if args.replicating: isleconfig.replicating = True - assert replic_ID is not None, "Error: Replication requires a replication ID to identify run to be replicated" + assert ( + replic_ID is not None + ), "Error: Replication requires a replication ID to identify run to be replicated" if args.randomseed: randomseed = args.randomseed seed = int(randomseed) @@ -140,8 +230,9 @@ def save_simulation(t, sim, sim_param, event_schedule, event_damage, exit_now=Fa if args.shownetwork: isleconfig.show_network = True """Option requires reloading of InsuranceSimulation so that modules to show network can be loaded. - # TODO: change all module imports of the form "from module import class" to "import module". """ + # TODO: change all module imports of the form "from module import class" to "import module". """ import insurancesimulation + importlib.reload(insurancesimulation) from insurancesimulation import InsuranceSimulation if args.showprogress: @@ -152,21 +243,38 @@ def save_simulation(t, sim, sim_param, event_schedule, event_damage, exit_now=Fa save_iter = args.save_iterations else: save_iter = 20000 - + from setup import SetupSim - setup = SetupSim() #Here the setup for the simulation is done. - [general_rc_event_schedule, general_rc_event_damage, np_seeds, random_seeds] = setup.obtain_ensemble(1) #Only one ensemble. This part will only be run locally (laptop). - log = main(simulation_parameters, general_rc_event_schedule[0], general_rc_event_damage[0], np_seeds[0], random_seeds[0], save_iter) - + setup = SetupSim() # Here the setup for the simulation is done. + [ + general_rc_event_schedule, + general_rc_event_damage, + np_seeds, + random_seeds, + ] = setup.obtain_ensemble( + 1 + ) # Only one ensemble. This part will only be run locally (laptop). + + log = main( + simulation_parameters, + general_rc_event_schedule[0], + general_rc_event_damage[0], + np_seeds[0], + random_seeds[0], + save_iter, + ) + """ Restore the log at the end of the single simulation run for saving and for potential further study """ - is_background = (not isleconfig.force_foreground) and (isleconfig.replicating or (replic_ID in locals())) + is_background = (not isleconfig.force_foreground) and ( + isleconfig.replicating or (replic_ID in locals()) + ) L = logger.Logger() L.restore_logger_object(list(log)) L.save_log(is_background) if isleconfig.save_network: L.save_network_data(ensemble=False) - + """ Obtain calibration score """ CS = calibrationscore.CalibrationScore(L) score = CS.test_all() diff --git a/visualisation.py b/visualisation.py index 81e670f..2771d97 100644 --- a/visualisation.py +++ b/visualisation.py @@ -14,8 +14,21 @@ if not os.path.isdir("figures"): os.makedirs("figures") + class TimeSeries(object): - def __init__(self, series_list, event_schedule, damage_schedule, title="",xlabel="Time", colour='k', axlst=None, fig=None, percentiles=None, alpha=0.7): + def __init__( + self, + series_list, + event_schedule, + damage_schedule, + title="", + xlabel="Time", + colour="k", + axlst=None, + fig=None, + percentiles=None, + alpha=0.7, + ): """Intialisation method for creating timeseries. Accepts: series_list: Type List. Contains contract, cash, operational, premium, profitloss data. @@ -36,14 +49,16 @@ def __init__(self, series_list, event_schedule, damage_schedule, title="",xlabel self.alpha = alpha self.percentiles = percentiles self.title = title - self.timesteps = [t for t in range(len(series_list[0][0]))] # assume all data series are the same size + self.timesteps = [ + t for t in range(len(series_list[0][0])) + ] # assume all data series are the same size self.events_schedule = event_schedule self.damage_schedule = damage_schedule if axlst is not None and fig is not None: self.axlst = axlst self.fig = fig else: - self.fig, self.axlst = plt.subplots(self.size,sharex=True) + self.fig, self.axlst = plt.subplots(self.size, sharex=True) def plot(self): """Method to plot time series. @@ -54,28 +69,46 @@ def plot(self): This method is called to plot a timeseries for five subplots of data for insurers/reinsurers. If called for a single run event times are plotted as vertical lines, if an ensemble run then no events but the average data is plotted with percentiles as deviations to the average.""" - single_categ_colours = ['b', 'b', 'b', 'b'] - for i, (series, series_label, fill_lower, fill_upper) in enumerate(self.series_list): - self.axlst[i].plot(self.timesteps, series,color=self.colour) + single_categ_colours = ["b", "b", "b", "b"] + for i, (series, series_label, fill_lower, fill_upper) in enumerate( + self.series_list + ): + self.axlst[i].plot(self.timesteps, series, color=self.colour) self.axlst[i].set_ylabel(series_label) if fill_lower is not None and fill_upper is not None: - self.axlst[i].fill_between(self.timesteps, fill_lower, fill_upper, color=self.colour, alpha=self.alpha) - - if self.events_schedule is not None: # Plots vertical lines for events if set. + self.axlst[i].fill_between( + self.timesteps, + fill_lower, + fill_upper, + color=self.colour, + alpha=self.alpha, + ) + + if ( + self.events_schedule is not None + ): # Plots vertical lines for events if set. for categ in range(len(self.events_schedule)): for event_time in self.events_schedule[categ]: index = self.events_schedule[categ].index(event_time) - if self.damage_schedule[categ][index] > 0.5: # Only plots line if event is significant - self.axlst[i].axvline(event_time, color=single_categ_colours[categ], alpha=self.damage_schedule[categ][index]) - self.axlst[self.size-1].set_xlabel(self.xlabel) + if ( + self.damage_schedule[categ][index] > 0.5 + ): # Only plots line if event is significant + self.axlst[i].axvline( + event_time, + color=single_categ_colours[categ], + alpha=self.damage_schedule[categ][index], + ) + self.axlst[self.size - 1].set_xlabel(self.xlabel) self.fig.suptitle(self.title) return self.fig, self.axlst class InsuranceFirmAnimation(object): - def __init__(self, cash_data, insure_contracts, event_schedule, type, save=True, perils=True): + def __init__( + self, cash_data, insure_contracts, event_schedule, type, save=True, perils=True + ): """Initialising method for the animation of insurance firm data. Accepts: cash_data: Type List of List of Lists: Contains the operational, ID and cash for each firm for each time. @@ -112,7 +145,9 @@ def animate(self): self.pies = [0, 0] self.fig, (self.ax1, self.ax2) = plt.subplots(1, 2) self.stream = self.data_stream() - self.animate = animation.FuncAnimation(self.fig, self.update, repeat=False, interval=20, save_count=998) + self.animate = animation.FuncAnimation( + self.fig, self.update, repeat=False, interval=20, save_count=998 + ) if self.save_condition: self.save() @@ -148,17 +183,17 @@ def update(self, i): axis, getting data from data_stream method. Can also be set such that the figure flashes red at an event time.""" self.ax1.clear() self.ax2.clear() - self.ax1.axis('equal') - self.ax2.axis('equal') + self.ax1.axis("equal") + self.ax2.axis("equal") cash_list, id_list, con_list = next(self.stream) - self.pies[0] = self.ax1.pie(cash_list, labels=id_list, autopct='%1.0f%%') + self.pies[0] = self.ax1.pie(cash_list, labels=id_list, autopct="%1.0f%%") self.ax1.set_title("Total cash : {:,.0f}".format(sum(cash_list))) - self.pies[1] = self.ax2.pie(con_list, labels=id_list, autopct='%1.0f%%') + self.pies[1] = self.ax2.pie(con_list, labels=id_list, autopct="%1.0f%%") self.ax2.set_title("Total contracts : {:,.0f}".format(sum(con_list))) self.fig.suptitle("%s Timestep : %i" % (self.type, i)) if self.perils_condition: if i == self.all_event_times[0]: - self.fig.suptitle('EVENT AT TIME %i!' % i) + self.fig.suptitle("EVENT AT TIME %i!" % i) self.all_event_times = self.all_event_times[1:] return self.pies @@ -167,9 +202,16 @@ def save(self): No accepted values. No return values.""" if self.type == "Insurance Firm": - self.animate.save("figures/animated_insurfirm_pie.mp4", writer="ffmpeg", dpi=200, fps=10) + self.animate.save( + "figures/animated_insurfirm_pie.mp4", writer="ffmpeg", dpi=200, fps=10 + ) elif self.type == "Reinsurance Firm": - self.animate.save("figures/animated_reinsurefirm_pie.mp4", writer="ffmpeg", dpi=200, fps=10) + self.animate.save( + "figures/animated_reinsurefirm_pie.mp4", + writer="ffmpeg", + dpi=200, + fps=10, + ) else: print("Incorrect Type for Saving") @@ -191,10 +233,12 @@ def insurer_pie_animation(self, run=0): Returns: self.ins_pie_anim: Type animation class instance. Not used outside this method but is saved.""" data = self.history_logs_list[run] - insurance_cash = np.array(data['insurance_firms_cash']) - contract_data = data['individual_contracts'] + insurance_cash = np.array(data["insurance_firms_cash"]) + contract_data = data["individual_contracts"] event_schedule = data["rc_event_schedule_initial"] - self.ins_pie_anim = InsuranceFirmAnimation(insurance_cash, contract_data, event_schedule, 'Insurance Firm', save=True) + self.ins_pie_anim = InsuranceFirmAnimation( + insurance_cash, contract_data, event_schedule, "Insurance Firm", save=True + ) self.ins_pie_anim.animate() return self.ins_pie_anim @@ -205,14 +249,28 @@ def reinsurer_pie_animation(self, run=0): Returns: self.reins_pie_anim: Type animation class instance. Not used outside this method but is saved.""" data = self.history_logs_list[run] - reinsurance_cash = np.array(data['reinsurance_firms_cash']) - contract_data = data['reinsurance_contracts'] + reinsurance_cash = np.array(data["reinsurance_firms_cash"]) + contract_data = data["reinsurance_contracts"] event_schedule = data["rc_event_schedule_initial"] - self.reins_pie_anim = InsuranceFirmAnimation(reinsurance_cash, contract_data, event_schedule, 'Reinsurance Firm', save=True) + self.reins_pie_anim = InsuranceFirmAnimation( + reinsurance_cash, + contract_data, + event_schedule, + "Reinsurance Firm", + save=True, + ) self.reins_pie_anim.animate() return self.reins_pie_anim - def insurer_time_series(self, singlerun=True, axlst=None, fig=None, title="Insurer", colour='black', percentiles=[25,75]): + def insurer_time_series( + self, + singlerun=True, + axlst=None, + fig=None, + title="Insurer", + colour="black", + percentiles=[25, 75], + ): """Method to create a timeseries for insurance firms' data. Accepts: singlerun: Type Boolean. Sets event schedule if a single run. @@ -229,17 +287,28 @@ def insurer_time_series(self, singlerun=True, axlst=None, fig=None, title="Insur timeseries, as this is only helpful in this case.""" if singlerun: events = self.history_logs_list[0]["rc_event_schedule_initial"] - damages = self.history_logs_list[0]['rc_event_damage_initial'] + damages = self.history_logs_list[0]["rc_event_damage_initial"] else: events = None damages = None # Take the element-wise means/medians of the ensemble set (axis=0) - contracts_agg = [history_logs['total_contracts'] for history_logs in self.history_logs_list] - profitslosses_agg = [history_logs['total_profitslosses'] for history_logs in self.history_logs_list] - operational_agg = [history_logs['total_operational'] for history_logs in self.history_logs_list] - cash_agg = [history_logs['total_cash'] for history_logs in self.history_logs_list] - premium_agg = [history_logs['market_premium'] for history_logs in self.history_logs_list] + contracts_agg = [ + history_logs["total_contracts"] for history_logs in self.history_logs_list + ] + profitslosses_agg = [ + history_logs["total_profitslosses"] + for history_logs in self.history_logs_list + ] + operational_agg = [ + history_logs["total_operational"] for history_logs in self.history_logs_list + ] + cash_agg = [ + history_logs["total_cash"] for history_logs in self.history_logs_list + ] + premium_agg = [ + history_logs["market_premium"] for history_logs in self.history_logs_list + ] contracts = np.mean(contracts_agg, axis=0) profitslosses = np.mean(profitslosses_agg, axis=0) @@ -247,16 +316,59 @@ def insurer_time_series(self, singlerun=True, axlst=None, fig=None, title="Insur cash = np.median(cash_agg, axis=0) premium = np.median(premium_agg, axis=0) - self.ins_time_series = TimeSeries([ - (contracts, 'Contracts', np.percentile(contracts_agg,percentiles[0], axis=0), np.percentile(contracts_agg, percentiles[1], axis=0)), - (profitslosses, 'Profitslosses', np.percentile(profitslosses_agg,percentiles[0], axis=0), np.percentile(profitslosses_agg, percentiles[1], axis=0)), - (operational, 'Operational', np.percentile(operational_agg,percentiles[0], axis=0), np.percentile(operational_agg, percentiles[1], axis=0)), - (cash, 'Cash', np.percentile(cash_agg,percentiles[0], axis=0), np.percentile(cash_agg, percentiles[1], axis=0)), - (premium, "Premium", np.percentile(premium_agg,percentiles[0], axis=0), np.percentile(premium_agg, percentiles[1], axis=0))], events, damages, title=title, xlabel="Time", axlst=axlst, fig=fig, colour=colour) + self.ins_time_series = TimeSeries( + [ + ( + contracts, + "Contracts", + np.percentile(contracts_agg, percentiles[0], axis=0), + np.percentile(contracts_agg, percentiles[1], axis=0), + ), + ( + profitslosses, + "Profitslosses", + np.percentile(profitslosses_agg, percentiles[0], axis=0), + np.percentile(profitslosses_agg, percentiles[1], axis=0), + ), + ( + operational, + "Operational", + np.percentile(operational_agg, percentiles[0], axis=0), + np.percentile(operational_agg, percentiles[1], axis=0), + ), + ( + cash, + "Cash", + np.percentile(cash_agg, percentiles[0], axis=0), + np.percentile(cash_agg, percentiles[1], axis=0), + ), + ( + premium, + "Premium", + np.percentile(premium_agg, percentiles[0], axis=0), + np.percentile(premium_agg, percentiles[1], axis=0), + ), + ], + events, + damages, + title=title, + xlabel="Time", + axlst=axlst, + fig=fig, + colour=colour, + ) fig, axlst = self.ins_time_series.plot() return fig, axlst - def reinsurer_time_series(self, singlerun=True, axlst=None, fig=None, title="Reinsurer", colour='black', percentiles=[25,75]): + def reinsurer_time_series( + self, + singlerun=True, + axlst=None, + fig=None, + title="Reinsurer", + colour="black", + percentiles=[25, 75], + ): """Method to create a timeseries for reinsurance firms' data. Accepts: singlerun: Type Boolean. Sets event schedule if a single run. @@ -273,17 +385,31 @@ def reinsurer_time_series(self, singlerun=True, axlst=None, fig=None, title="Rei timeseries, as this is only helpful in this case.""" if singlerun: events = self.history_logs_list[0]["rc_event_schedule_initial"] - damages = self.history_logs_list[0]['rc_event_damage_initial'] + damages = self.history_logs_list[0]["rc_event_damage_initial"] else: events = None damages = None # Take the element-wise means/medians of the ensemble set (axis=0) - reincontracts_agg = [history_logs['total_reincontracts'] for history_logs in self.history_logs_list] - reinprofitslosses_agg = [history_logs['total_reinprofitslosses'] for history_logs in self.history_logs_list] - reinoperational_agg = [history_logs['total_reinoperational'] for history_logs in self.history_logs_list] - reincash_agg = [history_logs['total_reincash'] for history_logs in self.history_logs_list] - catbonds_number_agg = [history_logs['total_catbondsoperational'] for history_logs in self.history_logs_list] + reincontracts_agg = [ + history_logs["total_reincontracts"] + for history_logs in self.history_logs_list + ] + reinprofitslosses_agg = [ + history_logs["total_reinprofitslosses"] + for history_logs in self.history_logs_list + ] + reinoperational_agg = [ + history_logs["total_reinoperational"] + for history_logs in self.history_logs_list + ] + reincash_agg = [ + history_logs["total_reincash"] for history_logs in self.history_logs_list + ] + catbonds_number_agg = [ + history_logs["total_catbondsoperational"] + for history_logs in self.history_logs_list + ] reincontracts = np.mean(reincontracts_agg, axis=0) reinprofitslosses = np.mean(reinprofitslosses_agg, axis=0) @@ -291,13 +417,47 @@ def reinsurer_time_series(self, singlerun=True, axlst=None, fig=None, title="Rei reincash = np.median(reincash_agg, axis=0) catbonds_number = np.median(catbonds_number_agg, axis=0) - self.reins_time_series = TimeSeries([ - (reincontracts, 'Contracts', np.percentile(reincontracts_agg,percentiles[0], axis=0), np.percentile(reincontracts_agg, percentiles[1], axis=0)), - (reinprofitslosses, 'Profitslosses', np.percentile(reinprofitslosses_agg,percentiles[0], axis=0), np.percentile(reinprofitslosses_agg, percentiles[1], axis=0)), - (reinoperational, 'Operational', np.percentile(reinoperational_agg,percentiles[0], axis=0), np.percentile(reinoperational_agg, percentiles[1], axis=0)), - (reincash, 'Cash', np.percentile(reincash_agg,percentiles[0], axis=0), np.percentile(reincash_agg, percentiles[1], axis=0)), - (catbonds_number, "Activate Cat Bonds", np.percentile(catbonds_number_agg,percentiles[0], axis=0), np.percentile(catbonds_number_agg, percentiles[1], axis=0)), - ], events, damages, title=title, xlabel="Time", axlst=axlst, fig=fig, colour=colour) + self.reins_time_series = TimeSeries( + [ + ( + reincontracts, + "Contracts", + np.percentile(reincontracts_agg, percentiles[0], axis=0), + np.percentile(reincontracts_agg, percentiles[1], axis=0), + ), + ( + reinprofitslosses, + "Profitslosses", + np.percentile(reinprofitslosses_agg, percentiles[0], axis=0), + np.percentile(reinprofitslosses_agg, percentiles[1], axis=0), + ), + ( + reinoperational, + "Operational", + np.percentile(reinoperational_agg, percentiles[0], axis=0), + np.percentile(reinoperational_agg, percentiles[1], axis=0), + ), + ( + reincash, + "Cash", + np.percentile(reincash_agg, percentiles[0], axis=0), + np.percentile(reincash_agg, percentiles[1], axis=0), + ), + ( + catbonds_number, + "Activate Cat Bonds", + np.percentile(catbonds_number_agg, percentiles[0], axis=0), + np.percentile(catbonds_number_agg, percentiles[1], axis=0), + ), + ], + events, + damages, + title=title, + xlabel="Time", + axlst=axlst, + fig=fig, + colour=colour, + ) fig, axlst = self.reins_time_series.plot() return fig, axlst @@ -339,7 +499,8 @@ def populate_scatter_data(self): for hlog in self.history_logs_list: # for each replication urc = np.diff(np.asarray(hlog["cumulative_unrecovered_claims"])) self.scatter_data["unrecovered_claims"] = np.hstack( - [self.scatter_data["unrecovered_claims"], np.extract(urc > 0, urc)]) + [self.scatter_data["unrecovered_claims"], np.extract(urc > 0, urc)] + ) """Record data on sizes of unrecovered_claims""" self.scatter_data["relative_unrecovered_claims"] = [] @@ -348,9 +509,13 @@ def populate_scatter_data(self): tcl = np.diff(np.asarray(hlog["cumulative_claims"])) rurc = urc / tcl self.scatter_data["relative_unrecovered_claims"] = np.hstack( - [self.scatter_data["unrecovered_claims"], np.extract(rurc > 0, rurc)]) + [self.scatter_data["unrecovered_claims"], np.extract(rurc > 0, rurc)] + ) try: - assert np.isinf(self.scatter_data["relative_unrecovered_claims"]).any() == False + assert ( + np.isinf(self.scatter_data["relative_unrecovered_claims"]).any() + == False + ) except: pass # pdb.set_trace() @@ -365,7 +530,9 @@ def populate_scatter_data(self): in_op = np.asarray(hlog["total_operational"])[:-1] rein_op = np.asarray(hlog["total_reinoperational"])[:-1] op = in_op + rein_op - exits = np.diff(np.asarray(hlog["cumulative_market_exits"], dtype=np.float64)) + exits = np.diff( + np.asarray(hlog["cumulative_market_exits"], dtype=np.float64) + ) assert (exits <= op).all() op[op == 0] = 1 @@ -379,13 +546,26 @@ def populate_scatter_data(self): """Record data""" self.scatter_data["bankruptcy_events"] = np.hstack( - [self.scatter_data["bankruptcy_events"], np.extract(exits > 0, exits)]) + [self.scatter_data["bankruptcy_events"], np.extract(exits > 0, exits)] + ) self.scatter_data["bankruptcy_events_relative"] = np.hstack( - [self.scatter_data["bankruptcy_events_relative"], np.extract(rel_exits > 0, rel_exits)]) + [ + self.scatter_data["bankruptcy_events_relative"], + np.extract(rel_exits > 0, rel_exits), + ] + ) self.scatter_data["bankruptcy_events_clustered"] = np.hstack( - [self.scatter_data["bankruptcy_events_clustered"], np.extract(exits2 > 0, exits2)]) + [ + self.scatter_data["bankruptcy_events_clustered"], + np.extract(exits2 > 0, exits2), + ] + ) self.scatter_data["bankruptcy_events_relative_clustered"] = np.hstack( - [self.scatter_data["bankruptcy_events_relative_clustered"], np.extract(rel_exits2 > 0, rel_exits2)]) + [ + self.scatter_data["bankruptcy_events_relative_clustered"], + np.extract(rel_exits2 > 0, rel_exits2), + ] + ) def show(self): plt.show() @@ -393,7 +573,7 @@ def show(self): class compare_riskmodels(object): - def __init__(self,vis_list, colour_list): + def __init__(self, vis_list, colour_list): """Initialises compare_riskmodels class. Accepts: vis_list: Type List of Visualisation class instances. Each instance is a different no. of risk models. @@ -403,8 +583,8 @@ def __init__(self,vis_list, colour_list): self.colour_list = colour_list self.insurer_fig = self.insurer_axlst = None self.reinsurer_fig = self.reinsurer_axlst = None - - def create_insurer_timeseries(self, fig=None, axlst=None, percentiles=[25,75]): + + def create_insurer_timeseries(self, fig=None, axlst=None, percentiles=[25, 75]): """Method to create separate insurer time series for all numbers of risk models using visualisations' insurer_time_series method. Loops through each separately and they are then saved automatically. Used for ensemble runs. @@ -416,13 +596,26 @@ def create_insurer_timeseries(self, fig=None, axlst=None, percentiles=[25,75]): risk_model = 0 for vis, colour in zip(self.vis_list, self.colour_list): risk_model += 1 - (self.insurer_fig, self.insurer_axlst) = vis.insurer_time_series(singlerun=False, fig=fig, axlst=axlst, - colour=colour, percentiles=percentiles, - title="%i Risk Model Insurer" % risk_model) - self.insurer_fig.savefig("figures/"+str(risk_model)+"risk_model(s)_insurer_ensemble_timeseries.png") - print("Saved " + str(risk_model) + " risk_model(s)_insurer_ensemble_timeseries") - - def create_reinsurer_timeseries(self, fig=None, axlst=None, percentiles=[25,75]): + (self.insurer_fig, self.insurer_axlst) = vis.insurer_time_series( + singlerun=False, + fig=fig, + axlst=axlst, + colour=colour, + percentiles=percentiles, + title="%i Risk Model Insurer" % risk_model, + ) + self.insurer_fig.savefig( + "figures/" + + str(risk_model) + + "risk_model(s)_insurer_ensemble_timeseries.png" + ) + print( + "Saved " + + str(risk_model) + + " risk_model(s)_insurer_ensemble_timeseries" + ) + + def create_reinsurer_timeseries(self, fig=None, axlst=None, percentiles=[25, 75]): """Method to create separate reinsurer time series for all numbers of risk models using visualisations' reinsurer_time_series method. Loops through each separately and they are then saved automatically. Used for ensemble runs. @@ -434,11 +627,24 @@ def create_reinsurer_timeseries(self, fig=None, axlst=None, percentiles=[25,75]) risk_model = 0 for vis, colour in zip(self.vis_list, self.colour_list): risk_model += 1 - (self.reinsurer_fig, self.reinsurer_axlst) = vis.reinsurer_time_series(singlerun=False, fig=fig, axlst=axlst, - colour=colour, percentiles=percentiles, - title="%i Risk Model Reinsurer" % risk_model) - self.reinsurer_fig.savefig("figures/"+str(risk_model)+"risk_model(s)_reinsurer_ensemble_timeseries.png") - print("Saved " + str(risk_model) + " risk_model(s)_reinsurer_ensemble_timeseries") + (self.reinsurer_fig, self.reinsurer_axlst) = vis.reinsurer_time_series( + singlerun=False, + fig=fig, + axlst=axlst, + colour=colour, + percentiles=percentiles, + title="%i Risk Model Reinsurer" % risk_model, + ) + self.reinsurer_fig.savefig( + "figures/" + + str(risk_model) + + "risk_model(s)_reinsurer_ensemble_timeseries.png" + ) + print( + "Saved " + + str(risk_model) + + " risk_model(s)_reinsurer_ensemble_timeseries" + ) def show(self): plt.show() @@ -447,8 +653,16 @@ def show(self): class CDF_distribution_plot: """Class for CDF/cCDF distribution plots using class CDFDistribution. This class arranges as many such plots stacked in one diagram as there are series in the history logs they are created from, i.e. len(vis_list).""" - def __init__(self, vis_list, colour_list, quantiles=[.25, .75], variable="reinsurance_firms_cash", timestep=-1, - plot_cCDF=True): + + def __init__( + self, + vis_list, + colour_list, + quantiles=[0.25, 0.75], + variable="reinsurance_firms_cash", + timestep=-1, + plot_cCDF=True, + ): """Constructor. Arguments: vis_list: list of visualisation objects - objects hilding the data @@ -476,7 +690,9 @@ def generate_plot(self, xlabel=None, filename=None): """Set x axis label and filename to default if not provided""" xlabel = xlabel if xlabel is not None else self.variable - filename = filename if filename is not None else "figures/CDF_plot_" + self.variable + filename = ( + filename if filename is not None else "figures/CDF_plot_" + self.variable + ) """Create figure with correct number of subplots""" self.fig, self.ax = plt.subplots(nrows=len(self.vis_list)) @@ -486,7 +702,10 @@ def generate_plot(self, xlabel=None, filename=None): all_data = np.asarray([]) for i in range(len(self.vis_list)): """Extract firm records from history logs""" - series_x = [replication[self.variable][self.timestep] for replication in self.vis_list[i].history_logs_list] + series_x = [ + replication[self.variable][self.timestep] + for replication in self.vis_list[i].history_logs_list + ] """Extract the capital holdings from the tuple""" for j in range(len(series_x)): series_x[j] = [firm[0] for firm in series_x[j] if firm[2]] @@ -495,21 +714,31 @@ def generate_plot(self, xlabel=None, filename=None): """Catch empty data sets""" if len(all_data) == 0: return - minmax = (np.min(all_data), np.max(all_data) / 2.) + minmax = (np.min(all_data), np.max(all_data) / 2.0) """Loop through simulation record series, populate subplot by subplot""" for i in range(len(self.vis_list)): """Extract firm records from history logs""" - series_x = [replication[self.variable][self.timestep] for replication in self.vis_list[i].history_logs_list] + series_x = [ + replication[self.variable][self.timestep] + for replication in self.vis_list[i].history_logs_list + ] """Extract the capital holdings from the tuple""" for j in range(len(series_x)): series_x[j] = [firm[0] for firm in series_x[j] if firm[2]] """Create CDFDistribution object and populate the subfigure using it""" VDP = CDFDistribution(series_x) c_xlabel = "" if i < len(self.vis_list) - 1 else xlabel - VDP.plot(ax=self.ax[i], ylabel="cCDF " + str(i + 1) + "RM", xlabel=c_xlabel, - upper_quantile=self.upper_quantile, lower_quantile=self.lower_quantile, color=self.colour_list[i], - plot_cCDF=True, xlims=minmax) + VDP.plot( + ax=self.ax[i], + ylabel="cCDF " + str(i + 1) + "RM", + xlabel=c_xlabel, + upper_quantile=self.upper_quantile, + lower_quantile=self.lower_quantile, + color=self.colour_list[i], + plot_cCDF=True, + xlims=minmax, + ) """Finish and save figure""" self.fig.tight_layout() @@ -521,6 +750,7 @@ def generate_plot(self, xlabel=None, filename=None): class Histogram_plot: """Class for Histogram plots using class Histograms. This class arranges as many such plots stacked in one diagram as there are series in the history logs they are created from, i.e. len(vis_list).""" + def __init__(self, vis_list, colour_list, variable="bankruptcy_events"): """Constructor. Arguments: @@ -533,7 +763,9 @@ def __init__(self, vis_list, colour_list, variable="bankruptcy_events"): self.colour_list = colour_list self.variable = variable - def generate_plot(self, xlabel=None, filename=None, logscale=False, minmax=None, VaR005guess=0.3): + def generate_plot( + self, xlabel=None, filename=None, logscale=False, minmax=None, VaR005guess=0.3 + ): """Method to generate and save the plot. Arguments: xlabel: str or None - the x axis label @@ -542,7 +774,11 @@ def generate_plot(self, xlabel=None, filename=None, logscale=False, minmax=None, """Set x axis label and filename to default if not provided""" xlabel = xlabel if xlabel is not None else self.variable - filename = filename if filename is not None else "figures/Histogram_plot_" + self.variable + filename = ( + filename + if filename is not None + else "figures/Histogram_plot_" + self.variable + ) """Create figure with correct number of subplots""" self.fig, self.ax = plt.subplots(nrows=len(self.vis_list)) @@ -567,20 +803,47 @@ def generate_plot(self, xlabel=None, filename=None, logscale=False, minmax=None, """Create Histogram object and populate the subfigure using it""" H = Histogram(scatter_data) c_xlabel = "" if i < len(self.vis_list) - 1 else xlabel - c_xtralabel = str(i + 1) + " risk models" if i > 0 else str(i + 1) + " risk model" + c_xtralabel = ( + str(i + 1) + " risk models" if i > 0 else str(i + 1) + " risk model" + ) c_ylabel = "Frequency" if i == 2 else "" - H.plot(ax=self.ax[i], ylabel=c_ylabel, xtralabel=c_xtralabel, xlabel=c_xlabel, color=self.colour_list[i], - num_bins=num_bins, logscale=logscale, xlims=minmax) - VaR005 = sorted(scatter_data, reverse=True)[int(round(len(scatter_data) * 200. / 4000.))] - realized_events_beyond = len(np.extract(scatter_data > VaR005guess, scatter_data)) - realized_expected_shortfall = np.mean(np.extract(scatter_data > VaR005guess, scatter_data)) - VaR005guess - print(self.variable, c_xtralabel, "Slope: ", 1 / scipy.stats.expon.fit(scatter_data)[0], - "1/200 threshold: ", VaR005, " #Events beyond: ", realized_events_beyond, "Relative: ", - realized_events_beyond * 1.0 / len(scatter_data), " Expected shortfall: ", - realized_expected_shortfall) + H.plot( + ax=self.ax[i], + ylabel=c_ylabel, + xtralabel=c_xtralabel, + xlabel=c_xlabel, + color=self.colour_list[i], + num_bins=num_bins, + logscale=logscale, + xlims=minmax, + ) + VaR005 = sorted(scatter_data, reverse=True)[ + int(round(len(scatter_data) * 200.0 / 4000.0)) + ] + realized_events_beyond = len( + np.extract(scatter_data > VaR005guess, scatter_data) + ) + realized_expected_shortfall = ( + np.mean(np.extract(scatter_data > VaR005guess, scatter_data)) + - VaR005guess + ) + print( + self.variable, + c_xtralabel, + "Slope: ", + 1 / scipy.stats.expon.fit(scatter_data)[0], + "1/200 threshold: ", + VaR005, + " #Events beyond: ", + realized_events_beyond, + "Relative: ", + realized_events_beyond * 1.0 / len(scatter_data), + " Expected shortfall: ", + realized_expected_shortfall, + ) """Finish and save figure""" - self.fig.tight_layout(pad=.1, w_pad=.1, h_pad=.1) + self.fig.tight_layout(pad=0.1, w_pad=0.1, h_pad=0.1) self.fig.savefig(filename + ".pdf") self.fig.savefig(filename + ".png", density=300) print("Saved " + self.variable + " histogram") @@ -608,7 +871,7 @@ def __init__(self, samples_x): self.quantile_series_y_lower = None self.quantile_series_y_upper = None - def make_figure(self, upper_quantile=.25, lower_quantile=.75): + def make_figure(self, upper_quantile=0.25, lower_quantile=0.75): """Method to do the necessary computations to create the CDF plot (incl. mean, median, quantiles. This method populates the variables that are plotted. Arguments: @@ -622,29 +885,46 @@ def make_figure(self, upper_quantile=.25, lower_quantile=.75): """Obtain x coordinates corresponding to the full ordered set of all y values (self.series_y) for each series""" set_of_series_x = [] for i in range(len(self.samples_x)): - x = [self.samples_x[i][np.argmax(self.samples_y[i] >= y)] if self.samples_y[i][0] <= y else 0 for y in - self.series_y] + x = [ + self.samples_x[i][np.argmax(self.samples_y[i] >= y)] + if self.samples_y[i][0] <= y + else 0 + for y in self.series_y + ] set_of_series_x.append(x) """Join x coordinates to matrix of size m x n (n: number of series, m: length of ordered set of y values (self.series_y))""" series_matrix_x = np.vstack(set_of_series_x) """Compute x quantiles, median, mean across all series""" - quantile_lower_x = np.quantile(series_matrix_x, .25, axis=0) - quantile_upper_x = np.quantile(series_matrix_x, .75, axis=0) - self.median_x = np.quantile(series_matrix_x, .50, axis=0) + quantile_lower_x = np.quantile(series_matrix_x, 0.25, axis=0) + quantile_upper_x = np.quantile(series_matrix_x, 0.75, axis=0) + self.median_x = np.quantile(series_matrix_x, 0.50, axis=0) self.mean_x = series_matrix_x.mean(axis=0) """Obtain x coordinates for quantile plots. This is the ordered set of all x coordinates in lower and upper quantile series.""" - self.quantile_series_x = np.unique(np.sort(np.hstack([quantile_lower_x, quantile_upper_x]))) + self.quantile_series_x = np.unique( + np.sort(np.hstack([quantile_lower_x, quantile_upper_x])) + ) """Obtain y coordinates for quantile plots. This is one y value for each x coordinate.""" # self.quantile_series_y_lower = [self.series_y[np.argmax(quantile_lower_x>=x)] if quantile_lower_x[0]<=x else 0 for x in self.quantile_series_x] - self.quantile_series_y_lower = np.asarray([self.series_y[np.argmax(quantile_lower_x >= x)] if np.sum( - np.argmax(quantile_lower_x >= x)) > 0 else np.max(self.series_y) for x in self.quantile_series_x]) + self.quantile_series_y_lower = np.asarray( + [ + self.series_y[np.argmax(quantile_lower_x >= x)] + if np.sum(np.argmax(quantile_lower_x >= x)) > 0 + else np.max(self.series_y) + for x in self.quantile_series_x + ] + ) self.quantile_series_y_upper = np.asarray( - [self.series_y[np.argmax(quantile_upper_x >= x)] if quantile_upper_x[0] <= x else 0 for x in - self.quantile_series_x]) + [ + self.series_y[np.argmax(quantile_upper_x >= x)] + if quantile_upper_x[0] <= x + else 0 + for x in self.quantile_series_x + ] + ) """The first value of lower must be zero""" self.quantile_series_y_lower[0] = 0.0 @@ -656,12 +936,24 @@ def reverse_CDF(self): The method overwrites the attributes used for plotting. Arguments: None. Returns: None.""" - self.series_y = 1. - self.series_y - self.quantile_series_y_lower = 1. - self.quantile_series_y_lower - self.quantile_series_y_upper = 1. - self.quantile_series_y_upper - - def plot(self, ax=None, ylabel="CDF(x)", xlabel="y", upper_quantile=.25, lower_quantile=.75, - force_recomputation=False, show=False, outputname=None, color="C2", plot_cCDF=False, xlims=None): + self.series_y = 1.0 - self.series_y + self.quantile_series_y_lower = 1.0 - self.quantile_series_y_lower + self.quantile_series_y_upper = 1.0 - self.quantile_series_y_upper + + def plot( + self, + ax=None, + ylabel="CDF(x)", + xlabel="y", + upper_quantile=0.25, + lower_quantile=0.75, + force_recomputation=False, + show=False, + outputname=None, + color="C2", + plot_cCDF=False, + xlims=None, + ): """Method to compile the plot. The plot is added to a provided matplotlib axes object or a new one is created. Arguments: ax: matplitlib axes - the system of coordinates into which to plot @@ -694,8 +986,13 @@ def plot(self, ax=None, ylabel="CDF(x)", xlabel="y", upper_quantile=.25, lower_q self.reverse_CDF() """Plot""" - ax.fill_between(self.quantile_series_x, self.quantile_series_y_lower, self.quantile_series_y_upper, - facecolor=color, alpha=0.25) + ax.fill_between( + self.quantile_series_x, + self.quantile_series_y_lower, + self.quantile_series_y_upper, + facecolor=color, + alpha=0.25, + ) ax.plot(self.median_x, self.series_y, color=color) ax.plot(self.mean_x, self.series_y, dashes=[3, 3], color=color) @@ -720,11 +1017,23 @@ def plot(self, ax=None, ylabel="CDF(x)", xlabel="y", upper_quantile=.25, lower_q class Histogram: """Class for plots of ensembles of distributions as CDF (cumulative distribution function) or cCDF (complementary cumulative distribution function) with mean, median, and quantiles""" + def __init__(self, sample_x): self.sample_x = sample_x - def plot(self, ax=None, ylabel="PDF(x)", xtralabel="", xlabel="x", num_bins=50, show=False, outputname=None, - color="C2", logscale=False, xlims=None): + def plot( + self, + ax=None, + ylabel="PDF(x)", + xtralabel="", + xlabel="x", + num_bins=50, + show=False, + outputname=None, + color="C2", + logscale=False, + xlims=None, + ): """Method to compile the plot. The plot is added to a provided matplotlib axes object or a new one is created. Arguments: ax: matplitlib axes - the system of coordinates into which to plot @@ -772,7 +1081,12 @@ def plot(self, ax=None, ylabel="PDF(x)", xtralabel="", xlabel="x", num_bins=50, class RiskModelSpecificCompare: - def __init__(self, infiletype="_premium.dat", refiletype="_reinpremium.dat", number_riskmodels=4): + def __init__( + self, + infiletype="_premium.dat", + refiletype="_reinpremium.dat", + number_riskmodels=4, + ): """Initialises class that plots the insurance and reinsurance data for all risk models for specified data. Accepts: infiletype: Type String. The insurance data to be plotted. @@ -801,16 +1115,18 @@ def __init__(self, infiletype="_premium.dat", refiletype="_reinpremium.dat", num for i in range(number_riskmodels): for j in range(len(self.filetypes)): # Get required data out of file. - filename = "data/"+self.riskmodels[i]+self.filetypes[j] + filename = "data/" + self.riskmodels[i] + self.filetypes[j] rfile = open(filename, "r") data = [eval(k) for k in rfile] rfile.close() - if uninsured_risks and j == 1: # Need operational data for calculating uninsured reinsurance risks. + if ( + uninsured_risks and j == 1 + ): # Need operational data for calculating uninsured reinsurance risks. rfile = open("data/" + self.riskmodels[i] + "_operational.dat", "r") n_insurers = [eval(d) for d in rfile] rfile.close() - n_pr = 4 # Number of peril categories + n_pr = 4 # Number of peril categories # Compute data series data_means = [] @@ -818,21 +1134,63 @@ def __init__(self, infiletype="_premium.dat", refiletype="_reinpremium.dat", num data_q25 = [] data_q75 = [] for k in range(len(data[0])): - if not uninsured_risks: # Used for most risks. + if not uninsured_risks: # Used for most risks. data_means.append(np.mean([item[k] for item in data])) data_q25.append(np.percentile([item[k] for item in data], 25)) data_q75.append(np.percentile([item[k] for item in data], 75)) data_medians.append(np.median([item[k] for item in data])) - elif uninsured_risks and j == 0: # Used for uninsured insurance risks. - data_means.append(np.mean([num_risks - item[k] for item in data])) - data_q25.append(np.percentile([num_risks - item[k] for item in data], 25)) - data_q75.append(np.percentile([num_risks - item[k] for item in data], 75)) - data_medians.append(np.median([num_risks - item[k] for item in data])) - elif uninsured_risks and j ==1: # Used for uninsured reinsurance risks. - data_means.append(np.mean([n_pr * n_insurers[n][k] - data[n][k] for n in range(len(n_insurers))])) - data_q25.append(np.percentile([n_pr * n_insurers[n][k] - data[n][k] for n in range(len(n_insurers))],25)) - data_q75.append(np.percentile([n_pr * n_insurers[n][k] - data[n][k] for n in range(len(n_insurers))],75)) - data_medians.append(np.median([n_pr * n_insurers[n][k] - data[n][k] for n in range(len(n_insurers))])) + elif ( + uninsured_risks and j == 0 + ): # Used for uninsured insurance risks. + data_means.append( + np.mean([num_risks - item[k] for item in data]) + ) + data_q25.append( + np.percentile([num_risks - item[k] for item in data], 25) + ) + data_q75.append( + np.percentile([num_risks - item[k] for item in data], 75) + ) + data_medians.append( + np.median([num_risks - item[k] for item in data]) + ) + elif ( + uninsured_risks and j == 1 + ): # Used for uninsured reinsurance risks. + data_means.append( + np.mean( + [ + n_pr * n_insurers[n][k] - data[n][k] + for n in range(len(n_insurers)) + ] + ) + ) + data_q25.append( + np.percentile( + [ + n_pr * n_insurers[n][k] - data[n][k] + for n in range(len(n_insurers)) + ], + 25, + ) + ) + data_q75.append( + np.percentile( + [ + n_pr * n_insurers[n][k] - data[n][k] + for n in range(len(n_insurers)) + ], + 75, + ) + ) + data_medians.append( + np.median( + [ + n_pr * n_insurers[n][k] - data[n][k] + for n in range(len(n_insurers)) + ] + ) + ) data_means = np.array(data_means) data_medians = np.array(data_medians) @@ -854,47 +1212,87 @@ def plot(self, outputfile): # List of colours and labels used in plotting colours = {"one": "red", "two": "blue", "three": "green", "four": "yellow"} - labels = {"reinexcess_capital": "Excess Capital (Reinsurers)", "excess_capital": "Excess Capital (Insurers)", - "cumulative_unrecovered_claims": "Uncovered Claims (cumulative)", - "cumulative_bankruptcies": "Bankruptcies (cumulative)", - "profitslosses": "Profits and Losses (Insurer)", "contracts": "Contracts (Insurers)", - "cash": "Liquidity (Insurers)", "operational": "Active Insurers", "premium": "Premium (Insurance)", - "reinprofitslosses": "Profits and Losses (Reinsurer)", "reincash": "Liquidity (Reinsurers)", - "reincontracts": "Contracts (Reinsurers)", "reinoperational": "Active Reinsurers", - "reinpremium": "Premium (Reinsurance)", "noninsured_risks": "Non-insured risks (Insurance)", - "noninsured_reinrisks": "Non-insured risks (Reinsurance)"} + labels = { + "reinexcess_capital": "Excess Capital (Reinsurers)", + "excess_capital": "Excess Capital (Insurers)", + "cumulative_unrecovered_claims": "Uncovered Claims (cumulative)", + "cumulative_bankruptcies": "Bankruptcies (cumulative)", + "profitslosses": "Profits and Losses (Insurer)", + "contracts": "Contracts (Insurers)", + "cash": "Liquidity (Insurers)", + "operational": "Active Insurers", + "premium": "Premium (Insurance)", + "reinprofitslosses": "Profits and Losses (Reinsurer)", + "reincash": "Liquidity (Reinsurers)", + "reincontracts": "Contracts (Reinsurers)", + "reinoperational": "Active Reinsurers", + "reinpremium": "Premium (Reinsurance)", + "noninsured_risks": "Non-insured risks (Insurance)", + "noninsured_reinrisks": "Non-insured risks (Reinsurance)", + } # Backup existing figures (so as not to overwrite them) outputfilename = "figures/" + outputfile + "_riskmodel_comparison.pdf" - backupfilename = "figures/" + outputfile + "_riskmodel_comparison_old_" + time.strftime('%Y_%b_%d_%H_%M') + ".pdf" + backupfilename = ( + "figures/" + + outputfile + + "_riskmodel_comparison_old_" + + time.strftime("%Y_%b_%d_%H_%M") + + ".pdf" + ) if os.path.exists(outputfilename): os.rename(outputfilename, backupfilename) # Create figure and two subplot axes then plot on them using loop. self.fig, self.axs = plt.subplots(2, 1) - for f in range(len(self.filetypes)): # Loop for plotting insurance then reinsurance data (length 2). + for f in range( + len(self.filetypes) + ): # Loop for plotting insurance then reinsurance data (length 2). maxlen_plots = 0 - for i in range(self.number_riskmodels): # Loop through number of risk models. + for i in range( + self.number_riskmodels + ): # Loop through number of risk models. # Needed for the fill_between method for plotting percentile data. if i <= 1: j = 0 else: - j = i-1 - filename = "data/"+self.riskmodels[i]+self.filetypes[f] - self.axs[f].plot(range(len(self.timeseries_dict["mean"][filename]))[1200:], self.timeseries_dict["mean"][filename][1200:], color=colours[self.riskmodels[i]], label=self.riskmodels[i]+" riskmodel(s)") - self.axs[f].fill_between(range(len(self.timeseries_dict["quantile25"]["data/"+self.riskmodels[j]+self.filetypes[f]]))[1200:],self.timeseries_dict["quantile25"][filename][1200:],self.timeseries_dict["quantile75"][filename][1200:],facecolor=colours[self.riskmodels[i]], alpha=0.25) - maxlen_plots = max(maxlen_plots, len(self.timeseries_dict["mean"][filename])) + j = i - 1 + filename = "data/" + self.riskmodels[i] + self.filetypes[f] + self.axs[f].plot( + range(len(self.timeseries_dict["mean"][filename]))[1200:], + self.timeseries_dict["mean"][filename][1200:], + color=colours[self.riskmodels[i]], + label=self.riskmodels[i] + " riskmodel(s)", + ) + self.axs[f].fill_between( + range( + len( + self.timeseries_dict["quantile25"][ + "data/" + self.riskmodels[j] + self.filetypes[f] + ] + ) + )[1200:], + self.timeseries_dict["quantile25"][filename][1200:], + self.timeseries_dict["quantile75"][filename][1200:], + facecolor=colours[self.riskmodels[i]], + alpha=0.25, + ) + maxlen_plots = max( + maxlen_plots, len(self.timeseries_dict["mean"][filename]) + ) # Labels axes. xticks = np.arange(1200, maxlen_plots, step=600) self.axs[f].set_xticks(xticks) - self.axs[f].set_xticklabels(["${0:d}$".format(int((xtc - 1200) / 12)) for xtc in xticks]); + self.axs[f].set_xticklabels( + ["${0:d}$".format(int((xtc - 1200) / 12)) for xtc in xticks] + ) ylabel = self.original_filetypes[f][1:-4] self.axs[f].set_ylabel(labels[ylabel]) # Adds legend to top subplot and x axis label to bottom subplot. - self.axs[0].legend(loc='best') - self.axs[1].set_xlabel('Years') + self.axs[0].legend(loc="best") + self.axs[1].set_xlabel("Years") plt.tight_layout() # Saves figure and notifies user (no plt.show so allows progress tracking) @@ -904,27 +1302,48 @@ def plot(self, outputfile): if __name__ == "__main__": # Use argparse to handle command line arguments - parser = argparse.ArgumentParser(description='Model the Insurance sector') - parser.add_argument("--single", action="store_true", help="plot a single run of the insurance model") - parser.add_argument("--pie", action="store_true", help="plot animated pie charts of contract and cash data") - parser.add_argument("--timeseries", action="store_true", help="plot time series of firm data") - parser.add_argument("--timeseries_comparison", action="store_true", help="plot insurance and reinsurance " - "time series for an ensemble of replications of " - "the insurance model") - parser.add_argument("--firmdistribution", action="store_true", - help="plot the CDFs of firm size distributions with quartiles indicating variation across " - "ensemble") - parser.add_argument("--bankruptcydistribution", action="store_true", - help="plot the histograms of bankruptcy events/unrecovered claims across ensemble") - parser.add_argument("--riskmodel_comparison", action="store_true", - help="Plot data comparing risk models for both insurance and reinsurance firms.") + parser = argparse.ArgumentParser(description="Model the Insurance sector") + parser.add_argument( + "--single", action="store_true", help="plot a single run of the insurance model" + ) + parser.add_argument( + "--pie", + action="store_true", + help="plot animated pie charts of contract and cash data", + ) + parser.add_argument( + "--timeseries", action="store_true", help="plot time series of firm data" + ) + parser.add_argument( + "--timeseries_comparison", + action="store_true", + help="plot insurance and reinsurance " + "time series for an ensemble of replications of " + "the insurance model", + ) + parser.add_argument( + "--firmdistribution", + action="store_true", + help="plot the CDFs of firm size distributions with quartiles indicating variation across " + "ensemble", + ) + parser.add_argument( + "--bankruptcydistribution", + action="store_true", + help="plot the histograms of bankruptcy events/unrecovered claims across ensemble", + ) + parser.add_argument( + "--riskmodel_comparison", + action="store_true", + help="Plot data comparing risk models for both insurance and reinsurance firms.", + ) args = parser.parse_args() if args.single: # load in data from the history_logs dictionary with open("data/history_logs.dat","r") as rfile: - with open("data/history_logs.dat","r") as rfile: - history_logs_list = [eval(k) for k in rfile] # one dict on each line + with open("data/history_logs.dat", "r") as rfile: + history_logs_list = [eval(k) for k in rfile] # one dict on each line # first create visualisation object, then create graph/animation objects as necessary vis = visualisation(history_logs_list) @@ -941,12 +1360,14 @@ def plot(self, outputfile): if args.timeseries_comparison or args.bankruptcydistribution: vis_list = [] - colour_list = ['red', 'blue', 'green', 'yellow'] + colour_list = ["red", "blue", "green", "yellow"] # Loads all risk model history logs data (very long :'( ) and creates list of visualisation class instances. - filenames = ["./data/" + x + "_history_logs.dat" for x in ["one", "two", "three", "four"]] + filenames = [ + "./data/" + x + "_history_logs.dat" for x in ["one", "two", "three", "four"] + ] for filename in filenames: - with open(filename, 'r') as rfile: + with open(filename, "r") as rfile: history_logs_list = [eval(k) for k in rfile] # one dict on each line vis_list.append(visualisation(history_logs_list)) @@ -962,41 +1383,81 @@ def plot(self, outputfile): for vis in vis_list: vis.populate_scatter_data() - HP = Histogram_plot(vis_list, colour_list, variable="bankruptcy_events_relative_clustered") - HP.generate_plot(logscale=True, xlabel="Share of bankrupt firms", minmax=[0, 0.5], - VaR005guess=0.1) # =0.056338028169014086) # this is the VaR threshold for 4 risk models with reinsuranc + HP = Histogram_plot( + vis_list, colour_list, variable="bankruptcy_events_relative_clustered" + ) + HP.generate_plot( + logscale=True, + xlabel="Share of bankrupt firms", + minmax=[0, 0.5], + VaR005guess=0.1, + ) # =0.056338028169014086) # this is the VaR threshold for 4 risk models with reinsuranc HP = Histogram_plot(vis_list, colour_list, variable="unrecovered_claims") - HP.generate_plot(logscale=True, xlabel="Damages not recovered", minmax=[0, 6450000], - VaR005guess=0.1) # =691186.8726311699) # this is the VaR threshold for 4 risk models with reinsurance + HP.generate_plot( + logscale=True, + xlabel="Damages not recovered", + minmax=[0, 6450000], + VaR005guess=0.1, + ) # =691186.8726311699) # this is the VaR threshold for 4 risk models with reinsurance if args.firmdistribution: vis_list = [] - colour_list = ['red', 'blue', 'green', 'yellow'] + colour_list = ["red", "blue", "green", "yellow"] # Loads all risk model history logs data (very long :'( ) and creates list of visualisation class instances. - filenames = ["./data/" + x + "_history_logs_complete.dat" for x in ["one", "two", "three", "four"]] + filenames = [ + "./data/" + x + "_history_logs_complete.dat" + for x in ["one", "two", "three", "four"] + ] for filename in filenames: - with open(filename, 'r') as rfile: + with open(filename, "r") as rfile: history_logs_list = [eval(k) for k in rfile] # one dict on each line vis_list.append(visualisation(history_logs_list)) # Creates CDF for firm size using cash as measure of size. - CP = CDF_distribution_plot(vis_list, colour_list, variable="insurance_firms_cash", timestep=-1, plot_cCDF=True) + CP = CDF_distribution_plot( + vis_list, + colour_list, + variable="insurance_firms_cash", + timestep=-1, + plot_cCDF=True, + ) CP.generate_plot(xlabel="Firm size (capital)") if not isleconfig.simulation_parameters["reinsurance_off"]: - CP = CDF_distribution_plot(vis_list, colour_list, variable="reinsurance_firms_cash", timestep=-1, - plot_cCDF=True) + CP = CDF_distribution_plot( + vis_list, + colour_list, + variable="reinsurance_firms_cash", + timestep=-1, + plot_cCDF=True, + ) CP.generate_plot(xlabel="Firm size (capital)") if args.riskmodel_comparison: # Lists of insurance and reinsurance data files to be compared (Must be same size and equivalent). - data_types = ["_noninsured_risks.dat", "_excess_capital.dat", "_cash.dat", "_contracts.dat", "_premium.dat", "_operational.dat"] - rein_data_types = ["_noninsured_reinrisks.dat", "_reinexcess_capital.dat", "_reincash.dat", "_reincontracts.dat", "_reinpremium.dat", "_reinoperational.dat"] + data_types = [ + "_noninsured_risks.dat", + "_excess_capital.dat", + "_cash.dat", + "_contracts.dat", + "_premium.dat", + "_operational.dat", + ] + rein_data_types = [ + "_noninsured_reinrisks.dat", + "_reinexcess_capital.dat", + "_reincash.dat", + "_reincontracts.dat", + "_reinpremium.dat", + "_reinoperational.dat", + ] # Loops through data types and loads, plots, and saves each one. for type in range(len(data_types)): - compare = RiskModelSpecificCompare(infiletype=data_types[type], refiletype=rein_data_types[type]) + compare = RiskModelSpecificCompare( + infiletype=data_types[type], refiletype=rein_data_types[type] + ) compare.plot(outputfile=data_types[type][1:-4]) # ਲ਼ diff --git a/visualization_network.py b/visualization_network.py index 503fede..40a5135 100644 --- a/visualization_network.py +++ b/visualization_network.py @@ -12,28 +12,44 @@ def __init__(self, event_schedule=None): No accepted values. This created the figure that the network will be displayed on so only called once, and only if show_network is True.""" - self.figure = plt.figure(num=None, figsize=(10, 8), dpi=100, facecolor='w', edgecolor='k') - self.save_data = {'unweighted_network': [], 'weighted_network': [], 'network_edgelabels': [], 'network_node_labels': [], 'number_of_agents': []} + self.figure = plt.figure( + num=None, figsize=(10, 8), dpi=100, facecolor="w", edgecolor="k" + ) + self.save_data = { + "unweighted_network": [], + "weighted_network": [], + "network_edgelabels": [], + "network_node_labels": [], + "number_of_agents": [], + } self.event_schedule = event_schedule def compute_measures(self): """Method to obtain the network distribution and print it. No accepted values. No return values.""" - #degrees = self.network.degree() + # degrees = self.network.degree() degree_distr = dict(self.network.degree()).values() in_degree_distr = dict(self.network.in_degree()).values() out_degree_distr = dict(self.network.out_degree()).values() is_connected = nx.is_weakly_connected(self.network) - #is_connected = nx.is_strongly_connected(self.network) # must always be False + # is_connected = nx.is_strongly_connected(self.network) # must always be False try: node_centralities = nx.eigenvector_centrality(self.network) except: node_centralities = nx.betweenness_centrality(self.network) # TODO: and more, choose more meaningful ones... - - print("Graph is connected: ", is_connected, "\nIn degrees ", in_degree_distr, "\nOut degrees", out_degree_distr, \ - "\nCentralities", node_centralities) + + print( + "Graph is connected: ", + is_connected, + "\nIn degrees ", + in_degree_distr, + "\nOut degrees", + out_degree_distr, + "\nCentralities", + node_centralities, + ) def update(self, insurancefirms, reinsurancefirms, catbonds): """Method to update the network. @@ -51,7 +67,11 @@ def update(self, insurancefirms, reinsurancefirms, catbonds): """obtain lists of operational entities""" op_entities = {} self.num_entities = {} - for firmtype, firmlist in [("insurers", self.insurancefirms), ("reinsurers", self.reinsurancefirms), ("catbonds", self.catbonds)]: + for firmtype, firmlist in [ + ("insurers", self.insurancefirms), + ("reinsurers", self.reinsurancefirms), + ("catbonds", self.catbonds), + ]: op_firmtype = [firm for firm in firmlist if firm.operational] op_entities[firmtype] = op_firmtype self.num_entities[firmtype] = len(op_firmtype) @@ -59,15 +79,21 @@ def update(self, insurancefirms, reinsurancefirms, catbonds): self.network_size = sum(self.num_entities.values()) """Create weighted adjacency matrix and category edge labels""" - weights_matrix = np.zeros(self.network_size ** 2).reshape(self.network_size, self.network_size) + weights_matrix = np.zeros(self.network_size ** 2).reshape( + self.network_size, self.network_size + ) self.edge_labels = {} self.node_labels = {} - for idx_to, firm in enumerate(op_entities["insurers"] + op_entities["reinsurers"]): + for idx_to, firm in enumerate( + op_entities["insurers"] + op_entities["reinsurers"] + ): self.node_labels[idx_to] = firm.id eolrs = firm.get_excess_of_loss_reinsurance() for eolr in eolrs: try: - idx_from = self.num_entities["insurers"] + (op_entities["reinsurers"] + op_entities["catbonds"]).index(eolr["reinsurer"]) + idx_from = self.num_entities["insurers"] + ( + op_entities["reinsurers"] + op_entities["catbonds"] + ).index(eolr["reinsurer"]) weights_matrix[idx_from][idx_to] = eolr["value"] self.edge_labels[idx_to, idx_from] = eolr["category"] except ValueError: @@ -77,10 +103,10 @@ def update(self, insurancefirms, reinsurancefirms, catbonds): adj_matrix = np.sign(weights_matrix) """Add this iteration of network data to be saved""" - self.save_data['unweighted_network'].append(adj_matrix.tolist()) - self.save_data['network_edge_labels'].append(self.edge_labels) - self.save_data['network_node_labels'].append(self.node_labels) - self.save_data['number_of_agents'].append(self.num_entities) + self.save_data["unweighted_network"].append(adj_matrix.tolist()) + self.save_data["network_edge_labels"].append(self.edge_labels) + self.save_data["network_node_labels"].append(self.node_labels) + self.save_data["number_of_agents"].append(self.num_entities) def visualize(self): """Method to add the network to the figure initialised in __init__. @@ -90,12 +116,23 @@ def visualize(self): corresponding to the category being reinsured, and adds a legend to indicate which node is insurer, reinsurer, or CatBond. This method allows the figure to be updated without a new figure being created or stopping the program.""" - plt.ion() # Turns on interactive graph mode. + plt.ion() # Turns on interactive graph mode. firmtypes = np.ones(self.network_size) - firmtypes[self.num_entities["insurers"]:self.num_entities["insurers"]+self.num_entities["reinsurers"]] = 0.5 - firmtypes[self.num_entities["insurers"]+self.num_entities["reinsurers"]:] = 1.3 - print("Number of insurers: %i, Number of Reinsurers: %i, CatBonds: %i" - % (self.num_entities["insurers"], self.num_entities["reinsurers"], self.num_entities['catbonds'])) + firmtypes[ + self.num_entities["insurers"] : self.num_entities["insurers"] + + self.num_entities["reinsurers"] + ] = 0.5 + firmtypes[ + self.num_entities["insurers"] + self.num_entities["reinsurers"] : + ] = 1.3 + print( + "Number of insurers: %i, Number of Reinsurers: %i, CatBonds: %i" + % ( + self.num_entities["insurers"], + self.num_entities["reinsurers"], + self.num_entities["catbonds"], + ) + ) # Either this or below create a network, this one has id's but no key. # pos = nx.spring_layout(self.network_unweighted) @@ -104,17 +141,56 @@ def visualize(self): "Draw Network" pos = nx.spring_layout(self.network_unweighted) - nx.draw_networkx_nodes(self.network_unweighted, pos, list(range(self.num_entities["insurers"])), - node_color='b', node_size=50, alpha=0.9, label='Insurer') - nx.draw_networkx_nodes(self.network_unweighted, pos, list(range(self.num_entities["insurers"], self.num_entities["insurers"]+self.num_entities["reinsurers"])), - node_color='r', node_size=50, alpha=0.9, label='Reinsurer') - nx.draw_networkx_nodes(self.network_unweighted, pos, list(range(self.num_entities["insurers"] + self.num_entities["reinsurers"], self.num_entities["insurers"] + self.num_entities["reinsurers"] + self.num_entities['catbonds'])), - node_color='g', node_size=50, alpha=0.9, label='CatBond') - nx.draw_networkx_edges(self.network_unweighted, pos, width=1.0, alpha=0.5, node_size=50) - nx.draw_networkx_edge_labels(self.network_unweighted, pos, self.edge_labels, font_size=5) - nx.draw_networkx_labels(self.network_unweighted, pos, self.node_labels, font_size=20) - plt.legend(scatterpoints=1, loc='upper right') - plt.axis('off') + nx.draw_networkx_nodes( + self.network_unweighted, + pos, + list(range(self.num_entities["insurers"])), + node_color="b", + node_size=50, + alpha=0.9, + label="Insurer", + ) + nx.draw_networkx_nodes( + self.network_unweighted, + pos, + list( + range( + self.num_entities["insurers"], + self.num_entities["insurers"] + self.num_entities["reinsurers"], + ) + ), + node_color="r", + node_size=50, + alpha=0.9, + label="Reinsurer", + ) + nx.draw_networkx_nodes( + self.network_unweighted, + pos, + list( + range( + self.num_entities["insurers"] + self.num_entities["reinsurers"], + self.num_entities["insurers"] + + self.num_entities["reinsurers"] + + self.num_entities["catbonds"], + ) + ), + node_color="g", + node_size=50, + alpha=0.9, + label="CatBond", + ) + nx.draw_networkx_edges( + self.network_unweighted, pos, width=1.0, alpha=0.5, node_size=50 + ) + nx.draw_networkx_edge_labels( + self.network_unweighted, pos, self.edge_labels, font_size=5 + ) + nx.draw_networkx_labels( + self.network_unweighted, pos, self.node_labels, font_size=20 + ) + plt.legend(scatterpoints=1, loc="upper right") + plt.axis("off") plt.show() """Update figure""" @@ -130,7 +206,9 @@ def __init__(self, network_data, num_iter): num_iter: Type Integer. Used to tell animation how many frames it should have. No return values. This class is given the loaded network data and then uses it to create an animated network.""" - self.figure = plt.figure(num=None, figsize=(10, 8), dpi=100, facecolor='w', edgecolor='k') + self.figure = plt.figure( + num=None, figsize=(10, 8), dpi=100, facecolor="w", edgecolor="k" + ) self.unweighted_network_data = network_data[0]["unweighted_network_data"] self.network_edge_labels = network_data[0]["network_edge_labels"] self.network_node_labels = network_data[0]["network_node_labels"] @@ -150,39 +228,91 @@ def update(self, i): No return values. This method is called from matplotlib.animate.FuncAnimation to update the plot to the next time iteration.""" self.figure.clear() - plt.suptitle('Network Timestep %i' % i) - unweighted_nx_network = nx.from_numpy_array(np.array(self.unweighted_network_data[i])) - pos = nx.kamada_kawai_layout(unweighted_nx_network) # Can also use circular/shell/spring - - nx.draw_networkx_nodes(unweighted_nx_network, pos, list(range(self.number_agent_type[i]["insurers"])), - node_color='b', node_size=50, alpha=0.9, label='Insurer') - nx.draw_networkx_nodes(unweighted_nx_network, pos, list( - range(self.number_agent_type[i]["insurers"], - self.number_agent_type[i]["insurers"] + self.number_agent_type[i]["reinsurers"])), - node_color='r', node_size=50, alpha=0.9, label='Reinsurer') - nx.draw_networkx_nodes(unweighted_nx_network, pos, list( - range(self.number_agent_type[i]["insurers"] + self.number_agent_type[i]["reinsurers"], - self.number_agent_type[i]["insurers"] + self.number_agent_type[i]["reinsurers"] + - self.number_agent_type[i]['catbonds'])), - node_color='g', node_size=50, alpha=0.9, label='CatBond') - nx.draw_networkx_edges(unweighted_nx_network, pos, width=1.0, alpha=0.5, node_size=50) - - nx.draw_networkx_edge_labels(self.unweighted_network_data[i], pos, self.network_edge_labels[i], font_size=3) - nx.draw_networkx_labels(self.unweighted_network_data[i], pos, self.network_node_labels[i], font_size=7) + plt.suptitle("Network Timestep %i" % i) + unweighted_nx_network = nx.from_numpy_array( + np.array(self.unweighted_network_data[i]) + ) + pos = nx.kamada_kawai_layout( + unweighted_nx_network + ) # Can also use circular/shell/spring + + nx.draw_networkx_nodes( + unweighted_nx_network, + pos, + list(range(self.number_agent_type[i]["insurers"])), + node_color="b", + node_size=50, + alpha=0.9, + label="Insurer", + ) + nx.draw_networkx_nodes( + unweighted_nx_network, + pos, + list( + range( + self.number_agent_type[i]["insurers"], + self.number_agent_type[i]["insurers"] + + self.number_agent_type[i]["reinsurers"], + ) + ), + node_color="r", + node_size=50, + alpha=0.9, + label="Reinsurer", + ) + nx.draw_networkx_nodes( + unweighted_nx_network, + pos, + list( + range( + self.number_agent_type[i]["insurers"] + + self.number_agent_type[i]["reinsurers"], + self.number_agent_type[i]["insurers"] + + self.number_agent_type[i]["reinsurers"] + + self.number_agent_type[i]["catbonds"], + ) + ), + node_color="g", + node_size=50, + alpha=0.9, + label="CatBond", + ) + nx.draw_networkx_edges( + unweighted_nx_network, pos, width=1.0, alpha=0.5, node_size=50 + ) + + nx.draw_networkx_edge_labels( + self.unweighted_network_data[i], + pos, + self.network_edge_labels[i], + font_size=3, + ) + nx.draw_networkx_labels( + self.unweighted_network_data[i], + pos, + self.network_node_labels[i], + font_size=7, + ) while self.all_events[0] == i: - plt.title('EVENT!') + plt.title("EVENT!") self.all_events = self.all_events[1:] - plt.legend(loc='upper right') - plt.axis('off') + plt.legend(loc="upper right") + plt.axis("off") def animate(self): """Method to create animation. No accepted values. No return values.""" - self.network_ani = animation.FuncAnimation(self.figure, self.update, frames=self.num_iter, repeat=False, - interval=50, save_count=self.num_iter) + self.network_ani = animation.FuncAnimation( + self.figure, + self.update, + frames=self.num_iter, + repeat=False, + interval=50, + save_count=self.num_iter, + ) def save_network_animation(self): """Method to save animation as MP4. @@ -190,14 +320,22 @@ def save_network_animation(self): No return values.""" if not os.path.isdir("figures"): os.makedirs("figures") - self.network_ani.save("figures/animated_network.mp4", writer="ffmpeg", dpi=200, fps=5) + self.network_ani.save( + "figures/animated_network.mp4", writer="ffmpeg", dpi=200, fps=5 + ) if __name__ == "__main__": # Use argparse to handle command line arguments - parser = argparse.ArgumentParser(description='Plot the network of the insurance sector') - parser.add_argument("--save", action="store_true", help="Save the network as an mp4") - parser.add_argument("--number_iterations", type=int, help="number of frames for animation") + parser = argparse.ArgumentParser( + description="Plot the network of the insurance sector" + ) + parser.add_argument( + "--save", action="store_true", help="Save the network as an mp4" + ) + parser.add_argument( + "--number_iterations", type=int, help="number of frames for animation" + ) args = parser.parse_args() args.save = True args.number_iterations = 199 From 951d8e3d61c858d39667200442ffcb4b20316881 Mon Sep 17 00:00:00 2001 From: Chris Hughes Date: Fri, 2 Aug 2019 16:24:47 +0100 Subject: [PATCH 082/125] Merge branch 'thomas' # Conflicts: # calibration_conditions.py # calibrationscore.py # catbond.py # distribution_wrapper_test.py # distributionreinsurance.py # distributiontruncated.py # ensemble.py # genericagent.py # genericagentabce.py # insurancecontract.py # insurancefirm.py # insurancesimulation.py # isleconfig.py # listify.py # logger.py # metainsurancecontract.py # metainsuranceorg.py # metaplotter.py # metaplotter_pl_timescale.py # metaplotter_pl_timescale_additional_measures.py # plotter.py # plotter_pl_timescale.py # reinsurancecontract.py # reinsurancefirm.py # resume.py # riskmodel.py # setup.py # start.py # visualisation.py # visualization_network.py --- calibration_conditions.py | 1 + genericclasses.py | 2 +- insurancefirms.py | 32 +++++++++------ insurancesimulation.py | 10 +---- isleconfig.py | 6 +-- logger.py | 8 ++-- metainsuranceorg.py | 83 ++++++++++++++++++--------------------- start.py | 3 -- visualisation.py | 1 + 9 files changed, 72 insertions(+), 74 deletions(-) diff --git a/calibration_conditions.py b/calibration_conditions.py index de0e490..7d8c51b 100644 --- a/calibration_conditions.py +++ b/calibration_conditions.py @@ -106,6 +106,7 @@ def condition_defaults_insurance( """Test for number of insurance bankruptcies (non zero, not all insurers)""" # series = logobj.history_logs['total_operational'] # if series[-1] != 0 and any(series[i]-series[i-1] < 0 for i in range(1,len(series))): + pass opseries = [ logobj.history_logs["insurance_firms_cash"][-1][i][2] for i in range(len(logobj.history_logs["insurance_firms_cash"][-1])) diff --git a/genericclasses.py b/genericclasses.py index 7c41ec8..28818f8 100644 --- a/genericclasses.py +++ b/genericclasses.py @@ -237,7 +237,7 @@ def add(self, contract: "ReinsuranceContract", value: float) -> None: if index != 0 and self.reinsured_regions[category][index - 1][1] > lower_bound: raise ValueError( "Attempted to add reinsurance overlapping with existing reinsurance \n" - f"Reinsured regions are {self.reinsured_regions[category]}" + f"Reinsured regions are {list(self.reinsured_regions[category])}" ) self.riskmodel.set_reinsurance_coverage( diff --git a/insurancefirms.py b/insurancefirms.py index f94b2ed..49632ba 100644 --- a/insurancefirms.py +++ b/insurancefirms.py @@ -65,12 +65,12 @@ def get_reinsurable_fraction(self, value_by_category): for categ in range(len(value_by_category)): value: float = value_by_category[categ] uncovered = self.reinsurance_profile.uncovered(categ) - maximum_excess: float = self.np_reinsurance_excess_fraction * value + maximum_excess: float = self.np_reinsurance_limit_fraction * value miniumum_deductible: float = self.np_reinsurance_deductible_fraction * value for region in uncovered: if region[1] > miniumum_deductible and region[0] < maximum_excess: total += min( - region[1] / value, self.np_reinsurance_excess_fraction + region[1] / value, self.np_reinsurance_limit_fraction ) - max(region[0] / value, self.np_reinsurance_deductible_fraction) total = total / len(value_by_category) return total @@ -293,13 +293,13 @@ def ask_reinsurance_non_proportional_by_category( tranches = self.reinsurance_profile.uncovered(categ_id) # Don't get reinsurance above maximum limit - while tranches[-1][1] > self.np_reinsurance_excess_fraction * total_value: - if tranches[-1][0] >= self.np_reinsurance_excess_fraction * total_value: + while tranches[-1][1] > self.np_reinsurance_limit_fraction * total_value: + if tranches[-1][0] >= self.np_reinsurance_limit_fraction * total_value: tranches.pop() else: tranches[-1] = ( tranches[-1][0], - self.np_reinsurance_excess_fraction * total_value, + self.np_reinsurance_limit_fraction * total_value, ) while ( tranches[0][0] < self.np_reinsurance_deductible_fraction * total_value @@ -317,18 +317,28 @@ def ask_reinsurance_non_proportional_by_category( tranches[0][1], ) for tranche in tranches: - if tranche[1] - tranche[0] <= 2: - # Small gaps are acceptable to avoid having trivial contracts + if (tranche[1] - tranche[0]) / total_value <= min( + 2 / total_value, + 0.05 + * ( + self.np_reinsurance_limit_fraction + - self.np_reinsurance_deductible_fraction + ), + ): + # Small gaps are acceptable to avoid having trivial contracts - we don't accept tranches with + # size less than two or 5% of the total reinsurable ammount tranches.remove(tranche) if not tranches: # If we've ended up with no tranches, give up and return return None - while len(tranches) < min_tranches: + while ( + len(tranches) < min_tranches + and not self.reinsurance_profile.all_contracts() + ): tranches = self.reinsurance_profile.split_longest(tranches) - if purpose == "rollover": - risks_to_return = [] + risks_to_return = [] for tranche in tranches: assert tranche[1] > tranche[0] risk = genericclasses.RiskProperties( @@ -436,7 +446,7 @@ def issue_cat_bond( insurancetype="excess-of-loss", number_risks=number_risks, deductible_fraction=self.np_reinsurance_deductible_fraction, - limit_fraction=self.np_reinsurance_excess_fraction, + limit_fraction=self.np_reinsurance_limit_fraction, periodized_total_premium=0, runtime=12, expiration=time + 12, diff --git a/insurancesimulation.py b/insurancesimulation.py index 6c9ba0d..7139e00 100644 --- a/insurancesimulation.py +++ b/insurancesimulation.py @@ -582,12 +582,6 @@ def iterate(self, t: int): self._update_model_counters() - for reinsurer in self.reinsurancefirms: - for i in range(len(self.inaccuracy)): - if reinsurer.operational: - if reinsurer.riskmodel.inaccuracy == self.inaccuracy[i]: - self.reinsurance_models_counter[i] += 1 - network_division = 1 # How often network is updated. if ( (isleconfig.show_network or isleconfig.save_network) @@ -712,9 +706,9 @@ def _inflict_peril(self, categ_id: int, damage: float, t: int): if isleconfig.verbose: print("**** PERIL", damage) damagevalues = np.random.beta( - a=1, b=1.0 / damage - 1, size=self.risks_counter[categ_id] + a=1, b=1.0 / damage - 1, size=len(affected_contracts) ) - uniformvalues = np.random.uniform(0, 1, size=self.risks_counter[categ_id]) + uniformvalues = np.random.uniform(0, 1, size=len(affected_contracts)) for i, contract in enumerate(affected_contracts): contract.explode(t, uniformvalues[i], damagevalues[i]) diff --git a/isleconfig.py b/isleconfig.py index e04c159..eff0a6b 100644 --- a/isleconfig.py +++ b/isleconfig.py @@ -9,7 +9,7 @@ # Should logs be small in ensemble runs (only aggregated level data)? slim_log = True buy_bankruptcies = False -enforce_regulations = True +enforce_regulations = False aid_relief = False simulation_parameters = { @@ -32,7 +32,7 @@ "mean_contract_runtime": 12, "contract_runtime_halfspread": 2, "default_contract_payment_period": 3, - "max_time": 1000, + "max_time": 300, "money_supply": 2000000000, "event_time_mean_separation": 100 / 3.0, "expire_immediately": False, @@ -114,6 +114,6 @@ "scale_inaccuracy": 0.3, # The smallest number of tranches that an insurer will issue when asking for reinsurance. Note: even if this is 1, # insurers will still end up with layered reinsurance to fill gaps - "min_tranches": 5, + "min_tranches": 1, "aid_budget": 1000000, } diff --git a/logger.py b/logger.py index 11cc006..cf3e384 100644 --- a/logger.py +++ b/logger.py @@ -11,7 +11,7 @@ "total_reincontracts total_reinoperational total_catbondsoperational market_premium " "market_reinpremium cumulative_bankruptcies cumulative_market_exits cumulative_unrecovered_claims " "cumulative_claims insurance_firms_cash reinsurance_firms_cash market_diffvar " - "rc_event_schedule_initial rc_event_damage_initial number_riskmodels individual_contracts reinsurance_contracts" + "rc_event_schedule_initial rc_event_damage_initial number_riskmodels individual_contracts reinsurance_contracts " "unweighted_network_data network_node_labels network_edge_labels number_of_agents " "cumulative_bought_firms cumulative_nonregulation_firms" ).split(" ") @@ -52,7 +52,7 @@ def __init__( insurance_sector = ( "total_cash total_excess_capital total_profitslosses " "total_contracts total_operational cumulative_bankruptcies " - "cumulative_market_exits cumulative_claims cumulative_unrecovered_claims" + "cumulative_market_exits cumulative_claims cumulative_unrecovered_claims " "cumulative_bought_firms cumulative_nonregulation_firms" ).split(" ") for _v in insurance_sector: @@ -163,7 +163,7 @@ def restore_logger_object(self, log): self.number_riskmodels = log["number_riskmodels"] """Restore history log""" - self.history_logs_to_save.append(log) + self.history_logs.update(log) def save_log(self, background_run): """Method to save log to disk of local machine. Distinguishes single and ensemble runs. @@ -210,7 +210,7 @@ def single_log_prepare(self): ) if os.path.exists(filename): os.rename(filename, backupfilename) - for data in self.history_logs_to_save: + for data in self.history_logs: to_log.append((filename, data, "a")) return to_log diff --git a/metainsuranceorg.py b/metainsuranceorg.py index 784f0ae..222fd7f 100644 --- a/metainsuranceorg.py +++ b/metainsuranceorg.py @@ -178,7 +178,7 @@ def __init__( self.np_reinsurance_deductible_fraction = simulation_parameters[ "default_non-proportional_reinsurance_deductible" ] - self.np_reinsurance_excess_fraction = simulation_parameters[ + self.np_reinsurance_limit_fraction = simulation_parameters[ "default_non-proportional_reinsurance_excess" ] self.np_reinsurance_premium_share = simulation_parameters[ @@ -187,7 +187,6 @@ def __init__( self.underwritten_contracts: MutableSequence["MetaInsuranceContract"] = [] self.is_insurer = True self.is_reinsurer = False - self.operational = True self.warning = False self.age = 0 @@ -203,7 +202,6 @@ def __init__( self.var_category = np.zeros( self.simulation_no_risk_categories ) # var_sum disaggregated by category - self.naccep = [] self.risks_retained = [] self.reinrisks_kept = [] self.balance_ratio = simulation_parameters["insurers_balance_ratio"] @@ -269,23 +267,13 @@ def iterate(self, time: int): ): # If not enough average cash then firm is closed and so no underwriting. return - if self.warning is False: - """request risks to be considered for underwriting in the next period, organised by insurance type""" - new_nonproportional_risks, new_risks = self.get_newrisks_by_type() - contracts_offered = len(new_risks) - if isleconfig.verbose and contracts_offered < 2 * contracts_dissolved: - print( - "Something wrong; agent {0:d} receives too few new contracts {1:d} <= {2:d}".format( - self.id, contracts_offered, 2 * contracts_dissolved - ) - ) """Collect and process new risks""" self.collect_process_evaluate_risks(time, contracts_dissolved) """adjust liquidity, borrow or invest""" # Not implemented - if not self.warning: + if self.operational and not self.warning: self.market_permanency(time) self.roll_over(time) @@ -399,25 +387,28 @@ def collect_process_evaluate_risks( # Here the new risks are organized by category. risks_per_categ = self.risks_reinrisks_organizer(new_risks) - - for repetition in range(self.recursion_limit): - # TODO: find an efficient way to stop the recursion if there are no more risks to accept or if it is - # not accepting any more over several iterations. Done, maybe? - # Here we process all the new risks in order to keep the portfolio as balanced as possible. - has_accepted_risks, not_accepted_risks = self.process_newrisks_insurer( - risks_per_categ, - acceptable_by_category, - var_per_risk_per_categ, - cash_left_by_categ, - time, + if risks_per_categ != [ + [] for _ in range(self.simulation_no_risk_categories) + ]: + for repetition in range(self.recursion_limit): + # TODO: find an efficient way to stop the recursion if there are no more risks to accept or if it is + # not accepting any more over several iterations. Done, maybe? + # Here we process all the new risks in order to keep the portfolio as balanced as possible. + has_accepted_risks, not_accepted_risks = self.process_newrisks_insurer( + risks_per_categ, + acceptable_by_category, + var_per_risk_per_categ, + cash_left_by_categ, + time, + ) + risks_per_categ = not_accepted_risks + # has_accepted_risks = False # Debug + if not has_accepted_risks: + # Stop condition implemented. Might solve the previous TODO. + break + self.simulation.return_risks( + list(chain.from_iterable(not_accepted_risks)) ) - risks_per_categ = not_accepted_risks - if not has_accepted_risks: - # Stop condition implemented. Might solve the previous TODO. - break - self.simulation.return_risks( - list(chain.from_iterable(not_accepted_risks)) - ) # print(self.id, " now has ", len(self.underwritten_contracts), " & returns ", len(not_accepted_risks)) self.update_risk_characterisations() @@ -468,6 +459,9 @@ def market_exit(self, time: int): self.enter_bankruptcy() because in this case all the obligations can be paid. After paying all the obligations this method dissolves the firm through the method self.dissolve().""" due = [item for item in self.obligations] + sum_due = sum([item.amount for item in due]) + if sum_due > self.cash: + self.enter_bankruptcy(time) for obligation in due: self._pay(obligation) self.obligations = [] @@ -606,23 +600,26 @@ def estimate_var(self) -> None: ) self.var_sum += self.var_category[category] - # Record reinsurance info - for reinsurance in self.category_reinsurance: - if reinsurance is not None: - current_reinsurance_info.append( - [reinsurance.deductible, reinsurance.excess] - ) - else: - current_reinsurance_info.append([0, 0]) + # TODO: Fix + # # Record reinsurance info + # for reinsurance in self.category_reinsurance: + # if reinsurance is not None: + # current_reinsurance_info.append( + # [reinsurance.deductible, reinsurance.excess] + # ) + # else: + # current_reinsurance_info.append([0, 0]) # Rotate lists and replace for up-to-date list for 12 iterations self.var_sum_last_periods = np.roll(self.var_sum_last_periods, 4) self.var_sum_last_periods[0] = self.var_category self.reinsurance_history = np.roll(self.reinsurance_history, 8) - self.reinsurance_history[0] = current_reinsurance_info + + # TODO: as above + # self.reinsurance_history[0] = current_reinsurance_info # Calculate average no. risks per category - if not sum(self.counter_category) == 0: + if sum(self.counter_category) != 0: self.var_counter_per_risk = self.var_counter / sum( self.counter_category ) @@ -795,11 +792,9 @@ def balanced_portfolio( # The balance condition is not taken into account if the cash reserve is far away from the limit. # (total_cash_employed_by_categ_post/self.cash <<< 1) cash_left_by_categ = self.cash - cash_reserved_by_categ_store - return True, cash_left_by_categ else: cash_left_by_categ = self.cash - cash_reserved_by_categ - return False, cash_left_by_categ def process_newrisks_reinsurer( diff --git a/start.py b/start.py index 2140092..6949682 100644 --- a/start.py +++ b/start.py @@ -75,9 +75,6 @@ def main( # Need to use t+1 as resume will start at time saved save_simulation(t + 1, simulation, sim_params, exit_now=False) - # Finish simulation, write logs - simulation.finalize() - # It is required to return this list to download all the data generated by a single run of the model from the cloud. return simulation.obtain_log(requested_logs) diff --git a/visualisation.py b/visualisation.py index 2c46f7c..ace9055 100644 --- a/visualisation.py +++ b/visualisation.py @@ -1340,6 +1340,7 @@ def plot(self, outputfile): args = parser.parse_args() if args.single: + from numpy import array # load in data from the history_logs dictionary with open("data/history_logs.dat","r") as rfile: with open("data/history_logs.dat", "r") as rfile: From 72f010be91897ef28919d58287ce8c86ad8a08bd Mon Sep 17 00:00:00 2001 From: KloskaT Date: Fri, 2 Aug 2019 17:00:22 +0100 Subject: [PATCH 083/125] Now looks at percentage difference between original and new values. Also can now use event data. Only looks at given range of data (remove transient period). Basic chi squared test but really need more replications (100+) --- comparison.py | 70 +++++++++++++++++++++++++++++++++++++++------------ 1 file changed, 54 insertions(+), 16 deletions(-) diff --git a/comparison.py b/comparison.py index bbc0c43..2a3c026 100644 --- a/comparison.py +++ b/comparison.py @@ -23,6 +23,8 @@ def __init__(self, original_filename, new_filename, extra_filename=None): self.extra = False self.extra_data = {} + self.event_damage = [] + self.event_schedule = [] self.original_averages = {} self.new_averages = {} self.extra_averages = {} @@ -43,7 +45,7 @@ def init_averages(self, avg_dict, data_dict): No return values.""" for data in data_dict: for key in data.keys(): - if "firms_cash" in key or key == "market_diffvar" or "event" in key or "riskmodels" in key: + if "firms_cash" in key or key == "market_diffvar" or "riskmodels" in key: pass elif key == "individual_contracts" or key == "reinsurance_contracts": avg_contract_per_firm = [] @@ -64,6 +66,10 @@ def init_averages(self, avg_dict, data_dict): avg_dict[key] = avg_contract_per_firm else: avg_dict[key] = [list1 + list2 for list1, list2 in zip(avg_dict[key], avg_contract_per_firm)] + elif key == "rc_event_schedule_initial": + self.event_schedule.append(data[key]) + elif key == "rc_event_damage_initial": + self.event_damage.append(data[key]) else: if key not in avg_dict.keys(): avg_dict[key] = data[key] @@ -72,44 +78,76 @@ def init_averages(self, avg_dict, data_dict): for key in avg_dict.keys(): avg_dict[key] = [value/len(data_dict) for value in avg_dict[key]] - def plot(self): + def plot(self, upper, lower, events=False): """Method to plot same type of data for different files on a plot. No accepted values. No return values.""" for key in self.original_averages.keys(): plt.figure() - original_values = self.original_averages[key] + original_values = self.original_averages[key][lower:upper] mean_original_values = np.mean(original_values) - new_values = self.new_averages[key] + new_values = self.new_averages[key][lower:upper] mean_new_values = np.mean(new_values) - xvalues = np.arange(1000) - plt.plot(xvalues[:500], original_values, label='Original Values', color="blue") - plt.plot(xvalues, new_values, label='New Values', color="red") + xvalues = np.arange(lower, upper) + percent_diff = self.stats(original_values, new_values) + plt.plot(xvalues, original_values, label='Original Values', color="blue") + plt.plot(xvalues, new_values, label='New Values, Avg Diff = %f%%' % percent_diff, color="red") if self.extra: - extra_values = self.extra_averages[key] + extra_values = self.extra_averages[key][lower:upper] mean_extra_values = np.mean(extra_values) - plt.plot(xvalues, extra_values, label="Extra Values", color="yellow") + percent_diff = self.stats(original_values, extra_values) + plt.plot(xvalues, extra_values, label="Extra Values, Avg Diff = %f%%" % percent_diff, color="yellow") if "cum" not in key: - plt.axhline(mean_new_values, linestyle='--', label="New Mean", color="red") + mean_diff = self.mean_diff(mean_original_values, mean_new_values) plt.axhline(mean_original_values, linestyle='--', label="Original Mean", color="blue") + plt.axhline(mean_new_values, linestyle='--', label="New Mean, Diff = %f%%" % mean_diff, color="red") if self.extra: - plt.axhline(mean_extra_values, linestyle='--', label='Extra Mean', color='yellow') + mean_diff = self.mean_diff(mean_original_values, mean_extra_values) + plt.axhline(mean_extra_values, linestyle='--', label='Extra Mean, Diff = %f%%' % mean_diff, color='yellow') + if events: + for categ_index in range(len(self.event_schedule[0])): + for event_index in range(len(self.event_schedule[0][categ_index])): + if self.event_damage[0][categ_index][event_index] > 0.5: + plt.axvline(self.event_schedule[0][categ_index][event_index], linestyle='-', color='green') plt.legend() plt.xlabel("Time") plt.ylabel(key) plt.show() - def test(self): + def ks_test(self): """Method to perform ks test on two sets of file data. Returns the D statistic and p-value. No accepted values. No return values.""" for key in self.original_averages.keys(): original_values = self.original_averages[key] - new_values = self.new_averages[key][:500] + new_values = self.new_averages[key] D, p = ss.ks_2samp(original_values, new_values) print("%s has p value: %f and D: %f" % (key, p, D)) + def chi_squared(self): + for key in self.original_averages.keys(): + original_values = self.original_averages[key][200:1000] + new_values = self.new_averages[key][200:1000] + fractional_diff = 0 + for time in range(len(original_values)): + if original_values[time] != 0: + fractional_diff += (original_values[time] - new_values[time])**2 / original_values[time] + print("%s has chi squared value: %f" % (key, fractional_diff/len(original_values))) + + def stats(self, original_values, new_values): + percentage_diff_sum = 0 + for time in range(len(original_values)): + if original_values[time] != 0: + percentage_diff_sum += np.absolute((original_values[time]-new_values[time])/original_values[time]) * 100 + return percentage_diff_sum / len(original_values) + + def mean_diff(self, original_mean, new_mean): + diff = (new_mean - original_mean) / original_mean + return diff * 100 + -CD = CompareData("data/single_history_logs.dat", "data/single_history_logs_old_2019_Jul_30_16_30.dat", "data/single_history_logs_old_2019_Jul_31_11_37.dat") -CD.test() -CD.plot() +CD = CompareData("data/single_history_logs_old_2019_Aug_02_12_53.dat", + "data/single_history_logs.dat", + "data/single_history_logs_old_2019_Aug_02_16_45.dat") +# CD.plot(events=False, upper=1000, lower=200) +CD.chi_squared() \ No newline at end of file From 4efb20a493db0fba4912912aec71a74d2358120d Mon Sep 17 00:00:00 2001 From: Chris Hughes Date: Mon, 5 Aug 2019 11:18:00 +0100 Subject: [PATCH 084/125] Remove existing code for proportional reinsurance, since it is not used. --- insurancecontract.py | 4 +- insurancefirms.py | 79 +++++----------------------------------- metainsurancecontract.py | 8 ++-- metainsuranceorg.py | 2 - reinsurancecontract.py | 4 +- 5 files changed, 17 insertions(+), 80 deletions(-) diff --git a/insurancecontract.py b/insurancecontract.py index a9c6227..a1192a0 100644 --- a/insurancecontract.py +++ b/insurancecontract.py @@ -21,7 +21,7 @@ def __init__( insurer: "MetaInsuranceOrg", risk: "RiskProperties", time: int, - premium: float, + per_value_premium: float, runtime: int, payment_period: int, expire_immediately: bool, @@ -35,7 +35,7 @@ def __init__( insurer, risk, time, - premium, + per_value_premium, runtime, payment_period, expire_immediately, diff --git a/insurancefirms.py b/insurancefirms.py index 49632ba..df31d1b 100644 --- a/insurancefirms.py +++ b/insurancefirms.py @@ -7,7 +7,7 @@ import genericclasses from typing import Optional, MutableSequence, Mapping -from typing import TYPE_CHECKING +from typing import TYPE_CHECKING, List if TYPE_CHECKING: pass @@ -232,40 +232,13 @@ def get_average_premium(self, categ_id: int) -> float: return 0 # will prevent any attempt to reinsure empty categories return weighted_premium_sum * 1.0 / total_weight - def ask_reinsurance(self, time: int): - """Method called specifically to call relevant reinsurance function for simulations reinsurance type. Only - non-proportional type is used as this is the one mainly used in reality. - Accepts: - time: Type Integer. - No return values.""" - if self.simulation_reinsurance_type == "proportional": - self.ask_reinsurance_proportional() - elif self.simulation_reinsurance_type == "non-proportional": - self.ask_reinsurance_non_proportional(time) - else: - raise ValueError( - f"Undefined reinsurance type {self.simulation_reinsurance_type}" - ) - - def ask_reinsurance_non_proportional(self, time: int): - """ Method for requesting excess of loss reinsurance for all underwritten contracts by category. - The method calculates the combined value at risk. With a probability it then creates a combined - reinsurance risk that may then be underwritten by a reinsurance firm. - Arguments: - time: integer - Returns None.""" - """Evaluate by risk category""" - for categ_id in range(self.simulation_no_risk_categories): - # TODO: find a way to decide whether to request reinsurance for category in this period, maybe a threshold? - self.ask_reinsurance_non_proportional_by_category(time, categ_id) - def ask_reinsurance_non_proportional_by_category( self, time: int, categ_id: int, purpose: str = "newrisk", min_tranches: int = None, - ) -> Optional[genericclasses.RiskProperties]: + ) -> Optional[List[genericclasses.RiskProperties]]: """Method to create a reinsurance risk for a given category for firm that calls it. Called from increase_ capacity_by_category, ask_reinsurance_non_proportional, and roll_over in metainsuranceorg. Accepts: @@ -318,15 +291,15 @@ def ask_reinsurance_non_proportional_by_category( ) for tranche in tranches: if (tranche[1] - tranche[0]) / total_value <= min( - 2 / total_value, - 0.05 + 10 / total_value, + 0.1 * ( self.np_reinsurance_limit_fraction - self.np_reinsurance_deductible_fraction ), ): # Small gaps are acceptable to avoid having trivial contracts - we don't accept tranches with - # size less than two or 5% of the total reinsurable ammount + # size less than ten or 10% of the total reinsurable ammount tranches.remove(tranche) if not tranches: @@ -334,9 +307,10 @@ def ask_reinsurance_non_proportional_by_category( return None while ( - len(tranches) < min_tranches - and not self.reinsurance_profile.all_contracts() + len(tranches) + self.reinsurance_profile.all_contracts() < min_tranches ): + # Make sure that the overall number of tranches after obtaining the requested reinsurance would be at + # least the minimal value. tranches = self.reinsurance_profile.split_longest(tranches) risks_to_return = [] for tranche in tranches: @@ -363,42 +337,6 @@ def ask_reinsurance_non_proportional_by_category( elif number_risks == 0 and purpose == "rollover": return None - def ask_reinsurance_proportional(self): - """Method to create proportional reinsurance risk. Not used in code as not really used in reality. - No accepted values. - No return values.""" - nonreinsured = [ - contract - for contract in self.underwritten_contracts - if contract.reincontract is None - ] - - nonreinsured.reverse() - - if len(nonreinsured) >= (1 - self.reinsurance_limit) * len( - self.underwritten_contracts - ): - counter = 0 - limitrein = len(nonreinsured) - (1 - self.reinsurance_limit) * len( - self.underwritten_contracts - ) - for contract in nonreinsured: - if counter < limitrein: - risk = genericclasses.RiskProperties( - value=contract.value, - category=contract.category, - owner=self, - reinsurance_share=1.0, - expiration=contract.expiration, - contract=contract, - risk_factor=contract.risk_factor, - ) - - self.simulation.append_reinrisks(risk) - counter += 1 - else: - break - def add_reinsurance(self, contract: ReinsuranceContract): """Method called by reinsurancecontract to add the reinsurance contract to the firms counter for the given category, normally used so only one reinsurance contract is issued per category at a time. @@ -493,6 +431,7 @@ def issue_cat_bond( due_time=time, purpose="bond", ) + self._pay(obligation) # TODO: is var_this_risk the correct amount? """register catbond""" self.simulation.add_agents(catbond.CatBond, "catbond", [new_catbond]) diff --git a/metainsurancecontract.py b/metainsurancecontract.py index 7ce8182..155c3c7 100644 --- a/metainsurancecontract.py +++ b/metainsurancecontract.py @@ -11,7 +11,7 @@ def __init__( insurer: "MetaInsuranceOrg", risk: "RiskProperties", time: int, - premium: float, + per_value_premium: float, runtime: int, payment_period: int, expire_immediately: bool, @@ -71,13 +71,13 @@ def __init__( self.deductible = round(self.deductible_fraction * self.value) # set excess from argument, risk property or default value, whichever first is not None - default_excess_fraction = 1.0 + default_limit_fraction = 1.0 self.limit_fraction = ( limit_fraction if limit_fraction is not None else risk.limit_fraction if risk.limit_fraction is not None - else default_excess_fraction + else default_limit_fraction ) self.limit = round(self.limit_fraction * self.value) @@ -90,7 +90,7 @@ def __init__( # setup payment schedule # TODO: excess and deductible should not be considered linearily in premium computation; this should be # shifted to the (re)insurer who supplies the premium as argument to the contract's constructor method - total_premium = premium * self.value + total_premium = per_value_premium * self.value self.periodized_premium = total_premium / self.runtime # N.B.: payment times and values are in reverse, so the earliest time is at the end! This is because popping diff --git a/metainsuranceorg.py b/metainsuranceorg.py index 222fd7f..14bfb81 100644 --- a/metainsuranceorg.py +++ b/metainsuranceorg.py @@ -833,8 +833,6 @@ def process_newrisks_reinsurer( accept, cash_left_by_categ, var_this_risk, self.excess_capital = self.riskmodel.evaluate( underwritten_risks, self.cash, risk ) - # TODO: change riskmodel.evaluate() to accept new risk to be evaluated and - # to account for existing non-proportional risks correctly -> DONE. if accept: # TODO: rename this to per_value_premium in insurancecontract.py to avoid confusion per_value_reinsurance_premium = ( diff --git a/reinsurancecontract.py b/reinsurancecontract.py index 0aed26a..7b90263 100644 --- a/reinsurancecontract.py +++ b/reinsurancecontract.py @@ -22,7 +22,7 @@ def __init__( insurer: "MetaInsuranceOrg", risk: "RiskProperties", time: int, - premium: float, + per_value_premium: float, runtime: int, payment_period: int, expire_immediately: bool, @@ -36,7 +36,7 @@ def __init__( insurer, risk, time, - premium, + per_value_premium, runtime, payment_period, expire_immediately, From 2ef064993ba498524e49328a68cb0e33f9148fc4 Mon Sep 17 00:00:00 2001 From: KloskaT Date: Mon, 5 Aug 2019 16:23:13 +0100 Subject: [PATCH 085/125] Fixed some bugs causing logged data to be incorrect. Included (or more excluded) safety margin from actual VaR calculation in regulator. --- centralbank.py | 8 ++++---- metainsuranceorg.py | 25 +++++++++++++------------ 2 files changed, 17 insertions(+), 16 deletions(-) diff --git a/centralbank.py b/centralbank.py index 45bd21e..2cafd07 100644 --- a/centralbank.py +++ b/centralbank.py @@ -78,7 +78,7 @@ def calculate_inflation(self, current_price, time): self.twelvemonth_CPI = (current_price - self.prices_list[-13])/self.prices_list[-13] self.actual_inflation = self.twelvemonth_CPI - def regulate(self, firm_id, firm_cash, firm_var, reinsurance, age): + def regulate(self, firm_id, firm_cash, firm_var, reinsurance, age, safety_margin): """Method to regulate firms Accepts: firm_id: Type Integer. Firms unique ID. @@ -102,8 +102,8 @@ def regulate(self, firm_id, firm_cash, firm_var, reinsurance, age): for iter in range(len(reinsurance)): reinsurance_capital = 0 for categ in range(len(reinsurance[iter])): - if firm_var[iter][categ] >= reinsurance[iter][categ][0]: # Check VaR greater than deductible - if firm_var[iter][categ] >= reinsurance[iter][categ][1]: # Check VaR greater than excess + if firm_var[iter][categ]/safety_margin >= reinsurance[iter][categ][0]: # Check VaR greater than deductible + if firm_var[iter][categ]/safety_margin >= reinsurance[iter][categ][1]: # Check VaR greater than excess reinsurance_capital += (reinsurance[iter][categ][1] - reinsurance[iter][categ][0]) else: reinsurance_capital += (firm_var[iter][categ] - reinsurance[iter][categ][0]) @@ -114,7 +114,7 @@ def regulate(self, firm_id, firm_cash, firm_var, reinsurance, age): else: cash_fractions.append(1) - avg_var_coverage = np.mean(cash_fractions) + avg_var_coverage = safety_margin * np.mean(cash_fractions) # VaR contains margin of safety (=2x) not actual value if avg_var_coverage >= 0.995: self.warnings[firm_id] = 0 diff --git a/metainsuranceorg.py b/metainsuranceorg.py index 500d959..0560885 100644 --- a/metainsuranceorg.py +++ b/metainsuranceorg.py @@ -162,16 +162,15 @@ def iterate(self, time): break self.simulation.return_reinrisks(not_accepted_reinrisks) - underwritten_risks = [{"value": contract.value, "category": contract.category, \ + underwritten_risks = [{"value": contract.value, "category": contract.category, \ "risk_factor": contract.risk_factor, "deductible": contract.deductible, \ "excess": contract.excess, "insurancetype": contract.insurancetype, \ "runtime": contract.runtime} for contract in self.underwritten_contracts if contract.reinsurance_share != 1.0] - - """obtain risk model evaluation (VaR) for underwriting decisions and for capacity specific decisions""" - # TODO: Enable reinsurance shares other than 0.0 and 1.0 - expected_profit, acceptable_by_category, cash_left_by_categ, var_per_risk_per_categ, self.excess_capital = self.riskmodel.evaluate(underwritten_risks, self.cash) - # TODO: resolve insurance reinsurance inconsistency (insurer underwrite after capacity decisions, reinsurers before). + """obtain risk model evaluation (VaR) for underwriting decisions and for capacity specific decisions""" + # TODO: Enable reinsurance shares other than 0.0 and 1.0 + expected_profit, acceptable_by_category, cash_left_by_categ, var_per_risk_per_categ, self.excess_capital = self.riskmodel.evaluate(underwritten_risks, self.cash) + # TODO: resolve insurance reinsurance inconsistency (insurer underwrite after capacity decisions, reinsurers before). # This is currently so because it minimizes the number of times we need to run self.riskmodel.evaluate(). # It would also be more consistent if excess capital would be updated at the end of the iteration. @@ -274,7 +273,12 @@ def dissolve(self, time, record): next iteration. Finally the type of dissolution is recorded and the operational state is set to false. Different class variables are reset during the process: self.risks_kept, self.reinrisks_kept, self.excess_capital and self.profits_losses.""" - [contract.dissolve(time) for contract in self.underwritten_contracts] # removing (dissolving) all risks immediately after bankruptcy (may not be realistic, they might instead be bought by another company) + # Record all unpaid claims (needs to be here to account for firms lost due to regulator/being sold) + sum_due = sum(item['amount'] for item in self.obligations if item['purpose'] == 'claim') + self.simulation.record_unrecovered_claims(sum_due - self.cash) + + # Removing (dissolving) all risks immediately after bankruptcy (may not be realistic, they might instead be bought by another company) + [contract.dissolve(time) for contract in self.underwritten_contracts] self.simulation.return_risks(self.risks_kept) self.risks_kept = [] self.reinrisks_kept = [] @@ -315,9 +319,6 @@ def effect_payments(self, time): if sum_due > self.cash: self.obligations += due self.enter_illiquidity(time) - self.simulation.record_unrecovered_claims(sum_due - self.cash) - # TODO: is this record of uncovered claims correct or should it be sum_due (since the company is impounded and self.cash will also not be paid out for quite some time)? - # TODO: effect partial payment else: for obligation in due: self.pay(obligation) @@ -331,7 +332,7 @@ def pay(self, obligation): amount = obligation["amount"] recipient = obligation["recipient"] purpose = obligation["purpose"] - if self.get_operational() and recipient.get_operational(): + if recipient.get_operational(): self.cash -= amount if purpose is not 'dividend': self.profits_losses -= amount @@ -780,7 +781,7 @@ def submit_regulator_report(self, time): No accepted values. No return values.""" condition = self.simulation.bank.regulate(self.id, self.cash_last_periods, self.var_sum_last_periods, - self.reinsurance_history, self.age) + self.reinsurance_history, self.age, self.riskmodel.margin_of_safety) if condition == "Good": self.warning = False if condition == "Warning": From cba79a88695024999fc5f8f7f98da3e6edebd919 Mon Sep 17 00:00:00 2001 From: KloskaT Date: Tue, 6 Aug 2019 11:59:25 +0100 Subject: [PATCH 086/125] Added more denominations to aid fractions. Removed comparison.py and added to visualisation.py. Other minor changes. --- centralbank.py | 3 +- comparison.py | 153 ------------------------------- insurancesimulation.py | 2 + reinsurancecontract.py | 9 +- start.py | 2 +- visualisation.py | 198 ++++++++++++++++++++++++++++++++++++++++- 6 files changed, 203 insertions(+), 164 deletions(-) delete mode 100644 comparison.py diff --git a/centralbank.py b/centralbank.py index 2cafd07..f23bed3 100644 --- a/centralbank.py +++ b/centralbank.py @@ -163,7 +163,8 @@ def provide_aid(self, insurance_firms, damage_fraction, time): all_firms_aid += aid given_aid_dict[insurer] = aid # Give each firm an equal fraction of claims - for fraction in [1, 0.9, 0.8, 0.7, 0.6, 0.5, 0.4, 0.3, 0.2, 0.1, 0]: + fractions = np.arange(0, 1.05, 0.05)[::-1] + for fraction in fractions: if self.aid_budget - (all_firms_aid * fraction) > 0: self.aid_budget -= (all_firms_aid * fraction) for key in given_aid_dict: diff --git a/comparison.py b/comparison.py deleted file mode 100644 index 2a3c026..0000000 --- a/comparison.py +++ /dev/null @@ -1,153 +0,0 @@ -import numpy as np -import scipy.stats as ss -import matplotlib.pyplot as plt - - -class CompareData: - def __init__(self, original_filename, new_filename, extra_filename=None): - """Initialises the CompareData class. Is provided with two or three filenames and unpacks them, also creating - dictionaries of the average values in case of ensemble/replication runs. - Accepts: - original_filename: Type String. - new_filename: Type String. - extra_filename: Type String. Defaults None but there in case of extra file to be compared.""" - with open(original_filename, "r") as rfile: - self.original_data = [eval(k) for k in rfile] - with open(new_filename, "r") as rfile: - self.new_data = [eval(k) for k in rfile] - if extra_filename is not None: - with open(extra_filename, "r") as rfile: - self.extra_data = [eval(k) for k in rfile] - self.extra = True - else: - self.extra = False - self.extra_data = {} - - self.event_damage = [] - self.event_schedule = [] - self.original_averages = {} - self.new_averages = {} - self.extra_averages = {} - dicts = [self.original_averages, self.new_averages, self.extra_averages] - datas = [self.original_data, self.new_data, self.extra_data] - for i in range(len(datas)): - if self.extra is False and i == 2: - pass - else: - self.init_averages(dicts[i], datas[i]) - - def init_averages(self, avg_dict, data_dict): - """Method that initliases the average value dictionaries for the files. Takes a complete data dict and adds the - average values to a different dict provided. - Accepts: - avg_dict: Type Dict. Initially should be empty. - data_dict: Type List of data dict. Each element is a data dict containing data from that replication. - No return values.""" - for data in data_dict: - for key in data.keys(): - if "firms_cash" in key or key == "market_diffvar" or "riskmodels" in key: - pass - elif key == "individual_contracts" or key == "reinsurance_contracts": - avg_contract_per_firm = [] - for t in range(len(data[key][0])): - total_contracts = 0 - for i in range(len(data[key])): - if data[key][i][t] > 0: - total_contracts += data[key][i][t] - if "re" in key: - firm_count = data["total_reinoperational"][t] - else: - firm_count = data["total_operational"][t] - if firm_count > 0: - avg_contract_per_firm.append(total_contracts / firm_count) - else: - avg_contract_per_firm.append(0) - if key not in avg_dict.keys(): - avg_dict[key] = avg_contract_per_firm - else: - avg_dict[key] = [list1 + list2 for list1, list2 in zip(avg_dict[key], avg_contract_per_firm)] - elif key == "rc_event_schedule_initial": - self.event_schedule.append(data[key]) - elif key == "rc_event_damage_initial": - self.event_damage.append(data[key]) - else: - if key not in avg_dict.keys(): - avg_dict[key] = data[key] - else: - avg_dict[key] = [list1 + list2 for list1, list2 in zip(avg_dict[key], data[key])] - for key in avg_dict.keys(): - avg_dict[key] = [value/len(data_dict) for value in avg_dict[key]] - - def plot(self, upper, lower, events=False): - """Method to plot same type of data for different files on a plot. - No accepted values. - No return values.""" - for key in self.original_averages.keys(): - plt.figure() - original_values = self.original_averages[key][lower:upper] - mean_original_values = np.mean(original_values) - new_values = self.new_averages[key][lower:upper] - mean_new_values = np.mean(new_values) - xvalues = np.arange(lower, upper) - percent_diff = self.stats(original_values, new_values) - plt.plot(xvalues, original_values, label='Original Values', color="blue") - plt.plot(xvalues, new_values, label='New Values, Avg Diff = %f%%' % percent_diff, color="red") - if self.extra: - extra_values = self.extra_averages[key][lower:upper] - mean_extra_values = np.mean(extra_values) - percent_diff = self.stats(original_values, extra_values) - plt.plot(xvalues, extra_values, label="Extra Values, Avg Diff = %f%%" % percent_diff, color="yellow") - if "cum" not in key: - mean_diff = self.mean_diff(mean_original_values, mean_new_values) - plt.axhline(mean_original_values, linestyle='--', label="Original Mean", color="blue") - plt.axhline(mean_new_values, linestyle='--', label="New Mean, Diff = %f%%" % mean_diff, color="red") - if self.extra: - mean_diff = self.mean_diff(mean_original_values, mean_extra_values) - plt.axhline(mean_extra_values, linestyle='--', label='Extra Mean, Diff = %f%%' % mean_diff, color='yellow') - if events: - for categ_index in range(len(self.event_schedule[0])): - for event_index in range(len(self.event_schedule[0][categ_index])): - if self.event_damage[0][categ_index][event_index] > 0.5: - plt.axvline(self.event_schedule[0][categ_index][event_index], linestyle='-', color='green') - plt.legend() - plt.xlabel("Time") - plt.ylabel(key) - plt.show() - - def ks_test(self): - """Method to perform ks test on two sets of file data. Returns the D statistic and p-value. - No accepted values. - No return values.""" - for key in self.original_averages.keys(): - original_values = self.original_averages[key] - new_values = self.new_averages[key] - D, p = ss.ks_2samp(original_values, new_values) - print("%s has p value: %f and D: %f" % (key, p, D)) - - def chi_squared(self): - for key in self.original_averages.keys(): - original_values = self.original_averages[key][200:1000] - new_values = self.new_averages[key][200:1000] - fractional_diff = 0 - for time in range(len(original_values)): - if original_values[time] != 0: - fractional_diff += (original_values[time] - new_values[time])**2 / original_values[time] - print("%s has chi squared value: %f" % (key, fractional_diff/len(original_values))) - - def stats(self, original_values, new_values): - percentage_diff_sum = 0 - for time in range(len(original_values)): - if original_values[time] != 0: - percentage_diff_sum += np.absolute((original_values[time]-new_values[time])/original_values[time]) * 100 - return percentage_diff_sum / len(original_values) - - def mean_diff(self, original_mean, new_mean): - diff = (new_mean - original_mean) / original_mean - return diff * 100 - - -CD = CompareData("data/single_history_logs_old_2019_Aug_02_12_53.dat", - "data/single_history_logs.dat", - "data/single_history_logs_old_2019_Aug_02_16_45.dat") -# CD.plot(events=False, upper=1000, lower=200) -CD.chi_squared() \ No newline at end of file diff --git a/insurancesimulation.py b/insurancesimulation.py index ebf2307..39fa47c 100644 --- a/insurancesimulation.py +++ b/insurancesimulation.py @@ -992,6 +992,8 @@ def add_firm_to_be_sold(self, firm, time, reason): self.selling_insurance_firms.append([firm, time, reason]) elif firm.is_reinsurer: self.selling_reinsurance_firms.append([firm, time, reason]) + else: + print("Not accepted type of firm") def get_firms_to_sell(self, type): """Method to get list of firms that are up for selling based on type. diff --git a/reinsurancecontract.py b/reinsurancecontract.py index 22fbf10..2b1818d 100644 --- a/reinsurancecontract.py +++ b/reinsurancecontract.py @@ -2,11 +2,10 @@ from metainsurancecontract import MetaInsuranceContract + class ReinsuranceContract(MetaInsuranceContract): """ReinsuranceContract class. Inherits from InsuranceContract. - Constructor is not currently required but may be used in the future to distinguish InsuranceContract - and ReinsuranceContract objects. The signature of this class' constructor is the same as that of the InsuranceContract constructor. The class has two methods (explode, mature) that overwrite methods in InsuranceContract.""" def __init__(self, insurer, properties, time, premium, runtime, payment_period, expire_immediately, initial_VaR=0.,\ @@ -45,8 +44,7 @@ def explode(self, time, damage_extent=None): self.current_claim += self.contract.claim # TODO: should proportional reinsurance claims be subject to excess_of_loss retrocession? If so, reorganize more straightforwardly self.expiration = time - #self.terminating = True - + def mature(self, time): """Mature method. Accepts arguments @@ -54,12 +52,11 @@ def mature(self, time): No return value. Removes any reinsurance functions this contract has and terminates any reinsurance contracts for this contract.""" - #self.terminating = True self.terminate_reinsurance(time) if self.insurancetype == "excess-of-loss": self.property_holder.delete_reinsurance(category=self.category, excess_fraction=self.excess_fraction, \ deductible_fraction=self.deductible_fraction, contract=self) - else: #TODO: ? Instead: if self.insurancetype == "proportional": + else: self.contract.unreinsure() diff --git a/start.py b/start.py index 8f933a5..931cdcb 100644 --- a/start.py +++ b/start.py @@ -154,7 +154,7 @@ def save_simulation(t, sim, sim_param, event_schedule, event_damage, exit_now=Fa save_iter = 20000 from setup import SetupSim - setup = SetupSim() #Here the setup for the simulation is done. + setup = SetupSim() # Here the setup for the simulation is done. [general_rc_event_schedule, general_rc_event_damage, np_seeds, random_seeds] = setup.obtain_ensemble(1) #Only one ensemble. This part will only be run locally (laptop). log = main(simulation_parameters, general_rc_event_schedule[0], general_rc_event_damage[0], np_seeds[0], random_seeds[0], save_iter) diff --git a/visualisation.py b/visualisation.py index 81e670f..c871706 100644 --- a/visualisation.py +++ b/visualisation.py @@ -14,6 +14,7 @@ if not os.path.isdir("figures"): os.makedirs("figures") + class TimeSeries(object): def __init__(self, series_list, event_schedule, damage_schedule, title="",xlabel="Time", colour='k', axlst=None, fig=None, percentiles=None, alpha=0.7): """Intialisation method for creating timeseries. @@ -744,7 +745,7 @@ def plot(self, ax=None, ylabel="PDF(x)", xtralabel="", xlabel="x", num_bins=50, ax = fig.add_subplot(111) """Plot""" - ax.hist(self.sample_x, bins=num_bins, color=color) + ax.hist(self.sample_x, bins=num_bins, color=color, histtype='step') """Set plot attributes""" ax.set_ylabel(ylabel) @@ -902,6 +903,180 @@ def plot(self, outputfile): print("Have saved " + outputfile + " data") +class ConfigCompare: + def __init__(self, original_filename, new_filename, extra_filename=None): + """Initialises the CompareData class. Is provided with two or three filenames and unpacks them, also creating + dictionaries of the average values in case of ensemble/replication runs. + Accepts: + original_filename: Type String. + new_filename: Type String. + extra_filename: Type String. Defaults None but there in case of extra file to be compared.""" + with open(original_filename, "r") as rfile: + self.original_data = [eval(k) for k in rfile] + with open(new_filename, "r") as rfile: + self.new_data = [eval(k) for k in rfile] + if extra_filename is not None: + with open(extra_filename, "r") as rfile: + self.extra_data = [eval(k) for k in rfile] + self.extra = True + else: + self.extra = False + self.extra_data = {} + + self.event_damage = [] + self.event_schedule = [] + self.original_averages = {} + self.new_averages = {} + self.extra_averages = {} + dicts = [self.original_averages, self.new_averages, self.extra_averages] + datas = [self.original_data, self.new_data, self.extra_data] + for i in range(len(datas)): + if self.extra is False and i == 2: + pass + else: + self.init_averages(dicts[i], datas[i]) + + def init_averages(self, avg_dict, data_dict): + """Method that initliases the average value dictionaries for the files. Takes a complete data dict and adds the + average values to a different dict provided. + Accepts: + avg_dict: Type Dict. Initially should be empty. + data_dict: Type List of data dict. Each element is a data dict containing data from that replication. + No return values.""" + for data in data_dict: + for key in data.keys(): + if "firms_cash" in key or key == "market_diffvar" or "riskmodels" in key: + pass + elif key == "individual_contracts" or key == "reinsurance_contracts": + avg_contract_per_firm = [] + for t in range(len(data[key][0])): + total_contracts = 0 + for i in range(len(data[key])): + if data[key][i][t] > 0: + total_contracts += data[key][i][t] + if "re" in key: + firm_count = data["total_reinoperational"][t] + else: + firm_count = data["total_operational"][t] + if firm_count > 0: + avg_contract_per_firm.append(total_contracts / firm_count) + else: + avg_contract_per_firm.append(0) + if key not in avg_dict.keys(): + avg_dict[key] = avg_contract_per_firm + else: + avg_dict[key] = [list1 + list2 for list1, list2 in zip(avg_dict[key], avg_contract_per_firm)] + elif key == "rc_event_schedule_initial": + self.event_schedule.append(data[key]) + elif key == "rc_event_damage_initial": + self.event_damage.append(data[key]) + else: + if key not in avg_dict.keys(): + avg_dict[key] = data[key] + else: + avg_dict[key] = [list1 + list2 for list1, list2 in zip(avg_dict[key], data[key])] + for key in avg_dict.keys(): + avg_dict[key] = [value/len(data_dict) for value in avg_dict[key]] + + def plot(self, upper, lower, events=False): + """Method to plot same type of data for different files on a plot. + No accepted values. + No return values.""" + for key in self.original_averages.keys(): + plt.figure() + original_values = self.original_averages[key][lower:upper] + mean_original_values = np.mean(original_values) + new_values = self.new_averages[key][lower:upper] + mean_new_values = np.mean(new_values) + xvalues = np.arange(lower, upper) + percent_diff = self.stats(original_values, new_values) + plt.plot(xvalues, original_values, label='Original Values', color="blue") + plt.plot(xvalues, new_values, label='New Values, Avg Diff = %f%%' % percent_diff, color="red") + if self.extra: + extra_values = self.extra_averages[key][lower:upper] + mean_extra_values = np.mean(extra_values) + percent_diff = self.stats(original_values, extra_values) + plt.plot(xvalues, extra_values, label="Extra Values, Avg Diff = %f%%" % percent_diff, color="yellow") + if "cum" not in key: + mean_diff = self.mean_diff(mean_original_values, mean_new_values) + plt.axhline(mean_original_values, linestyle='--', label="Original Mean", color="blue") + plt.axhline(mean_new_values, linestyle='--', label="New Mean, Diff = %f%%" % mean_diff, color="red") + if self.extra: + mean_diff = self.mean_diff(mean_original_values, mean_extra_values) + plt.axhline(mean_extra_values, linestyle='--', label='Extra Mean, Diff = %f%%' % mean_diff, color='yellow') + if events: + for categ_index in range(len(self.event_schedule[0])): + for event_index in range(len(self.event_schedule[0][categ_index])): + if self.event_damage[0][categ_index][event_index] > 0.5: + plt.axvline(self.event_schedule[0][categ_index][event_index], linestyle='-', color='green') + plt.legend() + plt.xlabel("Time") + plt.ylabel(key) + plt.show() + + def ks_test(self): + """Method to perform ks test on two sets of file data. Returns the D statistic and p-value. + No accepted values. + No return values.""" + for key in self.original_averages.keys(): + original_values = self.original_averages[key] + new_values = self.new_averages[key] + D, p = ss.ks_2samp(original_values, new_values) + print("%s has p value: %f and D: %f" % (key, p, D)) + + def chi_squared(self): + """Method for chi squared. Prints to screen. + No accepted values. + No return values.""" + for key in self.original_averages.keys(): + original_values = self.original_averages[key][200:1000] + new_values = self.new_averages[key][200:1000] + fractional_diff = 0 + for time in range(len(original_values)): + if original_values[time] != 0: + fractional_diff += (original_values[time] - new_values[time])**2 / original_values[time] + print("%s has chi squared value: %f" % (key, fractional_diff/len(original_values))) + + def stats(self, original_values, new_values): + """Method to calculate average difference between two data sets. Called from plot(). + Accepts: + original_values: Type List. + new_values: Type List. + Returns: + percentage_diff_sum: Type Float. Avg percentage difference.""" + percentage_diff_sum = 0 + for time in range(len(original_values)): + if original_values[time] != 0: + percentage_diff_sum += np.absolute((original_values[time]-new_values[time])/original_values[time]) * 100 + return percentage_diff_sum / len(original_values) + + def mean_diff(self, original_mean, new_mean): + """Method to calculate percentage difference between two means. Used by/for plotting. + Accepts: + original_mean: Type Float. + new_mean. Type Float. + Returns: + diff: Type Float.""" + diff = (new_mean - original_mean) / original_mean + return diff * 100 + + def stat_tests(self, upper, lower): + """A series of scipy statistical tests. Prints the results. Doesn't really make sense in terms of timeseries. + Accepts: + upper: Type Integer. Upper limit of data to be tested. + lower: Type Integer. Lower limit of data to be tested. + No return values.""" + for key in self.original_averages.keys(): + try: + ttest = scipy.stats.ttest_ind(self.original_averages[key][lower:upper], self.new_averages[key][lower:upper]) + chi = scipy.stats.chisquare(self.new_averages[key][lower:upper], self.original_averages[key][lower:upper]) + epps = scipy.stats.epps_singleton_2samp(self.original_averages[key][lower:upper], self.new_averages[key][lower:upper], t=(750, 900)) + wasser = scipy.stats.wasserstein_distance(self.original_averages[key][lower:upper], self.new_averages[key][lower:upper]) + print(key, "\n", ttest, "\n", chi, "\n", epps, "\n", "Wasserstein distance: ", wasser) + except: + pass + + if __name__ == "__main__": # Use argparse to handle command line arguments parser = argparse.ArgumentParser(description='Model the Insurance sector') @@ -918,13 +1093,20 @@ def plot(self, outputfile): help="plot the histograms of bankruptcy events/unrecovered claims across ensemble") parser.add_argument("--riskmodel_comparison", action="store_true", help="Plot data comparing risk models for both insurance and reinsurance firms.") + parser.add_argument("--config_compare_file1", action="store", dest="file1", + help="gives plots and stats about at least two different files. This is the original data.") + parser.add_argument("--config_compare_file2", action="store", dest="file2", + help="gives plots and stats about two different files. This is the new data.") + parser.add_argument("--config_compare_file3", action="store", dest="file3", + help="gives plots and stats about two different files. This is extra data.") + args = parser.parse_args() if args.single: # load in data from the history_logs dictionary with open("data/history_logs.dat","r") as rfile: - with open("data/history_logs.dat","r") as rfile: - history_logs_list = [eval(k) for k in rfile] # one dict on each line + with open("data/single_history_logs.dat", "r") as rfile: + history_logs_list = [eval(k) for k in rfile] # one dict on each line # first create visualisation object, then create graph/animation objects as necessary vis = visualisation(history_logs_list) @@ -999,4 +1181,14 @@ def plot(self, outputfile): compare = RiskModelSpecificCompare(infiletype=data_types[type], refiletype=rein_data_types[type]) compare.plot(outputfile=data_types[type][1:-4]) + if args.file1 is not None and args.file2 is not None: + # CD = ConfigCompare("data/single_history_logs_old_2019_Aug_02_12_53.dat", + # "data/single_history_logs.dat", + # "data/single_history_logs_old_2019_Aug_02_13_13.dat") + CD = ConfigCompare(args.file1, args.file2, args.file3) + CD.plot(events=False, upper=1000, lower=200) + CD.stat_tests(upper=1000, lower=200) + elif (args.file1 is not None and args.file2 is None) or (args.file1 is None and args.file2 is not None): + print("Need two data files for comparison") + # ਲ਼ From c83983cded461a8a1abb93170d0ab2ffbb710f51 Mon Sep 17 00:00:00 2001 From: KloskaT Date: Tue, 6 Aug 2019 18:24:48 +0100 Subject: [PATCH 087/125] Merged Chris Master Branch. Almost fixed regulator for multi layer reinsurance (loads of market exits but calculates reinsurance capital correctly) --- centralbank.py | 17 ++- genericclasses.py | 84 +++-------- insurancecontract.py | 54 ++----- insurancefirms.py | 152 ++++--------------- insurancesimulation.py | 170 ++++++--------------- isleconfig.py | 6 +- metainsurancecontract.py | 64 ++------ metainsuranceorg.py | 316 ++++++++++----------------------------- reinsurancecontract.py | 23 +-- setup_simulation.py | 12 +- start.py | 164 +++++--------------- 11 files changed, 252 insertions(+), 810 deletions(-) diff --git a/centralbank.py b/centralbank.py index f23bed3..01a1a7d 100644 --- a/centralbank.py +++ b/centralbank.py @@ -102,19 +102,20 @@ def regulate(self, firm_id, firm_cash, firm_var, reinsurance, age, safety_margin for iter in range(len(reinsurance)): reinsurance_capital = 0 for categ in range(len(reinsurance[iter])): - if firm_var[iter][categ]/safety_margin >= reinsurance[iter][categ][0]: # Check VaR greater than deductible - if firm_var[iter][categ]/safety_margin >= reinsurance[iter][categ][1]: # Check VaR greater than excess - reinsurance_capital += (reinsurance[iter][categ][1] - reinsurance[iter][categ][0]) + for contract in reinsurance[iter][categ]: + if firm_var[iter][categ] / safety_margin >= contract[0]: # Check VaR greater than deductible + if firm_var[iter][categ] / safety_margin >= contract[1]: # Check VaR greater than excess + reinsurance_capital += (contract[1] - contract[0]) + else: + reinsurance_capital += (firm_var[iter][categ] - contract[0]) else: - reinsurance_capital += (firm_var[iter][categ] - reinsurance[iter][categ][0]) - else: - reinsurance_capital += 0 # If below deductible no reinsurance + reinsurance_capital += 0 # If below deductible no reinsurance if sum(firm_var[iter]) > 0: - cash_fractions.append((firm_cash[iter]+reinsurance_capital)/sum(firm_var[iter])) + cash_fractions.append((firm_cash[iter] + reinsurance_capital) / sum(firm_var[iter])) else: cash_fractions.append(1) - avg_var_coverage = safety_margin * np.mean(cash_fractions) # VaR contains margin of safety (=2x) not actual value + avg_var_coverage = safety_margin * np.mean(cash_fractions) # VaR contains margin of safety (=2x) not actual value if avg_var_coverage >= 0.995: self.warnings[firm_id] = 0 diff --git a/genericclasses.py b/genericclasses.py index 28818f8..04ac163 100644 --- a/genericclasses.py +++ b/genericclasses.py @@ -62,17 +62,12 @@ def _pay(self, obligation: "Obligation"): if isleconfig.verbose: print(f"Payment not processed as firm {self.id} is not operational") - def get_operational(self) -> bool: + def get_operational(self) : """Method to return boolean of if agent is operational. Only used as check for payments. No accepted values Returns Boolean""" return self.operational - def iterate(self, time: int): - raise NotImplementedError( - "Iterate is not implemented in GenericAgent, should have be overridden" - ) - def _effect_payments(self, time: int): """Method for checking if any payments are due. Accepts: @@ -80,25 +75,17 @@ def _effect_payments(self, time: int): No return value Method checks firms list of obligations to see if ay are due for this time, then pays them. If the firm does not have enough cash then it enters illiquity, leaves the market, and matures all contracts.""" - # TODO: don't really want to be reconstructing lists every time (unless the obligations are naturally sorted by - # time, in which case this could be done slightly better). Low priority, but something to consider due = [item for item in self.obligations if item.due_time <= time] self.obligations = [item for item in self.obligations if item.due_time > time] - sum_due = sum([item.amount for item in due]) if sum_due > self.cash: self.obligations += due - self.enter_illiquidity(time, sum_due) + self.enter_illiquidity(time) else: for obligation in due: self._pay(obligation) - def enter_illiquidity(self, time: int, sum_due: float): - raise NotImplementedError() - - def receive_obligation( - self, amount: float, recipient: "GenericAgent", due_time: int, purpose: str - ): + def receive_obligation(self, amount: float, recipient: "GenericAgent", due_time: int, purpose: str): """Method for receiving obligations that the firm will have to pay. Accepts: amount: Type integer, how much will be paid @@ -107,10 +94,7 @@ def receive_obligation( purpose: Type string, why they are being paid No return value Adds obligation (Type DataDict) to list of obligations owed by the firm.""" - - obligation = Obligation( - amount=amount, recipient=recipient, due_time=due_time, purpose=purpose - ) + obligation = Obligation(amount=amount, recipient=recipient, due_time=due_time, purpose=purpose) self.obligations.append(obligation) def receive(self, amount: float): @@ -201,14 +185,10 @@ class ReinsuranceProfile: # TODO: add, remove, explode, get uninsured regions def __init__(self, riskmodel: "RiskModel"): - self.reinsured_regions: MutableSequence[ - SortedList[Tuple[int, int, "ReinsuranceContract"]] - ] + self.reinsured_regions: MutableSequence[SortedList[Tuple[int, int, "ReinsuranceContract"]]] - self.reinsured_regions = [ - SortedList(key=lambda x: x[0]) - for _ in range(isleconfig.simulation_parameters["no_categories"]) - ] + self.reinsured_regions = [SortedList(key=lambda x: x[0]) + for _ in range(isleconfig.simulation_parameters["no_categories"])] # Used for automatically updating the riskmodel when reinsurance is modified self.riskmodel = riskmodel @@ -219,30 +199,20 @@ def add(self, contract: "ReinsuranceContract", value: float) -> None: category = contract.category self.reinsured_regions[category].add((lower_bound, upper_bound, contract)) - index = self.reinsured_regions[category].index( - (lower_bound, upper_bound, contract) - ) + index = self.reinsured_regions[category].index((lower_bound, upper_bound, contract)) # Check for overlap with region to the right... - if ( - index + 1 < len(self.reinsured_regions[category]) - and self.reinsured_regions[category][index + 1][0] < upper_bound - ): - raise ValueError( - "Attempted to add reinsurance overlapping with existing reinsurance \n" - f"Reinsured regions are {self.reinsured_regions[category]}" - ) + if (index + 1 < len(self.reinsured_regions[category]) + and self.reinsured_regions[category][index + 1][0] < upper_bound): + raise ValueError("Attempted to add reinsurance overlapping with existing reinsurance \n" + f"Reinsured regions are {self.reinsured_regions[category]}") # ... and to the left if index != 0 and self.reinsured_regions[category][index - 1][1] > lower_bound: - raise ValueError( - "Attempted to add reinsurance overlapping with existing reinsurance \n" - f"Reinsured regions are {list(self.reinsured_regions[category])}" - ) + raise ValueError("Attempted to add reinsurance overlapping with existing reinsurance \n" + f"Reinsured regions are {list(self.reinsured_regions[category])}") - self.riskmodel.set_reinsurance_coverage( - value=value, coverage=self.reinsured_regions[category], category=category - ) + self.riskmodel.set_reinsurance_coverage(value=value, coverage=self.reinsured_regions[category], category=category) def remove(self, contract: "ReinsuranceContract", value: float) -> None: lower_bound = contract.deductible @@ -250,16 +220,10 @@ def remove(self, contract: "ReinsuranceContract", value: float) -> None: category = contract.category try: - self.reinsured_regions[category].remove( - (lower_bound, upper_bound, contract) - ) + self.reinsured_regions[category].remove((lower_bound, upper_bound, contract)) except ValueError: - raise ValueError( - "Attempting to remove a reinsurance contract that doesn't exist!" - ) - self.riskmodel.set_reinsurance_coverage( - value=value, coverage=self.reinsured_regions[category], category=category - ) + raise ValueError("Attempting to remove a reinsurance contract that doesn't exist!") + self.riskmodel.set_reinsurance_coverage(value=value, coverage=self.reinsured_regions[category], category=category) def uncovered(self, category: int) -> MutableSequence[Tuple[float, float]]: uncovered_regions = [] @@ -272,9 +236,7 @@ def uncovered(self, category: int) -> MutableSequence[Tuple[float, float]]: uncovered_regions.append((upper, np.inf)) return uncovered_regions - def contracts_to_explode( - self, category: int, damage: float - ) -> MutableSequence["ReinsuranceContract"]: + def contracts_to_explode(self, category: int, damage: float) -> MutableSequence["ReinsuranceContract"]: contracts = [] for region in self.reinsured_regions[category]: if region[0] < damage: @@ -289,14 +251,10 @@ def all_contracts(self) -> MutableSequence["ReinsuranceContract"]: return list(contracts) def update_value(self, value: float, category: int) -> None: - self.riskmodel.set_reinsurance_coverage( - value=value, coverage=self.reinsured_regions[category], category=category - ) + self.riskmodel.set_reinsurance_coverage(value=value, coverage=self.reinsured_regions[category], category=category) @staticmethod - def split_longest( - l: MutableSequence[Tuple[float, float]] - ) -> MutableSequence[Tuple[float, float]]: + def split_longest(l: MutableSequence[Tuple[float, float]]) -> MutableSequence[Tuple[float, float]]: max_width = 0 max_width_index = None for i, region in enumerate(l): diff --git a/insurancecontract.py b/insurancecontract.py index a9c6227..b1664b3 100644 --- a/insurancecontract.py +++ b/insurancecontract.py @@ -16,35 +16,12 @@ class InsuranceContract(metainsurancecontract.MetaInsuranceContract): The signature of this class' constructor is the same as that of the InsuranceContract constructor. The class has two methods (explode, mature) that overwrite methods in InsuranceContract.""" - def __init__( - self, - insurer: "MetaInsuranceOrg", - risk: "RiskProperties", - time: int, - premium: float, - runtime: int, - payment_period: int, - expire_immediately: bool, - initial_var: float = 0.0, - insurancetype: str = "proportional", - deductible_fraction: float = None, - limit_fraction: float = None, - reinsurance: float = 0, - ): - super().__init__( - insurer, - risk, - time, - premium, - runtime, - payment_period, - expire_immediately, - initial_var, - insurancetype, - deductible_fraction, - limit_fraction, - reinsurance, - ) + def __init__(self, insurer: "MetaInsuranceOrg", risk: "RiskProperties", time: int, premium: float, runtime: int, + payment_period: int, expire_immediately: bool, initial_var: float = 0.0, + insurancetype: str = "proportional", deductible_fraction: float = None, limit_fraction: float = None, + reinsurance: float = 0): + super().__init__(insurer, risk, time, premium, runtime, payment_period, expire_immediately, initial_var, + insurancetype, deductible_fraction, limit_fraction, reinsurance) # the property holder in an insurance contract should always be the simulation assert self.property_holder is self.insurer.simulation self.property_holder: "InsuranceSimulation" @@ -60,25 +37,14 @@ def explode(self, time, uniform_value=None, damage_extent=None): No return value. For registering damage and creating resulting claims (and payment obligations).""" if uniform_value is None: - raise ValueError( - "uniform_value must be passed to InsuranceContract.explode" - ) + raise ValueError("uniform_value must be passed to InsuranceContract.explode") if damage_extent is None: - raise ValueError( - "damage_extent must be passed to InsuranceContract.explode" - ) + raise ValueError("damage_extent must be passed to InsuranceContract.explode") if uniform_value < self.risk_factor: claim = min(self.limit, damage_extent * self.value) - self.deductible - self.insurer.register_claim( - claim - ) # Every insurance claim made is immediately registered. - + self.insurer.register_claim(claim) # Every insurance claim made is immediately registered. self.current_claim += claim - self.insurer.receive_obligation( - claim, self.property_holder, time + 2, "claim" - ) - # Insurer pays one time step after reinsurer to avoid bankruptcy. - # TODO: Is this realistic? Change this? + self.insurer.receive_obligation(claim, self.property_holder, time + 2, "claim") if self.expire_immediately: self.expiration = time # self.terminating = True diff --git a/insurancefirms.py b/insurancefirms.py index 49632ba..5ccf66d 100644 --- a/insurancefirms.py +++ b/insurancefirms.py @@ -175,14 +175,8 @@ def increase_capacity(self, time: int, max_var: float) -> float: capacity = self.get_capacity(max_var) return capacity - def increase_capacity_by_category( - self, - time: int, - categ_id: int, - reinsurance_price: float, - cat_bond_price: float, - force: bool = False, - ) -> bool: + def increase_capacity_by_category(self, time: int, categ_id: int, reinsurance_price: float, cat_bond_price: float, + force: bool = False,) -> bool: """Method to increase capacity. Only called by increase_capacity. Accepts: time: Type Integer @@ -243,9 +237,7 @@ def ask_reinsurance(self, time: int): elif self.simulation_reinsurance_type == "non-proportional": self.ask_reinsurance_non_proportional(time) else: - raise ValueError( - f"Undefined reinsurance type {self.simulation_reinsurance_type}" - ) + raise ValueError(f"Undefined reinsurance type {self.simulation_reinsurance_type}") def ask_reinsurance_non_proportional(self, time: int): """ Method for requesting excess of loss reinsurance for all underwritten contracts by category. @@ -259,13 +251,8 @@ def ask_reinsurance_non_proportional(self, time: int): # TODO: find a way to decide whether to request reinsurance for category in this period, maybe a threshold? self.ask_reinsurance_non_proportional_by_category(time, categ_id) - def ask_reinsurance_non_proportional_by_category( - self, - time: int, - categ_id: int, - purpose: str = "newrisk", - min_tranches: int = None, - ) -> Optional[genericclasses.RiskProperties]: + def ask_reinsurance_non_proportional_by_category(self, time: int, categ_id: int, purpose: str = "newrisk", + min_tranches: int = None,) -> Optional[genericclasses.RiskProperties]: """Method to create a reinsurance risk for a given category for firm that calls it. Called from increase_ capacity_by_category, ask_reinsurance_non_proportional, and roll_over in metainsuranceorg. Accepts: @@ -283,12 +270,7 @@ def ask_reinsurance_non_proportional_by_category( # TODO: how do we decide how many tranches? if min_tranches is None: min_tranches = isleconfig.simulation_parameters["min_tranches"] - [ - total_value, - avg_risk_factor, - number_risks, - periodized_total_premium, - ] = self.underwritten_risk_characterisation[categ_id] + [total_value, avg_risk_factor, number_risks, periodized_total_premium,] = self.underwritten_risk_characterisation[categ_id] if number_risks > 0: tranches = self.reinsurance_profile.uncovered(categ_id) @@ -297,34 +279,16 @@ def ask_reinsurance_non_proportional_by_category( if tranches[-1][0] >= self.np_reinsurance_limit_fraction * total_value: tranches.pop() else: - tranches[-1] = ( - tranches[-1][0], - self.np_reinsurance_limit_fraction * total_value, - ) - while ( - tranches[0][0] < self.np_reinsurance_deductible_fraction * total_value - ): - if ( - tranches[0][1] - <= self.np_reinsurance_deductible_fraction * total_value - ): + tranches[-1] = (tranches[-1][0],self.np_reinsurance_limit_fraction * total_value,) + while tranches[0][0] < self.np_reinsurance_deductible_fraction * total_value: + if tranches[0][1] <= self.np_reinsurance_deductible_fraction * total_value: tranches = tranches[1:] if len(tranches) == 0: break else: - tranches[0] = ( - self.np_reinsurance_deductible_fraction * total_value, - tranches[0][1], - ) + tranches[0] = (self.np_reinsurance_deductible_fraction * total_value, tranches[0][1],) for tranche in tranches: - if (tranche[1] - tranche[0]) / total_value <= min( - 2 / total_value, - 0.05 - * ( - self.np_reinsurance_limit_fraction - - self.np_reinsurance_deductible_fraction - ), - ): + if (tranche[1] - tranche[0]) / total_value <= min(2 / total_value, 0.05 * (self.np_reinsurance_limit_fraction - self.np_reinsurance_deductible_fraction),): # Small gaps are acceptable to avoid having trivial contracts - we don't accept tranches with # size less than two or 5% of the total reinsurable ammount tranches.remove(tranche) @@ -333,10 +297,7 @@ def ask_reinsurance_non_proportional_by_category( # If we've ended up with no tranches, give up and return return None - while ( - len(tranches) < min_tranches - and not self.reinsurance_profile.all_contracts() - ): + while len(tranches) < min_tranches and not self.reinsurance_profile.all_contracts(): tranches = self.reinsurance_profile.split_longest(tranches) risks_to_return = [] for tranche in tranches: @@ -352,8 +313,7 @@ def ask_reinsurance_non_proportional_by_category( periodized_total_premium=periodized_total_premium, runtime=12, expiration=time + 12, - risk_factor=avg_risk_factor, - ) # TODO: make runtime into a parameter + risk_factor=avg_risk_factor,) # TODO: make runtime into a parameter if purpose == "newrisk": self.simulation.append_reinrisks(risk) elif purpose == "rollover": @@ -367,21 +327,13 @@ def ask_reinsurance_proportional(self): """Method to create proportional reinsurance risk. Not used in code as not really used in reality. No accepted values. No return values.""" - nonreinsured = [ - contract - for contract in self.underwritten_contracts - if contract.reincontract is None - ] - + nonreinsured = [contract for contract in self.underwritten_contracts if contract.reincontract is None] nonreinsured.reverse() if len(nonreinsured) >= (1 - self.reinsurance_limit) * len( - self.underwritten_contracts - ): + self.underwritten_contracts): counter = 0 - limitrein = len(nonreinsured) - (1 - self.reinsurance_limit) * len( - self.underwritten_contracts - ) + limitrein = len(nonreinsured) - (1 - self.reinsurance_limit) * len(self.underwritten_contracts) for contract in nonreinsured: if counter < limitrein: risk = genericclasses.RiskProperties( @@ -391,8 +343,7 @@ def ask_reinsurance_proportional(self): reinsurance_share=1.0, expiration=contract.expiration, contract=contract, - risk_factor=contract.risk_factor, - ) + risk_factor=contract.risk_factor,) self.simulation.append_reinrisks(risk) counter += 1 @@ -419,9 +370,7 @@ def delete_reinsurance(self, contract: ReinsuranceContract): value = self.underwritten_risk_characterisation[contract.category][0] self.reinsurance_profile.remove(contract, value) - def issue_cat_bond( - self, time: int, categ_id: int, per_value_per_period_premium: int = 0 - ): + def issue_cat_bond(self, time: int, categ_id: int, per_value_per_period_premium: int = 0): """Method to issue cat bond to given firm for given category. Accepts: time: Type Integer. @@ -431,12 +380,7 @@ def issue_cat_bond( Method is only called by increase_capacity_by_category method when CatBond prices are lower than reinsurance. It then creates the CatBond as a quasi-reinsurance contract that is paid for immediately (by simulation) with no premium payments.""" - [ - total_value, - avg_risk_factor, - number_risks, - _, - ] = self.underwritten_risk_characterisation[categ_id] + [total_value, avg_risk_factor, number_risks, _,] = self.underwritten_risk_characterisation[categ_id] if number_risks > 0: # TODO: make runtime into a parameter risk = genericclasses.RiskProperties( @@ -450,36 +394,23 @@ def issue_cat_bond( periodized_total_premium=0, runtime=12, expiration=time + 12, - risk_factor=avg_risk_factor, - ) + risk_factor=avg_risk_factor,) _, _, var_this_risk, _ = self.riskmodel.evaluate([], self.cash, risk) per_period_premium = per_value_per_period_premium * risk.value total_premium = sum( - [ - per_period_premium * ((1 / (1 + self.interest_rate)) ** i) - for i in range(risk.runtime) - ] - ) # TODO: or is it range(1, risk["runtime"]+1)? + [per_period_premium * ((1 / (1 + self.interest_rate)) ** i) for i in range(risk.runtime)]) # catbond = CatBond(self.simulation, per_period_premium) # TODO: shift obtain_yield method to insurancesimulation, thereby making it unnecessary to drag # parameters like self.interest_rate from instance to instance and from class to class - new_catbond = catbond.CatBond( - self.simulation, per_period_premium, self.interest_rate - ) + new_catbond = catbond.CatBond(self.simulation, per_period_premium, self.interest_rate) """add contract; contract is a quasi-reinsurance contract""" - contract = ReinsuranceContract( - new_catbond, - risk, - time, - 0, - risk.runtime, + contract = ReinsuranceContract(new_catbond, risk, time, 0, risk.runtime, self.default_contract_payment_period, expire_immediately=self.simulation_parameters["expire_immediately"], initial_var=var_this_risk, - insurancetype=risk.insurancetype, - ) + insurancetype=risk.insurancetype,) # per_value_reinsurance_premium = 0 because the insurance firm make only one payment to catbond new_catbond.set_contract(contract) @@ -487,12 +418,8 @@ def issue_cat_bond( self.simulation.receive_obligation(var_this_risk, self, time, "bond") new_catbond.set_owner(self.simulation) """hand cash over to cat bond such that var_this_risk is covered""" - obligation = genericclasses.Obligation( - amount=var_this_risk + total_premium, - recipient=new_catbond, - due_time=time, - purpose="bond", - ) + obligation = genericclasses.Obligation(amount=var_this_risk + total_premium, recipient=new_catbond, + due_time=time, purpose="bond",) self._pay(obligation) # TODO: is var_this_risk the correct amount? """register catbond""" self.simulation.add_agents(catbond.CatBond, "catbond", [new_catbond]) @@ -517,9 +444,7 @@ def make_reinsurance_claims(self, time: int): for categ_id in range(self.simulation_no_risk_categories): if claims_this_turn[categ_id] > 0: - to_explode = self.reinsurance_profile.contracts_to_explode( - damage=claims_this_turn[categ_id], category=categ_id - ) + to_explode = self.reinsurance_profile.contracts_to_explode(damage=claims_this_turn[categ_id], category=categ_id) for contract in to_explode: contract.explode(time, damage_extent=claims_this_turn[categ_id]) @@ -531,28 +456,14 @@ def get_excess_of_loss_reinsurance(self) -> MutableSequence[Mapping]: reinsurance: Type list of DataDicts.""" reinsurance = [] for contract in self.reinsurance_profile.all_contracts(): - reinsurance.append( - { - "reinsurer": contract.insurer, - # QUERY: value vs excess? - "value": contract.value, - "category": contract.category, - } - ) + reinsurance.append({"reinsurer": contract.insurer, "value": contract.value, "category": contract.category,}) return reinsurance - def refresh_reinrisk( - self, time: int, old_contract: "ReinsuranceContract" - ) -> Optional[genericclasses.RiskProperties]: + def refresh_reinrisk(self, time: int, old_contract: "ReinsuranceContract") -> Optional[genericclasses.RiskProperties]: # TODO: Can be merged """Takes an expiring contract and returns a renewed risk to automatically offer to the existing reinsurer. The new risk has the same deductible and excess as the old one, but with an updated time""" - [ - total_value, - avg_risk_factor, - number_risks, - periodized_total_premium, - ] = self.underwritten_risk_characterisation[old_contract.category] + [total_value, avg_risk_factor, number_risks, periodized_total_premium,] = self.underwritten_risk_characterisation[old_contract.category] if number_risks == 0: # If the insurerer currently has no risks in that category it probably doesn't want reinsurance return None @@ -567,8 +478,7 @@ def refresh_reinrisk( periodized_total_premium=periodized_total_premium, runtime=12, expiration=time + 12, - risk_factor=avg_risk_factor, - ) + risk_factor=avg_risk_factor,) return risk diff --git a/insurancesimulation.py b/insurancesimulation.py index 85dfbc2..2fc938f 100644 --- a/insurancesimulation.py +++ b/insurancesimulation.py @@ -37,19 +37,11 @@ class InsuranceSimulation(GenericAgent): riskmodel_inaccuracy_parameter in the configuration - a randomly chosen category has its inaccuracy set to the inverse of that parameter, and the others are set to that parameter.""" - def __init__( - self, - override_no_riskmodels: bool, - replic_id: int, - simulation_parameters: MutableMapping, - rc_event_schedule: MutableSequence[MutableSequence[int]], - rc_event_damage: MutableSequence[MutableSequence[float]], - damage_distribution: "Distribution" = TruncatedDistWrapper( - lower_bound=0.25, - upper_bound=1.0, - dist=scipy.stats.pareto(b=2, loc=0, scale=0.25), - ), - ): + def __init__(self, override_no_riskmodels: bool, replic_id: int, simulation_parameters: MutableMapping, + rc_event_schedule: MutableSequence[MutableSequence[int]], + rc_event_damage: MutableSequence[MutableSequence[float]], + damage_distribution: "Distribution" = TruncatedDistWrapper(lower_bound=0.25, upper_bound=1.0, + dist=scipy.stats.pareto(b=2, loc=0, scale=0.25),),): """Initialises the simulation (Called from start.py) Accepts: override_no_riskmodels: Boolean determining if number of risk models should be overwritten @@ -189,13 +181,9 @@ def __init__( for risk in self.risks: self.risks_counter[risk.category] += 1 - self.inaccuracy: Sequence[Sequence[int]] = self._get_all_riskmodel_combinations( - self.simulation_parameters["riskmodel_inaccuracy_parameter"] - ) + self.inaccuracy: Sequence[Sequence[int]] = self._get_all_riskmodel_combinations(self.simulation_parameters["riskmodel_inaccuracy_parameter"]) - self.inaccuracy = random.sample( - self.inaccuracy, self.simulation_parameters["no_riskmodels"] - ) + self.inaccuracy = random.sample(self.inaccuracy, self.simulation_parameters["no_riskmodels"]) risk_model_configurations = [ { @@ -289,12 +277,8 @@ def __init__( self._time: Optional[int] = None self.RN: Optional[visualization_network.ReinsuranceNetwork] = None - def initialize_agent_parameters( - self, - firmtype: str, - simulation_parameters: Mapping[str, Any], - risk_model_configurations: Sequence[Mapping], - ): + def initialize_agent_parameters(self, firmtype: str, simulation_parameters: Mapping[str, Any], + risk_model_configurations: Sequence[Mapping],): """General function for initialising the agent parameters Takes the firm type as argument, also needing sim params and risk configs Creates the agent parameters of both firm types for the initial number specified in isleconfig.py @@ -372,13 +356,8 @@ def initialize_agent_parameters( ) ) - def add_agents( - self, - agent_class: type, - agent_class_string: str, - agents: "Sequence[GenericAgent]" = None, - n: int = 1, - ): + def add_agents(self, agent_class: type, agent_class_string: str, agents: "Sequence[GenericAgent]" = None, + n: int = 1,): """Method for building agents and adding them to the simulation. Can also add pre-made catbond agents directly Accepts: agent_class: class of the agent, InsuranceFirm, ReinsuranceFirm or CatBond @@ -499,25 +478,13 @@ def iterate(self, t: int): # identify perils and effect claims for categ_id in range(len(self.rc_event_schedule)): - if ( - self.rc_event_schedule[categ_id] - and self.rc_event_schedule[categ_id][0] < t - ): - warnings.warn( - "Something wrong; past events not deleted", RuntimeWarning - ) - if ( - len(self.rc_event_schedule[categ_id]) > 0 - and self.rc_event_schedule[categ_id][0] == t - ): + if (self.rc_event_schedule[categ_id] and self.rc_event_schedule[categ_id][0] < t): + warnings.warn("Something wrong; past events not deleted", RuntimeWarning) + if (len(self.rc_event_schedule[categ_id]) > 0 and self.rc_event_schedule[categ_id][0] == t): self.rc_event_schedule[categ_id] = self.rc_event_schedule[categ_id][1:] - damage_extent = copy.copy( - self.rc_event_damage[categ_id][0] - ) # Schedules of catastrophes and damages must me generated at the same time. + damage_extent = copy.copy(self.rc_event_damage[categ_id][0]) # Schedules of catastrophes and damages must me generated at the same time. self._inflict_peril(categ_id=categ_id, damage=damage_extent, t=t) self.rc_event_damage[categ_id] = self.rc_event_damage[categ_id][1:] - # TODO: Ideally don't want to be taking from the beginning of lists, consider having soonest events at - # the end of the list. Probably fine though, only happens once per iteration else: if isleconfig.verbose: print("Next peril ", self.rc_event_schedule[categ_id]) @@ -526,14 +493,10 @@ def iterate(self, t: int): if isleconfig.aid_relief: self.bank.adjust_aid_budget(time=t) if "damage_extent" in locals(): - op_firms = [ - firm for firm in self.insurancefirms if firm.operational is True - ] + op_firms = [firm for firm in self.insurancefirms if firm.operational is True] aid_dict = self.bank.provide_aid(op_firms, damage_extent, time=t) for key in aid_dict.keys(): - self.receive_obligation( - amount=aid_dict[key], recipient=key, due_time=t, purpose="aid" - ) + self.receive_obligation(amount=aid_dict[key], recipient=key, due_time=t, purpose="aid") # Shuffle risks (insurance and reinsurance risks) self._shuffle_risks() @@ -576,30 +539,20 @@ def iterate(self, t: int): for agent in self.catbonds: agent.iterate(t) - self.insurance_models_counter = np.zeros( - self.simulation_parameters["no_categories"] - ) + self.insurance_models_counter = np.zeros(self.simulation_parameters["no_categories"]) self._update_model_counters() network_division = 1 # How often network is updated. - if ( - (isleconfig.show_network or isleconfig.save_network) - and t % network_division == 0 - and t > 0 - ): + if (isleconfig.show_network or isleconfig.save_network) and t % network_division == 0 and t > 0: if t == network_division: # Only creates once instance so only one figure. - self.RN = visualization_network.ReinsuranceNetwork( - self.rc_event_schedule_initial - ) + self.RN = visualization_network.ReinsuranceNetwork(self.rc_event_schedule_initial) self.RN.update(self.insurancefirms, self.reinsurancefirms, self.catbonds) if isleconfig.show_network: self.RN.visualize() - if isleconfig.save_network and t == ( - self.simulation_parameters["max_time"] - 800 - ): + if isleconfig.save_network and t == (self.simulation_parameters["max_time"] - 800): self.RN.save_network_data() print("Network data has been saved to data/network_data.dat") @@ -611,36 +564,20 @@ def save_data(self): """ collect data """ total_cash_no = sum([firm.cash for firm in self.insurancefirms]) - total_excess_capital = sum( - [firm.get_excess_capital() for firm in self.insurancefirms] - ) - total_profitslosses = sum( - [firm.get_profitslosses() for firm in self.insurancefirms] - ) - total_contracts_no = sum( - [len(firm.underwritten_contracts) for firm in self.insurancefirms] - ) + total_excess_capital = sum([firm.get_excess_capital() for firm in self.insurancefirms]) + total_profitslosses = sum([firm.get_profitslosses() for firm in self.insurancefirms]) + total_contracts_no = sum([len(firm.underwritten_contracts) for firm in self.insurancefirms]) total_reincash_no = sum([firm.cash for firm in self.reinsurancefirms]) - total_reinexcess_capital = sum( - [firm.get_excess_capital() for firm in self.reinsurancefirms] - ) - total_reinprofitslosses = sum( - [firm.get_profitslosses() for firm in self.reinsurancefirms] - ) - total_reincontracts_no = sum( - [len(firm.underwritten_contracts) for firm in self.reinsurancefirms] - ) + total_reinexcess_capital = sum([firm.get_excess_capital() for firm in self.reinsurancefirms]) + total_reinprofitslosses = sum([firm.get_profitslosses() for firm in self.reinsurancefirms]) + total_reincontracts_no = sum([len(firm.underwritten_contracts) for firm in self.reinsurancefirms]) operational_no = sum([firm.operational for firm in self.insurancefirms]) reinoperational_no = sum([firm.operational for firm in self.reinsurancefirms]) catbondsoperational_no = sum([cb.operational for cb in self.catbonds]) """ collect agent-level data """ - insurance_firms = [ - (firm.cash, firm.id, firm.operational) for firm in self.insurancefirms - ] - reinsurance_firms = [ - (firm.cash, firm.id, firm.operational) for firm in self.reinsurancefirms - ] + insurance_firms = [(firm.cash, firm.id, firm.operational) for firm in self.insurancefirms] + reinsurance_firms = [(firm.cash, firm.id, firm.operational) for firm in self.reinsurancefirms] """ prepare dict """ current_log = {} # TODO: rewrite this as a single dictionary literal? @@ -659,27 +596,18 @@ def save_data(self): current_log["market_reinpremium"] = self.reinsurance_market_premium current_log["cumulative_bankruptcies"] = self.cumulative_bankruptcies current_log["cumulative_market_exits"] = self.cumulative_market_exits - current_log[ - "cumulative_unrecovered_claims" - ] = self.cumulative_unrecovered_claims + current_log["cumulative_unrecovered_claims"] = self.cumulative_unrecovered_claims current_log["cumulative_claims"] = self.cumulative_claims current_log["cumulative_bought_firms"] = self.cumulative_bought_firms - current_log[ - "cumulative_nonregulation_firms" - ] = self.cumulative_nonregulation_firms + current_log["cumulative_nonregulation_firms"] = self.cumulative_nonregulation_firms """ add agent-level data to dict""" current_log["insurance_firms_cash"] = insurance_firms current_log["reinsurance_firms_cash"] = reinsurance_firms current_log["market_diffvar"] = self.compute_market_diffvar() - current_log["individual_contracts"] = [ - len(firm.underwritten_contracts) for firm in self.insurancefirms - ] - - current_log["reinsurance_contracts"] = [ - len(firm.underwritten_contracts) for firm in self.reinsurancefirms - ] + current_log["individual_contracts"] = [len(firm.underwritten_contracts) for firm in self.insurancefirms] + current_log["reinsurance_contracts"] = [len(firm.underwritten_contracts) for firm in self.reinsurancefirms] """ call to Logger object """ self.logger.record_data(current_log) @@ -863,7 +791,6 @@ def get_market_premium(self) -> float: return self.market_premium def get_market_reinpremium(self) -> float: - # QUERY: What's the difference between this and get_reinsurance_premium below? """Get_market_reinpremium Method. Accepts no arguments. Returns: @@ -871,9 +798,7 @@ def get_market_reinpremium(self) -> float: This method returns the current reinsurance market premium.""" return self.reinsurance_market_premium - def get_reinsurance_premium( - self, np_reinsurance_deductible_fraction: float - ) -> float: + def get_reinsurance_premium(self, np_reinsurance_deductible_fraction: float): """Method to determine reinsurance premium based on deductible fraction Accepts: np_reinsurance_deductible_fraction: Type Integer @@ -930,9 +855,7 @@ def get_reinrisks(self) -> Sequence[RiskProperties]: np.random.shuffle(self.reinrisks) return self.reinrisks - def solicit_insurance_requests( - self, insurer: "MetaInsuranceOrg" - ) -> Sequence[RiskProperties]: + def solicit_insurance_requests(self, insurer: "MetaInsuranceOrg") -> Sequence[RiskProperties]: """Method for determining which risks are to be assessed by firms based on insurer weights Accepts: insurer: Type firm metainsuranceorg instance @@ -940,20 +863,18 @@ def solicit_insurance_requests( risks_to_be_sent: Type List""" risks_to_be_sent = self.risks[: int(self.insurers_weights[insurer.id])] self.risks = self.risks[int(self.insurers_weights[insurer.id]) :] - for risk in insurer.risks_retained: + for risk in insurer.risks_kept: risks_to_be_sent.append(risk) # QUERY: what actually is InsuranceFirm.risks_kept? Are we resending all their existing risks? # Or is it just a list of risk that have rolled over and so need to be re-evaluated - insurer.risks_retained = [] + insurer.risks_kept = [] np.random.shuffle(risks_to_be_sent) return risks_to_be_sent - def solicit_reinsurance_requests( - self, reinsurer: "MetaInsuranceOrg" - ) -> Sequence[RiskProperties]: + def solicit_reinsurance_requests(self, reinsurer: "MetaInsuranceOrg") -> Sequence[RiskProperties]: """Method for determining which reinsurance risks are to be assessed by firms based on reinsurer weights Accepts: id: Type integer @@ -990,9 +911,7 @@ def return_reinrisks(self, not_accepted_risks: Sequence[RiskProperties]): Returns None""" self.not_accepted_reinrisks += not_accepted_risks - def _get_all_riskmodel_combinations( - self, rm_factor: float - ) -> Sequence[Sequence[float]]: + def _get_all_riskmodel_combinations(self, rm_factor: float) -> Sequence[Sequence[float]]: """Method for calculating riskmodels for each category based on the risk model inaccuracy parameter, and is used purely to assign inaccuracy. Undervalues one risk category and overestimates all the rest. Accepts: @@ -1008,9 +927,7 @@ def _get_all_riskmodel_combinations( riskmodels.append(riskmodel_combination) return riskmodels - def firm_enters_market( - self, prob: float = -1, agent_type: str = "InsuranceFirm" - ) -> bool: + def firm_enters_market(self, prob: float = -1, agent_type: str = "InsuranceFirm") -> bool: """Method to determine if re/insurance firm enters the market based on set entry probabilities and a random integer generated between 0, 1. Accepts: @@ -1051,6 +968,12 @@ def record_market_exit(self): firm.""" self.cumulative_market_exits += 1 + def record_nonregulation_firm(self): + self.cumulative_nonregulation_firms += 1 + + def record_bought_firm(self): + self.cumulative_bought_firms += 1 + def record_unrecovered_claims(self, loss: float): """Method for recording unrecovered claims. If firm runs out of money it cannot _pay more claims and so that money is lost and recorded using this method. @@ -1141,7 +1064,6 @@ def reinsurance_entry_index(self) -> int: 0 : self.simulation_parameters["no_riskmodels"] ].argmin() - # noinspection PyMethodMayBeStatic def get_operational(self) -> bool: """Override get_operational to always return True, as the market will never die""" return True diff --git a/isleconfig.py b/isleconfig.py index eff0a6b..f4fbf92 100644 --- a/isleconfig.py +++ b/isleconfig.py @@ -2,14 +2,14 @@ replicating = False force_foreground = False verbose = False -showprogress = False +showprogress = True # Should network be visualized? This should be False by default, to be overridden by commandline arguments show_network = False save_network = False # Should logs be small in ensemble runs (only aggregated level data)? slim_log = True buy_bankruptcies = False -enforce_regulations = False +enforce_regulations = True aid_relief = False simulation_parameters = { @@ -32,7 +32,7 @@ "mean_contract_runtime": 12, "contract_runtime_halfspread": 2, "default_contract_payment_period": 3, - "max_time": 300, + "max_time": 1000, "money_supply": 2000000000, "event_time_mean_separation": 100 / 3.0, "expire_immediately": False, diff --git a/metainsurancecontract.py b/metainsurancecontract.py index 7ce8182..d12d388 100644 --- a/metainsurancecontract.py +++ b/metainsurancecontract.py @@ -6,21 +6,10 @@ class MetaInsuranceContract: - def __init__( - self, - insurer: "MetaInsuranceOrg", - risk: "RiskProperties", - time: int, - premium: float, - runtime: int, - payment_period: int, - expire_immediately: bool, - initial_var: float = 0.0, - insurancetype: str = "proportional", - deductible_fraction: float = None, - limit_fraction: float = None, - reinsurance: float = 0, - ): + def __init__(self, insurer: "MetaInsuranceOrg", risk: "RiskProperties", time: int, premium: float, runtime: int, + payment_period: int, expire_immediately: bool, initial_var: float = 0.0, + insurancetype: str = "proportional", deductible_fraction: float = None, limit_fraction: float = None, + reinsurance: float = 0,): """Constructor method. Accepts arguments insurer: Type InsuranceFirm. @@ -59,6 +48,7 @@ def __init__( self.terminating = False self.current_claim = 0 self.initial_VaR = initial_var + # set deductible from argument, risk property or default value, whichever first is not None default_deductible_fraction = 0.0 self.deductible_fraction = ( @@ -66,8 +56,7 @@ def __init__( if deductible_fraction is not None else risk.deductible_fraction if risk.deductible_fraction is not None - else default_deductible_fraction - ) + else default_deductible_fraction) self.deductible = round(self.deductible_fraction * self.value) # set excess from argument, risk property or default value, whichever first is not None @@ -77,8 +66,7 @@ def __init__( if limit_fraction is not None else risk.limit_fraction if risk.limit_fraction is not None - else default_excess_fraction - ) + else default_excess_fraction) self.limit = round(self.limit_fraction * self.value) @@ -95,21 +83,13 @@ def __init__( # N.B.: payment times and values are in reverse, so the earliest time is at the end! This is because popping # items off the end of lists is much easier than popping them off the start. - self.payment_times = [ - time + i for i in range(runtime - 1, -1, -1) if i % payment_period == 0 - ] + self.payment_times = [time + i for i in range(runtime - 1, -1, -1) if i % payment_period == 0] - self.payment_values = [total_premium / len(self.payment_times)] * len( - self.payment_times - ) + self.payment_values = [total_premium / len(self.payment_times)] * len(self.payment_times) # Embed contract in reinsurance network, if applicable if self.contract: - self.contract.reinsure( - reinsurer=self.insurer, - reinsurance_share=risk.reinsurance_share, - reincontract=self, - ) + self.contract.reinsure(reinsurer=self.insurer, reinsurance_share=risk.reinsurance_share, reincontract=self) # This flag is set to 1, when the contract is about to expire and there is an attempt to roll it over. self.roll_over_flag = 0 @@ -123,9 +103,7 @@ def check_payment_due(self, time: int): and removes from schedule.""" if len(self.payment_times) > 0 and time >= self.payment_times[-1]: # Create obligation for premium payment - self.property_holder.receive_obligation( - self.payment_values[-1], self.insurer, time, "premium" - ) + self.property_holder.receive_obligation(self.payment_values[-1], self.insurer, time, "premium") # Remove current payment from payment schedule del self.payment_times[-1] @@ -185,23 +163,3 @@ def unreinsure(self): self.reinsurance = 0 self.reinsurance_share = None - def explode(self, time, uniform_value=None, damage_extent=None): - """Explode method. - Accepts arguments - time: Type integer. The current time. - uniform_value: Not used - damage_extent: Type float. The absolute damage in excess-of-loss reinsurance (not relative as in - proportional contracts. - No return value. - Method marks the contract for termination. - """ - raise NotImplementedError() - - def mature(self, time): - """Mature method. - Accepts arguments - time: Type integer. The current time. - No return value. - Removes any reinsurance functions this contract has and terminates any reinsurance contracts for this - contract.""" - raise NotImplementedError() diff --git a/metainsuranceorg.py b/metainsuranceorg.py index a94e641..0dca814 100644 --- a/metainsuranceorg.py +++ b/metainsuranceorg.py @@ -74,9 +74,7 @@ def get_mean_std(x: Tuple[float, ...]) -> Tuple[float, float]: class MetaInsuranceOrg(GenericAgent): - def __init__( - self, simulation_parameters: Mapping, agent_parameters: AgentProperties - ): + def __init__(self, simulation_parameters: Mapping, agent_parameters: AgentProperties): """Constructor method. Accepts: Simulation_parameters: Type DataDict @@ -86,47 +84,28 @@ def __init__( super().__init__() self.simulation: "InsuranceSimulation" = simulation_parameters["simulation"] self.simulation_parameters: Mapping = simulation_parameters - self.contract_runtime_dist = scipy.stats.randint( - simulation_parameters["mean_contract_runtime"] - - simulation_parameters["contract_runtime_halfspread"], - simulation_parameters["mean_contract_runtime"] - + simulation_parameters["contract_runtime_halfspread"] - + 1, - ) - self.default_contract_payment_period: int = simulation_parameters[ - "default_contract_payment_period" - ] + self.contract_runtime_dist = scipy.stats.randint(simulation_parameters["mean_contract_runtime"] + - simulation_parameters["contract_runtime_halfspread"], + simulation_parameters["mean_contract_runtime"] + + simulation_parameters["contract_runtime_halfspread"] + 1) + self.default_contract_payment_period: int = simulation_parameters["default_contract_payment_period"] self.id = agent_parameters.id self.cash = agent_parameters.initial_cash self.capacity_target = self.cash * 0.9 - self.capacity_target_decrement_threshold = ( - agent_parameters.capacity_target_decrement_threshold - ) - self.capacity_target_increment_threshold = ( - agent_parameters.capacity_target_increment_threshold - ) - self.capacity_target_decrement_factor = ( - agent_parameters.capacity_target_decrement_factor - ) - self.capacity_target_increment_factor = ( - agent_parameters.capacity_target_increment_factor - ) + self.capacity_target_decrement_threshold = (agent_parameters.capacity_target_decrement_threshold) + self.capacity_target_increment_threshold = (agent_parameters.capacity_target_increment_threshold) + self.capacity_target_decrement_factor = (agent_parameters.capacity_target_decrement_factor) + self.capacity_target_increment_factor = (agent_parameters.capacity_target_increment_factor) self.excess_capital = self.cash self.premium = agent_parameters.norm_premium self.profit_target = agent_parameters.profit_target self.acceptance_threshold = agent_parameters.initial_acceptance_threshold # 0.5 - self.acceptance_threshold_friction = ( - agent_parameters.acceptance_threshold_friction - ) # 0.9 #1.0 to switch off + self.acceptance_threshold_friction = (agent_parameters.acceptance_threshold_friction) # 0.9 #1.0 to switch off self.interest_rate = agent_parameters.interest_rate self.reinsurance_limit = agent_parameters.reinsurance_limit self.simulation_no_risk_categories = simulation_parameters["no_categories"] - self.simulation_reinsurance_type = simulation_parameters[ - "simulation_reinsurance_type" - ] - self.dividend_share_of_profits = simulation_parameters[ - "dividend_share_of_profits" - ] + self.simulation_reinsurance_type = simulation_parameters["simulation_reinsurance_type"] + self.dividend_share_of_profits = simulation_parameters["dividend_share_of_profits"] # If the firm goes bankrupt then by default any further payments should be made to the simulation self.creditor = self.simulation @@ -139,18 +118,11 @@ def __init__( """Here we modify the margin of safety depending on the number of risks models available in the market. When is 0 all risk models have the same margin of safety. The reason for doing this is that with more risk models the firms tend to be closer to the max capacity""" - margin_of_safety_correction = ( - rm_config["margin_of_safety"] - + (simulation_parameters["no_riskmodels"] - 1) - * simulation_parameters["margin_increase"] - ) + margin_of_safety_correction = (rm_config["margin_of_safety"]+ (simulation_parameters["no_riskmodels"] - 1) * simulation_parameters["margin_increase"]) self.max_inaccuracy = rm_config["inaccuracy_by_categ"] - self.min_inaccuracy = self.max_inaccuracy * isleconfig.simulation_parameters[ - "scale_inaccuracy" - ] + np.ones(len(self.max_inaccuracy)) * ( - 1 - isleconfig.simulation_parameters["scale_inaccuracy"] - ) + self.min_inaccuracy = self.max_inaccuracy * isleconfig.simulation_parameters["scale_inaccuracy"] + \ + np.ones(len(self.max_inaccuracy)) * (1 - isleconfig.simulation_parameters["scale_inaccuracy"]) self.riskmodel: riskmodel.RiskModel = riskmodel.RiskModel( damage_distribution=rm_config["damage_distribution"], @@ -163,8 +135,7 @@ def __init__( init_profit_estimate=rm_config["norm_profit_markup"], margin_of_safety=margin_of_safety_correction, var_tail_prob=rm_config["var_tail_prob"], - inaccuracy=self.max_inaccuracy, - ) + inaccuracy=self.max_inaccuracy,) # Set up the reinsurance profile self.reinsurance_profile = ReinsuranceProfile(self.riskmodel) @@ -195,7 +166,8 @@ def __init__( self.var_counter_per_risk = 0 # average risk model inaccuracy across contracts self.var_sum = 0 # sum over initial VaR for all contracts self.var_sum_last_periods = list(np.zeros((12, 4), dtype=int)) - self.reinsurance_history = list(np.zeros((12, 4, 2), dtype=int)) + # self.reinsurance_history = list(np.zeros((12, 4, 2), dtype=int)) + self.reinsurance_history = [[],[],[],[],[],[],[],[],[],[],[],[]] self.counter_category = np.zeros(self.simulation_no_risk_categories) # var_counter disaggregated by category self.var_category = np.zeros(self.simulation_no_risk_categories) # var_sum disaggregated by category self.naccep = [] @@ -206,9 +178,7 @@ def __init__( self.cash_left_by_categ = [self.cash for i in range(self.simulation_parameters["no_categories"])] self.market_permanency_counter = 0 # TODO: make this into a dict - self.underwritten_risk_characterisation: MutableSequence[ - Tuple[float, float, int, float] - ] = [ + self.underwritten_risk_characterisation: MutableSequence[Tuple[float, float, int, float]] = [ (None, None, None, None) for _ in range(self.simulation_parameters["no_categories"]) ] @@ -265,9 +235,7 @@ def iterate(self, time: int): self.estimate_var() - def collect_process_evaluate_risks( - self, time: int, contracts_dissolved: int - ) -> None: + def collect_process_evaluate_risks(self, time: int, contracts_dissolved: int): if self.operational: self.update_risk_characterisations() for categ in range(len(self.counter_category)): @@ -286,9 +254,7 @@ def collect_process_evaluate_risks( then with proportional ones""" # Here the new reinrisks are organized by category. - reinrisks_per_categ = self.risks_reinrisks_organizer( - new_nonproportional_risks - ) + reinrisks_per_categ = self.risks_reinrisks_organizer(new_nonproportional_risks) assert self.recursion_limit > 0 for repetition in range(self.recursion_limit): @@ -304,9 +270,7 @@ def collect_process_evaluate_risks( if not has_accepted_risks: # Stop condition implemented. Might solve the previous TODO. break - self.simulation.return_reinrisks( - list(chain.from_iterable(not_accepted_reinrisks)) - ) + self.simulation.return_reinrisks(list(chain.from_iterable(not_accepted_reinrisks))) # TODO: This takes up a lot of processing time. Can we update the list instead of rebuilding it? underwritten_risks = [ @@ -348,8 +312,8 @@ def collect_process_evaluate_risks( # TODO: make independent of insurer/reinsurer, but change this to different deductible values """handle capital market interactions: capital history, dividends""" - self.cash_last_periods = np.roll(self.cash_last_periods, 1) - self.cash_last_periods[0] = self.cash + self.cash_last_periods = np.roll(self.cash_last_periods, -1) + self.cash_last_periods[-1] = self.cash self.adjust_dividends(time, actual_capacity) self.pay_dividends(time) @@ -363,9 +327,7 @@ def collect_process_evaluate_risks( # Here the new risks are organized by category. risks_per_categ = self.risks_reinrisks_organizer(new_risks) - if risks_per_categ != [ - [] for _ in range(self.simulation_no_risk_categories) - ]: + if risks_per_categ != [[] for _ in range(self.simulation_no_risk_categories)]: for repetition in range(self.recursion_limit): # TODO: find an efficient way to stop the recursion if there are no more risks to accept or if it is # not accepting any more over several iterations. Done, maybe? @@ -382,13 +344,11 @@ def collect_process_evaluate_risks( if not has_accepted_risks: # Stop condition implemented. Might solve the previous TODO. break - self.simulation.return_risks( - list(chain.from_iterable(not_accepted_risks)) - ) + self.simulation.return_risks(list(chain.from_iterable(not_accepted_risks))) # print(self.id, " now has ", len(self.underwritten_contracts), " & returns ", len(not_accepted_risks)) self.update_risk_characterisations() - def enter_illiquidity(self, time: int, sum_due: float): + def enter_illiquidity(self, time: int): """Enter_illiquidity Method. Accepts arguments time: Type integer. The current time. @@ -456,7 +416,7 @@ def dissolve(self, time, record): Different class variables are reset during the process: self.risks_kept, self.reinrisks_kept, self.excess_capital and self.profits_losses.""" # Record all unpaid claims (needs to be here to account for firms lost due to regulator/being sold) - sum_due = sum(item['amount'] for item in self.obligations if item['purpose'] == 'claim') + sum_due = sum(item.amount for item in self.obligations if item.purpose == 'claim') self.simulation.record_unrecovered_claims(sum_due - self.cash) # Removing (dissolving) all risks immediately after bankruptcy (may not be realistic, they might instead be bought by another company) @@ -464,22 +424,15 @@ def dissolve(self, time, record): self.simulation.return_risks(self.risks_kept) self.risks_kept = [] self.reinrisks_kept = [] - obligation = Obligation( - amount=self.cash, - recipient=self.simulation, - due_time=time, - purpose="Dissolution", - ) + obligation = Obligation(amount=self.cash, recipient=self.simulation, due_time=time, purpose="Dissolution") # This MUST be the last obligation before the dissolution of the firm. self._pay(obligation) # Excess of capital is 0 after bankruptcy or market exit. self.excess_capital = 0 # Profits and losses are 0 after bankruptcy or market exit. self.profits_losses = 0 - if self.operational: - # TODO: This seems... odd? - method_to_call = getattr(self.simulation, record) - method_to_call() + method_to_call = getattr(self.simulation, record) + method_to_call() for reincontract in self.reinsurance_profile.all_contracts(): reincontract.dissolve(time) self.operational = False @@ -492,46 +445,7 @@ def pay_dividends(self, time: int): If firm has positive profits will pay percentage of them as dividends. Currently pays to simulation. """ - - def effect_payments(self, time): - """Method for checking if any payments are due. - Accepts: - time: Type Integer - No return value - Method checks firms list of obligations to see if ay are due for this time, then pays them. If the firm - does not have enough cash then it enters illiquity, leaves the market, and matures all contracts.""" - due = [item for item in self.obligations if item["due_time"]<=time] - self.obligations = [item for item in self.obligations if item["due_time"]>time] - sum_due = sum([item["amount"] for item in due]) - if sum_due > self.cash: - self.obligations += due - self.enter_illiquidity(time) - else: - for obligation in due: - self.pay(obligation) - - def pay(self, obligation): - """Method to pay other class instances. - Accepts: - Obligation: Type DataDict - No return value - Method removes value payed from the agents cash and adds it to recipient agents cash.""" - amount = obligation["amount"] - recipient = obligation["recipient"] - purpose = obligation["purpose"] - if recipient.get_operational(): - self.cash -= amount - if purpose is not 'dividend': - self.profits_losses -= amount - recipient.receive(amount) - - def receive(self, amount): - """Method to accept cash payments. - Accepts: - amount: Type Integer - No return value""" - self.cash += amount - self.profits_losses += amount + self.receive_obligation(self.per_period_dividend, self.simulation, due_time=time, purpose="dividend") def mature_contracts(self, time: int) -> int: """Method to mature underwritten contracts that have expired @@ -596,35 +510,34 @@ def estimate_var(self) -> None: self.counter_category[contract.category] += 1 self.var_category[contract.category] += contract.initial_VaR - # Caclulate risks per category and sum of all VaR + # Calculate risks per category and sum of all VaR for category in range(len(self.counter_category)): self.var_counter += self.counter_category[category] * self.riskmodel.inaccuracy[category] self.var_sum += self.var_category[category] # Record reinsurance info - for reinsurance in self.category_reinsurance: - if reinsurance is not None: - current_reinsurance_info.append([reinsurance.deductible, reinsurance.excess]) + for region_list in self.reinsurance_profile.reinsured_regions: + current_region_info = [] + if len(region_list) > 0: + for contract in region_list: + current_region_info.append([contract[0], contract[1]]) else: - current_reinsurance_info.append([0, 0]) + current_region_info.append([0,0]) + current_reinsurance_info.append(current_region_info) # Rotate lists and replace for up-to-date list for 12 iterations - self.var_sum_last_periods = np.roll(self.var_sum_last_periods, 4) - self.var_sum_last_periods[0] = self.var_category - self.reinsurance_history = np.roll(self.reinsurance_history, 8) - self.reinsurance_history[0] = current_reinsurance_info - - # Calculate average no. risks per category - if sum(self.counter_category) != 0: - self.var_counter_per_risk = self.var_counter / sum( - self.counter_category - ) - else: - self.var_counter_per_risk = 0 + self.var_sum_last_periods = np.roll(self.var_sum_last_periods, -4) + self.var_sum_last_periods[-1] = self.var_category + self.reinsurance_history.append(current_reinsurance_info) + self.reinsurance_history.pop(0) + + # Calculate average no. risks per category + if sum(self.counter_category) != 0: + self.var_counter_per_risk = self.var_counter / sum(self.counter_category) + else: + self.var_counter_per_risk = 0 - def get_newrisks_by_type( - self - ) -> Tuple[Sequence[RiskProperties], Sequence[RiskProperties]]: + def get_newrisks_by_type(self) -> Tuple[Sequence[RiskProperties], Sequence[RiskProperties]]: """Method for soliciting new risks from insurance simulation then organising them based if non-proportional or not. No accepted Values. @@ -649,30 +562,11 @@ def get_newrisks_by_type( ] return new_nonproportional_risks, new_risks - def increase_capacity(self, time, var_by_category): - raise NotImplementedError( - "Method is not implemented in MetaInsuranceOrg, just in inheriting InsuranceFirm instances" - ) - - def adjust_dividends(self, time, actual_capacity): - raise NotImplementedError( - "Method not implemented. adjust_dividends method should be implemented in inheriting classes" - ) - - def adjust_capacity_target(self, time): - raise NotImplementedError( - "Method not implemented. adjust_capacity_target method should be implemented in inheriting classes" - ) - def update_risk_characterisations(self): for categ in range(self.simulation_no_risk_categories): - self.underwritten_risk_characterisation[ - categ - ] = self.characterise_underwritten_risks_by_category(categ) + self.underwritten_risk_characterisation[categ] = self.characterise_underwritten_risks_by_category(categ) - def characterise_underwritten_risks_by_category( - self, categ_id: int - ) -> Tuple[float, float, int, float]: + def characterise_underwritten_risks_by_category(self, categ_id: int) -> Tuple[float, float, int, float]: """Method to characterise associated risks in a given category in terms of value, number, avg risk factor, and total premium per iteration. Accepts: @@ -697,9 +591,7 @@ def characterise_underwritten_risks_by_category( avg_risk_factor /= number_risks return total_value, avg_risk_factor, number_risks, periodized_total_premium - def risks_reinrisks_organizer( - self, new_risks: Sequence[RiskProperties] - ) -> Sequence[Sequence[RiskProperties]]: + def risks_reinrisks_organizer(self, new_risks: Sequence[RiskProperties]) -> Sequence[Sequence[RiskProperties]]: """This method organizes the new risks received by the insurer (or reinsurer) by category. Accepts: new_risks: Type list of DataDicts @@ -722,12 +614,7 @@ def risks_reinrisks_organizer( # The method returns both risks_by_category and number_risks_categ. return risks_by_category - def balanced_portfolio( - self, - risk: RiskProperties, - cash_left_by_categ: np.ndarray, - var_per_risk: Optional[Sequence[float]], - ) -> Tuple[bool, np.ndarray]: + def balanced_portfolio(self, risk: RiskProperties, cash_left_by_categ: np.ndarray, var_per_risk: Optional[Sequence[float]]) -> Tuple[bool, np.ndarray]: """This method decides whether the portfolio is balanced enough to accept a new risk or not. If it is balanced enough return True otherwise False. This method also returns the cash available per category independently the risk is accepted or not. @@ -793,9 +680,7 @@ def balanced_portfolio( cash_left_by_categ = self.cash - cash_reserved_by_categ return False, cash_left_by_categ - def process_newrisks_reinsurer( - self, reinrisks_per_categ: Sequence[Sequence[RiskProperties]], time: int - ): + def process_newrisks_reinsurer(self, reinrisks_per_categ: Sequence[Sequence[RiskProperties]], time: int): """Method to decide if new risks are underwritten for the reinsurance firm. Accepts: reinrisks_per_categ: Type List of lists containing new reinsurance risks. @@ -851,19 +736,12 @@ def process_newrisks_reinsurer( ) if condition: - contract = reinsurancecontract.ReinsuranceContract( - self, - risk, - time, - per_value_reinsurance_premium, - risk.runtime, - self.default_contract_payment_period, - expire_immediately=self.simulation_parameters[ - "expire_immediately" - ], - initial_var=var_this_risk, - insurancetype=risk.insurancetype, - ) # TODO: implement excess of loss for reinsurance contracts + contract = reinsurancecontract.ReinsuranceContract(self, risk, time, per_value_reinsurance_premium, + risk.runtime, self.default_contract_payment_period, + expire_immediately=self.simulation_parameters[ + "expire_immediately"], initial_var=var_this_risk, + insurancetype=risk.insurancetype,) + # TODO: implement excess of loss for reinsurance contracts self.underwritten_contracts.append(contract) has_accepted_risks = True self.cash_left_by_categ = cash_left_by_categ @@ -874,14 +752,9 @@ def process_newrisks_reinsurer( return has_accepted_risks, not_accepted_reinrisks - def process_newrisks_insurer( - self, - risks_per_categ: Sequence[Sequence[RiskProperties]], - acceptable_by_category: Sequence[int], - var_per_risk_per_categ: Sequence[float], - cash_left_by_categ: Sequence[float], - time: int, - ) -> Tuple[bool, Sequence[Sequence[RiskProperties]]]: + def process_newrisks_insurer(self, risks_per_categ: Sequence[Sequence[RiskProperties]], + acceptable_by_category: Sequence[int],var_per_risk_per_categ: Sequence[float], + cash_left_by_categ: Sequence[float],time: int,) -> Tuple[bool, Sequence[Sequence[RiskProperties]]]: """Method to decide if new risks are underwritten for the insurance firm. Accepts: risks_per_categ: Type List of lists containing new risks. @@ -976,35 +849,19 @@ def market_permanency(self, time: int): avg_cash_left = get_mean(cash_left_by_categ) - if ( - self.cash < self.simulation_parameters["cash_permanency_limit"] - ): # If their level of cash is so low that they cannot underwrite anything they also leave the market. + if self.cash < self.simulation_parameters["cash_permanency_limit"]: # If their level of cash is so low that they cannot underwrite anything they also leave the market. self.market_exit(time) else: if self.is_insurer: - - if ( - len(self.underwritten_contracts) - < self.simulation_parameters[ - "insurance_permanency_contracts_limit" - ] - or avg_cash_left / self.cash - > self.simulation_parameters["insurance_permanency_ratio_limit"] - ): + if len(self.underwritten_contracts) < self.simulation_parameters["insurance_permanency_contracts_limit"] or avg_cash_left / self.cash > self.simulation_parameters["insurance_permanency_ratio_limit"]: # Insurers leave the market if they have contracts under the limit or an excess capital # over the limit for too long. self.market_permanency_counter += 1 else: self.market_permanency_counter = 0 - if ( - self.market_permanency_counter - >= self.simulation_parameters[ - "insurance_permanency_time_constraint" - ] - ): + if self.market_permanency_counter >= self.simulation_parameters["insurance_permanency_time_constraint"]: # Here we determine how much is too long. self.market_exit(time) - if self.is_reinsurer: if ( @@ -1071,36 +928,20 @@ def roll_over(self, time: int): if self.is_insurer: for contract in maturing_next: contract.roll_over_flag = 1 - if ( - next(uniform_rvs) - > self.simulation_parameters["insurance_retention"] - ): - self.simulation.return_risks( - [contract.risk] - ) # TODO: This is not a retention, so the roll_over_flag might be confusing in this case + if next(uniform_rvs) > self.simulation_parameters["insurance_retention"]: + self.simulation.return_risks([contract.risk]) # TODO: This is not a retention, so the roll_over_flag might be confusing in this case else: - self.risks_retained.append(contract.risk) + self.risks_kept.append(contract.risk) if self.is_reinsurer: for reincontract in maturing_next: if reincontract.property_holder.operational: reincontract.roll_over_flag = 1 - reinrisk = reincontract.property_holder.refresh_reinrisk( - time=time, old_contract=reincontract - ) - if ( - next(uniform_rvs) - < self.simulation_parameters["reinsurance_retention"] - ): + reinrisk = reincontract.property_holder.refresh_reinrisk(time=time, old_contract=reincontract) + if next(uniform_rvs)< self.simulation_parameters["reinsurance_retention"]: if reinrisk is not None: self.reinrisks_kept.append(reinrisk) - def make_reinsurance_claims(self, time: int): - raise NotImplementedError( - "MetaInsuranceOrg does not implement make_reinsurance_claims, " - "it should have been overridden" - ) - def update_risk_share(self): """Updates own value for share of all risks held by this firm. Has neither arguments nor a return value""" self.risk_share = self.simulation.get_risk_share(self) @@ -1111,9 +952,7 @@ def insurance_premium(self) -> float: Returns the market premium multiplied by a factor that scales linearly with self.risk_share between 1 and the max permissble adjustment""" max_adjustment = isleconfig.simulation_parameters["max_scale_premiums"] - premium = self.simulation.get_market_premium() * ( - 1 * (1 - self.risk_share) + max_adjustment * self.risk_share - ) + premium = self.simulation.get_market_premium() * (1 * (1 - self.risk_share) + max_adjustment * self.risk_share) return premium def adjust_riskmodel_inaccuracy(self): @@ -1124,10 +963,7 @@ def adjust_riskmodel_inaccuracy(self): by the share of risk this firm holds. """ if isleconfig.simulation_parameters["scale_inaccuracy"] != 1: - self.riskmodel.inaccuracy = ( - self.max_inaccuracy * (1 - self.risk_share) - + self.min_inaccuracy * self.risk_share - ) + self.riskmodel.inaccuracy = (self.max_inaccuracy * (1 - self.risk_share)+ self.min_inaccuracy * self.risk_share) def consider_buyout(self, type="insurer"): """Method to allow firm to decide if to buy one of the firms going bankrupt. diff --git a/reinsurancecontract.py b/reinsurancecontract.py index 37d920d..ccfbd7e 100644 --- a/reinsurancecontract.py +++ b/reinsurancecontract.py @@ -15,21 +15,10 @@ class ReinsuranceContract(metainsurancecontract.MetaInsuranceContract): The signature of this class' constructor is the same as that of the InsuranceContract constructor. The class has two methods (explode, mature) that overwrite methods in InsuranceContract.""" - def __init__( - self, - insurer: "MetaInsuranceOrg", - risk: "RiskProperties", - time: int, - premium: float, - runtime: int, - payment_period: int, - expire_immediately: bool, - initial_var: float = 0.0, - insurancetype: str = "proportional", - deductible_fraction: "Optional[float]" = None, - limit_fraction: "Optional[float]" = None, - reinsurance: float = 0, - ): + def __init__(self,insurer: "MetaInsuranceOrg", risk: "RiskProperties", time: int, premium: float, runtime: int, + payment_period: int, expire_immediately: bool, initial_var: float = 0.0, + insurancetype: str = "proportional", deductible_fraction: "Optional[float]" = None, + limit_fraction: "Optional[float]" = None, reinsurance: float = 0,): super().__init__( insurer, risk, @@ -53,9 +42,7 @@ def __init__( else: assert self.contract is not None - def explode( - self, time: int, uniform_value: None = None, damage_extent: float = None - ): + def explode(self, time: int, uniform_value: None = None, damage_extent: float = None): """Explode method. Accepts arguments time: Type integer. The current time. diff --git a/setup_simulation.py b/setup_simulation.py index f274d05..36017bb 100644 --- a/setup_simulation.py +++ b/setup_simulation.py @@ -53,11 +53,7 @@ def __init__(self): self.overwrite = False self.replications = None - def schedule( - self, replications: int - ) -> Tuple[ - MutableSequence[MutableSequence[int]], MutableSequence[MutableSequence[float]] - ]: + def schedule(self, replications: int) -> Tuple[MutableSequence[MutableSequence[int]], MutableSequence[MutableSequence[float]]]: for i in range(replications): # In this list will be stored the lists of times when there will be catastrophes for every category of the # model during a single run. ([[times for C1],[times for C2],[times for C3],[times for C4]]) @@ -91,7 +87,7 @@ def seeds(self, replications: int): # The argument (replications) is the number of replications. """draw random variates for random seeds""" for i in range(replications): - np_seed, random_seed = scipy.stats.randint.rvs(0, 2 ** 32 - 1, size=2) + np_seed, random_seed = scipy.stats.randint.rvs(0, 2 ** 31 - 1, size=2) self.np_seed.append(np_seed) self.random_seed.append(random_seed) @@ -160,9 +156,7 @@ def recall(self): "num_categories" ] - def obtain_ensemble( - self, replications: int, filepath: str = None, overwrite: bool = False - ) -> Tuple: + def obtain_ensemble(self, replications: int, filepath: str = None, overwrite: bool = False) -> Tuple: # This method returns all the information (schedules and seeds) required to run an ensemble of simulations of # the model. Since it also stores the information in a file it will be possible to replicate the ensemble at a # later time. The argument (replications) is the number of replications. diff --git a/start.py b/start.py index 55c38dc..ae74e57 100644 --- a/start.py +++ b/start.py @@ -23,35 +23,20 @@ """Creates data file for logs if does not exist""" if not os.path.isdir("data"): if os.path.exists("data"): - raise FileExistsError( - "./data exists as regular file. This filename is required for the logging directory" - ) + raise FileExistsError("./data exists as regular file. This filename is required for the logging directory") os.makedirs("data") # main function -def main( - sim_params: MutableMapping, - rc_event_schedule: MutableSequence[MutableSequence[int]], - rc_event_damage: MutableSequence[MutableSequence[float]], - np_seed: int, - random_seed: int, - save_iteration: int, - replic_id: int, - requested_logs: MutableSequence = None, - resume: bool = False, -) -> MutableSequence: +def main(sim_params: MutableMapping, rc_event_schedule: MutableSequence[MutableSequence[int]], + rc_event_damage: MutableSequence[MutableSequence[float]], np_seed: int, random_seed: int, save_iteration: int, + replic_id: int, requested_logs: MutableSequence = None,resume: bool = False) -> MutableSequence: if not resume: np.random.seed(np_seed) random.seed(random_seed) - sim_params["simulation"] = simulation = insurancesimulation.InsuranceSimulation( - override_no_riskmodels, - replic_id, - sim_params, - rc_event_schedule, - rc_event_damage, - ) + sim_params["simulation"] = simulation = insurancesimulation.InsuranceSimulation(override_no_riskmodels, replic_id, + sim_params, rc_event_schedule, rc_event_damage) time = 0 else: d = load_simulation() @@ -79,20 +64,9 @@ def main( return simulation.obtain_log(requested_logs) -def save_simulation( - t: int, - sim: insurancesimulation.InsuranceSimulation, - sim_param: MutableMapping, - exit_now: bool = False, -) -> None: - d = { - "np_seed": np.random.get_state(), - "random_seed": random.getstate(), - "time": t, - "simulation": sim, - "simulation_parameters": sim_param, - "isleconfig": {}, - } +def save_simulation(t: int, sim: insurancesimulation.InsuranceSimulation, sim_param: MutableMapping, exit_now: bool = False,) -> None: + d = {"np_seed": np.random.get_state(), "random_seed": random.getstate(), "time": t, "simulation": sim, + "simulation_parameters": sim_param, "isleconfig": {}} for key in isleconfig.__dict__: if not key.startswith("__"): d["isleconfig"][key] = isleconfig.__dict__[key] @@ -101,10 +75,7 @@ def save_simulation( pickle.dump(d, wfile, protocol=pickle.HIGHEST_PROTOCOL) with open("data/simulation_save.pkl", "br") as rfile: file_contents = rfile.read() - print( - "\nSaved simulation with hash:", - hashlib.sha512(str(file_contents).encode()).hexdigest(), - ) + print("\nSaved simulation with hash:",hashlib.sha512(str(file_contents).encode()).hexdigest()) if exit_now: exit(0) @@ -113,10 +84,7 @@ def save_simulation( def load_simulation() -> dict: # TODO: Fix! This doesn't work, the retrieved file is different to the saved one. with open("data/simulation_save.pkl", "br") as rfile: - print( - "\nLoading simulation with hash:", - hashlib.sha512(str(rfile.read()).encode()).hexdigest(), - ) + print("\nLoading simulation with hash:", hashlib.sha512(str(rfile.read()).encode()).hexdigest()) rfile.seek(0) file_contents = pickle.load(rfile) return file_contents @@ -127,69 +95,27 @@ def load_simulation() -> dict: """ use argparse to handle command line arguments""" parser = argparse.ArgumentParser(description="Model the Insurance sector") - parser.add_argument( - "-f", - "--file", - action="store", - help="the file to store the initial randomness in. Will be stored in ./data and appended with .islestore " - "(if it is not already). The default filepath is ./data/risk_event_schedules.islestore, which will be " - "overwritten event if --overwrite is not passed!", - ) - parser.add_argument( - "-r", - "--replicating", - action="store_true", - help="if this is a simulation run designed to replicate another, override the config file parameter. " - "You probably want to specify the --file to read from.", - ) - parser.add_argument( - "-o", - "--overwrite", - action="store_true", - help="allows overwriting of the file specified by -f", - ) - parser.add_argument( - "-p", "--showprogress", action="store_true", help="show timesteps" - ) - parser.add_argument( - "-v", "--verbose", action="store_true", help="more detailed output" - ) - parser.add_argument( - "--resume", - action="store_true", - help="Resume the simulation from a previous save in ./data/simulation_save.pkl. " - "All other arguments will be ignored", - ) - parser.add_argument( - "--oneriskmodel", - action="store_true", - help="allow overriding the number of riskmodels from the standard config (with 1)", - ) - parser.add_argument( - "--riskmodels", - type=int, - choices=[1, 2, 3, 4], - help="allow overriding the number of riskmodels from standard config (with 1 or other numbers)." - " Overrides --oneriskmodel", - ) - parser.add_argument( - "--randomseed", type=float, help="allow setting of numpy random seed" - ) - parser.add_argument( - "--foreground", - action="store_true", - help="force foreground runs even if replication ID is given (which defaults to background runs)", - ) - parser.add_argument( - "--shownetwork", - action="store_true", - help="show reinsurance relations as network", - ) - parser.add_argument( - "--save_iterations", - type=int, - help="number of iterations to iterate before saving world state", - ) + parser.add_argument("-f", "--file", action="store", + help="the file to store the initial randomness in. Will be stored in ./data and appended with " + ".islestore (if it is not already). The default filepath is " + "./data/risk_event_schedules.islestore, which will be overwritten event if --overwrite is " + "not passed!") + parser.add_argument("-r", "--replicating", action="store_true", help="if this is a simulation run designed to replicate another, override the config file parameter. " + "You probably want to specify the --file to read from.",) + parser.add_argument("-o", "--overwrite", action="store_true", help="allows overwriting of the file specified by -f") + parser.add_argument("-p", "--showprogress", action="store_true", help="show timesteps") + parser.add_argument("-v", "--verbose", action="store_true", help="more detailed output") + parser.add_argument("--resume", action="store_true", help="Resume the simulation from a previous save in " + "./data/simulation_save.pkl. All other arguments will be ignored",) + parser.add_argument("--oneriskmodel", action="store_true", help="allow overriding the number of riskmodels from the " + "standard config (with 1)",) + parser.add_argument("--riskmodels", type=int, choices=[1, 2, 3, 4], help="allow overriding the number of riskmodels " + "from standard config (with 1 or other numbers). Overrides --oneriskmodel",) + parser.add_argument("--randomseed", type=float, help="allow setting of numpy random seed") + parser.add_argument("--foreground", action="store_true", + help="force foreground runs even if replication ID is given, which defaults to background runs") + parser.add_argument("--shownetwork", action="store_true", help="show reinsurance relations as network") + parser.add_argument("--save_iterations", type=int, help="number of iterations to iterate before saving world state") args = parser.parse_args() if args.oneriskmodel: @@ -197,8 +123,6 @@ def load_simulation() -> dict: override_no_riskmodels = 1 if args.riskmodels: override_no_riskmodels = args.riskmodels - # if args.replicid: # TODO: track down all uses of replicid - # raise ValueError("--replicid is no longer supported, use --file") if args.file: filepath = args.file if args.overwrite: @@ -231,30 +155,16 @@ def load_simulation() -> dict: setup = SetupSim() # Here the setup for the simulation is done. # Only one ensemble. This part will only be run locally (laptop). - [ - general_rc_event_schedule, - general_rc_event_damage, - np_seeds, - random_seeds, - ] = setup.obtain_ensemble(1, filepath, overwrite) + [general_rc_event_schedule, general_rc_event_damage, np_seeds, random_seeds] = \ + setup.obtain_ensemble(1, filepath, overwrite) else: # We are resuming, so all of the necessary setup will be loaded from a file - general_rc_event_schedule = ( - general_rc_event_damage - ) = np_seeds = random_seeds = [None] + general_rc_event_schedule = (general_rc_event_damage) = np_seeds = random_seeds = [None] # Run the main program # Note that we pass the filepath as the replic_ID - log = main( - simulation_parameters, - general_rc_event_schedule[0], - general_rc_event_damage[0], - np_seeds[0], - random_seeds[0], - save_iter, - replic_id=1, - resume=args.resume, - ) + log = main(simulation_parameters, general_rc_event_schedule[0], general_rc_event_damage[0], np_seeds[0], + random_seeds[0], save_iter, replic_id=1, resume=args.resume) replic_ID = 1 """ Restore the log at the end of the single simulation run for saving and for potential further study """ From 02208298a1c59418691186c2d212356fd24bbe74 Mon Sep 17 00:00:00 2001 From: Chris Hughes Date: Tue, 6 Aug 2019 11:14:59 +0100 Subject: [PATCH 088/125] Implement catbonds. --- catbond.py | 48 +++---- insurancefirms.py | 306 ++++++++++++++++++++++------------------- insurancesimulation.py | 61 ++++++-- isleconfig.py | 2 +- metainsuranceorg.py | 37 +++-- reinsurancecontract.py | 36 ++++- riskmodel.py | 6 +- 7 files changed, 294 insertions(+), 202 deletions(-) diff --git a/catbond.py b/catbond.py index 3ee27e3..6deb5be 100644 --- a/catbond.py +++ b/catbond.py @@ -23,7 +23,6 @@ def __init__( simulation: "InsuranceSimulation", per_period_premium: float, owner: GenericAgent, - interest_rate: float = 0, ): """Initialising methods. Accepts: @@ -32,7 +31,7 @@ def __init__( owner: Type class This initialised the catbond class instance, inheriting methods from MetaInsuranceOrg.""" self.simulation = simulation - self.id: int = 0 + self.id: int = self.simulation.get_unique_catbond_id() self.underwritten_contracts: MutableSequence["MetaInsuranceContract"] = [] self.cash: float = 0 self.profits_losses: float = 0 @@ -40,12 +39,10 @@ def __init__( self.operational: bool = True self.owner: GenericAgent = owner self.per_period_dividend: float = per_period_premium - self.interest_rate: float = interest_rate - # TODO: shift obtain_yield method to insurancesimulation, thereby making it unnecessary to drag parameters like - # self.interest_rate from instance to instance and from class to class + self.creditor = self.simulation + self.expiration: int = None # self.simulation_no_risk_categories = self.simulation.simulation_parameters["no_categories"] - # TODO: change start and InsuranceSimulation so that it iterates CatBonds def iterate(self, time: int): """Method to perform CatBond duties for each time iteration. Accepts: @@ -53,7 +50,9 @@ def iterate(self, time: int): No return values For each time iteration this is called from insurancesimulation to perform duties: interest payments, _pay obligations, mature the contract if ended, make payments.""" - self.simulation.bank.award_interest(self, self.cash) + assert len(self.underwritten_contracts) == 1 + # Interest gets paid directly to the owner of the catbond (i.e. the simulation) + self.simulation.bank.award_interest(self.owner, self.cash) self._effect_payments(time) if isleconfig.verbose: print( @@ -64,28 +63,14 @@ def iterate(self, time: int): self.cash, self.operational, ) - - """mature contracts""" print("Number of underwritten contracts ", len(self.underwritten_contracts)) - maturing = [ - contract - for contract in self.underwritten_contracts - if contract.expiration <= time - ] - for contract in maturing: - self.underwritten_contracts.remove(contract) - contract.mature(time) - - """effect payments from contracts""" - for contract in self.underwritten_contracts: - contract.check_payment_due(time) - if not self.underwritten_contracts: - # If there are no contracts left, the bond is matured - self.mature_bond() # TODO: mature_bond method should check if operational + """mature contracts""" + if self.underwritten_contracts[0].expiration <= time: + self.underwritten_contracts[0].mature(time) + self.underwritten_contracts = [] + self.mature_bond() - # TODO: dividend should only be payed according to pre-arranged schedule, - # and only if no risk events have materialized so far else: if self.operational: self.pay_dividends(time) @@ -106,6 +91,7 @@ def set_contract(self, contract: "MetaInsuranceContract"): No return values Only one contract is ever added to the list of underwritten contracts as each CatBond is a contract itself.""" self.underwritten_contracts.append(contract) + self.expiration = contract.expiration def mature_bond(self): """Method to mature CatBond. @@ -121,7 +107,17 @@ def mature_bond(self): purpose="mature", ) self._pay(obligation) + self.obligations = [] self.simulation.delete_agents([self]) self.operational = False else: print("CatBond is not operational so cannot mature") + + def get_available_cash(self, time: int) -> float: + """Returns the amount of cash the CatBond has available to pay out in claims (i.e. not reserved for premiums). + Used to update limit on contract""" + return self.cash - self.per_period_dividend * (self.expiration - time + 1) + + def enter_illiquidity(self, time: int, sum_due: float): + + raise RuntimeError("CatBond has run out of money, that shouldn't happen") diff --git a/insurancefirms.py b/insurancefirms.py index df31d1b..b872b36 100644 --- a/insurancefirms.py +++ b/insurancefirms.py @@ -135,84 +135,46 @@ def increase_capacity(self, time: int, max_var: float) -> float: iteration unless not enough capacity to meet target.""" assert self.simulation_reinsurance_type == "non-proportional" """get prices""" - reinsurance_price = self.simulation.get_reinsurance_premium( - self.np_reinsurance_deductible_fraction - ) - cat_bond_price = self.simulation.get_cat_bond_price( - self.np_reinsurance_deductible_fraction - ) + capacity = None - if not reinsurance_price == cat_bond_price == float("inf"): - categ_ids = [ - categ_id for categ_id in range(self.simulation_no_risk_categories) - ] - if len(categ_ids) > 1: - np.random.shuffle(categ_ids) + if not (self.simulation.catbonds_off and self.simulation.reinsurance_off): + categ_ids = list(range(self.simulation_no_risk_categories)) + np.random.shuffle(categ_ids) while len(categ_ids) >= 1: categ_id = categ_ids.pop() capacity = self.get_capacity(max_var) - if ( - self.capacity_target < capacity - ): # just one per iteration, unless capital target is unmatched - if self.increase_capacity_by_category( - time, - categ_id, - reinsurance_price=reinsurance_price, - cat_bond_price=cat_bond_price, - force=False, - ): + if self.capacity_target < capacity: + # just one per iteration, unless capital target is unmatched + if self.increase_capacity_by_category(time, categ_id, force=False): categ_ids = [] else: - self.increase_capacity_by_category( - time, - categ_id, - reinsurance_price=reinsurance_price, - cat_bond_price=cat_bond_price, - force=True, - ) + self.increase_capacity_by_category(time, categ_id, force=True) # capacity is returned in order not to recompute more often than necessary if capacity is None: capacity = self.get_capacity(max_var) return capacity def increase_capacity_by_category( - self, - time: int, - categ_id: int, - reinsurance_price: float, - cat_bond_price: float, - force: bool = False, + self, time: int, categ_id: int, force: bool = False ) -> bool: """Method to increase capacity. Only called by increase_capacity. Accepts: time: Type Integer categ_id: Type integer. - reinsurance_price: Type Decimal. - cat_bond_price: Type Decimal. force: Type Boolean. Forces firm to get reinsurance/catbond or not. Returns Boolean to stop loop if firm has enough capacity. This method is given a category and prices of reinsurance/catbonds and will issue whichever one is cheaper to a firm for the given category. This is forced if firm does not have enough capacity to meet target otherwise will only issue if market premium is greater than firms average premium.""" if isleconfig.verbose: - print( - f"IF {self.id:d} increasing capacity in period {time:d}, cat bond price: {cat_bond_price:f}," - f" reinsurance premium {reinsurance_price:f}" - ) + print(f"IF {self.id:d} increasing capacity in period {time:d}.") if not force: actual_premium = self.get_average_premium(categ_id) possible_premium = self.simulation.get_market_premium() if actual_premium >= possible_premium: return False """on the basis of prices decide for obtaining reinsurance or for issuing cat bond""" - if reinsurance_price > cat_bond_price: - if isleconfig.verbose: - print(f"IF {self.id:d} issuing Cat bond in period {time:d}") - self.issue_cat_bond(time, categ_id) - else: - if isleconfig.verbose: - print(f"IF {self.id:d} getting reinsurance in period {time:d}") - self.ask_reinsurance_non_proportional_by_category(time, categ_id) + self.ask_reinsurance_non_proportional_by_category(time, categ_id) return True def get_average_premium(self, categ_id: int) -> float: @@ -289,17 +251,20 @@ def ask_reinsurance_non_proportional_by_category( self.np_reinsurance_deductible_fraction * total_value, tranches[0][1], ) - for tranche in tranches: - if (tranche[1] - tranche[0]) / total_value <= min( - 10 / total_value, + for tranche in tranches[:]: + # Use the slice so we aren't modifying while iterating + if (tranche[1] - tranche[0]) <= max( + 100, 0.1 * ( self.np_reinsurance_limit_fraction - self.np_reinsurance_deductible_fraction - ), + ) + * total_value, ): # Small gaps are acceptable to avoid having trivial contracts - we don't accept tranches with - # size less than ten or 10% of the total reinsurable ammount + # size less than 100 or 10% of the total reinsurable ammount + # TODO: the 10% limit should be removed if we have very many layers of reinsurance tranches.remove(tranche) if not tranches: @@ -307,7 +272,8 @@ def ask_reinsurance_non_proportional_by_category( return None while ( - len(tranches) + self.reinsurance_profile.all_contracts() < min_tranches + len(tranches) + len(self.reinsurance_profile.all_contracts()) + < min_tranches ): # Make sure that the overall number of tranches after obtaining the requested reinsurance would be at # least the minimal value. @@ -315,98 +281,75 @@ def ask_reinsurance_non_proportional_by_category( risks_to_return = [] for tranche in tranches: assert tranche[1] > tranche[0] - risk = genericclasses.RiskProperties( - value=total_value, - category=categ_id, - owner=self, - insurancetype="excess-of-loss", - number_risks=number_risks, - deductible_fraction=tranche[0] / total_value, - limit_fraction=tranche[1] / total_value, - periodized_total_premium=periodized_total_premium, - runtime=12, - expiration=time + 12, - risk_factor=avg_risk_factor, - ) # TODO: make runtime into a parameter - if purpose == "newrisk": - self.simulation.append_reinrisks(risk) - elif purpose == "rollover": + risk = self.reinsure_tranche( + categ_id, + tranche[0] / total_value, + tranche[1] / total_value, + time, + purpose, + ) + if purpose == "rollover": risks_to_return.append(risk) if purpose == "rollover": return risks_to_return - elif number_risks == 0 and purpose == "rollover": + elif purpose == "rollover": return None - def add_reinsurance(self, contract: ReinsuranceContract): - """Method called by reinsurancecontract to add the reinsurance contract to the firms counter for the given - category, normally used so only one reinsurance contract is issued per category at a time. - Accepts: - category: Type Integer. - contract: Type Class. Reinsurance contract issued to firm. - No return values.""" - value = self.underwritten_risk_characterisation[contract.category][0] - self.reinsurance_profile.add(contract, value) - - def delete_reinsurance(self, contract: ReinsuranceContract): - """Method called by reinsurancecontract to delete the reinsurance contract from the firms counter for the given - category, used so that another reinsurance contract can be issued for that category if needed. - Accepts: - category: Type Integer. - contract: Type Class. Reinsurance contract issued to firm. - No return values.""" - value = self.underwritten_risk_characterisation[contract.category][0] - self.reinsurance_profile.remove(contract, value) - - def issue_cat_bond( - self, time: int, categ_id: int, per_value_per_period_premium: int = 0 + def reinsure_tranche( + self, + category: int, + deductible_fraction: float, + limit_fraction: float, + time: int, + purpose: str, ): - """Method to issue cat bond to given firm for given category. - Accepts: - time: Type Integer. - categ_id: Type Integer. - per_value_per_period_premium: Type Integer. - No return values. - Method is only called by increase_capacity_by_category method when CatBond prices are lower than reinsurance. It - then creates the CatBond as a quasi-reinsurance contract that is paid for immediately (by simulation) with no - premium payments.""" [ total_value, avg_risk_factor, number_risks, - _, - ] = self.underwritten_risk_characterisation[categ_id] - if number_risks > 0: - # TODO: make runtime into a parameter - risk = genericclasses.RiskProperties( - value=total_value, - category=categ_id, - owner=self, - insurancetype="excess-of-loss", - number_risks=number_risks, - deductible_fraction=self.np_reinsurance_deductible_fraction, - limit_fraction=self.np_reinsurance_limit_fraction, - periodized_total_premium=0, - runtime=12, - expiration=time + 12, - risk_factor=avg_risk_factor, - ) - - _, _, var_this_risk, _ = self.riskmodel.evaluate([], self.cash, risk) - per_period_premium = per_value_per_period_premium * risk.value - total_premium = sum( - [ - per_period_premium * ((1 / (1 + self.interest_rate)) ** i) - for i in range(risk.runtime) - ] - ) # TODO: or is it range(1, risk["runtime"]+1)? - # catbond = CatBond(self.simulation, per_period_premium) - # TODO: shift obtain_yield method to insurancesimulation, thereby making it unnecessary to drag - # parameters like self.interest_rate from instance to instance and from class to class - new_catbond = catbond.CatBond( - self.simulation, per_period_premium, self.interest_rate + periodized_total_premium, + ] = self.underwritten_risk_characterisation[category] + risk = genericclasses.RiskProperties( + value=total_value, + category=category, + owner=self, + insurancetype="excess-of-loss", + number_risks=number_risks, + deductible_fraction=deductible_fraction, + limit_fraction=limit_fraction, + periodized_total_premium=periodized_total_premium, + runtime=12, + expiration=time + 12, + risk_factor=avg_risk_factor, + deductible=deductible_fraction * total_value, + limit=limit_fraction * total_value, + ) # TODO: make runtime into a parameter + reinsurance_type = self.decide_reinsurance_type(risk) + if reinsurance_type == "reinsurance": + if purpose == "newrisk": + self.simulation.append_reinrisks(risk) + return None + elif purpose == "rollover": + return risk + + elif reinsurance_type == "catbond": + # The whole premium is transfered to the bond at creation, not periodically + # TODO: Should the premium be periodic as for any other reinsurance? Would help, probably + risk.periodized_total_premium = 0 + total_premium = ( + self.get_catbond_price(risk) + * risk.value + * self.np_reinsurance_premium_share ) + if not self.cash >= total_premium: + # We can't actually afford to issue the catbond. Ideally this shouldn't be reached, but it is. + return None + per_period_premium = total_premium / risk.runtime + new_catbond = catbond.CatBond(self.simulation, per_period_premium, self) """add contract; contract is a quasi-reinsurance contract""" + # This automatically adds reinsurance to self.reinsurance_profile + # per_value_reinsurance_premium = 0 because the insurance firm make only one payment to catbond contract = ReinsuranceContract( new_catbond, risk, @@ -415,26 +358,107 @@ def issue_cat_bond( risk.runtime, self.default_contract_payment_period, expire_immediately=self.simulation_parameters["expire_immediately"], - initial_var=var_this_risk, insurancetype=risk.insurancetype, ) - # per_value_reinsurance_premium = 0 because the insurance firm make only one payment to catbond new_catbond.set_contract(contract) + """sell cat bond (to self.simulation)""" - self.simulation.receive_obligation(var_this_risk, self, time, "bond") + # amount changed from var_this_risk to total exposure + exposure = risk.value * (risk.limit_fraction - risk.deductible_fraction) + self.simulation.receive_obligation(exposure + 1, new_catbond, time, "bond") new_catbond.set_owner(self.simulation) - """hand cash over to cat bond such that var_this_risk is covered""" + + """hand cash over to cat bond to cover the premium payouts""" + # If we added this as an obligation in the normal way, there would be a risk that the firm would go under + # before paying, which would cause the catbond to never really have been created which is confusing + obligation = genericclasses.Obligation( - amount=var_this_risk + total_premium, + amount=total_premium, recipient=new_catbond, due_time=time, purpose="bond", ) - - self._pay(obligation) # TODO: is var_this_risk the correct amount? + self._pay(obligation) """register catbond""" self.simulation.add_agents(catbond.CatBond, "catbond", [new_catbond]) + else: + # print(f"\nIF {self.id} attempted to get reinsurance for {risk.limit-risk.deductible:.0f} xs" + # f" {risk.deductible:.0f} but it was too expensive") + return None + + def decide_reinsurance_type(self, risk: genericclasses.RiskProperties) -> str: + """Decides whether to get catbond or reinsurance for risk with given properties""" + # This should be the only place where VaR is evaluated. It should be moved out if we want to use it for + # pricing etc. + catbond_price = ( + self.get_catbond_price(risk) + * risk.value + * self.np_reinsurance_premium_share + ) + reinsurance_price = ( + self.get_reinsurance_price(risk) + * risk.value + * self.np_reinsurance_premium_share + ) + if catbond_price == reinsurance_price == float("inf"): + return "nope" + + _, _, var_this_risk, _ = self.riskmodel.evaluate([], self.cash, risk) + capacity_gain = var_this_risk * self.riskmodel.margin_of_safety + if catbond_price < reinsurance_price: + if capacity_gain < catbond_price: + # If we won't actually gain any capacity due to the loss in capital, don't do it! + # TODO: Does this make sense for reinsurance? + return "nope" + else: + return "catbond" + else: + if capacity_gain < reinsurance_price: + # TODO: This uses the total premium as the capacity loss due to premium expenditure - is this right? + return "nope" + else: + return "reinsurance" + + def get_catbond_price(self, risk: genericclasses.RiskProperties) -> float: + """Returns the total per-risk premium for a catbond """ + # TODO: take limit into account as well as deductible + assert risk.deductible_fraction is not None + return self.simulation.get_cat_bond_price( + risk.deductible_fraction, risk.limit_fraction + ) + + def get_reinsurance_price(self, risk: genericclasses.RiskProperties) -> float: + """Returns the total per-risk premium for reinsurance""" + # TODO: take limit into account as well as deductible + assert risk.deductible_fraction is not None + return self.simulation.get_reinsurance_premium( + risk.deductible_fraction, risk.limit_fraction + ) + + def add_reinsurance(self, contract: ReinsuranceContract, force_value: float = None): + """Add reinsurance to the reinsurance profile. Value is given as contract.value is set when contract is offered, + not when it is accepted. + Value can be forced if we are updating an old contract rather than issuing a new one. + Accepts: + category: Type Integer. + contract: Type Class. Reinsurance contract issued to firm. + No return values.""" + if force_value is not None: + value = force_value + else: + value = self.underwritten_risk_characterisation[contract.category][0] + self.reinsurance_profile.add(contract, value) + + def delete_reinsurance(self, contract: ReinsuranceContract): + """Method called by reinsurancecontract to delete the reinsurance contract from the firms counter for the given + category, used so that another reinsurance contract can be issued for that category if needed. + Accepts: + category: Type Integer. + contract: Type Class. Reinsurance contract issued to firm. + No return values.""" + value = self.underwritten_risk_characterisation[contract.category][0] + self.reinsurance_profile.remove(contract, value) def make_reinsurance_claims(self, time: int): """Method to make reinsurance claims. diff --git a/insurancesimulation.py b/insurancesimulation.py index 7139e00..1253873 100644 --- a/insurancesimulation.py +++ b/insurancesimulation.py @@ -100,6 +100,7 @@ def __init__( else: self.risk_factor_distribution = Constant(loc=1.0) # self.risk_value_distribution = scipy.stats.uniform(loc=100, scale=9900) + # TODO: Should this be a parameter self.risk_value_distribution = Constant(loc=1000) risk_factor_mean = self.risk_factor_distribution.mean() @@ -116,6 +117,9 @@ def __init__( self.simulation_parameters["mean_contract_runtime"] / self.cat_separation_distribution.mean() ) + + # norm_premium is the basic per-risk premium (for a contract of average length) charged to cover expected + # losses and underwriting costs self.norm_premium: float = ( expected_damage_frequency * self.damage_distribution.mean() @@ -228,6 +232,7 @@ def __init__( } self.insurer_id_counter: int = 0 self.reinsurer_id_counter: int = 0 + self.catbond_id_counter: int = 0 self.initialize_agent_parameters( "insurancefirm", simulation_parameters, risk_model_configurations @@ -510,14 +515,12 @@ def iterate(self, t: int): len(self.rc_event_schedule[categ_id]) > 0 and self.rc_event_schedule[categ_id][0] == t ): + # Schedules of catastrophes and damages must me generated at the same time. self.rc_event_schedule[categ_id] = self.rc_event_schedule[categ_id][1:] - damage_extent = copy.copy( - self.rc_event_damage[categ_id][0] - ) # Schedules of catastrophes and damages must me generated at the same time. + damage_extent = copy.copy(self.rc_event_damage[categ_id][0]) self._inflict_peril(categ_id=categ_id, damage=damage_extent, t=t) - self.rc_event_damage[categ_id] = self.rc_event_damage[categ_id][1:] - # TODO: Ideally don't want to be taking from the beginning of lists, consider having soonest events at - # the end of the list. Probably fine though, only happens once per iteration + del self.rc_event_damage[categ_id][0] + else: if isleconfig.verbose: print("Next peril ", self.rc_event_schedule[categ_id]) @@ -526,9 +529,7 @@ def iterate(self, t: int): if isleconfig.aid_relief: self.bank.adjust_aid_budget(time=t) if "damage_extent" in locals(): - op_firms = [ - firm for firm in self.insurancefirms if firm.operational is True - ] + op_firms = [firm for firm in self.insurancefirms if firm.operational] aid_dict = self.bank.provide_aid(op_firms, damage_extent, time=t) for key in aid_dict.keys(): self.receive_obligation( @@ -545,8 +546,10 @@ def iterate(self, t: int): for reinagent in self.reinsurancefirms: if reinagent.operational: reinagent.iterate(t) + if reinagent.cash < 0: print(f"Reinsurer {reinagent.id} has negative cash") + if isleconfig.buy_bankruptcies: for reinagent in self.reinsurancefirms: if reinagent.operational: @@ -562,8 +565,10 @@ def iterate(self, t: int): for agent in self.insurancefirms: if agent.operational: agent.iterate(t) + if agent.cash < 0: print(f"Insurer {agent.id} has negative cash") + if isleconfig.buy_bankruptcies: for agent in self.insurancefirms: if agent.operational: @@ -573,7 +578,9 @@ def iterate(self, t: int): self.reset_selling_firms() # Iterate catbonds - for agent in self.catbonds: + for agent in self.catbonds.copy(): + # If we don't take the copy we have a *very* bad time with some *very* frustrating debugging, as catbonds + # can delete themselves from self.catbonds, which causes some catbonds to not be iterated(!) agent.iterate(t) self.insurance_models_counter = np.zeros( @@ -603,6 +610,22 @@ def iterate(self, t: int): self.RN.save_network_data() print("Network data has been saved to data/network_data.dat") + # import matplotlib.pyplot as plt + # + # f1 = self.get_reinsurance_premium + # f2 = self.get_cat_bond_price + # x = np.linspace(0, 1, 50) + # y1 = [f1(x_n) for x_n in x] + # y2 = [f2(x_n) for x_n in x] + # plt.plot(x, y1, label="Reinsurance") + # plt.plot(x, y2, label="CatBond") + # plt.legend() + # plt.title( + # "self.reinsurance_market_premium = " + # + str(self.reinsurance_market_premium) + # ) + # plt.show() + def save_data(self): """Method to collect statistics about the current state of the simulation. Will pass these to the Logger object (self.logger) to be recorded. @@ -815,6 +838,7 @@ def _adjust_market_premium(self, capital: float): linearly with the capital available in the insurance market and viceversa. The premium reduces until it reaches a minimum below which no insurer is willing to reduce further the price. This method is only called in the self.iterate() method of this class.""" + # QUERY: Why is initial_agent_cash used here? self.market_premium = self.norm_premium * ( self.simulation_parameters["upper_price_limit"] - self.simulation_parameters["premium_sensitivity"] @@ -872,23 +896,24 @@ def get_market_reinpremium(self) -> float: return self.reinsurance_market_premium def get_reinsurance_premium( - self, np_reinsurance_deductible_fraction: float + self, deductible_fraction: float, limit_fraction: float = 1 ) -> float: """Method to determine reinsurance premium based on deductible fraction Accepts: np_reinsurance_deductible_fraction: Type Integer Returns reinsurance premium (Type: Integer)""" - # TODO: cut this out of the insurance market premium -> OBSOLETE?? # TODO: make max_reduction into simulation_parameter ? if self.reinsurance_off: return float("inf") else: max_reduction = 0.1 return self.reinsurance_market_premium * ( - 1.0 - max_reduction * np_reinsurance_deductible_fraction + 1.0 - max_reduction * (1 - limit_fraction + deductible_fraction) ) - def get_cat_bond_price(self, np_reinsurance_deductible_fraction: float) -> float: + def get_cat_bond_price( + self, deductible_fraction: float, excess_fraction: float = 1 + ) -> float: """Method to calculate and return catbond price. If catbonds are not desired will return infinity so no catbonds will be issued. Otherwise calculates based on reinsurance market premium, catbond premium, deductible fraction. @@ -903,10 +928,11 @@ def get_cat_bond_price(self, np_reinsurance_deductible_fraction: float) -> float max_reduction = 0.9 max_cat_bond_surcharge = 0.5 # QUERY: again, what does max_reduction represent? + # TODO: How should this relate to deductible and excess? return self.reinsurance_market_premium * ( 1.0 + max_cat_bond_surcharge - - max_reduction * np_reinsurance_deductible_fraction + - max_reduction * (deductible_fraction + 1 - excess_fraction) ) def append_reinrisks(self, reinrisk: RiskProperties): @@ -1123,6 +1149,11 @@ def get_unique_reinsurer_id(self) -> int: self.reinsurer_id_counter += 1 return current_id + def get_unique_catbond_id(self) -> int: + current_id = self.catbond_id_counter + self.catbond_id_counter += 1 + return current_id + def insurance_entry_index(self) -> int: """Method that returns the entry index for insurance firms, i.e. the index for the initial agent parameters that is taken from the list of already created parameters. diff --git a/isleconfig.py b/isleconfig.py index eff0a6b..d9571bf 100644 --- a/isleconfig.py +++ b/isleconfig.py @@ -49,7 +49,7 @@ "default_non-proportional_reinsurance_excess": 1.0, "default_non-proportional_reinsurance_premium_share": 0.3, "static_non-proportional_reinsurance_levels": False, - "catbonds_off": True, + "catbonds_off": False, "reinsurance_off": False, "capacity_target_decrement_threshold": 1.8, "capacity_target_increment_threshold": 1.2, diff --git a/metainsuranceorg.py b/metainsuranceorg.py index 14bfb81..6581026 100644 --- a/metainsuranceorg.py +++ b/metainsuranceorg.py @@ -387,9 +387,7 @@ def collect_process_evaluate_risks( # Here the new risks are organized by category. risks_per_categ = self.risks_reinrisks_organizer(new_risks) - if risks_per_categ != [ - [] for _ in range(self.simulation_no_risk_categories) - ]: + if risks_per_categ != [[]] * self.simulation_no_risk_categories: for repetition in range(self.recursion_limit): # TODO: find an efficient way to stop the recursion if there are no more risks to accept or if it is # not accepting any more over several iterations. Done, maybe? @@ -483,6 +481,8 @@ def dissolve(self, time: int, record: str): self.excess_capital and self.profits_losses.""" for contract in self.underwritten_contracts: contract.dissolve(time) + for contract in self.reinsurance_profile.all_contracts(): + contract.dissolve(time) # removing (dissolving) all risks immediately after bankruptcy (may not be realistic, # they might instead be bought by another company) # TODO: implement buyouts @@ -834,17 +834,22 @@ def process_newrisks_reinsurer( underwritten_risks, self.cash, risk ) if accept: - # TODO: rename this to per_value_premium in insurancecontract.py to avoid confusion - per_value_reinsurance_premium = ( - self.np_reinsurance_premium_share - * risk.periodized_total_premium - * risk.runtime - * ( - self.simulation.get_market_reinpremium() - / self.simulation.get_market_premium() - ) - / risk.value + # TODO: What exactly is this based on? How should reinsurance pricing work? + # per_value_reinsurance_premium = ( + # self.np_reinsurance_premium_share + # * risk.periodized_total_premium + # * risk.runtime + # * ( + # self.simulation.get_market_reinpremium() + # / self.simulation.get_market_premium() + # ) + # / risk.value + # ) + + new_per_value_rein_premium = ( + self.get_reinsurance_price(risk) * self.np_reinsurance_premium_share ) + per_value_reinsurance_premium = new_per_value_rein_premium # Here it is check whether the portfolio is balanced or not if the reinrisk # (risk_to_insure) is underwritten. Return True if it is balanced. False otherwise. @@ -1037,6 +1042,12 @@ def market_permanency(self, time: int): # Here we determine how much is too long. self.market_exit(time) + def get_reinsurance_price(self, risk: RiskProperties) -> float: + """Returns the total per-value premium for reinsurance""" + raise NotImplementedError("No.") + + # TODO: Error message maybe + def register_claim(self, claim: float): """Method to register claims. Accepts: diff --git a/reinsurancecontract.py b/reinsurancecontract.py index 7b90263..b508434 100644 --- a/reinsurancecontract.py +++ b/reinsurancecontract.py @@ -2,11 +2,13 @@ from typing import Optional from typing import TYPE_CHECKING +from math import floor if TYPE_CHECKING: from insurancefirms import InsuranceFirm from metainsuranceorg import MetaInsuranceOrg from genericclasses import RiskProperties + from catbond import Catbond class ReinsuranceContract(metainsurancecontract.MetaInsuranceContract): @@ -67,9 +69,12 @@ def explode( No return value. Method marks the contract for termination. """ + # Just a type hint since for a generic insurance contract property_holder can be the simulation + self.property_holder: "InsuranceFirm" + assert uniform_value is None if damage_extent is None: - raise ValueError("Damage extend should be given") + raise ValueError("Damage extent should be given") if damage_extent > self.deductible: # Proportional reinsurance is triggered by the individual reinsured contracts at the time of explosion. # Since EoL reinsurance isn't triggered until the insurer manually makes a claim, this would mean that @@ -97,6 +102,29 @@ def explode( self.expiration = time # self.terminating = True + elif type(self.insurer).__name__ == "CatBond": + # Catbonds can only pay out a certain amount in their lifetime, so we update the reinsurance coverage + # for the issuer + # TODO: Allow for catbonds that can pay out multiple times? + self.insurer: "Catbond" + remaining_cb_cash = self.insurer.get_available_cash(time) - claim + assert remaining_cb_cash >= 0 + if remaining_cb_cash < 2: + # If the claim uses up all the catbond's remaining money, the contract ends + self.expiration = time + elif remaining_cb_cash < self.limit - self.deductible: + # If the claim uses up enough money that the catbond can't pay out the full exposure, update the + # contract to reflect that + self.property_holder.delete_reinsurance(contract=self) + self.limit = self.deductible + remaining_cb_cash + self.limit = floor(self.limit) + self.limit_fraction = self.limit / self.value + self.property_holder.add_reinsurance( + contract=self, force_value=self.value + ) + else: + # If the catbond still has enough money to pay out the full exposure, no need to change anything. + pass def mature(self, time: int): """Mature method. @@ -105,10 +133,12 @@ def mature(self, time: int): No return value. Removes any reinsurance functions this contract has and terminates any reinsurance contracts for this contract.""" - # self.terminating = True + # Just a type hint since for a generic insurance contract property_holder can be the simulation + self.property_holder: "InsuranceFirm" + self.terminate_reinsurance(time) if self.insurancetype == "excess-of-loss": self.property_holder.delete_reinsurance(contract=self) - else: # TODO: ? Instead: if self.insurancetype == "proportional": + else: self.contract.unreinsure() diff --git a/riskmodel.py b/riskmodel.py index 9b2a4e7..49209b6 100644 --- a/riskmodel.py +++ b/riskmodel.py @@ -272,7 +272,6 @@ def evaluate_excess_of_loss( # compute liquidity requirements from existing contracts for risk in categ_risks: - # QUERY: Expected in this context means damage at var_tail_prob rather than expectation? var_damage = ( percentage_value_at_risk * risk.value @@ -292,9 +291,10 @@ def evaluate_excess_of_loss( * offered_risk.risk_factor * self.inaccuracy[categ_id] ) - var_claim_fraction = ( + var_claim_fraction = max( min(var_damage_fraction, offered_risk.limit_fraction) - - offered_risk.deductible_fraction + - offered_risk.deductible_fraction, + 0, ) var_claim_total = var_claim_fraction * offered_risk.value From ae8e0ea88fdc6823780f7dd5b08927d8eae5070b Mon Sep 17 00:00:00 2001 From: KloskaT Date: Wed, 7 Aug 2019 16:09:08 +0100 Subject: [PATCH 089/125] Fixed bug causing most firms to exit market (firms with warnings dont ask for risks). Fixed other issues causing data to no be saved as it should. --- insurancesimulation.py | 137 ++++++++++------------------- logger.py | 34 ++------ metainsuranceorg.py | 175 ++++++++++++++----------------------- setup_simulation.py | 2 +- visualization_network.py | 182 ++++++++++----------------------------- 5 files changed, 168 insertions(+), 362 deletions(-) diff --git a/insurancesimulation.py b/insurancesimulation.py index 2fc938f..842bbd9 100644 --- a/insurancesimulation.py +++ b/insurancesimulation.py @@ -478,9 +478,9 @@ def iterate(self, t: int): # identify perils and effect claims for categ_id in range(len(self.rc_event_schedule)): - if (self.rc_event_schedule[categ_id] and self.rc_event_schedule[categ_id][0] < t): + if self.rc_event_schedule[categ_id] and self.rc_event_schedule[categ_id][0] < t: warnings.warn("Something wrong; past events not deleted", RuntimeWarning) - if (len(self.rc_event_schedule[categ_id]) > 0 and self.rc_event_schedule[categ_id][0] == t): + if len(self.rc_event_schedule[categ_id]) > 0 and self.rc_event_schedule[categ_id][0] == t: self.rc_event_schedule[categ_id] = self.rc_event_schedule[categ_id][1:] damage_extent = copy.copy(self.rc_event_damage[categ_id][0]) # Schedules of catastrophes and damages must me generated at the same time. self._inflict_peril(categ_id=categ_id, damage=damage_extent, t=t) @@ -543,7 +543,7 @@ def iterate(self, t: int): self._update_model_counters() - network_division = 1 # How often network is updated. + """network_division = 1 # How often network is updated. if (isleconfig.show_network or isleconfig.save_network) and t % network_division == 0 and t > 0: if t == network_division: # Only creates once instance so only one figure. self.RN = visualization_network.ReinsuranceNetwork(self.rc_event_schedule_initial) @@ -554,7 +554,7 @@ def iterate(self, t: int): self.RN.visualize() if isleconfig.save_network and t == (self.simulation_parameters["max_time"] - 800): self.RN.save_network_data() - print("Network data has been saved to data/network_data.dat") + print("Network data has been saved to data/network_data.dat")""" def save_data(self): """Method to collect statistics about the current state of the simulation. Will pass these to the @@ -609,6 +609,13 @@ def save_data(self): current_log["individual_contracts"] = [len(firm.underwritten_contracts) for firm in self.insurancefirms] current_log["reinsurance_contracts"] = [len(firm.underwritten_contracts) for firm in self.reinsurancefirms] + if isleconfig.save_network: + adj_list, node_labels, edge_labels, num_entities = self.update_network_data() + current_log["unweighted_network_data"] = adj_list + current_log["network_node_labels"] = node_labels + current_log["network_edge_labels"] = edge_labels + current_log["number_of_agents"] = num_entities + """ call to Logger object """ self.logger.record_data(current_log) @@ -831,17 +838,11 @@ def get_cat_bond_price(self, np_reinsurance_deductible_fraction: float) -> float return self.reinsurance_market_premium * ( 1.0 + max_cat_bond_surcharge - - max_reduction * np_reinsurance_deductible_fraction - ) + - max_reduction * np_reinsurance_deductible_fraction) def append_reinrisks(self, reinrisk: RiskProperties): """Method for appending reinrisks to simulation instance. Called from insurancefirm Accepts: item (Type: List)""" - - # For debugging: - # for old_reinrisk in self.reinrisks: - # if reinrisk.owner is old_reinrisk.owner and reinrisk.category == old_reinrisk.category: - # pass if reinrisk: self.reinrisks.append(reinrisk) @@ -882,10 +883,8 @@ def solicit_reinsurance_requests(self, reinsurer: "MetaInsuranceOrg") -> Sequenc reinsurer: Type firm metainsuranceorg instance Returns: reinrisks_to_be_sent: Type List""" - reinrisks_to_be_sent = self.reinrisks[ - : int(self.reinsurers_weights[reinsurer.id]) - ] - self.reinrisks = self.reinrisks[int(self.reinsurers_weights[reinsurer.id]) :] + reinrisks_to_be_sent = self.reinrisks[:int(self.reinsurers_weights[reinsurer.id])] + self.reinrisks = self.reinrisks[int(self.reinsurers_weights[reinsurer.id]):] for reinrisk in reinsurer.reinrisks_kept: reinrisks_to_be_sent.append(reinrisk) @@ -920,9 +919,7 @@ def _get_all_riskmodel_combinations(self, rm_factor: float) -> Sequence[Sequence riskmodels: Type list""" riskmodels = [] for i in range(self.simulation_parameters["no_categories"]): - riskmodel_combination = rm_factor * np.ones( - self.simulation_parameters["no_categories"] - ) + riskmodel_combination = rm_factor * np.ones(self.simulation_parameters["no_categories"]) riskmodel_combination[i] = 1 / rm_factor riskmodels.append(riskmodel_combination) return riskmodels @@ -938,17 +935,11 @@ def firm_enters_market(self, prob: float = -1, agent_type: str = "InsuranceFirm" # TODO: Do firms really enter the market randomly, with at most one in each timestep? if prob == -1: if agent_type == "InsuranceFirm": - prob = self.simulation_parameters[ - "insurance_firm_market_entry_probability" - ] + prob = self.simulation_parameters["insurance_firm_market_entry_probability"] elif agent_type == "ReinsuranceFirm": - prob = self.simulation_parameters[ - "reinsurance_firm_market_entry_probability" - ] + prob = self.simulation_parameters["reinsurance_firm_market_entry_probability"] else: - raise ValueError( - f"Unknown agent type. Simulation requested to create agent of type {agent_type}" - ) + raise ValueError(f"Unknown agent type. Simulation requested to create agent of type {agent_type}") return np.random.random() < prob def record_bankruptcy(self): @@ -969,14 +960,25 @@ def record_market_exit(self): self.cumulative_market_exits += 1 def record_nonregulation_firm(self): + """Method to record non-regulation firm exits.. + Accepts no arguments. + No return value. + This method is used to record the firms that leave the market due to the regulator. It is + only called from the method dissolve() from the class metainsuranceorg.py after the dissolution of the + firm, and only if the regulator is working.""" self.cumulative_nonregulation_firms += 1 def record_bought_firm(self): + """Method to record a firm bought. + Accepts no arguments. + No return value. + This method is used to record the number of firms that have been bought. Only called from buyout() in + metainsuranceorg.py after all obligations and contracts have been transferred to buyer.""" self.cumulative_bought_firms += 1 def record_unrecovered_claims(self, loss: float): """Method for recording unrecovered claims. If firm runs out of money it cannot _pay more claims and so that - money is lost and recorded using this method. + money is lost and recorded using this method. Called at start of dissolve to catch all instances necessary. Accepts: loss: Type integer, value of lost claim No return value""" @@ -1000,33 +1002,15 @@ def log(self): def compute_market_diffvar(self) -> float: """Method for calculating difference between number of all firms and the total value at risk. Used only in save data when adding to the logger data dict.""" - totalina = sum( - [ - firm.var_counter_per_risk - for firm in self.insurancefirms - if firm.operational - ] - ) - + totalina = sum([firm.var_counter_per_risk for firm in self.insurancefirms if firm.operational]) totalreal = len([firm for firm in self.insurancefirms if firm.operational]) # Real VaR is 1 for each firm, we think - totalina += sum( - [ - reinfirm.var_counter_per_risk - for reinfirm in self.reinsurancefirms - if reinfirm.operational - ] - ) - - totalreal += len( - [reinfirm for reinfirm in self.reinsurancefirms if reinfirm.operational] - ) + totalina += sum([reinfirm.var_counter_per_risk for reinfirm in self.reinsurancefirms if reinfirm.operational]) + totalreal += len([reinfirm for reinfirm in self.reinsurancefirms if reinfirm.operational]) totaldiff = totalina - totalreal - return totaldiff - # self.history_logs['market_diffvar'].append(totaldiff) def get_unique_insurer_id(self) -> int: """Method for getting unique id for insurer. Used in initialising agents in start.py and insurancesimulation. @@ -1051,18 +1035,14 @@ def insurance_entry_index(self) -> int: that is taken from the list of already created parameters. Returns: Indices of the type of riskmodel that the least firms are using.""" - return self.insurance_models_counter[ - 0 : self.simulation_parameters["no_riskmodels"] - ].argmin() + return self.insurance_models_counter[0: self.simulation_parameters["no_riskmodels"]].argmin() def reinsurance_entry_index(self) -> int: """Method that returns the entry index for reinsurance firms, i.e. the index for the initial agent parameters that is taken from the list of already created parameters. Returns: Indices of the type of riskmodel that the least reinsurance firms are using.""" - return self.reinsurance_models_counter[ - 0 : self.simulation_parameters["no_riskmodels"] - ].argmin() + return self.reinsurance_models_counter[0:self.simulation_parameters["no_riskmodels"]].argmin() def get_operational(self) -> bool: """Override get_operational to always return True, as the market will never die""" @@ -1081,18 +1061,11 @@ def reinsurance_capital_entry(self) -> float: if len(capital_per_non_re_cat) > 0: # We only perform this action if there are reinsurance contracts that have # not been reinsured in the last time period. - capital_per_non_re_cat = np.random.choice( - capital_per_non_re_cat, 10 - ) # Only 10 values sampled randomly are considered. (Too low?) - entry = max( - capital_per_non_re_cat - ) # For market entry the maximum of the sample is considered. - entry = ( - 2 * entry - ) # The capital market entry of those values will be the double of the maximum. + capital_per_non_re_cat = np.random.choice(capital_per_non_re_cat, 10) # Only 10 values sampled randomly are considered. (Too low?) + entry = max(capital_per_non_re_cat) # For market entry the maximum of the sample is considered. + entry = (2 * entry) # The capital market entry of those values will be the double of the maximum. else: # Otherwise the default reinsurance cash market entry is considered. entry = self.simulation_parameters["initial_reinagent_cash"] - return entry # The capital market entry is returned. def reset_pls(self): @@ -1111,7 +1084,6 @@ def reset_pls(self): def get_risk_share(self, firm: "MetaInsuranceOrg") -> float: """Method to determine the total percentage of risks in the market that are held by a particular firm. - For insurers uses insurance risks, for reinsurers uses reinsurance risks Calculates the Accepts: @@ -1121,13 +1093,8 @@ def get_risk_share(self, firm: "MetaInsuranceOrg") -> float: if firm.is_insurer: total = self.simulation_parameters["no_risks"] elif firm.is_reinsurer: - total = sum( - [ - reinfirm.number_underwritten_contracts() - for reinfirm in self.reinsurancefirms - ] - + [len(self.reinrisks)] - ) + total = sum([reinfirm.number_underwritten_contracts() for reinfirm in self.reinsurancefirms] + + [len(self.reinrisks)]) else: raise ValueError("Firm is neither insurer or reinsurer, which is odd") if total == 0: @@ -1171,15 +1138,9 @@ def get_firms_to_sell(self, type): Returns: firms_info_sent: Type List of Lists. Contains firm, type and reason.""" if type == "insurer": - firms_info_sent = [ - (firm, time, reason) - for firm, time, reason in self.selling_insurance_firms - ] + firms_info_sent = [(firm, time, reason) for firm, time, reason in self.selling_insurance_firms] elif type == "reinsurer": - firms_info_sent = [ - (firm, time, reason) - for firm, time, reason in self.selling_reinsurance_firms - ] + firms_info_sent = [(firm, time, reason) for firm, time, reason in self.selling_reinsurance_firms] else: print("No accepted type for selling") return firms_info_sent @@ -1225,11 +1186,7 @@ def update_network_data(self): """obtain lists of operational entities""" op_entities = {} num_entities = {} - for firmtype, firmlist in [ - ("insurers", self.insurancefirms), - ("reinsurers", self.reinsurancefirms), - ("catbonds", self.catbonds), - ]: + for firmtype, firmlist in [("insurers", self.insurancefirms), ("reinsurers", self.reinsurancefirms), ("catbonds", self.catbonds)]: op_firmtype = [firm for firm in firmlist if firm.operational] op_entities[firmtype] = op_firmtype num_entities[firmtype] = len(op_firmtype) @@ -1240,16 +1197,12 @@ def update_network_data(self): weights_matrix = np.zeros(network_size ** 2).reshape(network_size, network_size) edge_labels = {} node_labels = {} - for idx_to, firm in enumerate( - op_entities["insurers"] + op_entities["reinsurers"] - ): + for idx_to, firm in enumerate(op_entities["insurers"] + op_entities["reinsurers"]): node_labels[idx_to] = firm.id eolrs = firm.get_excess_of_loss_reinsurance() for eolr in eolrs: try: - idx_from = num_entities["insurers"] + ( - op_entities["reinsurers"] + op_entities["catbonds"] - ).index(eolr["reinsurer"]) + idx_from = num_entities["insurers"] + (op_entities["reinsurers"] + op_entities["catbonds"]).index(eolr["reinsurer"]) weights_matrix[idx_from][idx_to] = eolr["value"] edge_labels[idx_to, idx_from] = eolr["category"] except ValueError: diff --git a/logger.py b/logger.py index cf3e384..bc3e221 100644 --- a/logger.py +++ b/logger.py @@ -18,12 +18,7 @@ class Logger: - def __init__( - self, - no_riskmodels=None, - rc_event_schedule_initial=None, - rc_event_damage_initial=None, - ): + def __init__(self, no_riskmodels=None, rc_event_schedule_initial=None, rc_event_damage_initial=None): """Constructor. Prepares history_logs atribute as dict for the logs. Records initial event schedule of simulation run. Arguments @@ -103,14 +98,10 @@ def record_data(self, data_dict): self.history_logs[key].append(data_dict[key]) if key == "individual_contracts": for i in range(len(data_dict["individual_contracts"])): - self.history_logs["individual_contracts"][i].append( - data_dict["individual_contracts"][i] - ) + self.history_logs["individual_contracts"][i].append(data_dict["individual_contracts"][i]) if key == "reinsurance_contracts": for i in range(len(data_dict["reinsurance_contracts"])): - self.history_logs["reinsurance_contracts"][i].append( - data_dict["reinsurance_contracts"][i] - ) + self.history_logs["reinsurance_contracts"][i].append(data_dict["reinsurance_contracts"][i]) def obtain_log(self, requested_logs=None): if requested_logs is None: @@ -150,12 +141,7 @@ def restore_logger_object(self, log): self.network_data["network_node_labels"] = log["network_node_labels"] self.network_data["network_edge_labels"] = log["network_edge_labels"] self.network_data["number_of_agents"] = log["number_of_agents"] - del ( - log["number_of_agents"], - log["network_edge_labels"], - log["network_node_labels"], - log["unweighted_network_data"], - ) + del (log["number_of_agents"], log["network_edge_labels"], log["network_node_labels"], log["unweighted_network_data"]) """Extract environment variables (number of risk models and risk event schedule)""" self.rc_event_schedule_initial = log["rc_event_schedule_initial"] @@ -163,7 +149,7 @@ def restore_logger_object(self, log): self.number_riskmodels = log["number_riskmodels"] """Restore history log""" - self.history_logs.update(log) + self.history_logs_to_save.append(log) def save_log(self, background_run): """Method to save log to disk of local machine. Distinguishes single and ensemble runs. @@ -205,12 +191,10 @@ def single_log_prepare(self): Element 3: operation parameter (w-write or a-append).""" to_log = [] filename = "data/single_history_logs.dat" - backupfilename = ( - "data/single_history_logs_old_" + time.strftime("%Y_%b_%d_%H_%M") + ".dat" - ) + backupfilename = ("data/single_history_logs_old_" + time.strftime("%Y_%b_%d_%H_%M") + ".dat") if os.path.exists(filename): os.rename(filename, backupfilename) - for data in self.history_logs: + for data in self.history_logs_to_save: to_log.append((filename, data, "a")) return to_log @@ -223,9 +207,7 @@ def save_network_data(self, ensemble): filename_prefix = {1: "one", 2: "two", 3: "three", 4: "four"} fpf = filename_prefix[self.number_riskmodels] network_logs = [] - network_logs.append( - ("data/" + fpf + "_network_data.dat", self.network_data, "a") - ) + network_logs.append(("data/" + fpf + "_network_data.dat", self.network_data, "a")) for filename, data, operation_character in network_logs: with open(filename, operation_character) as wfile: diff --git a/metainsuranceorg.py b/metainsuranceorg.py index 0dca814..2198d85 100644 --- a/metainsuranceorg.py +++ b/metainsuranceorg.py @@ -166,8 +166,7 @@ def __init__(self, simulation_parameters: Mapping, agent_parameters: AgentProper self.var_counter_per_risk = 0 # average risk model inaccuracy across contracts self.var_sum = 0 # sum over initial VaR for all contracts self.var_sum_last_periods = list(np.zeros((12, 4), dtype=int)) - # self.reinsurance_history = list(np.zeros((12, 4, 2), dtype=int)) - self.reinsurance_history = [[],[],[],[],[],[],[],[],[],[],[],[]] + self.reinsurance_history = [[], [], [], [], [], [], [], [], [], [], [], []] self.counter_category = np.zeros(self.simulation_no_risk_categories) # var_counter disaggregated by category self.var_category = np.zeros(self.simulation_no_risk_categories) # var_sum disaggregated by category self.naccep = [] @@ -241,62 +240,52 @@ def collect_process_evaluate_risks(self, time: int, contracts_dissolved: int): for categ in range(len(self.counter_category)): value = self.underwritten_risk_characterisation[categ][0] self.reinsurance_profile.update_value(value, categ) - """request risks to be considered for underwriting in the next period and collect those for this period""" - new_nonproportional_risks, new_risks = self.get_newrisks_by_type() - contracts_offered = len(new_risks) - if isleconfig.verbose and contracts_offered < 2 * contracts_dissolved: - print( - f"Something wrong; agent {self.id} receives too few new contracts {contracts_offered} " - f"<= {2 * contracts_dissolved}" - ) - - """deal with non-proportional risks first as they must evaluate each request separatly, - then with proportional ones""" - - # Here the new reinrisks are organized by category. - reinrisks_per_categ = self.risks_reinrisks_organizer(new_nonproportional_risks) - - assert self.recursion_limit > 0 - for repetition in range(self.recursion_limit): - # TODO: find an efficient way to stop the loop if there are no more risks to accept or if it is - # not accepting any more over several iterations. - # Here we process all the new reinrisks in order to keep the portfolio as balanced as possible. - has_accepted_risks, not_accepted_reinrisks = self.process_newrisks_reinsurer( - reinrisks_per_categ, time - ) - # The loop only runs once in my tests, what needs tweaking to have firms not accept risks? - reinrisks_per_categ = not_accepted_reinrisks - if not has_accepted_risks: - # Stop condition implemented. Might solve the previous TODO. - break - self.simulation.return_reinrisks(list(chain.from_iterable(not_accepted_reinrisks))) - - # TODO: This takes up a lot of processing time. Can we update the list instead of rebuilding it? - underwritten_risks = [ - RiskProperties( - owner=self, - value=contract.value, - category=contract.category, - risk_factor=contract.risk_factor, - deductible=contract.deductible, - limit=contract.limit, - insurancetype=contract.insurancetype, - runtime=contract.runtime, - ) - for contract in self.underwritten_contracts - if contract.reinsurance_share != 1.0 - ] + # Only get risks if firm not issued warning (breaks otherwise) + if not self.warning: + """request risks to be considered for underwriting in the next period and collect those for this period""" + new_nonproportional_risks, new_risks = self.get_newrisks_by_type() + contracts_offered = len(new_risks) + if isleconfig.verbose and contracts_offered < 2 * contracts_dissolved: + print(f"Something wrong; agent {self.id} receives too few new contracts {contracts_offered} " + f"<= {2 * contracts_dissolved}") + + """deal with non-proportional risks first as they must evaluate each request separately, + then with proportional ones""" + + # Here the new reinrisks are organized by category. + reinrisks_per_categ = self.risks_reinrisks_organizer(new_nonproportional_risks) + + assert self.recursion_limit > 0 + for repetition in range(self.recursion_limit): + # TODO: find an efficient way to stop the loop if there are no more risks to accept or if it is + # not accepting any more over several iterations. + # Here we process all the new reinrisks in order to keep the portfolio as balanced as possible. + has_accepted_risks, not_accepted_reinrisks = self.process_newrisks_reinsurer( + reinrisks_per_categ, time) + + # The loop only runs once in my tests, what needs tweaking to have firms not accept risks? + reinrisks_per_categ = not_accepted_reinrisks + if not has_accepted_risks: + # Stop condition implemented. Might solve the previous TODO. + break + self.simulation.return_reinrisks(list(chain.from_iterable(not_accepted_reinrisks))) + + underwritten_risks = [RiskProperties( + owner=self, + value=contract.value, + category=contract.category, + risk_factor=contract.risk_factor, + deductible=contract.deductible, + limit=contract.limit, + insurancetype=contract.insurancetype, + runtime=contract.runtime) + for contract in self.underwritten_contracts + if contract.reinsurance_share != 1.0] """obtain risk model evaluation (VaR) for underwriting decisions and for capacity specific decisions""" # TODO: Enable reinsurance shares other than 0.0 and 1.0 - [ - _, - acceptable_by_category, - cash_left_by_categ, - var_per_risk_per_categ, - self.excess_capital, - ] = self.riskmodel.evaluate(underwritten_risks, self.cash) + [_, acceptable_by_category, cash_left_by_categ, var_per_risk_per_categ, self.excess_capital] = self.riskmodel.evaluate(underwritten_risks, self.cash) # TODO: resolve insurance reinsurance inconsistency (insurer underwrite after capacity decisions, # reinsurers before). @@ -317,6 +306,7 @@ def collect_process_evaluate_risks(self, time: int, contracts_dissolved: int): self.adjust_dividends(time, actual_capacity) self.pay_dividends(time) + # Firms only decide to underwrite if not issued a warning if not self.warning: """make underwriting decisions, category-wise""" growth_limit = max(50, 2 * len(self.underwritten_contracts) + contracts_dissolved) @@ -329,23 +319,17 @@ def collect_process_evaluate_risks(self, time: int, contracts_dissolved: int): risks_per_categ = self.risks_reinrisks_organizer(new_risks) if risks_per_categ != [[] for _ in range(self.simulation_no_risk_categories)]: for repetition in range(self.recursion_limit): - # TODO: find an efficient way to stop the recursion if there are no more risks to accept or if it is - # not accepting any more over several iterations. Done, maybe? # Here we process all the new risks in order to keep the portfolio as balanced as possible. has_accepted_risks, not_accepted_risks = self.process_newrisks_insurer( risks_per_categ, acceptable_by_category, var_per_risk_per_categ, cash_left_by_categ, - time, - ) + time) risks_per_categ = not_accepted_risks - # has_accepted_risks = False # Debug if not has_accepted_risks: - # Stop condition implemented. Might solve the previous TODO. break self.simulation.return_risks(list(chain.from_iterable(not_accepted_risks))) - # print(self.id, " now has ", len(self.underwritten_contracts), " & returns ", len(not_accepted_risks)) self.update_risk_characterisations() def enter_illiquidity(self, time: int): @@ -393,6 +377,7 @@ def market_exit(self, time): sum_due = sum([item.amount for item in due]) if sum_due > self.cash: self.enter_bankruptcy(time) + print("Dissolved due to market exit") for obligation in due: self._pay(obligation) self.obligations = [] @@ -416,21 +401,22 @@ def dissolve(self, time, record): Different class variables are reset during the process: self.risks_kept, self.reinrisks_kept, self.excess_capital and self.profits_losses.""" # Record all unpaid claims (needs to be here to account for firms lost due to regulator/being sold) - sum_due = sum(item.amount for item in self.obligations if item.purpose == 'claim') - self.simulation.record_unrecovered_claims(sum_due - self.cash) + if record != "record_market_exit": # Market exits already pay all obligations + sum_due = sum(item.amount for item in self.obligations) # Also records dividends/premiums + self.simulation.record_unrecovered_claims(sum_due - self.cash) # Removing (dissolving) all risks immediately after bankruptcy (may not be realistic, they might instead be bought by another company) [contract.dissolve(time) for contract in self.underwritten_contracts] self.simulation.return_risks(self.risks_kept) self.risks_kept = [] self.reinrisks_kept = [] + obligation = Obligation(amount=self.cash, recipient=self.simulation, due_time=time, purpose="Dissolution") - # This MUST be the last obligation before the dissolution of the firm. - self._pay(obligation) - # Excess of capital is 0 after bankruptcy or market exit. - self.excess_capital = 0 - # Profits and losses are 0 after bankruptcy or market exit. - self.profits_losses = 0 + self._pay(obligation) # This MUST be the last obligation before the dissolution of the firm. + + self.excess_capital = 0 # Excess of capital is 0 after bankruptcy or market exit. + self.profits_losses = 0 # Profits and losses are 0 after bankruptcy or market exit. + method_to_call = getattr(self.simulation, record) method_to_call() for reincontract in self.reinsurance_profile.all_contracts(): @@ -712,29 +698,17 @@ def process_newrisks_reinsurer(self, reinrisks_per_categ: Sequence[Sequence[Risk if contract.insurancetype == "excess-of-loss" ] accept, cash_left_by_categ, var_this_risk, self.excess_capital = self.riskmodel.evaluate( - underwritten_risks, self.cash, risk - ) + underwritten_risks, self.cash, risk) # TODO: change riskmodel.evaluate() to accept new risk to be evaluated and # to account for existing non-proportional risks correctly -> DONE. if accept: # TODO: rename this to per_value_premium in insurancecontract.py to avoid confusion per_value_reinsurance_premium = ( - self.np_reinsurance_premium_share - * risk.periodized_total_premium - * risk.runtime - * ( - self.simulation.get_market_reinpremium() - / self.simulation.get_market_premium() - ) - / risk.value - ) - + self.np_reinsurance_premium_share * risk.periodized_total_premium * risk.runtime + * (self.simulation.get_market_reinpremium() / self.simulation.get_market_premium()) / risk.value) # Here it is check whether the portfolio is balanced or not if the reinrisk # (risk_to_insure) is underwritten. Return True if it is balanced. False otherwise. - condition, cash_left_by_categ = self.balanced_portfolio( - risk, cash_left_by_categ, None - ) - + condition, cash_left_by_categ = self.balanced_portfolio(risk, cash_left_by_categ, None) if condition: contract = reinsurancecontract.ReinsuranceContract(self, risk, time, per_value_reinsurance_premium, risk.runtime, self.default_contract_payment_period, @@ -778,9 +752,7 @@ def process_newrisks_insurer(self, risks_per_categ: Sequence[Sequence[RiskProper if risk.contract and risk.contract.expiration > time: # In this case the risk being inspected already has a contract, so we are deciding whether to # give reinsurance for it # QUERY: is this correct? - [condition, cash_left_by_categ] = self.balanced_portfolio( - risk, cash_left_by_categ, None - ) + [condition, cash_left_by_categ] = self.balanced_portfolio(risk, cash_left_by_categ, None) # Here we check whether the portfolio is balanced or not if the reinrisk (risk_to_insure) is # underwritten. Return True if it is balanced. False otherwise. if condition: @@ -791,10 +763,7 @@ def process_newrisks_insurer(self, risks_per_categ: Sequence[Sequence[RiskProper self.insurance_premium(), risk.expiration - time, self.default_contract_payment_period, - expire_immediately=self.simulation_parameters[ - "expire_immediately" - ], - ) + expire_immediately=self.simulation_parameters["expire_immediately"]) self.underwritten_contracts.append(contract) has_accepted_risks = True self.cash_left_by_categ = cash_left_by_categ @@ -803,26 +772,16 @@ def process_newrisks_insurer(self, risks_per_categ: Sequence[Sequence[RiskProper not_accepted_risks[risk.category].append(risk) else: - [condition, cash_left_by_categ] = self.balanced_portfolio( - risk, cash_left_by_categ, var_per_risk_per_categ - ) + [condition, cash_left_by_categ] = self.balanced_portfolio(risk, cash_left_by_categ, var_per_risk_per_categ) # In this case there is no contact currently associated with the risk, so we decide whether # to insure it # Here it is check whether the portfolio is balanced or not if the risk (risk_to_insure) is # underwritten. Return True if it is balanced. False otherwise. if condition: - contract = insurancecontract.InsuranceContract( - self, - risk, - time, - self.simulation.get_market_premium(), - random_runtime, - self.default_contract_payment_period, - expire_immediately=self.simulation_parameters[ - "expire_immediately" - ], - initial_var=var_per_risk_per_categ[risk.category], - ) + contract = insurancecontract.InsuranceContract(self, risk, time, self.simulation.get_market_premium(), + random_runtime, self.default_contract_payment_period, + expire_immediately=self.simulation_parameters["expire_immediately"], + initial_var=var_per_risk_per_categ[risk.category]) self.underwritten_contracts.append(contract) has_accepted_risks = True self.cash_left_by_categ = cash_left_by_categ @@ -963,7 +922,7 @@ def adjust_riskmodel_inaccuracy(self): by the share of risk this firm holds. """ if isleconfig.simulation_parameters["scale_inaccuracy"] != 1: - self.riskmodel.inaccuracy = (self.max_inaccuracy * (1 - self.risk_share)+ self.min_inaccuracy * self.risk_share) + self.riskmodel.inaccuracy = (self.max_inaccuracy * (1 - self.risk_share) + self.min_inaccuracy * self.risk_share) def consider_buyout(self, type="insurer"): """Method to allow firm to decide if to buy one of the firms going bankrupt. diff --git a/setup_simulation.py b/setup_simulation.py index 36017bb..1e05079 100644 --- a/setup_simulation.py +++ b/setup_simulation.py @@ -73,7 +73,7 @@ def schedule(self, replications: int) -> Tuple[MutableSequence[MutableSequence[i total += int(math.ceil(separation_time)) if total < self.max_time: event_schedule.append(total) - event_damage.append(self.damage_distribution.rvs()) + event_damage.append(self.damage_distribution.rvs()[0]) rc_event_schedule.append(event_schedule) rc_event_damage.append(event_damage) diff --git a/visualization_network.py b/visualization_network.py index f7a1a65..6d91a77 100644 --- a/visualization_network.py +++ b/visualization_network.py @@ -12,16 +12,13 @@ def __init__(self, event_schedule=None): No accepted values. This created the figure that the network will be displayed on so only called once, and only if show_network is True.""" - self.figure = plt.figure( - num=None, figsize=(10, 8), dpi=100, facecolor="w", edgecolor="k" - ) + self.figure = plt.figure(num=None, figsize=(10, 8), dpi=100, facecolor="w", edgecolor="k") self.save_data = { "unweighted_network": [], "weighted_network": [], "network_edgelabels": [], "network_node_labels": [], - "number_of_agents": [], - } + "number_of_agents": []} self.event_schedule = event_schedule def compute_measures(self): @@ -70,11 +67,7 @@ def update(self, insurancefirms, reinsurancefirms, catbonds): """obtain lists of operational entities""" op_entities = {} self.num_entities = {} - for firmtype, firmlist in [ - ("insurers", self.insurancefirms), - ("reinsurers", self.reinsurancefirms), - ("catbonds", self.catbonds), - ]: + for firmtype, firmlist in [("insurers", self.insurancefirms), ("reinsurers", self.reinsurancefirms), ("catbonds", self.catbonds)]: op_firmtype = [firm for firm in firmlist if firm.operational] op_entities[firmtype] = op_firmtype self.num_entities[firmtype] = len(op_firmtype) @@ -82,21 +75,15 @@ def update(self, insurancefirms, reinsurancefirms, catbonds): self.network_size = sum(self.num_entities.values()) """Create weighted adjacency matrix and category edge labels""" - weights_matrix = np.zeros(self.network_size ** 2).reshape( - self.network_size, self.network_size - ) + weights_matrix = np.zeros(self.network_size ** 2).reshape(self.network_size, self.network_size) self.edge_labels = {} self.node_labels = {} - for idx_to, firm in enumerate( - op_entities["insurers"] + op_entities["reinsurers"] - ): + for idx_to, firm in enumerate(op_entities["insurers"] + op_entities["reinsurers"]): self.node_labels[idx_to] = firm.id eolrs = firm.get_excess_of_loss_reinsurance() for eolr in eolrs: try: - idx_from = self.num_entities["insurers"] + ( - op_entities["reinsurers"] + op_entities["catbonds"] - ).index(eolr["reinsurer"]) + idx_from = self.num_entities["insurers"] + (op_entities["reinsurers"] + op_entities["catbonds"]).index(eolr["reinsurer"]) weights_matrix[idx_from][idx_to] = eolr["value"] self.edge_labels[idx_to, idx_from] = eolr["category"] except ValueError: @@ -121,21 +108,10 @@ def visualize(self): program.""" plt.ion() # Turns on interactive graph mode. firmtypes = np.ones(self.network_size) - firmtypes[ - self.num_entities["insurers"] : self.num_entities["insurers"] - + self.num_entities["reinsurers"] - ] = 0.5 - firmtypes[ - self.num_entities["insurers"] + self.num_entities["reinsurers"] : - ] = 1.3 - print( - "Number of insurers: %i, Number of Reinsurers: %i, CatBonds: %i" - % ( - self.num_entities["insurers"], - self.num_entities["reinsurers"], - self.num_entities["catbonds"], - ) - ) + firmtypes[self.num_entities["insurers"] : self.num_entities["insurers"]+ self.num_entities["reinsurers"]] = 0.5 + firmtypes[self.num_entities["insurers"] + self.num_entities["reinsurers"] :] = 1.3 + print("Number of insurers: %i, Number of Reinsurers: %i, CatBonds: %i" % (self.num_entities["insurers"], + self.num_entities["reinsurers"], self.num_entities["catbonds"])) # Either this or below create a network, this one has id's but no key. # pos = nx.spring_layout(self.network_unweighted) @@ -151,47 +127,27 @@ def visualize(self): node_color="b", node_size=50, alpha=0.9, - label="Insurer", - ) + label="Insurer",) nx.draw_networkx_nodes( self.network_unweighted, pos, - list( - range( - self.num_entities["insurers"], - self.num_entities["insurers"] + self.num_entities["reinsurers"], - ) - ), + list(range(self.num_entities["insurers"],self.num_entities["insurers"] + self.num_entities["reinsurers"])), node_color="r", node_size=50, alpha=0.9, - label="Reinsurer", - ) + label="Reinsurer") nx.draw_networkx_nodes( self.network_unweighted, pos, - list( - range( - self.num_entities["insurers"] + self.num_entities["reinsurers"], - self.num_entities["insurers"] - + self.num_entities["reinsurers"] - + self.num_entities["catbonds"], - ) - ), + list(range(self.num_entities["insurers"] + self.num_entities["reinsurers"], self.num_entities["insurers"] + + self.num_entities["reinsurers"] + self.num_entities["catbonds"])), node_color="g", node_size=50, alpha=0.9, - label="CatBond", - ) - nx.draw_networkx_edges( - self.network_unweighted, pos, width=1.0, alpha=0.5, node_size=50 - ) - nx.draw_networkx_edge_labels( - self.network_unweighted, pos, self.edge_labels, font_size=5 - ) - nx.draw_networkx_labels( - self.network_unweighted, pos, self.node_labels, font_size=20 - ) + label="CatBond") + nx.draw_networkx_edges(self.network_unweighted, pos, width=1.0, alpha=0.5, node_size=50) + nx.draw_networkx_edge_labels(self.network_unweighted, pos, self.edge_labels, font_size=5) + nx.draw_networkx_labels(self.network_unweighted, pos, self.node_labels, font_size=20) plt.legend(scatterpoints=1, loc="upper right") plt.axis("off") plt.show() @@ -209,9 +165,7 @@ def __init__(self, network_data, num_iter): num_iter: Type Integer. Used to tell animation how many frames it should have. No return values. This class is given the loaded network data and then uses it to create an animated network.""" - self.figure = plt.figure( - num=None, figsize=(10, 8), dpi=100, facecolor="w", edgecolor="k" - ) + self.figure = plt.figure(num=None, figsize=(10, 8), dpi=100, facecolor="w", edgecolor="k") self.unweighted_network_data = network_data[0]["unweighted_network_data"] self.network_edge_labels = network_data[0]["network_edge_labels"] self.network_node_labels = network_data[0]["network_node_labels"] @@ -232,12 +186,8 @@ def update(self, i): This method is called from matplotlib.animate.FuncAnimation to update the plot to the next time iteration.""" self.figure.clear() plt.suptitle("Network Timestep %i" % i) - unweighted_nx_network = nx.from_numpy_array( - np.array(self.unweighted_network_data[i]) - ) - pos = nx.kamada_kawai_layout( - unweighted_nx_network - ) # Can also use circular/shell/spring + unweighted_nx_network = nx.from_numpy_array(np.array(self.unweighted_network_data[i])) + pos = nx.kamada_kawai_layout(unweighted_nx_network) # Can also use circular/shell/spring nx.draw_networkx_nodes( unweighted_nx_network, @@ -246,61 +196,37 @@ def update(self, i): node_color="b", node_size=50, alpha=0.9, - label="Insurer", - ) + label="Insurer",) nx.draw_networkx_nodes( unweighted_nx_network, pos, - list( - range( - self.number_agent_type[i]["insurers"], - self.number_agent_type[i]["insurers"] - + self.number_agent_type[i]["reinsurers"], - ) - ), + list(range(self.number_agent_type[i]["insurers"],self.number_agent_type[i]["insurers"] + + self.number_agent_type[i]["reinsurers"])), node_color="r", node_size=50, alpha=0.9, - label="Reinsurer", - ) + label="Reinsurer") nx.draw_networkx_nodes( unweighted_nx_network, pos, - list( - range( - self.number_agent_type[i]["insurers"] - + self.number_agent_type[i]["reinsurers"], - self.number_agent_type[i]["insurers"] - + self.number_agent_type[i]["reinsurers"] - + self.number_agent_type[i]["catbonds"], - ) - ), + list(range(self.number_agent_type[i]["insurers"] + self.number_agent_type[i]["reinsurers"], + self.number_agent_type[i]["insurers"] + self.number_agent_type[i]["reinsurers"] + + self.number_agent_type[i]["catbonds"])), node_color="g", node_size=50, alpha=0.9, - label="CatBond", - ) - nx.draw_networkx_edges( - unweighted_nx_network, pos, width=1.0, alpha=0.5, node_size=50 - ) - - nx.draw_networkx_edge_labels( - self.unweighted_network_data[i], - pos, - self.network_edge_labels[i], - font_size=3, - ) - nx.draw_networkx_labels( - self.unweighted_network_data[i], - pos, - self.network_node_labels[i], - font_size=7, - ) + label="CatBond",) + nx.draw_networkx_edges(unweighted_nx_network, pos, width=1.0, alpha=0.5, node_size=50) - while self.all_events[0] == i: - plt.title("EVENT!") - self.all_events = self.all_events[1:] + nx.draw_networkx_edge_labels(self.unweighted_network_data[i], pos, self.network_edge_labels[i],font_size=3) + nx.draw_networkx_labels(self.unweighted_network_data[i], pos, self.network_node_labels[i], font_size=7,) + if len(self.all_events) > 0: + while self.all_events[0] == i: + plt.title("EVENT!") + self.all_events = self.all_events[1:] + if len(self.all_events) == 0: + break plt.legend(loc="upper right") plt.axis("off") @@ -308,14 +234,8 @@ def animate(self): """Method to create animation. No accepted values. No return values.""" - self.network_ani = animation.FuncAnimation( - self.figure, - self.update, - frames=self.num_iter, - repeat=False, - interval=50, - save_count=self.num_iter, - ) + self.network_ani = animation.FuncAnimation(self.figure, self.update, frames=self.num_iter, repeat=False, + interval=50, save_count=self.num_iter) def save_network_animation(self): """Method to save animation as MP4. @@ -323,29 +243,21 @@ def save_network_animation(self): No return values.""" if not os.path.isdir("figures"): os.makedirs("figures") - self.network_ani.save( - "figures/animated_network.mp4", writer="ffmpeg", dpi=200, fps=5 - ) + self.network_ani.save("figures/animated_network.mp4", writer="ffmpeg", dpi=200, fps=5) if __name__ == "__main__": # Use argparse to handle command line arguments - parser = argparse.ArgumentParser( - description="Plot the network of the insurance sector" - ) - parser.add_argument( - "--save", action="store_true", help="Save the network as an mp4" - ) - parser.add_argument( - "--number_iterations", type=int, help="number of frames for animation" - ) + parser = argparse.ArgumentParser(description="Plot the network of the insurance sector") + parser.add_argument("--save", action="store_true", help="Save the network as an mp4") + parser.add_argument("--number_iterations", type=int, help="number of frames for animation") args = parser.parse_args() args.save = True - # args.number_iterations = 199 + if args.number_iterations: num_iter = args.number_iterations else: - num_iter = 100 + num_iter = 999 # Access stored network data with open("data/network_data.dat", "r") as rfile: From 666144b799e544017f4a72e009f21e9cbe99d44c Mon Sep 17 00:00:00 2001 From: Chris Hughes Date: Thu, 8 Aug 2019 10:06:32 +0100 Subject: [PATCH 090/125] Fix bugs --- genericclasses.py | 8 ++++---- insurancesimulation.py | 20 ++++++++++++++------ isleconfig.py | 4 ++-- logger.py | 16 ++++++++-------- metainsuranceorg.py | 32 ++++++++++++++++++++------------ visualisation.py | 22 +++++++++++++--------- 6 files changed, 61 insertions(+), 41 deletions(-) diff --git a/genericclasses.py b/genericclasses.py index 28818f8..631d173 100644 --- a/genericclasses.py +++ b/genericclasses.py @@ -7,7 +7,7 @@ import isleconfig -from typing import Mapping, MutableSequence, Union, Tuple +from typing import Mapping, MutableSequence, Union, Tuple, List from typing import TYPE_CHECKING if TYPE_CHECKING: @@ -283,10 +283,10 @@ def contracts_to_explode( break return contracts - def all_contracts(self) -> MutableSequence["ReinsuranceContract"]: + def all_contracts(self) -> List["ReinsuranceContract"]: regions = chain.from_iterable(self.reinsured_regions) - contracts = map(lambda x: x[2], regions) - return list(contracts) + contracts = list(map(lambda x: x[2], regions)) + return contracts def update_value(self, value: float, category: int) -> None: self.riskmodel.set_reinsurance_coverage( diff --git a/insurancesimulation.py b/insurancesimulation.py index 1253873..23f5c58 100644 --- a/insurancesimulation.py +++ b/insurancesimulation.py @@ -152,11 +152,11 @@ def __init__( if ( rc_event_schedule is not None and rc_event_damage is not None ): # If we have schedules pass as arguments we used them. - self.rc_event_schedule = copy.copy(rc_event_schedule) - self.rc_event_schedule_initial = copy.copy(rc_event_schedule) + self.rc_event_schedule = copy.deepcopy(rc_event_schedule) + self.rc_event_schedule_initial = copy.deepcopy(rc_event_schedule) - self.rc_event_damage = copy.copy(rc_event_damage) - self.rc_event_damage_initial = copy.copy(rc_event_damage) + self.rc_event_damage = copy.deepcopy(rc_event_damage) + self.rc_event_damage_initial = copy.deepcopy(rc_event_damage) else: # Otherwise the schedules and damages are generated. raise Exception("No event schedules and damages supplied") @@ -946,9 +946,15 @@ def append_reinrisks(self, reinrisk: RiskProperties): if reinrisk: self.reinrisks.append(reinrisk) - def remove_reinrisks(self, risko: RiskProperties): + def remove_reinrisks( + self, risko: RiskProperties = None, firm: "MetaInsuranceOrg" = None + ): + """Either removes a single reinrisk or all reinrisks requested by a given firm (probably because it has gone + under)""" if risko is not None: self.reinrisks.remove(risko) + elif firm is not None: + self.reinrisks = [risk for risk in self.reinrisks if risk.owner is not firm] def get_reinrisks(self) -> Sequence[RiskProperties]: """Method for shuffling reinsurance risks @@ -987,13 +993,15 @@ def solicit_reinsurance_requests( reinsurer: Type firm metainsuranceorg instance Returns: reinrisks_to_be_sent: Type List""" + reinrisks_to_be_sent = self.reinrisks[ : int(self.reinsurers_weights[reinsurer.id]) ] self.reinrisks = self.reinrisks[int(self.reinsurers_weights[reinsurer.id]) :] for reinrisk in reinsurer.reinrisks_kept: - reinrisks_to_be_sent.append(reinrisk) + if reinrisk.owner.operational: + reinrisks_to_be_sent.append(reinrisk) reinsurer.reinrisks_kept = [] diff --git a/isleconfig.py b/isleconfig.py index d9571bf..60563e0 100644 --- a/isleconfig.py +++ b/isleconfig.py @@ -32,7 +32,7 @@ "mean_contract_runtime": 12, "contract_runtime_halfspread": 2, "default_contract_payment_period": 3, - "max_time": 300, + "max_time": 1000, "money_supply": 2000000000, "event_time_mean_separation": 100 / 3.0, "expire_immediately": False, @@ -113,7 +113,7 @@ # can be perfectly accurate, a value of 1 disables changes in inaccuracy based on size "scale_inaccuracy": 0.3, # The smallest number of tranches that an insurer will issue when asking for reinsurance. Note: even if this is 1, - # insurers will still end up with layered reinsurance to fill gaps + # insurers may still end up with layered reinsurance to fill gaps "min_tranches": 1, "aid_budget": 1000000, } diff --git a/logger.py b/logger.py index cf3e384..aefab60 100644 --- a/logger.py +++ b/logger.py @@ -98,15 +98,16 @@ def record_data(self, data_dict): Arguments data_dict: Type dict. Data with the same keys as are used in self.history_log(). Returns None.""" + a = 1 for key in data_dict.keys(): - if key != "individual_contracts" and key != "reinsurance_contracts": + if key not in ["individual_contracts", "reinsurance_contracts"]: self.history_logs[key].append(data_dict[key]) - if key == "individual_contracts": + elif key == "individual_contracts": for i in range(len(data_dict["individual_contracts"])): self.history_logs["individual_contracts"][i].append( data_dict["individual_contracts"][i] ) - if key == "reinsurance_contracts": + elif key == "reinsurance_contracts": for i in range(len(data_dict["reinsurance_contracts"])): self.history_logs["reinsurance_contracts"][i].append( data_dict["reinsurance_contracts"][i] @@ -126,8 +127,6 @@ def obtain_log(self, requested_logs=None): self.history_logs["rc_event_schedule_initial"] = self.rc_event_schedule_initial """Parse logs to be returned""" - if requested_logs is None: - requested_logs = LOG_DEFAULT log = {name: self.history_logs[name] for name in requested_logs} """Convert to list and return""" @@ -163,7 +162,8 @@ def restore_logger_object(self, log): self.number_riskmodels = log["number_riskmodels"] """Restore history log""" - self.history_logs.update(log) + self.history_logs_to_save.append(log) + self.history_logs = log def save_log(self, background_run): """Method to save log to disk of local machine. Distinguishes single and ensemble runs. @@ -210,8 +210,8 @@ def single_log_prepare(self): ) if os.path.exists(filename): os.rename(filename, backupfilename) - for data in self.history_logs: - to_log.append((filename, data, "a")) + for history_log in self.history_logs_to_save: + to_log.append((filename, history_log, "w")) return to_log def save_network_data(self, ensemble): diff --git a/metainsuranceorg.py b/metainsuranceorg.py index 6581026..32a8b46 100644 --- a/metainsuranceorg.py +++ b/metainsuranceorg.py @@ -260,7 +260,7 @@ def iterate(self, time: int): if self.operational: # Firms submit cash and var data for regulation every 12 iterations - if time % 12 == 0 and isleconfig.enforce_regulations is True: + if time % 12 == 0 and isleconfig.enforce_regulations: self.submit_regulator_report(time) if ( self.operational is False @@ -460,10 +460,12 @@ def market_exit(self, time: int): sum_due = sum([item.amount for item in due]) if sum_due > self.cash: self.enter_bankruptcy(time) - for obligation in due: - self._pay(obligation) - self.obligations = [] - self.dissolve(time, "record_market_exit") + else: + for obligation in due: + assert self.cash > obligation.amount + self._pay(obligation) + self.obligations = [] + self.dissolve(time, "record_market_exit") def dissolve(self, time: int, record: str): """Dissolve Method. @@ -481,11 +483,12 @@ def dissolve(self, time: int, record: str): self.excess_capital and self.profits_losses.""" for contract in self.underwritten_contracts: contract.dissolve(time) - for contract in self.reinsurance_profile.all_contracts(): + for contract in self.reinsurance_profile.all_contracts().copy(): + # Underwriter will remove next timestep contract.dissolve(time) - # removing (dissolving) all risks immediately after bankruptcy (may not be realistic, - # they might instead be bought by another company) - # TODO: implement buyouts + # Mature all underwritten contracts + self.mature_contracts(time) + self.simulation.return_risks(self.risks_retained) self.risks_retained = [] self.reinrisks_kept = [] @@ -501,12 +504,16 @@ def dissolve(self, time: int, record: str): self.excess_capital = 0 # Profits and losses are 0 after bankruptcy or market exit. self.profits_losses = 0 + # Retract any active requests for reinsurance + self.simulation.remove_reinrisks(firm=self) if self.operational: # TODO: This seems... odd? method_to_call = getattr(self.simulation, record) method_to_call() - for reincontract in self.reinsurance_profile.all_contracts(): - reincontract.dissolve(time) + else: + pass + # Set to None so trying to add more obligations throws an error + self.obligations = None self.operational = False def pay_dividends(self, time: int): @@ -815,6 +822,7 @@ def process_newrisks_reinsurer( # Here we take only one risk per category at a time to achieve risk[C1], risk[C2], risk[C3], # risk[C4], risk[C1], risk[C2], ... if possible. assert risk + assert risk.owner.operational # TODO: Move this out of the loop (maybe somewhere else entirely) and update it when needed underwritten_risks = [ RiskProperties( @@ -1111,7 +1119,7 @@ def roll_over(self, time: int): next(uniform_rvs) < self.simulation_parameters["reinsurance_retention"] ): - if reinrisk is not None: + if reinrisk is not None and reinrisk.owner.operational: self.reinrisks_kept.append(reinrisk) def make_reinsurance_claims(self, time: int): diff --git a/visualisation.py b/visualisation.py index ace9055..c47b6d2 100644 --- a/visualisation.py +++ b/visualisation.py @@ -28,6 +28,7 @@ def __init__( fig=None, percentiles=None, alpha=0.7, + length=None, ): """Intialisation method for creating timeseries. Accepts: @@ -54,6 +55,7 @@ def __init__( ] # assume all data series are the same size self.events_schedule = event_schedule self.damage_schedule = damage_schedule + self.length = length if axlst is not None and fig is not None: self.axlst = axlst self.fig = fig @@ -69,13 +71,14 @@ def plot(self): This method is called to plot a timeseries for five subplots of data for insurers/reinsurers. If called for a single run event times are plotted as vertical lines, if an ensemble run then no events but the average data is plotted with percentiles as deviations to the average.""" - single_categ_colours = ["b", "b", "b", "b"] + single_categ_colours = ["y", "r", "g", "b"] for i, (series, series_label, fill_lower, fill_upper) in enumerate( self.series_list ): self.axlst[i].plot(self.timesteps, series, color=self.colour) self.axlst[i].set_ylabel(series_label) - + if self.length is not None: + self.axlst[i].set_xlim([0, self.length]) if fill_lower is not None and fill_upper is not None: self.axlst[i].fill_between( self.timesteps, @@ -85,15 +88,13 @@ def plot(self): alpha=self.alpha, ) - if ( - self.events_schedule is not None - ): # Plots vertical lines for events if set. + if self.events_schedule is not None: + # Plots vertical lines for events if set. for categ in range(len(self.events_schedule)): for event_time in self.events_schedule[categ]: index = self.events_schedule[categ].index(event_time) - if ( - self.damage_schedule[categ][index] > 0.5 - ): # Only plots line if event is significant + if self.damage_schedule[categ][index] > 0.5: + # Only plots line if event is significant self.axlst[i].axvline( event_time, color=single_categ_colours[categ], @@ -316,6 +317,8 @@ def insurer_time_series( cash = np.median(cash_agg, axis=0) premium = np.median(premium_agg, axis=0) + ts_length = len(contracts) + self.ins_time_series = TimeSeries( [ ( @@ -356,6 +359,7 @@ def insurer_time_series( axlst=axlst, fig=fig, colour=colour, + length=ts_length, ) fig, axlst = self.ins_time_series.plot() return fig, axlst @@ -1343,7 +1347,7 @@ def plot(self, outputfile): from numpy import array # load in data from the history_logs dictionary with open("data/history_logs.dat","r") as rfile: - with open("data/history_logs.dat", "r") as rfile: + with open("data/single_history_logs.dat", "r") as rfile: history_logs_list = [eval(k) for k in rfile] # one dict on each line # first create visualisation object, then create graph/animation objects as necessary From 37392cc19ecb027afebf48115738ba6caf241925 Mon Sep 17 00:00:00 2001 From: Chris Hughes Date: Thu, 8 Aug 2019 16:51:48 +0100 Subject: [PATCH 091/125] Optimisations - updates risk characterisation instead of re-calculating, - added set-like class for unhashable elements --- catbond.py | 6 +- genericclasses.py | 62 +++++++++- insurancefirms.py | 46 +++++--- insurancesimulation.py | 26 +++-- isleconfig.py | 2 +- metainsurancecontract.py | 4 + metainsuranceorg.py | 247 ++++++++++++++++++++++++++++----------- riskmodel.py | 4 +- 8 files changed, 293 insertions(+), 104 deletions(-) diff --git a/catbond.py b/catbond.py index 6deb5be..2b59975 100644 --- a/catbond.py +++ b/catbond.py @@ -2,7 +2,7 @@ from metainsuranceorg import MetaInsuranceOrg from genericclasses import Obligation, GenericAgent -from typing import MutableSequence +from typing import Collection from typing import TYPE_CHECKING if TYPE_CHECKING: @@ -32,10 +32,10 @@ def __init__( This initialised the catbond class instance, inheriting methods from MetaInsuranceOrg.""" self.simulation = simulation self.id: int = self.simulation.get_unique_catbond_id() - self.underwritten_contracts: MutableSequence["MetaInsuranceContract"] = [] + self.underwritten_contracts: Collection["MetaInsuranceContract"] = [] self.cash: float = 0 self.profits_losses: float = 0 - self.obligations: MutableSequence[Obligation] = [] + self.obligations: Collection[Obligation] = [] self.operational: bool = True self.owner: GenericAgent = owner self.per_period_dividend: float = per_period_premium diff --git a/genericclasses.py b/genericclasses.py index 631d173..0713edd 100644 --- a/genericclasses.py +++ b/genericclasses.py @@ -7,7 +7,7 @@ import isleconfig -from typing import Mapping, MutableSequence, Union, Tuple, List +from typing import Mapping, MutableSequence, Union, Tuple, List, Collection, TypeVar from typing import TYPE_CHECKING if TYPE_CHECKING: @@ -25,7 +25,7 @@ class GenericAgent: def __init__(self): self.cash: float = 0 - self.obligations: MutableSequence["Obligation"] = [] + self.obligations: Collection["Obligation"] = [] self.operational: bool = True self.profits_losses: float = 0 self.creditor = None @@ -172,6 +172,21 @@ class Obligation: purpose: str +@dataclasses.dataclass +class RiskChar: + """Class for holding characterisation of held risks""" + + total_value: float + avg_risk_factor: float + number_risks: int + periodized_total_premium: float + weighted_premium: float + total_var: float + + def __iter__(self): + return iter(dataclasses.astuple(self)[:-1]) + + class ConstantGen(stats.rv_continuous): def _pdf(self, x: float, *args) -> float: a = np.float_(x == 0) @@ -274,7 +289,7 @@ def uncovered(self, category: int) -> MutableSequence[Tuple[float, float]]: def contracts_to_explode( self, category: int, damage: float - ) -> MutableSequence["ReinsuranceContract"]: + ) -> Collection["ReinsuranceContract"]: contracts = [] for region in self.reinsured_regions[category]: if region[0] < damage: @@ -311,3 +326,44 @@ def split_longest( l.insert(max_width_index, (mid, upper)) l.insert(max_width_index, (lower, mid)) return l + + +T = TypeVar("T") + + +class IdSet(Collection[T]): + """ + A generic collection of objects that distinguishes objects by (i.e. a is b) rather than equality (a == b). + Thanks to that distinction, does not require contents to be hashable - basically a set for non-hashable objects + that ignores equality. + """ + + def __init__(self, seq: Collection[T] = None): + self._dict = {} + if seq is not None: + for item in seq: + self.add(item) + + def __hash__(self): + return None + + def __len__(self) -> int: + return len(self._dict) + + def __iter__(self) -> T: + yield from self._dict.values() + + def __contains__(self, item: T) -> bool: + return id(item) in self._dict + + def add(self, item: T) -> None: + if item not in self: + self._dict[id(item)] = item + else: + raise ValueError("Adding item that is already in container") + + def remove(self, item: T) -> None: + if item in self: + del self._dict[id(item)] + else: + raise ValueError("Item not found in container") diff --git a/insurancefirms.py b/insurancefirms.py index b872b36..8d253f5 100644 --- a/insurancefirms.py +++ b/insurancefirms.py @@ -5,7 +5,7 @@ from reinsurancecontract import ReinsuranceContract import isleconfig import genericclasses -from typing import Optional, MutableSequence, Mapping +from typing import Optional, Collection, Mapping from typing import TYPE_CHECKING, List @@ -51,8 +51,9 @@ def get_reinsurance_var_estimate(self, max_var: float) -> float: reinsurance_VaR_estimate: Type Decimal. This method takes the max VaR and mulitiplies it by a factor that estimates the VaR if another reinsurance contract was to be taken. Called by the adjust_target_capacity and get_capacity methods.""" + # TODO: Should be total_value, or maybe the total amount of exposure (rather than number_risks) values = [ - self.underwritten_risk_characterisation[categ][2] + self.underwritten_risk_characterisation[categ].number_risks for categ in range(self.simulation_parameters["no_categories"]) ] reinsurance_factor_estimate = self.get_reinsurable_fraction(values) @@ -183,13 +184,20 @@ def get_average_premium(self, categ_id: int) -> float: categ_id: Type Integer. Returns: premium payments left/total value of contracts: Type Decimal""" - weighted_premium_sum = 0 - total_weight = 0 - for contract in self.underwritten_contracts: - if contract.category == categ_id: - total_weight += contract.value - contract_premium = contract.periodized_premium * contract.runtime - weighted_premium_sum += contract_premium + # weighted_premium_sum = 0 + # total_weight = 0 + # for contract in self.underwritten_contracts: + # if contract.category == categ_id: + # total_weight += contract.value + # contract_premium = contract.periodized_premium * contract.runtime + # weighted_premium_sum += contract_premium + + total_weight = self.underwritten_risk_characterisation[ + categ_id + ].total_value # TODO: Should use exposure + weighted_premium_sum = self.underwritten_risk_characterisation[ + categ_id + ].weighted_premium if total_weight == 0: return 0 # will prevent any attempt to reinsure empty categories return weighted_premium_sum * 1.0 / total_weight @@ -218,12 +226,10 @@ def ask_reinsurance_non_proportional_by_category( # TODO: how do we decide how many tranches? if min_tranches is None: min_tranches = isleconfig.simulation_parameters["min_tranches"] - [ - total_value, - avg_risk_factor, - number_risks, - periodized_total_premium, - ] = self.underwritten_risk_characterisation[categ_id] + + total_value = self.underwritten_risk_characterisation[categ_id].total_value + number_risks = self.underwritten_risk_characterisation[categ_id].number_risks + if number_risks > 0: tranches = self.reinsurance_profile.uncovered(categ_id) @@ -308,6 +314,7 @@ def reinsure_tranche( avg_risk_factor, number_risks, periodized_total_premium, + _, ] = self.underwritten_risk_characterisation[category] risk = genericclasses.RiskProperties( value=total_value, @@ -447,7 +454,9 @@ def add_reinsurance(self, contract: ReinsuranceContract, force_value: float = No if force_value is not None: value = force_value else: - value = self.underwritten_risk_characterisation[contract.category][0] + value = self.underwritten_risk_characterisation[ + contract.category + ].total_value self.reinsurance_profile.add(contract, value) def delete_reinsurance(self, contract: ReinsuranceContract): @@ -457,7 +466,7 @@ def delete_reinsurance(self, contract: ReinsuranceContract): category: Type Integer. contract: Type Class. Reinsurance contract issued to firm. No return values.""" - value = self.underwritten_risk_characterisation[contract.category][0] + value = self.underwritten_risk_characterisation[contract.category].total_value self.reinsurance_profile.remove(contract, value) def make_reinsurance_claims(self, time: int): @@ -486,7 +495,7 @@ def make_reinsurance_claims(self, time: int): for contract in to_explode: contract.explode(time, damage_extent=claims_this_turn[categ_id]) - def get_excess_of_loss_reinsurance(self) -> MutableSequence[Mapping]: + def get_excess_of_loss_reinsurance(self) -> Collection[Mapping]: """Method to return list containing the reinsurance for each category interms of the reinsurer, value of contract and category. Only used for network visualisation. No accepted values. @@ -515,6 +524,7 @@ def refresh_reinrisk( avg_risk_factor, number_risks, periodized_total_premium, + _, ] = self.underwritten_risk_characterisation[old_contract.category] if number_risks == 0: # If the insurerer currently has no risks in that category it probably doesn't want reinsurance diff --git a/insurancesimulation.py b/insurancesimulation.py index 23f5c58..827ae4f 100644 --- a/insurancesimulation.py +++ b/insurancesimulation.py @@ -14,7 +14,15 @@ from genericclasses import GenericAgent, RiskProperties, AgentProperties, Constant import catbond -from typing import Mapping, MutableMapping, MutableSequence, Sequence, Any, Optional +from typing import ( + Mapping, + MutableMapping, + MutableSequence, + Sequence, + Any, + Optional, + Collection, +) from typing import TYPE_CHECKING if TYPE_CHECKING: @@ -176,7 +184,7 @@ def __init__( size=self.simulation_parameters["no_risks"], ) - self.risks: MutableSequence[RiskProperties] = [ + self.risks: Collection[RiskProperties] = [ RiskProperties( risk_factor=rrisk_factors[i], value=rvalues[i], @@ -242,17 +250,17 @@ def __init__( ) "Agent lists" - self.reinsurancefirms: MutableSequence = [] - self.insurancefirms: MutableSequence = [] - self.catbonds: MutableSequence = [] + self.reinsurancefirms: Collection = [] + self.insurancefirms: Collection = [] + self.catbonds: Collection = [] "Lists of agent weights" self.insurers_weights: MutableMapping[int, float] = {} self.reinsurers_weights: MutableMapping[int, float] = {} "List of reinsurance risks offered for underwriting" - self.reinrisks: MutableSequence[RiskProperties] = [] - self.not_accepted_reinrisks: MutableSequence[RiskProperties] = [] + self.reinrisks: Collection[RiskProperties] = [] + self.not_accepted_reinrisks: Collection[RiskProperties] = [] "Cumulative variables for history and logging" self.cumulative_bankruptcies: int = 0 @@ -553,7 +561,7 @@ def iterate(self, t: int): if isleconfig.buy_bankruptcies: for reinagent in self.reinsurancefirms: if reinagent.operational: - reinagent.consider_buyout(type="reinsurer") + reinagent.consider_buyout(firm_type="reinsurer") # remove all non-accepted reinsurance risks self.reinrisks = [] @@ -572,7 +580,7 @@ def iterate(self, t: int): if isleconfig.buy_bankruptcies: for agent in self.insurancefirms: if agent.operational: - agent.consider_buyout(type="insurer") + agent.consider_buyout(firm_type="insurer") # Reset list of bankrupt insurance firms self.reset_selling_firms() diff --git a/isleconfig.py b/isleconfig.py index 60563e0..3ca7cb0 100644 --- a/isleconfig.py +++ b/isleconfig.py @@ -32,7 +32,7 @@ "mean_contract_runtime": 12, "contract_runtime_halfspread": 2, "default_contract_payment_period": 3, - "max_time": 1000, + "max_time": 300, "money_supply": 2000000000, "event_time_mean_separation": 100 / 3.0, "expire_immediately": False, diff --git a/metainsurancecontract.py b/metainsurancecontract.py index 155c3c7..64ebb24 100644 --- a/metainsurancecontract.py +++ b/metainsurancecontract.py @@ -50,6 +50,10 @@ def __init__( self.value = risk.value self.contract = risk.contract # May be None self.risk = risk + + # The risk object that will be stored in the underwritten_risks container of the insurer + self.underwritten_risk: "RiskProperties" = None + self.insurancetype = insurancetype or risk.insurancetype self.runtime = runtime diff --git a/metainsuranceorg.py b/metainsuranceorg.py index 32a8b46..1d8295e 100644 --- a/metainsuranceorg.py +++ b/metainsuranceorg.py @@ -15,6 +15,8 @@ AgentProperties, Obligation, ReinsuranceProfile, + IdSet, + RiskChar, ) from typing import ( @@ -26,6 +28,7 @@ Iterable, Callable, Any, + Collection, ) from typing import TYPE_CHECKING @@ -63,7 +66,7 @@ def get_mean(x: Sequence[float]) -> float: # A quick check tells me that we don't need a very large cache for this, as it only tends to repeat a couple of times. -@functools.lru_cache(maxsize=16) +@functools.lru_cache(maxsize=10) def get_mean_std(x: Tuple[float, ...]) -> Tuple[float, float]: # At the moment this is always called with a no_category length array # I have tested the numpy versions of this, they are slower for small arrays but much, much faster for large ones @@ -184,7 +187,8 @@ def __init__( self.np_reinsurance_premium_share = simulation_parameters[ "default_non-proportional_reinsurance_premium_share" ] - self.underwritten_contracts: MutableSequence["MetaInsuranceContract"] = [] + self.underwritten_contracts: IdSet["MetaInsuranceContract"] = IdSet() + self.underwritten_risks: IdSet["RiskProperties"] = IdSet() self.is_insurer = True self.is_reinsurer = False self.warning = False @@ -211,13 +215,13 @@ def __init__( self.simulation_parameters["no_categories"] ) self.market_permanency_counter = 0 - # TODO: make this into a dict - self.underwritten_risk_characterisation: MutableSequence[ - Tuple[float, float, int, float] - ] = [ - (None, None, None, None) + self.underwritten_risk_characterisation: MutableSequence[RiskChar] = [ + RiskChar(0, 0, 0, 0, 0, 0) for _ in range(self.simulation_parameters["no_categories"]) ] + self.total_risk_factor = [ + [] for _ in range(self.simulation_parameters["no_categories"]) + ] # The share of all risks that this firm holds. Gets updated every timestep self.risk_share = 0 @@ -284,9 +288,8 @@ def collect_process_evaluate_risks( self, time: int, contracts_dissolved: int ) -> None: if self.operational: - self.update_risk_characterisations() for categ in range(len(self.counter_category)): - value = self.underwritten_risk_characterisation[categ][0] + value = self.underwritten_risk_characterisation[categ].total_value self.reinsurance_profile.update_value(value, categ) """request risks to be considered for underwriting in the next period and collect those for this period""" new_nonproportional_risks, new_risks = self.get_newrisks_by_type() @@ -323,8 +326,7 @@ def collect_process_evaluate_risks( list(chain.from_iterable(not_accepted_reinrisks)) ) - # TODO: This takes up a lot of processing time. Can we update the list instead of rebuilding it? - underwritten_risks = [ + """underwritten_risks = [ RiskProperties( owner=self, value=contract.value, @@ -338,6 +340,17 @@ def collect_process_evaluate_risks( for contract in self.underwritten_contracts if contract.reinsurance_share != 1.0 ] + if not underwritten_risks == self.underwritten_risks: + assert all( + [ + ( + underwritten_risks[i] in self.underwritten_risks + and self.underwritten_risks[i] in underwritten_risks + ) + for i in range(len(underwritten_risks)) + ] + ) + """ """obtain risk model evaluation (VaR) for underwriting decisions and for capacity specific decisions""" # TODO: Enable reinsurance shares other than 0.0 and 1.0 @@ -347,7 +360,7 @@ def collect_process_evaluate_risks( cash_left_by_categ, var_per_risk_per_categ, self.excess_capital, - ] = self.riskmodel.evaluate(underwritten_risks, self.cash) + ] = self.riskmodel.evaluate(self.underwritten_risks, self.cash) # TODO: resolve insurance reinsurance inconsistency (insurer underwrite after capacity decisions, # reinsurers before). @@ -358,8 +371,6 @@ def collect_process_evaluate_risks( max_var_by_categ = self.cash - self.excess_capital self.adjust_capacity_target(max_var_by_categ) - self.update_risk_characterisations() - actual_capacity = self.increase_capacity(time, max_var_by_categ) # TODO: make independent of insurer/reinsurer, but change this to different deductible values @@ -408,7 +419,6 @@ def collect_process_evaluate_risks( list(chain.from_iterable(not_accepted_risks)) ) # print(self.id, " now has ", len(self.underwritten_contracts), " & returns ", len(not_accepted_risks)) - self.update_risk_characterisations() def enter_illiquidity(self, time: int, sum_due: float): """Enter_illiquidity Method. @@ -507,7 +517,6 @@ def dissolve(self, time: int, record: str): # Retract any active requests for reinsurance self.simulation.remove_reinrisks(firm=self) if self.operational: - # TODO: This seems... odd? method_to_call = getattr(self.simulation, record) method_to_call() else: @@ -527,6 +536,140 @@ def pay_dividends(self, time: int): self.receive_obligation(self.per_period_dividend, self.owner, time, "dividend") + def underwrite(self, contract: "MetaInsuranceContract"): + self.underwritten_contracts.add(contract) + if contract.reinsurance_share != 1: + risk = RiskProperties( + owner=self, + value=contract.value, + category=contract.category, + risk_factor=contract.risk_factor, + deductible=contract.deductible, + limit=contract.limit, + insurancetype=contract.insurancetype, + runtime=contract.runtime, + ) + self.underwritten_risks.add(risk) + contract.underwritten_risk = risk + + new_characterisation = RiskChar( + total_value=self.underwritten_risk_characterisation[ + contract.category + ].total_value + + contract.value, + avg_risk_factor=( + self.underwritten_risk_characterisation[ + contract.category + ].avg_risk_factor + * self.underwritten_risk_characterisation[ + contract.category + ].number_risks + + contract.risk_factor + ) + / ( + self.underwritten_risk_characterisation[contract.category].number_risks + + 1 + ), + number_risks=self.underwritten_risk_characterisation[ + contract.category + ].number_risks + + 1, + periodized_total_premium=self.underwritten_risk_characterisation[ + contract.category + ].periodized_total_premium + + contract.periodized_premium, + weighted_premium=self.underwritten_risk_characterisation[ + contract.category + ].weighted_premium + + contract.periodized_premium * contract.runtime, + total_var=self.underwritten_risk_characterisation[ + contract.category + ].total_var + + contract.initial_VaR, + ) + # if new_characterisation[1] != 1.0: + # print(new_characterisation[1]) + # assert new_characterisation[:3] == self.risk_char_slow(contract.category)[:3] + # assert np.isclose(new_characterisation[3], self.risk_char_slow(contract.category)[3]) + self.underwritten_risk_characterisation[ + contract.category + ] = new_characterisation + # (total_value, avg_risk_factor, number_risks, periodized_total_premium, weighted_premium) + + # Old functions: + """ + for categ in range(self.simulation_no_risk_categories): + self.underwritten_risk_characterisation[ + categ + ] = self.characterise_underwritten_risks_by_category(categ) + + def risk_char_slow(self, categ_id): + total_value = 0 + avg_risk_factor = 0 + number_risks = 0 + periodized_total_premium = 0 + for contract in self.underwritten_contracts: + if contract.category == categ_id: + total_value += contract.value + avg_risk_factor += contract.risk_factor + number_risks += 1 + periodized_total_premium += contract.periodized_premium + if number_risks > 0: + avg_risk_factor /= number_risks + return total_value, avg_risk_factor, number_risks, periodized_total_premium, weighted_premium + """ + + def de_underwrite(self, contract: "MetaInsuranceContract"): + self.underwritten_contracts.remove(contract) + if contract.reinsurance_share != 1: + risk = contract.underwritten_risk + self.underwritten_risks.remove(risk) + + if self.underwritten_risk_characterisation[contract.category].number_risks != 1: + new_characterisation = RiskChar( + total_value=self.underwritten_risk_characterisation[ + contract.category + ].total_value + - contract.value, + avg_risk_factor=( + self.underwritten_risk_characterisation[ + contract.category + ].avg_risk_factor + * self.underwritten_risk_characterisation[ + contract.category + ].number_risks + - contract.risk_factor + ) + / ( + self.underwritten_risk_characterisation[ + contract.category + ].number_risks + - 1 + ), + number_risks=self.underwritten_risk_characterisation[ + contract.category + ].number_risks + - 1, + periodized_total_premium=self.underwritten_risk_characterisation[ + contract.category + ].periodized_total_premium + - contract.periodized_premium, + weighted_premium=self.underwritten_risk_characterisation[ + contract.category + ].weighted_premium + - contract.periodized_premium * contract.runtime, + total_var=self.underwritten_risk_characterisation[ + contract.category + ].total_var + - contract.initial_VaR, + ) + else: + new_characterisation = RiskChar(0, 0, 0, 0, 0, 0) + + self.underwritten_risk_characterisation[ + contract.category + ] = new_characterisation + def obtain_yield(self, time: int): """Method to obtain intereset on cash reserves Accepts: @@ -549,7 +692,7 @@ def mature_contracts(self, time: int) -> int: if contract.expiration <= time ] for contract in maturing: - self.underwritten_contracts.remove(contract) + self.de_underwrite(contract) contract.mature(time) return len(maturing) @@ -595,12 +738,18 @@ def estimate_var(self) -> None: if self.operational: # Extract initial VaR per category - for contract in self.underwritten_contracts: - self.counter_category[contract.category] += 1 - self.var_category[contract.category] += contract.initial_VaR - + # for contract in self.underwritten_contracts: + # self.counter_category[contract.category] += 1 + # self.var_category[contract.category] += contract.initial_VaR # Caclulate risks per category and sum of all VaR for category in range(len(self.counter_category)): + self.counter_category[ + category + ] = self.underwritten_risk_characterisation[category].number_risks + self.var_category[category] = self.underwritten_risk_characterisation[ + category + ].total_var + self.var_counter += ( self.counter_category[category] * self.riskmodel.inaccuracy[category] @@ -675,39 +824,6 @@ def adjust_capacity_target(self, time): "Method not implemented. adjust_capacity_target method should be implemented in inheriting classes" ) - def update_risk_characterisations(self): - for categ in range(self.simulation_no_risk_categories): - self.underwritten_risk_characterisation[ - categ - ] = self.characterise_underwritten_risks_by_category(categ) - - def characterise_underwritten_risks_by_category( - self, categ_id: int - ) -> Tuple[float, float, int, float]: - """Method to characterise associated risks in a given category in terms of value, number, avg risk factor, and - total premium per iteration. - Accepts: - categ_id: Type Integer. The given category for characterising risks. - Returns: - total_value: Type Decimal. Total value of all contracts in the category. - avg_risk_facotr: Type Decimal. Avg risk factor of all contracted risks in category. - number_risks: Type Integer. Total number of contracted risks in category. - periodised_total_premium: Total value per month of all contracts premium payments.""" - # TODO: Update this instead of recalculating so much - total_value = 0 - avg_risk_factor = 0 - number_risks = 0 - periodized_total_premium = 0 - for contract in self.underwritten_contracts: - if contract.category == categ_id: - total_value += contract.value - avg_risk_factor += contract.risk_factor - number_risks += 1 - periodized_total_premium += contract.periodized_premium - if number_risks > 0: - avg_risk_factor /= number_risks - return total_value, avg_risk_factor, number_risks, periodized_total_premium - def risks_reinrisks_organizer( self, new_risks: Sequence[RiskProperties] ) -> Sequence[Sequence[RiskProperties]]: @@ -879,7 +995,7 @@ def process_newrisks_reinsurer( initial_var=var_this_risk, insurancetype=risk.insurancetype, ) # TODO: implement excess of loss for reinsurance contracts - self.underwritten_contracts.append(contract) + self.underwrite(contract) has_accepted_risks = True self.cash_left_by_categ = cash_left_by_categ else: @@ -919,7 +1035,7 @@ def process_newrisks_insurer( if acceptable_by_category[risk.category] > 0: if risk.contract and risk.contract.expiration > time: # In this case the risk being inspected already has a contract, so we are deciding whether to - # give reinsurance for it # QUERY: is this correct? + # give (proportional) reinsurance for it # QUERY: is this correct? [condition, cash_left_by_categ] = self.balanced_portfolio( risk, cash_left_by_categ, None ) @@ -937,7 +1053,7 @@ def process_newrisks_insurer( "expire_immediately" ], ) - self.underwritten_contracts.append(contract) + self.underwrite(contract) has_accepted_risks = True self.cash_left_by_categ = cash_left_by_categ acceptable_by_category[risk.category] -= 1 @@ -965,7 +1081,7 @@ def process_newrisks_insurer( ], initial_var=var_per_risk_per_categ[risk.category], ) - self.underwritten_contracts.append(contract) + self.underwrite(contract) has_accepted_risks = True self.cash_left_by_categ = cash_left_by_categ acceptable_by_category[risk.category] -= 1 @@ -1052,9 +1168,7 @@ def market_permanency(self, time: int): def get_reinsurance_price(self, risk: RiskProperties) -> float: """Returns the total per-value premium for reinsurance""" - raise NotImplementedError("No.") - - # TODO: Error message maybe + raise NotImplementedError("Should have been overridden") def register_claim(self, claim: float): """Method to register claims. @@ -1156,7 +1270,7 @@ def adjust_riskmodel_inaccuracy(self): + self.min_inaccuracy * self.risk_share ) - def consider_buyout(self, type="insurer"): + def consider_buyout(self, firm_type="insurer"): """Method to allow firm to decide if to buy one of the firms going bankrupt. Accepts: type: Type string. Used to decide if insurance or reinsurance firm. @@ -1164,11 +1278,11 @@ def consider_buyout(self, type="insurer"): This method is called for both types of firms to consider buying one firm going bankrupt for only this iteration It has a chance (based on market share) to buyout other firm if its excess capital is large enough to cover the other firms value at risk multiplied by its margin of safety. Will call buyout() if necessary.""" - firms_to_consider = self.simulation.get_firms_to_sell(type) + firms_to_consider = self.simulation.get_firms_to_sell(firm_type) firms_further_considered = [] for firm, time, reason in firms_to_consider: - all_firms_cash = self.simulation.get_total_firm_cash(type) + all_firms_cash = self.simulation.get_total_firm_cash(firm_type) all_obligations = sum( [obligation["amount"] for obligation in firm.obligations] ) @@ -1240,7 +1354,7 @@ def buyout(self, firm, firm_cost, time): for contract in firm.underwritten_contracts: contract.insurer = self - self.underwritten_contracts.append(contract) + self.underwrite(contract) for obli in firm.obligations: self.receive_obligation( obli["amount"], obli["recipient"], obli["due_time"], obli["purpose"] @@ -1274,6 +1388,3 @@ def submit_regulator_report(self, time): self.operational = False else: self.dissolve(time, "record_nonregulation_firm") - for contract in self.underwritten_contracts: - contract.mature(time) - self.underwritten_contracts = [] diff --git a/riskmodel.py b/riskmodel.py index 49209b6..adf028d 100644 --- a/riskmodel.py +++ b/riskmodel.py @@ -5,7 +5,7 @@ import isleconfig from distributionreinsurance import ReinsuranceDistWrapper -from typing import Sequence, Tuple, Union, Optional, MutableSequence +from typing import Sequence, Tuple, Union, Optional, MutableSequence, Collection from typing import TYPE_CHECKING @@ -311,7 +311,7 @@ def evaluate_excess_of_loss( # noinspection PyUnboundLocalVariable def evaluate( self, - risks: Sequence["RiskProperties"], + risks: Collection["RiskProperties"], cash: Union[float, Sequence[float]], offered_risk: Optional["RiskProperties"] = None, ) -> Union[ From bbeaecf1a914481315ed3a64b0d497af9d97206d Mon Sep 17 00:00:00 2001 From: Chris Hughes Date: Fri, 9 Aug 2019 11:18:08 +0100 Subject: [PATCH 092/125] Optimisations - updates risk characterisation instead of re-calculating, - added set-like class for unhashable elements --- catbond.py | 2 ++ genericclasses.py | 15 ++++++++++++--- insurancefirms.py | 32 ++++++++++++++++---------------- isleconfig.py | 2 +- metainsuranceorg.py | 18 ++---------------- reinsurancecontract.py | 1 + 6 files changed, 34 insertions(+), 36 deletions(-) diff --git a/catbond.py b/catbond.py index 2b59975..4f146ae 100644 --- a/catbond.py +++ b/catbond.py @@ -41,6 +41,7 @@ def __init__( self.per_period_dividend: float = per_period_premium self.creditor = self.simulation self.expiration: int = None + self.triggered = 0 # self.simulation_no_risk_categories = self.simulation.simulation_parameters["no_categories"] def iterate(self, time: int): @@ -99,6 +100,7 @@ def mature_bond(self): No return values When the catbond contract matures this is called which pays the value of the catbond to the simulation, and is then deleted from the list of agents.""" + if self.operational: obligation = Obligation( amount=self.cash, diff --git a/genericclasses.py b/genericclasses.py index 0713edd..9a57ca0 100644 --- a/genericclasses.py +++ b/genericclasses.py @@ -7,7 +7,16 @@ import isleconfig -from typing import Mapping, MutableSequence, Union, Tuple, List, Collection, TypeVar +from typing import ( + Mapping, + MutableSequence, + Union, + Tuple, + List, + Collection, + TypeVar, + Set, +) from typing import TYPE_CHECKING if TYPE_CHECKING: @@ -25,7 +34,7 @@ class GenericAgent: def __init__(self): self.cash: float = 0 - self.obligations: Collection["Obligation"] = [] + self.obligations: MutableSequence["Obligation"] = [] self.operational: bool = True self.profits_losses: float = 0 self.creditor = None @@ -162,7 +171,7 @@ class AgentProperties: interest_rate: float -@dataclasses.dataclass +@dataclasses.dataclass(frozen=True) class Obligation: """Class for holding the properties of an obligation""" diff --git a/insurancefirms.py b/insurancefirms.py index 8d253f5..f3d93c6 100644 --- a/insurancefirms.py +++ b/insurancefirms.py @@ -309,13 +309,12 @@ def reinsure_tranche( time: int, purpose: str, ): - [ - total_value, - avg_risk_factor, - number_risks, - periodized_total_premium, - _, - ] = self.underwritten_risk_characterisation[category] + (total_value, avg_risk_factor, number_risks, periodized_total_premium) = ( + self.underwritten_risk_characterisation[category].total_value, + self.underwritten_risk_characterisation[category].avg_risk_factor, + self.underwritten_risk_characterisation[category].number_risks, + self.underwritten_risk_characterisation[category].periodized_total_premium, + ) risk = genericclasses.RiskProperties( value=total_value, category=category, @@ -429,7 +428,6 @@ def decide_reinsurance_type(self, risk: genericclasses.RiskProperties) -> str: def get_catbond_price(self, risk: genericclasses.RiskProperties) -> float: """Returns the total per-risk premium for a catbond """ - # TODO: take limit into account as well as deductible assert risk.deductible_fraction is not None return self.simulation.get_cat_bond_price( risk.deductible_fraction, risk.limit_fraction @@ -437,7 +435,6 @@ def get_catbond_price(self, risk: genericclasses.RiskProperties) -> float: def get_reinsurance_price(self, risk: genericclasses.RiskProperties) -> float: """Returns the total per-risk premium for reinsurance""" - # TODO: take limit into account as well as deductible assert risk.deductible_fraction is not None return self.simulation.get_reinsurance_premium( risk.deductible_fraction, risk.limit_fraction @@ -519,13 +516,16 @@ def refresh_reinrisk( # TODO: Can be merged """Takes an expiring contract and returns a renewed risk to automatically offer to the existing reinsurer. The new risk has the same deductible and excess as the old one, but with an updated time""" - [ - total_value, - avg_risk_factor, - number_risks, - periodized_total_premium, - _, - ] = self.underwritten_risk_characterisation[old_contract.category] + (total_value, avg_risk_factor, number_risks, periodized_total_premium) = ( + self.underwritten_risk_characterisation[old_contract.category].total_value, + self.underwritten_risk_characterisation[ + old_contract.category + ].avg_risk_factor, + self.underwritten_risk_characterisation[old_contract.category].number_risks, + self.underwritten_risk_characterisation[ + old_contract.category + ].periodized_total_premium, + ) if number_risks == 0: # If the insurerer currently has no risks in that category it probably doesn't want reinsurance return None diff --git a/isleconfig.py b/isleconfig.py index 3ca7cb0..60563e0 100644 --- a/isleconfig.py +++ b/isleconfig.py @@ -32,7 +32,7 @@ "mean_contract_runtime": 12, "contract_runtime_halfspread": 2, "default_contract_payment_period": 3, - "max_time": 300, + "max_time": 1000, "money_supply": 2000000000, "event_time_mean_separation": 100 / 3.0, "expire_immediately": False, diff --git a/metainsuranceorg.py b/metainsuranceorg.py index 1d8295e..573fb9d 100644 --- a/metainsuranceorg.py +++ b/metainsuranceorg.py @@ -522,6 +522,7 @@ def dissolve(self, time: int, record: str): else: pass # Set to None so trying to add more obligations throws an error + # TODO: this is okay, right? self.obligations = None self.operational = False @@ -939,23 +940,8 @@ def process_newrisks_reinsurer( # risk[C4], risk[C1], risk[C2], ... if possible. assert risk assert risk.owner.operational - # TODO: Move this out of the loop (maybe somewhere else entirely) and update it when needed - underwritten_risks = [ - RiskProperties( - owner=self, - value=contract.value, - category=contract.category, - risk_factor=contract.risk_factor, - deductible=contract.deductible, - limit=contract.limit, - insurancetype=contract.insurancetype, - runtime_left=(contract.expiration - time), - ) - for contract in self.underwritten_contracts - if contract.insurancetype == "excess-of-loss" - ] accept, cash_left_by_categ, var_this_risk, self.excess_capital = self.riskmodel.evaluate( - underwritten_risks, self.cash, risk + self.underwritten_risks, self.cash, risk ) if accept: # TODO: What exactly is this based on? How should reinsurance pricing work? diff --git a/reinsurancecontract.py b/reinsurancecontract.py index b508434..b21dede 100644 --- a/reinsurancecontract.py +++ b/reinsurancecontract.py @@ -107,6 +107,7 @@ def explode( # for the issuer # TODO: Allow for catbonds that can pay out multiple times? self.insurer: "Catbond" + self.insurer.triggered += 1 remaining_cb_cash = self.insurer.get_available_cash(time) - claim assert remaining_cb_cash >= 0 if remaining_cb_cash < 2: From a1c106ec2e204d2e38d51858a35adba996ee834f Mon Sep 17 00:00:00 2001 From: KloskaT Date: Fri, 9 Aug 2019 17:33:39 +0100 Subject: [PATCH 093/125] Final. Changes due to Chris' changes. Tweaking firm pricing. Removed operational condition for firm pay due to firm buyouts. Insurance claims made t+1 not t+2 for better profit tracking. --- centralbank.py | 2 +- genericclasses.py | 12 ++++-------- insurancecontract.py | 2 +- metainsuranceorg.py | 22 ++++++++++++++-------- visualisation.py | 1 + 5 files changed, 21 insertions(+), 18 deletions(-) diff --git a/centralbank.py b/centralbank.py index 01a1a7d..a203ebb 100644 --- a/centralbank.py +++ b/centralbank.py @@ -159,7 +159,7 @@ def provide_aid(self, insurance_firms, damage_fraction, time): given_aid_dict = {} if damage_fraction > 0.50: for insurer in insurance_firms: - claims = sum([ob['amount'] for ob in insurer.obligations if ob["purpose"] == "claim" and ob["due_time"] == time + 2]) + claims = sum([ob.amount for ob in insurer.obligations if ob.purpose == "claim" and ob.due_time == time + 2]) aid = claims * damage_fraction all_firms_aid += aid given_aid_dict[insurer] = aid diff --git a/genericclasses.py b/genericclasses.py index 04ac163..4f918c1 100644 --- a/genericclasses.py +++ b/genericclasses.py @@ -53,14 +53,10 @@ def _pay(self, obligation: "Obligation"): f"Redirecting payment with purpose {purpose} due to non-operational firm {recipient.id}" ) recipient = recipient.creditor - if self.get_operational(): - self.cash -= amount - if purpose != "dividend": - self.profits_losses -= amount - recipient.receive(amount) - else: - if isleconfig.verbose: - print(f"Payment not processed as firm {self.id} is not operational") + self.cash -= amount + if purpose != "dividend": + self.profits_losses -= amount + recipient.receive(amount) def get_operational(self) : """Method to return boolean of if agent is operational. Only used as check for payments. diff --git a/insurancecontract.py b/insurancecontract.py index b1664b3..fa11f09 100644 --- a/insurancecontract.py +++ b/insurancecontract.py @@ -44,7 +44,7 @@ def explode(self, time, uniform_value=None, damage_extent=None): claim = min(self.limit, damage_extent * self.value) - self.deductible self.insurer.register_claim(claim) # Every insurance claim made is immediately registered. self.current_claim += claim - self.insurer.receive_obligation(claim, self.property_holder, time + 2, "claim") + self.insurer.receive_obligation(claim, self.property_holder, time + 1, "claim") if self.expire_immediately: self.expiration = time # self.terminating = True diff --git a/metainsuranceorg.py b/metainsuranceorg.py index 2198d85..a2aaa3d 100644 --- a/metainsuranceorg.py +++ b/metainsuranceorg.py @@ -403,7 +403,7 @@ def dissolve(self, time, record): # Record all unpaid claims (needs to be here to account for firms lost due to regulator/being sold) if record != "record_market_exit": # Market exits already pay all obligations sum_due = sum(item.amount for item in self.obligations) # Also records dividends/premiums - self.simulation.record_unrecovered_claims(sum_due - self.cash) + self.simulation.record_unrecovered_claims(max(0, sum_due - self.cash)) # Removing (dissolving) all risks immediately after bankruptcy (may not be realistic, they might instead be bought by another company) [contract.dissolve(time) for contract in self.underwritten_contracts] @@ -936,13 +936,16 @@ def consider_buyout(self, type="insurer"): firms_further_considered = [] for firm, time, reason in firms_to_consider: + cagr = (firm.cash_last_periods[11]/firm.cash_last_periods[0])**(1/12) - 1 # Aggregate growth over last 12 + cagr = max(0, cagr) # Negative growth set to 0 + ddm_stock = firm.per_period_dividend/(0.12-cagr) # Use DDM model to evaluate price of all stock based on total dividend all_firms_cash = self.simulation.get_total_firm_cash(type) - all_obligations = sum([obligation["amount"] for obligation in firm.obligations]) + all_obligations = sum([obligation.amount for obligation in firm.obligations]) total_premium = sum([np.mean(contract.payment_values) for contract in firm.underwritten_contracts if len(contract.payment_values) > 0]) - if self.excess_capital > self.riskmodel.margin_of_safety * firm.var_sum + all_obligations - total_premium: - firm_likelihood = 0.25 + (1.5 * firm.cash + np.mean(firm.cash_last_periods[1:12]) + self.cash)/all_firms_cash + firm_price = total_premium + ddm_stock + (np.mean(firm.cash_last_periods[0:12])**2)/all_firms_cash # Price based on ddm stock value, cash flow from premiums, and market capitalization + if self.excess_capital > firm.var_sum + all_obligations - total_premium: + firm_likelihood = 0.25 + (1.5 * firm.cash + np.mean(firm.cash_last_periods[0:11]) + self.cash)/all_firms_cash # Likelihood depends on size of other firm and itself firm_likelihood = min(1, 2*firm_likelihood) - firm_price = (firm.var_sum/10) + total_premium + firm.per_period_dividend firm_sell_reason = reason firms_further_considered.append([firm, firm_likelihood, firm_price, firm_sell_reason]) @@ -976,10 +979,13 @@ def buyout(self, firm, firm_cost, time): print("Reinsurer %i has bought %i for %d with total cash %d" % (self.id, firm.id, firm_cost, self.cash)) for contract in firm.underwritten_contracts: - contract.insurer = self + if contract.insurancetype == "proportional": + contract.insurer = self + else: + contract.property_holder = self # In case of reinsurance however not observed. self.underwritten_contracts.append(contract) for obli in firm.obligations: - self.receive_obligation(obli['amount'], obli["recipient"], obli["due_time"], obli["purpose"]) + self.receive_obligation(obli.amount, obli.recipient, obli.due_time, obli.purpose) firm.obligations = [] firm.underwritten_contracts = [] @@ -1003,6 +1009,6 @@ def submit_regulator_report(self, time): else: self.dissolve(time, "record_nonregulation_firm") for contract in self.underwritten_contracts: - contract.mature(time) + contract.mature(time) # Mature contracts so they are returned to simulation as firm non-op self.underwritten_contracts = [] diff --git a/visualisation.py b/visualisation.py index c871706..55614a6 100644 --- a/visualisation.py +++ b/visualisation.py @@ -1185,6 +1185,7 @@ def stat_tests(self, upper, lower): # CD = ConfigCompare("data/single_history_logs_old_2019_Aug_02_12_53.dat", # "data/single_history_logs.dat", # "data/single_history_logs_old_2019_Aug_02_13_13.dat") + # Get files of data that resulted from different conditions to compare. Can handle any number of replications. CD = ConfigCompare(args.file1, args.file2, args.file3) CD.plot(events=False, upper=1000, lower=200) CD.stat_tests(upper=1000, lower=200) From 656aebf9da4acad9d3b3b65ca3851be5649e9e8f Mon Sep 17 00:00:00 2001 From: KloskaT Date: Fri, 9 Aug 2019 17:44:47 +0100 Subject: [PATCH 094/125] Changed aid check in line with change to insurance claims' claim time. Added file on interactive visualisation using bokeh. --- centralbank.py | 2 +- interactive_visualisation.py | 96 ++++++++++++++++++++++++++++++++++++ 2 files changed, 97 insertions(+), 1 deletion(-) create mode 100644 interactive_visualisation.py diff --git a/centralbank.py b/centralbank.py index a203ebb..c6d7fbb 100644 --- a/centralbank.py +++ b/centralbank.py @@ -159,7 +159,7 @@ def provide_aid(self, insurance_firms, damage_fraction, time): given_aid_dict = {} if damage_fraction > 0.50: for insurer in insurance_firms: - claims = sum([ob.amount for ob in insurer.obligations if ob.purpose == "claim" and ob.due_time == time + 2]) + claims = sum([ob.amount for ob in insurer.obligations if ob.purpose == "claim" and ob.due_time == time + 1]) aid = claims * damage_fraction all_firms_aid += aid given_aid_dict[insurer] = aid diff --git a/interactive_visualisation.py b/interactive_visualisation.py new file mode 100644 index 0000000..6d74ad1 --- /dev/null +++ b/interactive_visualisation.py @@ -0,0 +1,96 @@ +import networkx as nx +from tornado.ioloop import IOLoop +import numpy as np + +from bokeh.models.widgets import Slider +from bokeh.models import Plot, Range1d, MultiLine, Circle, HoverTool, TapTool, BoxSelectTool +from bokeh.models.graphs import from_networkx, NodesAndLinkedEdges, EdgesAndLinkedNodes +from bokeh.palettes import Spectral4 +from bokeh.application.handlers import FunctionHandler +from bokeh.application import Application +from bokeh.layouts import row, WidgetBox +from bokeh.server.server import Server + + +io_loop = IOLoop.current() + +with open("./data/network_data.dat", "r") as rfile: + network_data_dict = [eval(k) for k in rfile] + +unweighted_network_data = network_data_dict[0]["unweighted_network_data"] +network_edge_labels = network_data_dict[0]["network_edge_labels"] +network_node_labels = network_data_dict[0]["network_node_labels"] +number_agent_type = network_data_dict[0]["number_of_agents"] + +#doc = output_file("bokeh/networkx_graph_demo.html") + +def modify_network(doc): + def make_dataset(time_iter): + types = {'insurers': [], 'reinsurers':[], 'catbonds':[]} + unweighted_nx_network = nx.from_numpy_array(np.array(unweighted_network_data[time_iter])) + nx.set_edge_attributes(unweighted_nx_network, network_edge_labels[time_iter], "categ") + nx.set_node_attributes(unweighted_nx_network, network_node_labels[time_iter], "id") + for i in range(number_agent_type[time_iter]['insurers']): + unweighted_nx_network.node[i]['type']='Insurer' + for i in range(number_agent_type[time_iter]['reinsurers']): + unweighted_nx_network.node[i+number_agent_type[time_iter]['insurers']]['type']='Reinsurer' + for i in range(number_agent_type[time_iter]['catbonds']): + unweighted_nx_network.node[i+number_agent_type[time_iter]['insurers']+number_agent_type[time_iter]['reinsurers']]['type']='CatBond' + nx.set_node_attributes(unweighted_nx_network, types, 'type') + return unweighted_nx_network + + def make_plot(unweighted_nx_network): + plot = Plot(plot_width=600, plot_height=600, x_range=Range1d(-1.1,1.1), y_range=Range1d(-1.1,1.1)) + plot.title.text = "Insurance Network Demo" + graph_renderer = from_networkx(unweighted_nx_network, nx.kamada_kawai_layout, scale=1, center=(0,0)) + + graph_renderer.node_renderer.glyph = Circle(size=15, fill_color=Spectral4[0]) + graph_renderer.node_renderer.selection_glyph = Circle(size=15, fill_color=Spectral4[2]) + graph_renderer.node_renderer.hover_glyph = Circle(size=15, fill_color=Spectral4[1]) + graph_renderer.node_renderer.glyph.properties_with_values() + + graph_renderer.edge_renderer.glyph = MultiLine(line_color="#CCCCCC", line_alpha=0.8, line_width=5) + graph_renderer.edge_renderer.selection_glyph = MultiLine(line_color=Spectral4[2], line_width=5) + graph_renderer.edge_renderer.hover_glyph = MultiLine(line_color=Spectral4[1], line_width=5) + graph_renderer.edge_renderer.glyph.properties_with_values() + + graph_renderer.selection_policy = NodesAndLinkedEdges() + node_hover_tool = HoverTool(tooltips=[('ID', '@id'), ('Type', '@type')]) + + #graph_renderer.inspection_policy = EdgesAndLinkedNodes() + #edge_hover_tool = HoverTool(tooltips=[('Category', '@categ')]) + + plot.add_tools(node_hover_tool, TapTool(), BoxSelectTool()) + + plot.renderers.append(graph_renderer) + return plot + + def update(attr, old, new): + doc.clear() + network = make_dataset(0) + if new > 0: + new_network = make_dataset(new) + network.update(new_network) + + timeselect_slider = Slider(start=0, end=1000, value=new, value_throttled=new, step=1, title="Time", callback_policy='mouseup') + timeselect_slider.on_change('value_throttled', update) + + p = make_plot(network) + + controls = WidgetBox(timeselect_slider) + layout = row(controls, p) + + doc.add_root(layout) + + update('', old=-1, new=0) + + +network_app = Application(FunctionHandler(modify_network)) + +server = Server({'/': network_app}, io_loop=io_loop) +server.start() + +if __name__ == '__main__': + print('Opening Bokeh application on http://localhost:5006/') + io_loop.add_callback(server.show, "/") + io_loop.start() From e06666ce64980f4284b2b07365bc4b36afe882c4 Mon Sep 17 00:00:00 2001 From: KloskaT Date: Fri, 9 Aug 2019 17:56:32 +0100 Subject: [PATCH 095/125] Updated readme --- README.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 7444826..e16be95 100644 --- a/README.md +++ b/README.md @@ -55,7 +55,8 @@ bash starter_three.sh #### Single runs Use the script ```visualisation.py [--single]``` from the command line to plot data from a single run. It also takes the -arguments ```[--pie] [--timeseries]``` for which data representation is wanted. +arguments ```[--pie] [--timeseries]``` for which data representation is wanted. The argument ```[--config_compare_filex ]``` +where ```x``` can be 1,2 or 3 is used for comparing two sets of data (singular or with replications) with different conditions. If the necessary data has been saved a network animation can also be created by running ```visualization_network.py``` which takes the arguments ```[--save] [--number_iterations]``` if you want the animation to be saved as an mp4, and how From 2c68d0846e99e4a2f012e9c01fca9b1ba53aa6b3 Mon Sep 17 00:00:00 2001 From: Chris Hughes Date: Mon, 12 Aug 2019 15:27:36 +0100 Subject: [PATCH 096/125] Investigating the distribution of cat damage for potential implementation --- catbond.py | 2 +- distributionaggregate.py | 325 +++++++++++++++++++++++++++++++++++++++ distributiontruncated.py | 7 +- insurancefirms.py | 10 +- insurancesimulation.py | 126 +++++++-------- isleconfig.py | 1 + metainsuranceorg.py | 13 +- reinsurancecontract.py | 35 ++++- riskmodel.py | 10 +- sum_distribution.py | 155 +++++++++++++++++++ 10 files changed, 588 insertions(+), 96 deletions(-) create mode 100644 distributionaggregate.py create mode 100644 sum_distribution.py diff --git a/catbond.py b/catbond.py index 4f146ae..c3d1ffc 100644 --- a/catbond.py +++ b/catbond.py @@ -31,6 +31,7 @@ def __init__( owner: Type class This initialised the catbond class instance, inheriting methods from MetaInsuranceOrg.""" self.simulation = simulation + self.simulation_parameters = simulation.simulation_parameters self.id: int = self.simulation.get_unique_catbond_id() self.underwritten_contracts: Collection["MetaInsuranceContract"] = [] self.cash: float = 0 @@ -41,7 +42,6 @@ def __init__( self.per_period_dividend: float = per_period_premium self.creditor = self.simulation self.expiration: int = None - self.triggered = 0 # self.simulation_no_risk_categories = self.simulation.simulation_parameters["no_categories"] def iterate(self, time: int): diff --git a/distributionaggregate.py b/distributionaggregate.py new file mode 100644 index 0000000..bb07f42 --- /dev/null +++ b/distributionaggregate.py @@ -0,0 +1,325 @@ +import pickle as pkl +from typing import Tuple + +import scipy.stats +import scipy.interpolate +import scipy.signal +import numpy as np +from genericclasses import RiskProperties + +with open("./pdf_data.pkl", "rb") as rfile: + pdfs: dict = pkl.load(rfile) + + +def find_nearest(array, value): + """Given an array and a value, returns the element of the array nearest to the value""" + array = np.asarray(array) + idx = (np.abs(array - value)).argmin() + return array[idx] + + +def find_nearest_index(array, value): + """Given an array and a value, returns the index of the element of the array nearest to the value""" + array = np.asarray(array) + idx = (np.abs(array - value)).argmin() + return idx + + +class AggregateDistribtion(scipy.stats.rv_continuous): + """ + Overall damage distribution for n risks in the same category. Distribution is normalised to lie between 0 and 1, + so needs to be mulitplied by total_value + """ + + def __init__(self, n): + super().__init__(a=0, b=1) + self.n = n + try: + self._pdf_data = pdfs[n] + self._pdf_x = np.array(self._pdf_data[0]) / self.n + self._pdf_y = np.array(self._pdf_data[1]) * self.n + except KeyError: + n_fake = find_nearest(pdfs.keys(), self.n) + print( + f"Warning: tried to use non-existant pre-computed distribution for n = {self.n}" + ) + print(f"Using scaled version of closest available value, {n_fake}") + self._pdf_data = pdfs[n_fake] + self._pdf_x = np.array(self._pdf_data[0]) / n_fake + self._pdf_y = np.array(self._pdf_data[1]) * n_fake + + # The stored data usually isn't perfectly normalised due to numerical inaccuracy / floating point errors + self._pdf_y = self._pdf_y / np.sum(self._pdf_y) * len(self._pdf_x) + + self._cdf_y = np.cumsum(self._pdf_y) / len(self._pdf_x) + + self._pdf = scipy.interpolate.interp1d( + self._pdf_x, + self._pdf_y, + kind="cubic", + bounds_error=False, + fill_value=0, + assume_sorted=True, + ) + + self._cdf = scipy.interpolate.interp1d( + self._pdf_x, + self._cdf_y, + kind="linear", + bounds_error=False, + fill_value=(0, 1), + assume_sorted=True, + ) + + def _rvs(self, *args): + raise Exception("Shouldn't generate rvs from this, there are better ways...") + + +def contract_risk( + number_risks: int, + value_per_risk: int, + deductible: int, + limit: int, + runtime: int, + max_claims: int, + var_tail_size: float = 0.005, + cat_frequency: float = 3 / 100, + max_res: int = 2000, + approx: bool = True, +) -> Tuple[float, float, int, int]: + """ + Calculates various risk statistics for a contract. + Uses the pre-calculated density functions for aggregate claims, so should be accurate even for only a few risks. + If the total value (number_risks * value_per_risk) is very high (>10,000) then the calculations will be less + accurate (not by much), as the calculations will be scaled for performance reasons. This is controled by max_res + MU = monetary units + Args: + number_risks: The number of risk being (re)insured + value_per_risk: The value of each risk (all risks have the same value) in MU + deductible: The deductible of the contract under consideration in MU + limit: The limit of the contract under consideration in MU + runtime: The total runtime of the contract + max_claims: The maximum number of claims that can be made. If 0, will be assumed equal to runtime. + var_tail_size: The tail size to use when calculating the VaR (default 0.005) + cat_frequency: The probability of a catastrophe in each time unit (default 3/100) + max_res: The maximum number of value increments used in the calculations. Should be at least 1000 + + Returns: + expected_total_claim: The expectation of the total claims under this contract + std: The standard deviation of the total claims + var: The VaR in MU at the tail size given + exposure: limit - deductible + """ + + # We assume throughout that claims are only whole-number monetary units, and are the ceiling of the actual damage. + total_value = number_risks * value_per_risk + + # We change value_per_risk to get the resolution of the calculations to a suitable scale + if total_value > max_res: + factor = total_value / max_res + total_value = int(round(total_value / factor)) + deductible = int(round(deductible / factor)) + limit = int(round(limit / factor)) + else: + factor = 0 + + claim_amounts = np.arange(total_value + 1) + damage_probabilities = AggregateDistribtion(number_risks).pdf( + claim_amounts / total_value + ) + # Change from a density to a pmf + damage_probabilities = damage_probabilities / np.sum(damage_probabilities) + claim_probabilities = np.zeros_like(damage_probabilities) + claim_probabilities[0 : limit - deductible] = damage_probabilities[deductible:limit] + claim_probabilities[0] += np.sum(damage_probabilities[0:deductible]) + claim_probabilities[limit - deductible] += np.sum(damage_probabilities[limit:]) + # plt.plot(claim_amounts, damage_probabilities) + # plt.plot(claim_amounts, claim_probabilities) + # plt.show() + + if not (0 < max_claims <= runtime): + max_claims = runtime + + per_event_exposure = min(limit, total_value) - deductible + exposure = per_event_exposure * max_claims + + total_claim_values = np.arange(exposure + 1) + total_claim_probabilities = np.zeros(total_claim_values.shape) + for number_claims in range(max_claims + 1): + p = scipy.stats.binom.pmf(n=runtime, k=number_claims, p=cat_frequency) + if approx and p < 0.01: + # Skip if probability of this number of events is close to trivial + continue + if number_claims == max_claims: + # Add on all the events where there are more cats than the max number of claims + p += scipy.stats.binom.sf(n=runtime, k=number_claims, p=cat_frequency) + + if number_claims == 0: + # [1, 0, 0, ...] + dist_this_number = np.array([1.0] + [0] * exposure) + else: + dist_this_number = claim_probabilities.copy() + if number_claims > 1: + dist_this_number = fftconvolve_n(claim_probabilities, number_claims) + if len(dist_this_number) > len(total_claim_probabilities): + total_claim_probabilities += ( + p * dist_this_number[: len(total_claim_probabilities)] + ) + else: + total_claim_probabilities[: len(dist_this_number)] += p * dist_this_number + + # Try to minimise the effect of numerical error and approximation by normalising + total_claim_probabilities = total_claim_probabilities / np.sum( + total_claim_probabilities + ) + total_claim_cumulative_probabilities = np.cumsum(total_claim_probabilities) + + if total_claim_cumulative_probabilities[-1] >= 1 - var_tail_size: + var = total_claim_values[ + np.searchsorted( + total_claim_cumulative_probabilities, 1 - var_tail_size, side="left" + ) + ] + else: + raise RuntimeError + + expected_total_claim = np.dot(total_claim_values, total_claim_probabilities) + expected_square_total_claim = np.dot( + total_claim_values ** 2, total_claim_probabilities + ) + variance = expected_square_total_claim - expected_total_claim ** 2 + std = np.sqrt(variance) + + assert max(round(expected_total_claim), var) <= exposure + if factor: + expected_total_claim *= factor + var = round(var * factor) + exposure = round(exposure * factor) + std = round(std * factor) + + return float(expected_total_claim), float(std), int(var), int(exposure) + + +def get_contract_risk( + risk: RiskProperties, params: dict, max_claims=0 +) -> Tuple[float, float, int, int]: + tail_size = params["value_at_risk_tail_probability"] + cat_sep = params["event_time_mean_separation"] + for prop in [ + "number_risks", + "value", + "deductible_fraction", + "limit_fraction", + "runtime", + ]: + assert risk.__dict__[prop] is not None + assert risk.deductible_fraction < risk.limit_fraction <= 1 + return contract_risk( + number_risks=risk.number_risks, + value_per_risk=int(round(risk.value / risk.number_risks)), + deductible=int(round(risk.deductible_fraction * risk.value)), + limit=int(round(risk.limit_fraction * risk.value)), + runtime=risk.runtime, + max_claims=max_claims, + var_tail_size=tail_size, + cat_frequency=1 / cat_sep, + ) + + +def fftconvolve_n(inp, n, axes=None): + """ + Uses fft to convolve an array with itself n times. + This function is a modification of scipy.signal.fftconvolve. + Takes an input of a single numpy array-like and convolves it with itself n times using a fast fourier transform. + + Args: + inp: + n: + axes: + + Returns: + + """ + from scipy.fftpack.helper import _init_nd_shape_and_axes_sorted + import scipy.fftpack as fftpack + + inp = np.asarray(inp) + + if inp.ndim == 0: # scalar input + return inp ** n + elif inp.size == 0: + return np.array([]) + + _, axes = _init_nd_shape_and_axes_sorted(inp, shape=None, axes=axes) + + if axes is not None and not axes.size: + raise ValueError("when provided, axes cannot be empty") + + shape = np.array(inp.shape) + shape[axes] = shape[axes] * n - (n - 1) # This is vital! + + # Speed up FFT by padding to optimal size for FFTPACK + fshape = [fftpack.helper.next_fast_len(d) for d in shape[axes]] + fslice = tuple([slice(sz) for sz in shape]) + + sp = np.fft.rfftn(inp, fshape, axes=axes) + ret = np.fft.irfftn(sp ** n, fshape, axes=axes)[fslice].copy() + + return ret + + +def main(): + import matplotlib.pyplot as plt + + for _ in range(100): + risks = [] + for res in np.logspace(start=1, stop=5, num=50): + risks.append( + contract_risk( + number_risks=200, + value_per_risk=10000, + deductible=100 * 10000, + limit=200 * 10000, + runtime=12, + max_claims=1, + max_res=int(res), + ) + ) + # risks = np.asarray(risks) + # plt.plot(np.logspace(start=1, stop=5, num=50), risks, "rx") + # for statistic in risks[-1]: + # plt.axhline(y=statistic, lw=1) + # plt.xscale('log') + # plt.legend() + # plt.show() + + # import distributiontruncated + # + # est_dist = distributiontruncated.TruncatedDistWrapper( + # lower_bound=0.25, + # upper_bound=1.0, + # dist=scipy.stats.pareto(b=2, loc=0, scale=0.25), + # ) + # # lb = 1 + # # ub = 100 + # # ns = range(lb, ub) + # # vars = [] + # # for n in ns: + # # vars.append(AggregateDistribtion(n=n).ppf(1 - 0.025)) + # # plt.plot(range(lb, ub), vars, label="VaR for each n") + # # plt.plot(range(lb, ub), [est_dist.ppf(1 - 0.025) for _ in range(lb, ub)], + # label="Estimated VaR using truncated Pareto") + # ns = [1, 2, 3, 4, 5, 10, 20, 50, 100] + # x = np.linspace(0, 1, num=100) + # + # for n in ns: + # dist = AggregateDistribtion(n=n) + # plt.plot(x, dist.cdf(x), label=f"n = {n}") + # + # plt.plot(x, est_dist.cdf(x), label="Truncated Pareto") + # plt.legend() + # plt.show() + + +if __name__ == "__main__": + main() diff --git a/distributiontruncated.py b/distributiontruncated.py index a6916ca..eadf97d 100644 --- a/distributiontruncated.py +++ b/distributiontruncated.py @@ -13,7 +13,7 @@ def __init__(self, dist, lower_bound=0.0, upper_bound=1.0): self.upper_bound = upper_bound assert self.upper_bound > self.lower_bound - @functools.lru_cache(maxsize=1024) + # @functools.lru_cache(maxsize=1024) def pdf(self, x): # TODO: begone, arrays x = np.array(x, ndmin=1) @@ -28,7 +28,7 @@ def pdf(self, x): r = float(r) return r - @functools.lru_cache(maxsize=1024) + # @functools.lru_cache(maxsize=1024) def cdf(self, x): # TODO: rm arrays x = np.array(x, ndmin=1) @@ -46,7 +46,7 @@ def cdf(self, x): r = float(r) return r - @functools.lru_cache(maxsize=1024) + # @functools.lru_cache(maxsize=1024) def ppf(self, x): # TODO: probably no need for arrays x = np.array(x, ndmin=1) @@ -56,6 +56,7 @@ def ppf(self, x): ) def rvs(self, size=1): + # We could also use inverse transform sampling # Sample RVs from the original distribution and then throw out the ones that are outside the bounds. init_sample_size = int(ceil(size / self.normalizing_factor * 1.1)) sample = self.dist.rvs(size=init_sample_size) diff --git a/insurancefirms.py b/insurancefirms.py index f3d93c6..1be8a24 100644 --- a/insurancefirms.py +++ b/insurancefirms.py @@ -330,6 +330,8 @@ def reinsure_tranche( deductible=deductible_fraction * total_value, limit=limit_fraction * total_value, ) # TODO: make runtime into a parameter + assert risk.deductible_fraction < risk.limit_fraction <= 1 + reinsurance_type = self.decide_reinsurance_type(risk) if reinsurance_type == "reinsurance": if purpose == "newrisk": @@ -341,6 +343,7 @@ def reinsure_tranche( elif reinsurance_type == "catbond": # The whole premium is transfered to the bond at creation, not periodically # TODO: Should the premium be periodic as for any other reinsurance? Would help, probably + # TODO: Allow for catbonds to be paid out multiple times risk.periodized_total_premium = 0 total_premium = ( self.get_catbond_price(risk) @@ -535,13 +538,16 @@ def refresh_reinrisk( owner=self, insurancetype="excess-of-loss", number_risks=number_risks, - deductible_fraction=old_contract.deductible / total_value, - limit_fraction=old_contract.limit / total_value, + deductible_fraction=min(old_contract.deductible / total_value, 1), + limit_fraction=min(old_contract.limit / total_value, 1), periodized_total_premium=periodized_total_premium, runtime=12, expiration=time + 12, risk_factor=avg_risk_factor, ) + if risk.deductible_fraction == risk.limit_fraction == 1: + return None + assert risk.deductible_fraction < risk.limit_fraction <= 1 return risk diff --git a/insurancesimulation.py b/insurancesimulation.py index 827ae4f..af7573e 100644 --- a/insurancesimulation.py +++ b/insurancesimulation.py @@ -69,7 +69,6 @@ def __init__( "Override one-riskmodel case (this is to ensure all other parameters are truly identical for comparison runs)" if override_no_riskmodels: simulation_parameters["no_riskmodels"] = override_no_riskmodels - # QUERY: why do we keep duplicates of so many simulation parameters (and then not use many of them)? self.number_riskmodels: int = simulation_parameters["no_riskmodels"] "Save parameters, sets parameters of sim according to isleconfig.py" @@ -81,13 +80,12 @@ def __init__( self.simulation_parameters: MutableMapping = simulation_parameters self.simulation_parameters["simulation"] = self - # QUERY: The distribution given is bounded by [0.25, 1.0]. Should this always be the case? "Unpacks parameters and sets distributions" self.damage_distribution: "Distribution" = damage_distribution self.catbonds_off: bool = simulation_parameters["catbonds_off"] self.reinsurance_off: bool = simulation_parameters["reinsurance_off"] - # TODO: research whether this is accurate, is it different for different types of catastrophy? + # TODO: It's actually geometric (in effect) - change? self.cat_separation_distribution = scipy.stats.expon( 0, simulation_parameters["event_time_mean_separation"] ) @@ -108,8 +106,9 @@ def __init__( else: self.risk_factor_distribution = Constant(loc=1.0) # self.risk_value_distribution = scipy.stats.uniform(loc=100, scale=9900) - # TODO: Should this be a parameter - self.risk_value_distribution = Constant(loc=1000) + self.risk_value_distribution = Constant( + loc=simulation_parameters["value_per_risk"] + ) risk_factor_mean = self.risk_factor_distribution.mean() @@ -146,7 +145,6 @@ def __init__( self.bank = CentralBank(self.cash) "set up risk categories" - # QUERY What do risk categories represent? Different types of catastrophes? self.riskcategories: Sequence[int] = list( range(self.simulation_parameters["no_categories"]) ) @@ -168,10 +166,9 @@ def __init__( else: # Otherwise the schedules and damages are generated. raise Exception("No event schedules and damages supplied") - "Set up risks" + """Set up risks""" risk_value_mean = self.risk_value_distribution.mean() - # QUERY: What are risk factors? Are "risk_factor" values other than one meaningful at present? rrisk_factors = self.risk_factor_distribution.rvs( size=self.simulation_parameters["no_risks"] ) @@ -252,7 +249,7 @@ def __init__( "Agent lists" self.reinsurancefirms: Collection = [] self.insurancefirms: Collection = [] - self.catbonds: Collection = [] + self.catbonds: list = [] "Lists of agent weights" self.insurers_weights: MutableMapping[int, float] = {} @@ -310,7 +307,7 @@ def initialize_agent_parameters( ): """General function for initialising the agent parameters Takes the firm type as argument, also needing sim params and risk configs - Creates the agent parameters of both firm types for the initial number specified in isleconfig.py + Creates the agent parameters of both firm types for the initial number specified in isleconfig.py Returns None""" if firmtype == "insurancefirm": no_firms = simulation_parameters["no_insurancefirms"] @@ -486,6 +483,7 @@ def iterate(self, t: int): t: Integer, current time step Returns None""" + damage_extent = None self._time = t if isleconfig.verbose: print() @@ -536,7 +534,7 @@ def iterate(self, t: int): # Provide government aid if damage severe enough if isleconfig.aid_relief: self.bank.adjust_aid_budget(time=t) - if "damage_extent" in locals(): + if damage_extent is not None: op_firms = [firm for firm in self.insurancefirms if firm.operational] aid_dict = self.bank.provide_aid(op_firms, damage_extent, time=t) for key in aid_dict.keys(): @@ -612,11 +610,13 @@ def iterate(self, t: int): if isleconfig.show_network: self.RN.visualize() - if isleconfig.save_network and t == ( - self.simulation_parameters["max_time"] - 800 - ): - self.RN.save_network_data() - print("Network data has been saved to data/network_data.dat") + + # TODO: Investigate and fix? + # if isleconfig.save_network and t == ( + # self.simulation_parameters["max_time"] - 800 + # ): + # self.RN.save_network_data() + # print("Network data has been saved to data/network_data.dat") # import matplotlib.pyplot as plt # @@ -674,44 +674,36 @@ def save_data(self): ] """ prepare dict """ - current_log = {} # TODO: rewrite this as a single dictionary literal? - current_log["total_cash"] = total_cash_no - current_log["total_excess_capital"] = total_excess_capital - current_log["total_profitslosses"] = total_profitslosses - current_log["total_contracts"] = total_contracts_no - current_log["total_operational"] = operational_no - current_log["total_reincash"] = total_reincash_no - current_log["total_reinexcess_capital"] = total_reinexcess_capital - current_log["total_reinprofitslosses"] = total_reinprofitslosses - current_log["total_reincontracts"] = total_reincontracts_no - current_log["total_reinoperational"] = reinoperational_no - current_log["total_catbondsoperational"] = catbondsoperational_no - current_log["market_premium"] = self.market_premium - current_log["market_reinpremium"] = self.reinsurance_market_premium - current_log["cumulative_bankruptcies"] = self.cumulative_bankruptcies - current_log["cumulative_market_exits"] = self.cumulative_market_exits - current_log[ - "cumulative_unrecovered_claims" - ] = self.cumulative_unrecovered_claims - current_log["cumulative_claims"] = self.cumulative_claims - current_log["cumulative_bought_firms"] = self.cumulative_bought_firms - current_log[ - "cumulative_nonregulation_firms" - ] = self.cumulative_nonregulation_firms - - """ add agent-level data to dict""" - current_log["insurance_firms_cash"] = insurance_firms - current_log["reinsurance_firms_cash"] = reinsurance_firms - current_log["market_diffvar"] = self.compute_market_diffvar() - - current_log["individual_contracts"] = [ - len(firm.underwritten_contracts) for firm in self.insurancefirms - ] - - current_log["reinsurance_contracts"] = [ - len(firm.underwritten_contracts) for firm in self.reinsurancefirms - ] - + current_log = { + "total_cash": total_cash_no, + "total_excess_capital": total_excess_capital, + "total_profitslosses": total_profitslosses, + "total_contracts": total_contracts_no, + "total_operational": operational_no, + "total_reincash": total_reincash_no, + "total_reinexcess_capital": total_reinexcess_capital, + "total_reinprofitslosses": total_reinprofitslosses, + "total_reincontracts": total_reincontracts_no, + "total_reinoperational": reinoperational_no, + "total_catbondsoperational": catbondsoperational_no, + "market_premium": self.market_premium, + "market_reinpremium": self.reinsurance_market_premium, + "cumulative_bankruptcies": self.cumulative_bankruptcies, + "cumulative_market_exits": self.cumulative_market_exits, + "cumulative_unrecovered_claims": self.cumulative_unrecovered_claims, + "cumulative_claims": self.cumulative_claims, + "cumulative_bought_firms": self.cumulative_bought_firms, + "cumulative_nonregulation_firms": self.cumulative_nonregulation_firms, + "insurance_firms_cash": insurance_firms, + "reinsurance_firms_cash": reinsurance_firms, + "market_diffvar": self.compute_market_diffvar(), + "individual_contracts": [ + len(firm.underwritten_contracts) for firm in self.insurancefirms + ], + "reinsurance_contracts": [ + len(firm.underwritten_contracts) for firm in self.reinsurancefirms + ], + } """ call to Logger object """ self.logger.record_data(current_log) @@ -736,6 +728,7 @@ def _inflict_peril(self, categ_id: int, damage: float, t: int): ] if isleconfig.verbose: print("**** PERIL", damage) + # TODO: Why beta in particular? damagevalues = np.random.beta( a=1, b=1.0 / damage - 1, size=len(affected_contracts) ) @@ -803,7 +796,7 @@ def _reset_insurance_weights(self): if operational_no > 0: - if risks_no > operational_no: # TODO: as above + if risks_no > operational_no: weights = risks_no / operational_no for insurer in self.insurancefirms: self.insurers_weights[insurer.id] = math.floor(weights) @@ -813,7 +806,6 @@ def _reset_insurance_weights(self): self.insurers_weights[operational_firms[s].id] += 1 def _update_model_counters(self): - # TODO: this and the next look like they could be cleaner for insurer in self.insurancefirms: if insurer.operational: for i in range(len(self.inaccuracy)): @@ -929,7 +921,6 @@ def get_cat_bond_price( np_reinsurance_deductible_fraction: Type Integer Returns: Calculated catbond price.""" - # TODO: implement function dependent on total capital in cat bonds and on deductible () # TODO: make max_reduction and max_cat_bond_surcharge into simulation_parameters ? if self.catbonds_off: return float("inf") @@ -964,7 +955,7 @@ def remove_reinrisks( elif firm is not None: self.reinrisks = [risk for risk in self.reinrisks if risk.owner is not firm] - def get_reinrisks(self) -> Sequence[RiskProperties]: + def get_reinrisks(self) -> Collection[RiskProperties]: """Method for shuffling reinsurance risks Returns: reinsurance risks""" np.random.shuffle(self.reinrisks) @@ -983,8 +974,6 @@ def solicit_insurance_requests( for risk in insurer.risks_retained: risks_to_be_sent.append(risk) - # QUERY: what actually is InsuranceFirm.risks_kept? Are we resending all their existing risks? - # Or is it just a list of risk that have rolled over and so need to be re-evaluated insurer.risks_retained = [] np.random.shuffle(risks_to_be_sent) @@ -1060,7 +1049,6 @@ def firm_enters_market( Returns: True if firm can enter market False if firm cannot enter market""" - # TODO: Do firms really enter the market randomly, with at most one in each timestep? if prob == -1: if agent_type == "InsuranceFirm": prob = self.simulation_parameters[ @@ -1260,19 +1248,19 @@ def get_risk_share(self, firm: "MetaInsuranceOrg") -> float: else: return firm.number_underwritten_contracts() / total - def get_total_firm_cash(self, type): + def get_total_firm_cash(self, firm_type): """Method to get sum of all cash of firms of a given type. Called from consider_buyout() but could be used for setting market premium. Accepts: type: Type String. Returns: sum_capital: Type Integer.""" - if type == "insurer": + if firm_type == "insurer": sum_capital = sum([agent.get_cash() for agent in self.insurancefirms]) - elif type == "reinsurer": + elif firm_type == "reinsurer": sum_capital = sum([agent.get_cash() for agent in self.reinsurancefirms]) else: - print("No accepted type for cash") + raise ValueError("No accepted type for cash") return sum_capital def add_firm_to_be_sold(self, firm, time, reason): @@ -1287,24 +1275,24 @@ def add_firm_to_be_sold(self, firm, time, reason): elif firm.is_reinsurer: self.selling_reinsurance_firms.append([firm, time, reason]) - def get_firms_to_sell(self, type): + def get_firms_to_sell(self, firm_type): """Method to get list of firms that are up for selling based on type. Accepts: type: Type String. Returns: firms_info_sent: Type List of Lists. Contains firm, type and reason.""" - if type == "insurer": + if firm_type == "insurer": firms_info_sent = [ (firm, time, reason) for firm, time, reason in self.selling_insurance_firms ] - elif type == "reinsurer": + elif firm_type == "reinsurer": firms_info_sent = [ (firm, time, reason) for firm, time, reason in self.selling_reinsurance_firms ] else: - print("No accepted type for selling") + raise ValueError("No accepted type for selling") return firms_info_sent def remove_sold_firm(self, firm, time, reason): diff --git a/isleconfig.py b/isleconfig.py index 60563e0..7cd23a9 100644 --- a/isleconfig.py +++ b/isleconfig.py @@ -105,6 +105,7 @@ "upper_price_limit": 1.2, "lower_price_limit": 0.85, "no_risks": 20000, + "value_per_risk": 1000, # Determines the maximum upscaling of premiums based on insurer size - set to 1 to disable scaled premiums. # High values will give bigger insurers more money # Values between 0 and 1 will make premiums decrease for bigger insurers. diff --git a/metainsuranceorg.py b/metainsuranceorg.py index 573fb9d..ec202a9 100644 --- a/metainsuranceorg.py +++ b/metainsuranceorg.py @@ -77,9 +77,7 @@ def get_mean_std(x: Tuple[float, ...]) -> Tuple[float, float]: class MetaInsuranceOrg(GenericAgent): - def __init__( - self, simulation_parameters: Mapping, agent_parameters: AgentProperties - ): + def __init__(self, simulation_parameters: dict, agent_parameters: AgentProperties): """Constructor method. Accepts: Simulation_parameters: Type DataDict @@ -88,7 +86,7 @@ def __init__( and insurance firm classes. Initialises all necessary values provided by config file.""" super().__init__() self.simulation: "InsuranceSimulation" = simulation_parameters["simulation"] - self.simulation_parameters: Mapping = simulation_parameters + self.simulation_parameters: dict = simulation_parameters self.contract_runtime_dist = scipy.stats.randint( simulation_parameters["mean_contract_runtime"] - simulation_parameters["contract_runtime_halfspread"], @@ -400,8 +398,8 @@ def collect_process_evaluate_risks( risks_per_categ = self.risks_reinrisks_organizer(new_risks) if risks_per_categ != [[]] * self.simulation_no_risk_categories: for repetition in range(self.recursion_limit): - # TODO: find an efficient way to stop the recursion if there are no more risks to accept or if it is - # not accepting any more over several iterations. Done, maybe? + # TODO: find an efficient way to stop the recursion if there are no more risks to accept or if + # it is not accepting any more over several iterations. Done, maybe? # Here we process all the new risks in order to keep the portfolio as balanced as possible. has_accepted_risks, not_accepted_risks = self.process_newrisks_insurer( risks_per_categ, @@ -522,7 +520,6 @@ def dissolve(self, time: int, record: str): else: pass # Set to None so trying to add more obligations throws an error - # TODO: this is okay, right? self.obligations = None self.operational = False @@ -713,7 +710,7 @@ def get_excess_capital(self) -> float: def number_underwritten_contracts(self) -> int: return len(self.underwritten_contracts) - def get_underwritten_contracts(self) -> Sequence["MetaInsuranceContract"]: + def get_underwritten_contracts(self) -> Collection["MetaInsuranceContract"]: return self.underwritten_contracts def get_profitslosses(self) -> float: diff --git a/reinsurancecontract.py b/reinsurancecontract.py index b21dede..e141efa 100644 --- a/reinsurancecontract.py +++ b/reinsurancecontract.py @@ -8,7 +8,7 @@ from insurancefirms import InsuranceFirm from metainsuranceorg import MetaInsuranceOrg from genericclasses import RiskProperties - from catbond import Catbond + from catbond import CatBond class ReinsuranceContract(metainsurancecontract.MetaInsuranceContract): @@ -48,6 +48,7 @@ def __init__( limit_fraction, reinsurance, ) + self.times_triggered = 0 # self.is_reinsurancecontract = True self.property_holder: "InsuranceFirm" if self.insurancetype not in ["excess-of-loss", "proportional"]: @@ -57,6 +58,25 @@ def __init__( else: assert self.contract is not None + # import distributionaggregate + # if type(self.insurer).__name__ == "CatBond": + # max_claims = 1 + # expected_total_claim, std, var, exposure = distributionaggregate.get_contract_risk( + # risk, params=self.insurer.simulation_parameters, max_claims=max_claims, + # ) + # total_premium = self.insurer.per_period_dividend * self.runtime + # # Initial purchase cost is exposure + 1 + # expected_return = ((exposure + 1) - expected_total_claim + total_premium) / (exposure + 1) - 1 + # print(f"Catbond created with total return of {expected_return:.1%}") + # else: + # max_claims = 0 + # expected_total_claim, std, var, exposure = distributionaggregate.get_contract_risk( + # risk, params=self.insurer.simulation_parameters, max_claims=max_claims, + # ) + # total_premium = self.periodized_premium * self.runtime + # expected_return = total_premium - expected_total_claim + # print(f"Reinsurance contract created with expected return of MU{expected_return:.0f}") + def explode( self, time: int, uniform_value: None = None, damage_extent: float = None ): @@ -80,6 +100,7 @@ def explode( # Since EoL reinsurance isn't triggered until the insurer manually makes a claim, this would mean that # proportional reinsurance pays out a turn earlier than EoL. As such, proportional insurance claims are # delayed for 1 turn. + self.times_triggered += 1 if self.insurancetype == "excess-of-loss": claim = min(self.limit, damage_extent) - self.deductible self.insurer.receive_obligation( @@ -103,11 +124,9 @@ def explode( self.expiration = time # self.terminating = True elif type(self.insurer).__name__ == "CatBond": - # Catbonds can only pay out a certain amount in their lifetime, so we update the reinsurance coverage + # Catbonds can only pay out a certain value in their lifetime, so we update the reinsurance coverage # for the issuer - # TODO: Allow for catbonds that can pay out multiple times? - self.insurer: "Catbond" - self.insurer.triggered += 1 + self.insurer: "CatBond" remaining_cb_cash = self.insurer.get_available_cash(time) - claim assert remaining_cb_cash >= 0 if remaining_cb_cash < 2: @@ -126,6 +145,12 @@ def explode( else: # If the catbond still has enough money to pay out the full exposure, no need to change anything. pass + # TODO: These should be parameters + elif self.times_triggered % 2 == 0: + # For a standard reinsurance contract, every two triggers the premium goes up by 10% + self.periodized_premium *= 1.1 + for i in range(len(self.payment_values)): + self.payment_values[i] *= 1.1 def mature(self, time: int): """Mature method. diff --git a/riskmodel.py b/riskmodel.py index adf028d..e425c45 100644 --- a/riskmodel.py +++ b/riskmodel.py @@ -1,5 +1,5 @@ import math -from copy import deepcopy +import copy import numpy as np @@ -43,8 +43,7 @@ def __init__( self.damage_distribution: MutableSequence["Distribution"] = [ damage_distribution for _ in range(self.category_number) ] - self.underlying_distribution = deepcopy(self.damage_distribution) - # self.inaccuracy = np.random.uniform(9/10., 10/9., size=self.category_number) + self.underlying_distribution = copy.deepcopy(self.damage_distribution) self.inaccuracy: Sequence[float] = inaccuracy def get_ppf(self, categ_id: int, tail_size: float) -> float: @@ -253,7 +252,6 @@ def evaluate_excess_of_loss( offered risk (if applicable) is then calculated (should only be one).""" cash_left_by_categ = np.copy(cash) assert len(cash_left_by_categ) == self.category_number - # prepare variables additional_required = np.zeros(self.category_number) additional_var_per_categ = np.zeros(self.category_number) @@ -263,9 +261,6 @@ def evaluate_excess_of_loss( for categ_id in range(self.category_number): categ_risks = risks_by_categ[categ_id] - # TODO: allow for different risk distributions for different categories - # TODO: factor in risk_factors - # QUERY: both done? percentage_value_at_risk = self.get_ppf( categ_id=categ_id, tail_size=self.var_tail_prob ) @@ -348,7 +343,6 @@ def evaluate( # TODO: split this into two functions # ensure that any risk to be considered supplied directly as argument is non-proportional/excess-of-loss assert (offered_risk is None) or offered_risk.insurancetype == "excess-of-loss" - # construct cash_left_by_categ as a sequence, defining remaining liquidity by category if not isinstance(cash, (np.ndarray, list)): cash_left_by_categ = np.ones(self.category_number) * cash diff --git a/sum_distribution.py b/sum_distribution.py new file mode 100644 index 0000000..0849e10 --- /dev/null +++ b/sum_distribution.py @@ -0,0 +1,155 @@ +import numpy as np +import scipy.stats +import scipy.integrate +import scipy.interpolate +import distributiontruncated +import functools +import matplotlib.pyplot as plt +from mpl_toolkits.mplot3d import Axes3D +import pickle as pkl +import multiprocessing as mp +import itertools + +non_truncated = scipy.stats.pareto(b=2, loc=0, scale=0.25) + +damage_distribution = distributiontruncated.TruncatedDistWrapper( + lower_bound=0.25, upper_bound=1.0, dist=non_truncated +) + + +@functools.lru_cache(maxsize=4096) +def sum_beta_pdf(damage, n, resolution=5000): + # Calculate the pdf of the sum of n beta variables + x = np.linspace(start=0, stop=1, num=resolution) + # the pdf of beta is inf at 1, so we slightly perturb the maximum point to avoid making a mess + w = 5 + x[-1] = (w * x[-1] + x[-2]) / (w + 1) + y = scipy.stats.beta(a=1, b=1 / damage - 1).pdf(x) + # Turn y into a pmf + y = y / sum(y) + # Setting n=n * resolution - (n - 1) is very important + pdf = np.fft.irfft(np.fft.rfft(y, n=n * resolution - (n - 1)) ** n) + # Go from a pmf back to a pdf + pdf = pdf * resolution + x_new = np.linspace(start=0, stop=n, num=len(pdf)) + return scipy.interpolate.interp1d( + x_new, pdf, kind="cubic", bounds_error=False, fill_value=0, assume_sorted=True + ) + + +def plot_mc(ns, no_mc_sims=100000, norm_approx=False, kde=True, hist=True): + assert kde or hist + print("Doing MC simulation for n = " + ", ".join([str(n) for n in ns])) + for n in ns: + results = [] + impacts = damage_distribution.rvs(no_mc_sims) + for impact in impacts: + if len(results) % 100 == 0: + print(f"\rn = {n}: {len(results)/no_mc_sims:.1%}", end="") + if not norm_approx: + damage = sum(scipy.stats.beta(a=1, b=1 / impact - 1).rvs(size=n)) + else: + damage = scipy.stats.norm( + loc=n * impact, + scale=np.sqrt(n * impact ** 2 * (1 - impact) / (1 + impact)), + ).rvs() + results.append(damage) + print(f"\rn = {n}: 100.0%") + results = np.array(results) + x = np.linspace(start=results.min(), stop=results.max(), num=500) + if kde: + bw = 0.03 + approx_dist = scipy.stats.gaussian_kde(results, bw_method=bw) + y = approx_dist.evaluate(x) + plt.plot(x, y, label=f"Monte Carlo KDE, n = {n}") + if hist: + plt.hist( + results, + bins="auto", + density=True, + histtype="step", + label=f"Monte Carlo Histogram, n = {n}", + ) + + +@functools.lru_cache(maxsize=4096) +def integrand(x, z, n): + approx = n >= 20 + if approx: + return scipy.stats.norm( + loc=n * x, scale=np.sqrt(n * x ** 2 * (1 - x) / (1 + x)) + ).pdf(z) * damage_distribution.pdf(x) + else: + return sum_beta_pdf(x, n)(z) * damage_distribution.pdf(x) + + +def calc_pdf(z, n): + return scipy.integrate.quad(integrand, 0.25, 1, args=(z, n))[0] + + +def make_save_pdf_data(ns): + resolution = 500 + with mp.Pool(processes=round(mp.cpu_count() / 2)) as p: + tasks = {} + for n in ns: + args = zip( + np.linspace(start=0, stop=n, num=resolution), itertools.repeat(n) + ) + tasks[n] = p.starmap_async(calc_pdf, args) + results = { + n: (np.linspace(start=0, stop=n, num=resolution), tasks[n].get()) + for n in ns + } + with open("./pdf_data.pkl", "wb") as wfile: + pkl.dump(results, wfile, protocol=pkl.HIGHEST_PROTOCOL) + + +def plot_pdfs(pdfs, ns=None, normalise=False): + if ns is None: + ns = pdfs.keys() + for n in ns: + x = np.array(pdfs[n][0]) + y = np.array(pdfs[n][1]) + if normalise: + x = x / n + y = y * n + plt.plot(x, y, label=f"Calculated pdf, n = {n}") + + +def plot_3d(pdfs): + x = [] + y = [] + z = [] + for n in pdfs: + assert len(pdfs[n][0]) == len(pdfs[n][1]) + x += list(np.array(pdfs[n][0]) / n) + z += list(np.array(pdfs[n][1]) * n) + y += [n] * len(pdfs[n][0]) + x = np.array(x) + y = np.array(y) + z = np.array(z) + print(len(x), len(y), len(z)) + fig = plt.figure(figsize=(16, 10)) + ax = fig.add_subplot(111, projection="3d") + surf = ax.plot_trisurf(x[::100], y[::100], z[::100], label="pdf surface") + surf._facecolors2d = surf._facecolors3dp + surf._edgecolors2d = surf._edgecolors3d + + +# ns = range(1, 2001) + +# make_save_pdf_data(ns) + +with open("./pdf_data.pkl", "rb") as rfile: + pdfs: dict = pkl.load(rfile) +# ns = list(pdfs.keys()) +ns = [10, 25, 50, 100, 200] +plot_pdfs(pdfs, ns, normalise=True) +x = np.linspace(start=0, stop=1, num=500) +plt.plot(x, damage_distribution.pdf(x), label="Base truncated Pareto") +# plot_mc(ns, kde=True, no_mc_sims=1000000) +plt.legend() +# plt.axis([0, 1050, 0, plt.axis()[3]]) +plt.show() + +# TODO: Probably shouldn't be using rightmost endpoint throughout (doesn't make much difference probably but is still wrong) From 97e767b4ca04cb908bfdc0fc15ddb8cc240907fb Mon Sep 17 00:00:00 2001 From: Chris Hughes Date: Mon, 19 Aug 2019 12:07:11 +0100 Subject: [PATCH 097/125] Merge in a couple of changes from branch "sum_distributions" --- genericclasses.py | 23 +++++++++++++++++------ insurancefirms.py | 10 ++++++++-- insurancesimulation.py | 15 ++++++--------- isleconfig.py | 5 +++-- logger.py | 2 +- metainsuranceorg.py | 9 +++------ reinsurancecontract.py | 4 +++- riskmodel.py | 4 ---- setup_simulation.py | 3 ++- start.py | 20 +++++++++----------- 10 files changed, 52 insertions(+), 43 deletions(-) diff --git a/genericclasses.py b/genericclasses.py index f3b2cd7..a7ca5c5 100644 --- a/genericclasses.py +++ b/genericclasses.py @@ -62,17 +62,26 @@ def _pay(self, obligation: "Obligation"): f"Redirecting payment with purpose {purpose} due to non-operational firm {recipient.id}" ) recipient = recipient.creditor - self.cash -= amount - if purpose != "dividend": - self.profits_losses -= amount - recipient.receive(amount) + if self.get_operational(): + self.cash -= amount + if purpose != "dividend": + self.profits_losses -= amount + recipient.receive(amount) + else: + if isleconfig.verbose: + print(f"Payment not processed as firm {self.id} is not operational") - def get_operational(self): + def get_operational(self) -> bool: """Method to return boolean of if agent is operational. Only used as check for payments. No accepted values Returns Boolean""" return self.operational + def iterate(self, time: int): + raise NotImplementedError( + "Iterate is not implemented in GenericAgent, should have be overridden" + ) + def _effect_payments(self, time: int): """Method for checking if any payments are due. Accepts: @@ -80,6 +89,8 @@ def _effect_payments(self, time: int): No return value Method checks firms list of obligations to see if ay are due for this time, then pays them. If the firm does not have enough cash then it enters illiquity, leaves the market, and matures all contracts.""" + # TODO: don't really want to be reconstructing lists every time (unless the obligations are naturally sorted by + # time, in which case this could be done slightly better). Low priority, but something to consider due = [item for item in self.obligations if item.due_time <= time] self.obligations = [item for item in self.obligations if item.due_time > time] sum_due = sum([item.amount for item in due]) @@ -90,7 +101,7 @@ def _effect_payments(self, time: int): for obligation in due: self._pay(obligation) - def enter_illiquidity(self, time, sum_due): + def enter_illiquidity(self, time: int, sum_due: float): raise NotImplementedError("Should've been overridden") def receive_obligation( diff --git a/insurancefirms.py b/insurancefirms.py index 730f432..4f50b57 100644 --- a/insurancefirms.py +++ b/insurancefirms.py @@ -331,6 +331,8 @@ def reinsure_tranche( deductible=deductible_fraction * total_value, limit=limit_fraction * total_value, ) # TODO: make runtime into a parameter + assert risk.deductible_fraction < risk.limit_fraction <= 1 + reinsurance_type = self.decide_reinsurance_type(risk) if reinsurance_type == "reinsurance": if purpose == "newrisk": @@ -342,6 +344,7 @@ def reinsure_tranche( elif reinsurance_type == "catbond": # The whole premium is transfered to the bond at creation, not periodically # TODO: Should the premium be periodic as for any other reinsurance? Would help, probably + # TODO: Allow for catbonds to be paid out multiple times risk.periodized_total_premium = 0 total_premium = ( self.get_catbond_price(risk) @@ -535,13 +538,16 @@ def refresh_reinrisk( owner=self, insurancetype="excess-of-loss", number_risks=number_risks, - deductible_fraction=old_contract.deductible / total_value, - limit_fraction=old_contract.limit / total_value, + deductible_fraction=min(old_contract.deductible / total_value, 1), + limit_fraction=min(old_contract.limit / total_value, 1), periodized_total_premium=periodized_total_premium, runtime=12, expiration=time + 12, risk_factor=avg_risk_factor, ) + if risk.deductible_fraction == risk.limit_fraction == 1: + return None + assert risk.deductible_fraction < risk.limit_fraction <= 1 return risk diff --git a/insurancesimulation.py b/insurancesimulation.py index e4deeaa..1344ac4 100644 --- a/insurancesimulation.py +++ b/insurancesimulation.py @@ -69,7 +69,6 @@ def __init__( "Override one-riskmodel case (this is to ensure all other parameters are truly identical for comparison runs)" if override_no_riskmodels: simulation_parameters["no_riskmodels"] = override_no_riskmodels - # QUERY: why do we keep duplicates of so many simulation parameters (and then not use many of them)? self.number_riskmodels: int = simulation_parameters["no_riskmodels"] "Save parameters, sets parameters of sim according to isleconfig.py" @@ -81,13 +80,12 @@ def __init__( self.simulation_parameters: MutableMapping = simulation_parameters self.simulation_parameters["simulation"] = self - # QUERY: The distribution given is bounded by [0.25, 1.0]. Should this always be the case? "Unpacks parameters and sets distributions" self.damage_distribution: "Distribution" = damage_distribution self.catbonds_off: bool = simulation_parameters["catbonds_off"] self.reinsurance_off: bool = simulation_parameters["reinsurance_off"] - # TODO: research whether this is accurate, is it different for different types of catastrophy? + # TODO: It's actually geometric (in effect) - change? self.cat_separation_distribution = scipy.stats.expon( 0, simulation_parameters["event_time_mean_separation"] ) @@ -108,8 +106,9 @@ def __init__( else: self.risk_factor_distribution = Constant(loc=1.0) # self.risk_value_distribution = scipy.stats.uniform(loc=100, scale=9900) - # TODO: Should this be a parameter - self.risk_value_distribution = Constant(loc=1000) + self.risk_value_distribution = Constant( + loc=simulation_parameters["value_per_risk"] + ) risk_factor_mean = self.risk_factor_distribution.mean() @@ -146,7 +145,6 @@ def __init__( self.bank = CentralBank(self.cash) "set up risk categories" - # QUERY What do risk categories represent? Different types of catastrophes? self.riskcategories: Sequence[int] = list( range(self.simulation_parameters["no_categories"]) ) @@ -168,10 +166,9 @@ def __init__( else: # Otherwise the schedules and damages are generated. raise Exception("No event schedules and damages supplied") - "Set up risks" + """Set up risks""" risk_value_mean = self.risk_value_distribution.mean() - # QUERY: What are risk factors? Are "risk_factor" values other than one meaningful at present? rrisk_factors = self.risk_factor_distribution.rvs( size=self.simulation_parameters["no_risks"] ) @@ -252,7 +249,7 @@ def __init__( "Agent lists" self.reinsurancefirms: Collection = [] self.insurancefirms: Collection = [] - self.catbonds: Collection = [] + self.catbonds: list = [] "Lists of agent weights" self.insurers_weights: MutableMapping[int, float] = {} diff --git a/isleconfig.py b/isleconfig.py index 82aec17..90c0eea 100644 --- a/isleconfig.py +++ b/isleconfig.py @@ -2,14 +2,14 @@ replicating = False force_foreground = False verbose = False -showprogress = True +showprogress = False # Should network be visualized? This should be False by default, to be overridden by commandline arguments show_network = False save_network = False # Should logs be small in ensemble runs (only aggregated level data)? slim_log = True buy_bankruptcies = False -enforce_regulations = True +enforce_regulations = False aid_relief = False simulation_parameters = { @@ -106,6 +106,7 @@ "upper_price_limit": 1.2, "lower_price_limit": 0.85, "no_risks": 20000, + "value_per_risk": 1000, # Determines the maximum upscaling of premiums based on insurer size - set to 1 to disable scaled premiums. # High values will give bigger insurers more money # Values between 0 and 1 will make premiums decrease for bigger insurers. diff --git a/logger.py b/logger.py index 3791b95..e6f1da3 100644 --- a/logger.py +++ b/logger.py @@ -106,7 +106,7 @@ def record_data(self, data_dict): self.history_logs["individual_contracts"][i].append( data_dict["individual_contracts"][i] ) - if key == "reinsurance_contracts": + elif key == "reinsurance_contracts": for i in range(len(data_dict["reinsurance_contracts"])): self.history_logs["reinsurance_contracts"][i].append( data_dict["reinsurance_contracts"][i] diff --git a/metainsuranceorg.py b/metainsuranceorg.py index c755aa9..042e756 100644 --- a/metainsuranceorg.py +++ b/metainsuranceorg.py @@ -77,9 +77,7 @@ def get_mean_std(x: Tuple[float, ...]) -> Tuple[float, float]: class MetaInsuranceOrg(GenericAgent): - def __init__( - self, simulation_parameters: Mapping, agent_parameters: AgentProperties - ): + def __init__(self, simulation_parameters: dict, agent_parameters: AgentProperties): """Constructor method. Accepts: Simulation_parameters: Type DataDict @@ -88,7 +86,7 @@ def __init__( and insurance firm classes. Initialises all necessary values provided by config file.""" super().__init__() self.simulation: "InsuranceSimulation" = simulation_parameters["simulation"] - self.simulation_parameters: Mapping = simulation_parameters + self.simulation_parameters: dict = simulation_parameters self.contract_runtime_dist = scipy.stats.randint( simulation_parameters["mean_contract_runtime"] - simulation_parameters["contract_runtime_halfspread"], @@ -215,8 +213,7 @@ def __init__( self.simulation_parameters["no_categories"] ) self.market_permanency_counter = 0 - # TODO: make this into a dict - self.underwritten_risk_characterisation: Sequence[RiskChar] = [ + self.underwritten_risk_characterisation: MutableSequence[RiskChar] = [ RiskChar(0, 0, 0, 0, 0, 0) for _ in range(self.simulation_parameters["no_categories"]) ] diff --git a/reinsurancecontract.py b/reinsurancecontract.py index 516df77..7b77edf 100644 --- a/reinsurancecontract.py +++ b/reinsurancecontract.py @@ -48,6 +48,7 @@ def __init__( limit_fraction, reinsurance, ) + self.times_triggered = 0 # self.is_reinsurancecontract = True self.property_holder: "InsuranceFirm" if self.insurancetype not in ["excess-of-loss", "proportional"]: @@ -80,6 +81,7 @@ def explode( # Since EoL reinsurance isn't triggered until the insurer manually makes a claim, this would mean that # proportional reinsurance pays out a turn earlier than EoL. As such, proportional insurance claims are # delayed for 1 turn. + self.times_triggered += 1 if self.insurancetype == "excess-of-loss": claim = min(self.limit, damage_extent) - self.deductible self.insurer.receive_obligation( @@ -104,7 +106,7 @@ def explode( # self.terminating = True elif type(self.insurer).__name__ == "CatBond": # Don't want to have to import CatBond, so do it this way - # Catbonds can only pay out a certain amount in their lifetime, so we update the reinsurance coverage + # Catbonds can only pay out a certain value in their lifetime, so we update the reinsurance coverage # for the issuer # TODO: Allow for catbonds that can pay out multiple times? self.insurer: "CatBond" diff --git a/riskmodel.py b/riskmodel.py index adf028d..905ac88 100644 --- a/riskmodel.py +++ b/riskmodel.py @@ -44,7 +44,6 @@ def __init__( damage_distribution for _ in range(self.category_number) ] self.underlying_distribution = deepcopy(self.damage_distribution) - # self.inaccuracy = np.random.uniform(9/10., 10/9., size=self.category_number) self.inaccuracy: Sequence[float] = inaccuracy def get_ppf(self, categ_id: int, tail_size: float) -> float: @@ -263,9 +262,6 @@ def evaluate_excess_of_loss( for categ_id in range(self.category_number): categ_risks = risks_by_categ[categ_id] - # TODO: allow for different risk distributions for different categories - # TODO: factor in risk_factors - # QUERY: both done? percentage_value_at_risk = self.get_ppf( categ_id=categ_id, tail_size=self.var_tail_prob ) diff --git a/setup_simulation.py b/setup_simulation.py index 14e2e9a..8ba0a42 100644 --- a/setup_simulation.py +++ b/setup_simulation.py @@ -77,6 +77,7 @@ def schedule( total += int(math.ceil(separation_time)) if total < self.max_time: event_schedule.append(total) + # TODO: Why [0]? event_damage.append(self.damage_distribution.rvs()[0]) rc_event_schedule.append(event_schedule) rc_event_damage.append(event_damage) @@ -91,7 +92,7 @@ def seeds(self, replications: int): # The argument (replications) is the number of replications. """draw random variates for random seeds""" for i in range(replications): - np_seed, random_seed = scipy.stats.randint.rvs(0, 2 ** 31 - 1, size=2) + np_seed, random_seed = scipy.stats.randint.rvs(0, 2 ** 32 - 1, size=2) self.np_seed.append(np_seed) self.random_seed.append(random_seed) diff --git a/start.py b/start.py index a530b04..c6a0b2e 100644 --- a/start.py +++ b/start.py @@ -131,10 +131,9 @@ def load_simulation() -> dict: "-f", "--file", action="store", - help="the file to store the initial randomness in. Will be stored in ./data and appended with " - ".islestore (if it is not already). The default filepath is " - "./data/risk_event_schedules.islestore, which will be overwritten event if --overwrite is " - "not passed!", + help="the file to store the initial randomness in. Will be stored in ./data and appended with .islestore " + "(if it is not already). The default filepath is ./data/risk_event_schedules.islestore, which will be " + "overwritten event if --overwrite is not passed!", ) parser.add_argument( "-r", @@ -158,21 +157,20 @@ def load_simulation() -> dict: parser.add_argument( "--resume", action="store_true", - help="Resume the simulation from a previous save in " - "./data/simulation_save.pkl. All other arguments will be ignored", + help="Resume the simulation from a previous save in ./data/simulation_save.pkl. " + "All other arguments will be ignored", ) parser.add_argument( "--oneriskmodel", action="store_true", - help="allow overriding the number of riskmodels from the " - "standard config (with 1)", + help="allow overriding the number of riskmodels from the standard config (with 1)", ) parser.add_argument( "--riskmodels", type=int, choices=[1, 2, 3, 4], - help="allow overriding the number of riskmodels " - "from standard config (with 1 or other numbers). Overrides --oneriskmodel", + help="allow overriding the number of riskmodels from standard config (with 1 or other numbers)." + " Overrides --oneriskmodel", ) parser.add_argument( "--randomseed", type=float, help="allow setting of numpy random seed" @@ -180,7 +178,7 @@ def load_simulation() -> dict: parser.add_argument( "--foreground", action="store_true", - help="force foreground runs even if replication ID is given, which defaults to background runs", + help="force foreground runs even if replication ID is given (which defaults to background runs)", ) parser.add_argument( "--shownetwork", From d6ecb9436d5e6caa26ee12f5c027c5df550d287d Mon Sep 17 00:00:00 2001 From: Chris Hughes Date: Mon, 19 Aug 2019 16:32:25 +0100 Subject: [PATCH 098/125] Squashing some old TODOs --- catbond.py | 6 +- distributionreinsurance.py | 126 ++++++++++++++++++++----------------- distributiontruncated.py | 65 +++++++++---------- ensemble.py | 45 ++++++------- genericclasses.py | 40 ++++++++++-- insurancecontract.py | 2 - insurancefirms.py | 41 +++++------- insurancesimulation.py | 8 +-- isleconfig.py | 1 + logger.py | 2 +- metainsurancecontract.py | 5 -- metainsuranceorg.py | 53 +++++++--------- reinsurancecontract.py | 2 - riskmodel.py | 2 - setup_simulation.py | 10 ++- 15 files changed, 205 insertions(+), 203 deletions(-) diff --git a/catbond.py b/catbond.py index 4f146ae..071e81f 100644 --- a/catbond.py +++ b/catbond.py @@ -10,14 +10,10 @@ from metainsurancecontract import MetaInsuranceContract -# TODO: This and MetaInsuranceOrg should probably both subclass something simple - a MetaAgent, say. MetaInsuranceOrg -# can do more than a CatBond should be able to! - - # noinspection PyAbstractClass class CatBond(MetaInsuranceOrg): # noinspection PyMissingConstructor - # TODO inheret GenericAgent instead of MetaInsuranceOrg? + # TODO inheret GenericAgent instead of MetaInsuranceOrg? Or maybe some common root def __init__( self, simulation: "InsuranceSimulation", diff --git a/distributionreinsurance.py b/distributionreinsurance.py index 15640e0..bf670f7 100644 --- a/distributionreinsurance.py +++ b/distributionreinsurance.py @@ -1,18 +1,22 @@ -import functools +from genericclasses import weak_lru_cache import numpy as np import scipy.stats import warnings class ReinsuranceDistWrapper: - """ Wrapper for modifying the risk to an insurance company when they have EoL reinsurance - - dist is a distribution taking values in [0, 1] (as the damage distribution should) # QUERY: Check this - lower_bound is the least reinsured risk (lowest priority), upper_bound is the greatest reinsured risk - Note that the bounds are in terms of the values of the distribution, not the probabilities. - - Coverage is a list of tuples, each tuple representing a region that is reinsured. Coverage is in money, will be - divided by value.""" + """ + Wrapper for modifying the risk to an insurance company when they have EoL reinsurance. + The reinsurance can be given either by passing upper_bound and lower_bound or by passing coverage. + + Args: + dist: the distribution to modify. Must be a probability distribution on [0, 1] + lower_bound: the lower bound of reinsurance, given as a proportion of the possible damage + upper_bound: the upper bound of reinsurance, given as a proportion of the possible damage + coverage: the reinsurance coverage, given as the actual values reinsured. Is a list of tuples, each tuple + representing a region of coverage. Values will be divided by value + value: the total value of the reinsured assets, used to divide coverage + """ def __init__( self, dist, lower_bound=None, upper_bound=None, coverage=None, value=None @@ -39,94 +43,104 @@ def __init__( lower_bound = 0 elif upper_bound is None: upper_bound = 1 - assert 0 <= lower_bound < upper_bound <= 1 self.coverage = [(lower_bound, upper_bound)] else: self.coverage = [ - (region[0] / value, region[1] / value) for region in coverage + (region[0] / value, min(region[1] / value, 1)) + for region in coverage + if region[0] < value ] - if self.coverage and self.coverage[0][0] == 0: - warnings.warn("Adding reinsurance for 0 damage - probably not right!") - # TODO: verify distribution bounds here - # self.redistributed_share = dist.cdf(upper_bound) - dist.cdf(lower_bound) + for region in self.coverage: + assert 0 <= region[0] < region[1] <= 1 - @functools.lru_cache(maxsize=512) + if self.dist.cdf(0) != 0 or self.dist.cdf(1) != 1: + raise ValueError( + "Distribution passed is not bounded 0 <= x <= 1, so is not supported" + ) + + @weak_lru_cache(maxsize=512) def truncation(self, x): - """ Takes a value x and returns the ammount of damage required for x damage to be absorbed by the firm. - Also returns whether the value was on a boundary (point of discontinuity) (to make pdf, cdf work on edge cases) + """ Takes an array-like x and returns the ammount of damage required for x damage to be absorbed by the firm. + Also returns whether the value was on a boundary (point of discontinuity) (to make pdf, cdf both work on edge + cases) """ - # TODO: doesn't work with arrays, fix? - if not np.isscalar(x): - x = x[0] - boundary = False + # Need to take a copy of x as we will be working directly on it + x = np.array(x) + + boundary = np.zeros_like(x, dtype=bool) for region in self.coverage: - if x < region[0]: - return x, boundary + if (x <= region[0]).all(): + break else: - if x == region[0]: - boundary = True - x += region[1] - region[0] + boundary[x == region[0]] = True + x[x > region[0]] += region[1] - region[0] return x, boundary + @weak_lru_cache(maxsize=512) def inverse_truncation(self, p): """ Returns the inverse of the above function, which is continuous and well-defined """ - # TODO: needs to work with arrays - adjustment = 0 + + # Don't need to copy p, since no modifications are made to it + p = np.asarray(p) + adjustment = np.zeros_like(p) for region in self.coverage: # These bounds are probabilities - if p <= region[0]: - return p - adjustment - elif p < region[1]: - return region[0] - adjustment - else: - adjustment += region[1] - region[0] + if (p < region[0]).all(): + break + adjustment[region[0] <= p < region[1]] += ( + p[region[0] <= p < region[1]] - region[0] + ) + adjustment[p >= region[1]] += region[1] - region[0] return p - adjustment - @functools.lru_cache(maxsize=512) + @weak_lru_cache(maxsize=512) def pdf(self, x): # derivative of truncation is 1 at all points of continuity, so only need to modify at boundaries result, boundary = self.truncation(x) - if boundary: - return np.inf - else: - return self.dist.pdf(result) - - @functools.lru_cache(maxsize=512) + to_return = np.zeros_like(result) + to_return[np.logical_not(boundary)] = self.dist.pdf( + result[np.logical_not(boundary)] + ) + to_return[boundary] = np.inf + return to_return + + @weak_lru_cache(maxsize=512) def cdf(self, x): # cdf is right-continuous modification, so doesn't care about the discontinuity result, _ = self.truncation(x) return self.dist.cdf(result) - @functools.lru_cache(maxsize=512) + @weak_lru_cache(maxsize=512) def ppf(self, p): - if type(p) is not float: - p = p[0] + p = np.asarray(p) return self.inverse_truncation(self.dist.ppf(p)) - def rvs(self, size=1): + def rvs(self, size=None): sample = self.dist.rvs(size=size) - sample = map(self.inverse_truncation, sample) - return sample + sample = self.inverse_truncation(sample) + if size is None: + return sample[0] + else: + return sample if __name__ == "__main__": - # TODO: Check with coverage = [] from distributiontruncated import TruncatedDistWrapper import matplotlib.pyplot as plt non_truncated = TruncatedDistWrapper(scipy.stats.pareto(b=2, loc=0, scale=0.5)) # truncated = ReinsuranceDistWrapper(lower_bound=0, upper_bound=1, dist=non_truncated) truncated = ReinsuranceDistWrapper( - dist=non_truncated, value=10, coverage=[(6.5, 7), (8, 9)] + dist=non_truncated, value=10, coverage=[(6, 6.25), (7, 7.5)] ) - x1 = np.linspace(non_truncated.ppf(0.01), non_truncated.ppf(0.99), 100).flatten() + x1 = np.linspace(non_truncated.ppf(0.01), non_truncated.ppf(0.99), 100) - y1 = list(map(non_truncated.pdf, x1)) - y2 = list(map(truncated.pdf, x1)) - plt.plot(x1, y1, "r+") - plt.plot(x1, y2, "bx") - plt.legend(["non truncated", "truncated"]) + y1 = non_truncated.pdf(x1) + y2 = truncated.pdf(x1) + plt.plot(x1, y1, "r+", label="non truncated") + plt.plot(x1, y2, "bx", label="truncated") + plt.legend() plt.show() # pdb.set_trace() diff --git a/distributiontruncated.py b/distributiontruncated.py index a6916ca..ca9ba20 100644 --- a/distributiontruncated.py +++ b/distributiontruncated.py @@ -2,7 +2,7 @@ import numpy as np from math import ceil import scipy.integrate -import functools +from genericclasses import weak_lru_cache class TruncatedDistWrapper: @@ -13,49 +13,39 @@ def __init__(self, dist, lower_bound=0.0, upper_bound=1.0): self.upper_bound = upper_bound assert self.upper_bound > self.lower_bound - @functools.lru_cache(maxsize=1024) + @weak_lru_cache(maxsize=1024) def pdf(self, x): - # TODO: begone, arrays - x = np.array(x, ndmin=1) - r = map( - lambda y: self.dist.pdf(y) / self.normalizing_factor - if (self.lower_bound <= y <= self.upper_bound) - else 0, - x, - ) - r = np.array(list(r)) - if len(r.flatten()) == 1: - r = float(r) - return r + x = np.asarray(x) + + result = np.zeros_like(x) + mask = self.lower_bound <= x <= self.upper_bound + result[mask] = self.dist.pdf(x[mask]) / self.normalizing_factor + return result - @functools.lru_cache(maxsize=1024) + @weak_lru_cache(maxsize=1024) def cdf(self, x): - # TODO: rm arrays - x = np.array(x, ndmin=1) - r = map( - lambda y: 0 - if y < self.lower_bound - else 1 - if y > self.upper_bound - else (self.dist.cdf(y) - self.dist.cdf(self.lower_bound)) - / self.normalizing_factor, - x, - ) - r = np.array(list(r)) - if len(r.flatten()) == 1: - r = float(r) - return r + x = np.asarray(x) + result = np.zeros_like(x) + result[x > self.upper_bound] = 1 + mask = self.lower_bound <= x <= self.upper_bound + result[mask] = ( + self.dist.cdf(x[mask]) - self.dist.cdf(self.lower_bound) + ) / self.normalizing_factor + return result - @functools.lru_cache(maxsize=1024) + @weak_lru_cache(maxsize=1024) def ppf(self, x): - # TODO: probably no need for arrays - x = np.array(x, ndmin=1) + x = np.asarray(x) assert (x >= 0).all() and (x <= 1).all() return self.dist.ppf( x * self.normalizing_factor + self.dist.cdf(self.lower_bound) ) - def rvs(self, size=1): + def rvs(self, passed_size=None): + if passed_size is None: + size = 1 + else: + size = passed_size # Sample RVs from the original distribution and then throw out the ones that are outside the bounds. init_sample_size = int(ceil(size / self.normalizing_factor * 1.1)) sample = self.dist.rvs(size=init_sample_size) @@ -64,11 +54,14 @@ def rvs(self, size=1): ] while len(sample) < size: sample = np.append(sample, self.rvs(size - len(sample))) - return sample[:size] + if passed_size is not None: + return sample[:size] + else: + return sample[0] # Cache could be replaced with a simple "if is None" cache, might offer a small performance gain. # Also this could be a read-only @property, but then again so could a lot of things. - @functools.lru_cache(maxsize=1) + @weak_lru_cache(maxsize=1) def mean(self): mean_estimate, mean_error = scipy.integrate.quad( lambda x: x * self.pdf(x), self.lower_bound, self.upper_bound diff --git a/ensemble.py b/ensemble.py index 349739d..ab319bb 100644 --- a/ensemble.py +++ b/ensemble.py @@ -67,7 +67,7 @@ def rake(hostname): "market_premium": "_premium.dat", "market_reinpremium": "_reinpremium.dat", "cumulative_bankruptcies": "_cumulative_bankruptcies.dat", - "cumulative_market_exits": "_cumulative_market_exits", # TODO: correct filename + "cumulative_market_exits": "_cumulative_market_exits.dat", "cumulative_unrecovered_claims": "_cumulative_unrecovered_claims.dat", "cumulative_claims": "_cumulative_claims.dat", "cumulative_bought_firms": "_cumulative_bought_firms.dat", @@ -205,32 +205,35 @@ def rake(hostname): + requested_logs[name] ) - # TODO: write to the files one at a time with a 'with ... as ... :' - for name in logfile_dict: - wfiles_dict[name] = open(logfile_dict[name], "w") + # with ... as would be awkward here, so use try ... finally + try: + for name in logfile_dict: + wfiles_dict[name] = open(logfile_dict[name], "w") - """Recreate logger object locally and save logs""" + """Recreate logger object locally and save logs""" - """Create local object""" - log = logger.Logger() + """Create local object""" + log = logger.Logger() - for i in range(len(job)): - """Populate logger object with logs obtained from remote simulation run""" - log.restore_logger_object(list(result[i])) + for i in range(len(job)): + """Populate logger object with logs obtained from remote simulation run""" + log.restore_logger_object(list(result[i])) - """Save logs as dict (to _history_logs.dat)""" - log.save_log(True) - if isleconfig.save_network: - log.save_network_data(ensemble=True) + """Save logs as dict (to _history_logs.dat)""" + log.save_log(True) + if isleconfig.save_network: + log.save_network_data(ensemble=True) - """Save logs as individual files""" - for name in logfile_dict: - wfiles_dict[name].write(str(delistified_result[i][name]) + "\n") + """Save logs as individual files""" + for name in logfile_dict: + wfiles_dict[name].write(str(delistified_result[i][name]) + "\n") - """Once the data is stored in disk the files are closed""" - for name in logfile_dict: - wfiles_dict[name].close() - del wfiles_dict[name] + finally: + """Once the data is stored in disk the files are closed""" + for name in logfile_dict: + if name in wfiles_dict: + wfiles_dict[name].close() + del wfiles_dict[name] if __name__ == "__main__": diff --git a/genericclasses.py b/genericclasses.py index a7ca5c5..b0d1275 100644 --- a/genericclasses.py +++ b/genericclasses.py @@ -1,6 +1,7 @@ from itertools import chain import dataclasses +from functools import lru_cache from sortedcontainers import SortedList import numpy as np from scipy import stats @@ -55,7 +56,6 @@ def _pay(self, obligation: "Obligation"): raise ValueError( "Attempting to pay an obligation for a negative ammount - something is wrong" ) - # TODO: Think about what happens when paying non-operational firms while not recipient.get_operational(): if isleconfig.verbose: print( @@ -89,8 +89,8 @@ def _effect_payments(self, time: int): No return value Method checks firms list of obligations to see if ay are due for this time, then pays them. If the firm does not have enough cash then it enters illiquity, leaves the market, and matures all contracts.""" - # TODO: don't really want to be reconstructing lists every time (unless the obligations are naturally sorted by - # time, in which case this could be done slightly better). Low priority, but something to consider + + # This isn't run too frequently, but we could consider using a SortedList or similar due = [item for item in self.obligations if item.due_time <= time] self.obligations = [item for item in self.obligations if item.due_time > time] sum_due = sum([item.amount for item in due]) @@ -189,6 +189,7 @@ class RiskChar: periodized_total_premium: float weighted_premium: float total_var: float + total_exposure: float def __iter__(self): return iter(dataclasses.astuple(self)[:-1]) @@ -221,7 +222,6 @@ class ReinsuranceProfile: regions are tuples, (priority, priority+limit, contract), so the contract covers losses in the region (priority, priority + limit)""" - # TODO: add, remove, explode, get uninsured regions def __init__(self, riskmodel: "RiskModel"): self.reinsured_regions: MutableSequence[ SortedList[Tuple[int, int, "ReinsuranceContract"]] @@ -305,8 +305,11 @@ def contracts_to_explode( break return contracts - def all_contracts(self) -> List["ReinsuranceContract"]: - regions = chain.from_iterable(self.reinsured_regions) + def all_contracts(self, category: int = None) -> List["ReinsuranceContract"]: + if category is None: + regions = chain.from_iterable(self.reinsured_regions) + else: + regions = self.reinsured_regions[category] contracts = list(map(lambda x: x[2], regions)) return contracts @@ -374,3 +377,28 @@ def remove(self, item: T) -> None: del self._dict[id(item)] else: raise ValueError("Item not found in container") + + +def weak_lru_cache(maxsize=128, typed=False): + """ + A wrapper around functools.lru_cache that ignores the cache upon encountering unhashable arguments. + Also exposes the lru_cache cache_info and cache_clear functions + Args: + Args are as in lru_cache + + """ + + def lru_wrapped(user_function): + cached_func = lru_cache(maxsize, typed)(user_function) + + def func_wrapper(*func_args, **func_kwargs): + try: + return cached_func(*func_args, **func_kwargs) + except TypeError: + return user_function(*func_args, **func_kwargs) + + func_wrapper.cache_info = cached_func.cache_info + func_wrapper.cache_clear = cached_func.cache_clear + return func_wrapper + + return lru_wrapped diff --git a/insurancecontract.py b/insurancecontract.py index 38e572a..ee789ff 100644 --- a/insurancecontract.py +++ b/insurancecontract.py @@ -29,7 +29,6 @@ def __init__( insurancetype: str = "proportional", deductible_fraction: float = None, limit_fraction: float = None, - reinsurance: float = 0, ): super().__init__( insurer, @@ -43,7 +42,6 @@ def __init__( insurancetype, deductible_fraction, limit_fraction, - reinsurance, ) # the property holder in an insurance contract should always be the simulation assert self.property_holder is self.insurer.simulation diff --git a/insurancefirms.py b/insurancefirms.py index 4f50b57..6641f07 100644 --- a/insurancefirms.py +++ b/insurancefirms.py @@ -51,9 +51,8 @@ def get_reinsurance_var_estimate(self, max_var: float) -> float: reinsurance_VaR_estimate: Type Decimal. This method takes the max VaR and mulitiplies it by a factor that estimates the VaR if another reinsurance contract was to be taken. Called by the adjust_target_capacity and get_capacity methods.""" - # TODO: Should be total_value, or maybe the total amount of exposure (rather than number_risks) values = [ - self.underwritten_risk_characterisation[categ].number_risks + self.underwritten_risk_characterisation[categ].total_value for categ in range(self.simulation_parameters["no_categories"]) ] reinsurance_factor_estimate = self.get_reinsurable_fraction(values) @@ -85,7 +84,6 @@ def adjust_capacity_target(self, max_var: float): capacity target to max VaR is above/below a predetermined limit.""" reinsurance_var_estimate = self.get_reinsurance_var_estimate(max_var) if max_var + reinsurance_var_estimate == 0: - # TODO: why is this being called with max_var = 0 anyway? capacity_target_var_ratio_estimate = np.inf else: capacity_target_var_ratio_estimate = ( @@ -179,22 +177,12 @@ def increase_capacity_by_category( return True def get_average_premium(self, categ_id: int) -> float: - """Method to calculate and return the firms average premium for all currently underwritten contracts. + """Method to calculate and return the firms average premium per exposure for all underwritten contracts. Accepts: categ_id: Type Integer. Returns: premium payments left/total value of contracts: Type Decimal""" - # weighted_premium_sum = 0 - # total_weight = 0 - # for contract in self.underwritten_contracts: - # if contract.category == categ_id: - # total_weight += contract.value - # contract_premium = contract.periodized_premium * contract.runtime - # weighted_premium_sum += contract_premium - - total_weight = self.underwritten_risk_characterisation[ - categ_id - ].total_value # TODO: Should use exposure + total_weight = self.underwritten_risk_characterisation[categ_id].total_exposure weighted_premium_sum = self.underwritten_risk_characterisation[ categ_id ].weighted_premium @@ -259,9 +247,10 @@ def ask_reinsurance_non_proportional_by_category( ) for tranche in tranches[:]: # Use the slice so we aren't modifying while iterating + min_proportion = 1 / (min_tranches * 3) if (tranche[1] - tranche[0]) <= max( 100, - 0.1 + min_proportion * ( self.np_reinsurance_limit_fraction - self.np_reinsurance_deductible_fraction @@ -269,17 +258,15 @@ def ask_reinsurance_non_proportional_by_category( * total_value, ): # Small gaps are acceptable to avoid having trivial contracts - we don't accept tranches with - # size less than 100 or 10% of the total reinsurable ammount - # TODO: the 10% limit should be removed if we have very many layers of reinsurance + # size less than 100 or one third of the typical tranche size tranches.remove(tranche) if not tranches: # If we've ended up with no tranches, give up and return return None - # TODO: Should only look at contracts in the current category while ( - len(tranches) + len(self.reinsurance_profile.all_contracts()) + len(tranches) + len(self.reinsurance_profile.all_contracts(categ_id)) < min_tranches ): # Make sure that the overall number of tranches after obtaining the requested reinsurance would be at @@ -316,6 +303,7 @@ def reinsure_tranche( self.underwritten_risk_characterisation[category].number_risks, self.underwritten_risk_characterisation[category].periodized_total_premium, ) + runtime = isleconfig.simulation_parameters["reinsurance_contract_runtime"] risk = genericclasses.RiskProperties( value=total_value, category=category, @@ -325,12 +313,12 @@ def reinsure_tranche( deductible_fraction=deductible_fraction, limit_fraction=limit_fraction, periodized_total_premium=periodized_total_premium, - runtime=12, - expiration=time + 12, + runtime=runtime, + expiration=time + runtime, risk_factor=avg_risk_factor, deductible=deductible_fraction * total_value, limit=limit_fraction * total_value, - ) # TODO: make runtime into a parameter + ) assert risk.deductible_fraction < risk.limit_fraction <= 1 reinsurance_type = self.decide_reinsurance_type(risk) @@ -478,8 +466,6 @@ def make_reinsurance_claims(self, time: int): This method calculates the total amount of claims this iteration per category, and explodes (see reinsurance contracts) any reinsurance contracts present for one of the contracts (currently always zero). Then, for a category with reinsurance and claims, the applicable reinsurance contract is exploded.""" - # TODO: reorganize this with risk category ledgers - # TODO: Put facultative insurance claims here claims_this_turn = np.zeros(self.simulation_no_risk_categories) for contract in self.underwritten_contracts: categ_id, claims, is_proportional = contract.get_and_reset_current_claim() @@ -532,6 +518,7 @@ def refresh_reinrisk( if number_risks == 0: # If the insurerer currently has no risks in that category it probably doesn't want reinsurance return None + runtime = isleconfig.simulation_parameters["reinsurance_contract_runtime"] risk = genericclasses.RiskProperties( value=total_value, category=old_contract.category, @@ -541,8 +528,8 @@ def refresh_reinrisk( deductible_fraction=min(old_contract.deductible / total_value, 1), limit_fraction=min(old_contract.limit / total_value, 1), periodized_total_premium=periodized_total_premium, - runtime=12, - expiration=time + 12, + runtime=runtime, + expiration=time + runtime, risk_factor=avg_risk_factor, ) if risk.deductible_fraction == risk.limit_fraction == 1: diff --git a/insurancesimulation.py b/insurancesimulation.py index 1344ac4..a8bd728 100644 --- a/insurancesimulation.py +++ b/insurancesimulation.py @@ -91,7 +91,7 @@ def __init__( ) # Risk factors represent, for example, the earthquake risk for a particular house (compare to the value) - # TODO: Implement! Think about insureres rejecting risks under certain situations (high risk factor) + # TODO: Implement! Think about insurers rejecting risks under certain situations (high risk factor) self.risk_factor_lower_bound: float = simulation_parameters[ "risk_factor_lower_bound" ] @@ -970,8 +970,6 @@ def solicit_insurance_requests( for risk in insurer.risks_retained: risks_to_be_sent.append(risk) - # QUERY: what actually is InsuranceFirm.risks_kept? Are we resending all their existing risks? - # Or is it just a list of risk that have rolled over and so need to be re-evaluated insurer.risks_retained = [] np.random.shuffle(risks_to_be_sent) @@ -994,11 +992,11 @@ def solicit_reinsurance_requests( ] self.reinrisks = self.reinrisks[int(self.reinsurers_weights[reinsurer.id]) :] - for reinrisk in reinsurer.reinrisks_kept: + for reinrisk in reinsurer.reinrisks_retained: if reinrisk.owner.operational: reinrisks_to_be_sent.append(reinrisk) - reinsurer.reinrisks_kept = [] + reinsurer.reinrisks_retained = [] np.random.shuffle(reinrisks_to_be_sent) diff --git a/isleconfig.py b/isleconfig.py index 90c0eea..37289c7 100644 --- a/isleconfig.py +++ b/isleconfig.py @@ -31,6 +31,7 @@ "dividend_share_of_profits": 0.4, "mean_contract_runtime": 12, "contract_runtime_halfspread": 2, + "reinsurance_contract_runtime": 12, "default_contract_payment_period": 3, "max_time": 1000, "money_supply": 2000000000, diff --git a/logger.py b/logger.py index e6f1da3..4f7258b 100644 --- a/logger.py +++ b/logger.py @@ -46,7 +46,7 @@ def __init__( """Variables pertaining to insurance sector""" # TODO: should we not have `cumulative_bankruptcies` and - # `cumulative_market_exits` for both insurance firms and reinsurance firms? + # `cumulative_market_exits` for both insurance firms and reinsurance firms? # `cumulative_claims`: Here are stored the total cumulative claims received # by the whole insurance sector until a certain time. insurance_sector = ( diff --git a/metainsurancecontract.py b/metainsurancecontract.py index 0f1e084..a41afd2 100644 --- a/metainsurancecontract.py +++ b/metainsurancecontract.py @@ -19,7 +19,6 @@ def __init__( insurancetype: str = "proportional", deductible_fraction: float = None, limit_fraction: float = None, - reinsurance: float = 0, ): """Constructor method. Accepts arguments @@ -40,7 +39,6 @@ def __init__( Returns InsuranceContract. Creates InsuranceContract, saves parameters. Creates obligation for premium payment. Includes contract in reinsurance network if applicable (e.g. if this is a ReinsuranceContract).""" - # TODO: argument reinsurance seems senseless; remove? # Save parameters self.insurer: "MetaInsuranceOrg" = insurer @@ -87,7 +85,6 @@ def __init__( self.limit = round(self.limit_fraction * self.value) - self.reinsurance = reinsurance self.reinsurer = None self.reincontract = None self.reinsurance_share = None @@ -175,7 +172,6 @@ def reinsure(self, reinsurer, reinsurance_share, reincontract): No return value. Adds parameters for reinsurance of the current contract.""" self.reinsurer = reinsurer - self.reinsurance = self.value * reinsurance_share self.reinsurance_share = reinsurance_share self.reincontract = reincontract assert self.reinsurance_share in [None, 0.0, 1.0] @@ -187,7 +183,6 @@ def unreinsure(self): Removes parameters for reinsurance of the current contract. To be called when reinsurance has terminated.""" self.reinsurer = None self.reincontract = None - self.reinsurance = 0 self.reinsurance_share = None def explode(self, time, uniform_value=None, damage_extent=None): diff --git a/metainsuranceorg.py b/metainsuranceorg.py index 042e756..57dafac 100644 --- a/metainsuranceorg.py +++ b/metainsuranceorg.py @@ -131,7 +131,7 @@ def __init__(self, simulation_parameters: dict, agent_parameters: AgentPropertie # If the firm goes bankrupt then by default any further payments should be made to the simulation self.creditor = self.simulation - self.owner = self.simulation # TODO: Make this into agent_parameter value? + self.owner = self.simulation self.per_period_dividend = 0 self.cash_last_periods = list(np.zeros(12, dtype=int) * self.cash) @@ -205,7 +205,7 @@ def __init__(self, simulation_parameters: dict, agent_parameters: AgentPropertie self.simulation_no_risk_categories ) # var_sum disaggregated by category self.risks_retained = [] - self.reinrisks_kept = [] + self.reinrisks_retained = [] self.balance_ratio = simulation_parameters["insurers_balance_ratio"] self.recursion_limit = simulation_parameters["insurers_recursion_limit"] # QUERY: Should this have to sum to self.cash @@ -214,7 +214,7 @@ def __init__(self, simulation_parameters: dict, agent_parameters: AgentPropertie ) self.market_permanency_counter = 0 self.underwritten_risk_characterisation: MutableSequence[RiskChar] = [ - RiskChar(0, 0, 0, 0, 0, 0) + RiskChar(0, 0, 0, 0, 0, 0, 0) for _ in range(self.simulation_parameters["no_categories"]) ] self.total_risk_factor = [ @@ -312,8 +312,6 @@ def collect_process_evaluate_risks( assert self.recursion_limit > 0 not_accepted_reinrisks = None for repetition in range(self.recursion_limit): - # TODO: find an efficient way to stop the loop if there are no more risks to accept or if it is - # not accepting any more over several iterations. # Here we process all the new reinrisks in order to keep the portfolio as balanced as possible. has_accepted_risks, not_accepted_reinrisks = self.process_newrisks_reinsurer( reinrisks_per_categ, time @@ -322,7 +320,6 @@ def collect_process_evaluate_risks( # The loop only runs once in my tests, what needs tweaking to have firms not accept risks? reinrisks_per_categ = not_accepted_reinrisks if not has_accepted_risks: - # Stop condition implemented. Might solve the previous TODO. break self.simulation.return_reinrisks( list(chain.from_iterable(not_accepted_reinrisks)) @@ -472,7 +469,7 @@ def dissolve(self, time: int, record: str): self.simulation.return_risks(self.risks_retained) self.risks_retained = [] - self.reinrisks_kept = [] + self.reinrisks_retained = [] obligation = Obligation( amount=self.cash, recipient=self.simulation, @@ -557,6 +554,10 @@ def underwrite(self, contract: "MetaInsuranceContract"): contract.category ].total_var + contract.initial_VaR, + total_exposure=self.underwritten_risk_characterisation[ + contract.category + ].total_exposure + + (contract.limit - contract.deductible), ) # if new_characterisation[1] != 1.0: # print(new_characterisation[1]) @@ -633,23 +634,18 @@ def de_underwrite(self, contract: "MetaInsuranceContract"): contract.category ].total_var - contract.initial_VaR, + total_exposure=self.underwritten_risk_characterisation[ + contract.category + ].total_exposure + - (contract.limit - contract.deductible), ) else: - new_characterisation = RiskChar(0, 0, 0, 0, 0, 0) + new_characterisation = RiskChar(0, 0, 0, 0, 0, 0, 0) self.underwritten_risk_characterisation[ contract.category ] = new_characterisation - # TODO: Check usage and delete - # def obtain_yield(self, time: int): - # """Method to obtain intereset on cash reserves - # Accepts: - # time: Type integer - # No return value""" - # amount = self.cash * self.interest_rate - # self.simulation.receive_obligation(amount, self, time, "yields") - def mature_contracts(self, time: int) -> int: """Method to mature underwritten contracts that have expired Accepts: @@ -1184,7 +1180,7 @@ def roll_over(self, time: int): < self.simulation_parameters["reinsurance_retention"] ): if reinrisk is not None and reinrisk.owner.operational: - self.reinrisks_kept.append(reinrisk) + self.reinrisks_retained.append(reinrisk) def make_reinsurance_claims(self, time: int): raise NotImplementedError( @@ -1294,17 +1290,15 @@ def buyout(self, firm, firm_cost, time): This method causes buyer to receive obligation to buy firm. Sets all the bought firms contracts as its own. Then clears bought firms contracts and dissolves it. Only called from consider_buyout().""" self.receive_obligation(firm_cost, self.simulation, time, "buyout") - - if self.is_insurer and firm.is_insurer: - print( - "Insurer %i has bought %i for %d with total cash %d" - % (self.id, firm.id, firm_cost, self.cash) - ) - elif self.is_reinsurer and firm.is_reinsurer: - print( - "Reinsurer %i has bought %i for %d with total cash %d" - % (self.id, firm.id, firm_cost, self.cash) - ) + if isleconfig.verbose or True: # DEBUG + if self.is_insurer and firm.is_insurer: + print( + f"Insurer {self.id:d} has bought {firm.id:d} for {firm.cost:d} with total cash {self.cash:d}" + ) + elif self.is_reinsurer and firm.is_reinsurer: + print( + f"Reinsurer {self.id:d} has bought {firm.id:d} for {firm_cost:d} with total cash {self.cash:d}" + ) for contract in firm.underwritten_contracts: if contract.insurancetype == "proportional": @@ -1318,6 +1312,7 @@ def buyout(self, firm, firm_cost, time): obli.amount, obli.recipient, obli.due_time, obli.purpose ) + firm.creditor = self firm.obligations = [] firm.underwritten_contracts = [] firm.dissolve(time, "record_bought_firm") diff --git a/reinsurancecontract.py b/reinsurancecontract.py index 7b77edf..9c1d680 100644 --- a/reinsurancecontract.py +++ b/reinsurancecontract.py @@ -32,7 +32,6 @@ def __init__( insurancetype: str = "proportional", deductible_fraction: "Optional[float]" = None, limit_fraction: "Optional[float]" = None, - reinsurance: float = 0, ): super().__init__( insurer, @@ -46,7 +45,6 @@ def __init__( insurancetype, deductible_fraction, limit_fraction, - reinsurance, ) self.times_triggered = 0 # self.is_reinsurancecontract = True diff --git a/riskmodel.py b/riskmodel.py index 905ac88..a8eaee5 100644 --- a/riskmodel.py +++ b/riskmodel.py @@ -30,7 +30,6 @@ def __init__( ) -> None: self.cat_separation_distribution = cat_separation_distribution self.norm_premium = norm_premium - # QUERY: Whis was this passed as an argument and then ignored? self.var_tail_prob = var_tail_prob self.expire_immediately = expire_immediately self.category_number = category_number @@ -180,7 +179,6 @@ def evaluate_proportional( * average_exposure * self.margin_of_safety ) - # QUERY: Is the margin of safety appiled twice? (above and below) # record liquidity requirement and apply margin of safety for liquidity requirement necessary_liquidity += var_per_risk * len(categ_risks) diff --git a/setup_simulation.py b/setup_simulation.py index 8ba0a42..293d1cc 100644 --- a/setup_simulation.py +++ b/setup_simulation.py @@ -33,10 +33,9 @@ def __init__(self): self.max_time = self.simulation_parameters["max_time"] self.no_categories = self.simulation_parameters["no_categories"] - """set distribution""" # TODO: this should be a parameter - non_truncated = scipy.stats.pareto( - b=2, loc=0, scale=0.25 - ) # It is assumed that the damages of the catastrophes are drawn from a truncated Pareto distribution. + """set distribution""" + # It is assumed that the damages of the catastrophes are drawn from a truncated Pareto distribution. + non_truncated = scipy.stats.pareto(b=2, loc=0, scale=0.25) self.damage_distribution = TruncatedDistWrapper( lower_bound=0.25, upper_bound=1.0, dist=non_truncated ) @@ -77,8 +76,7 @@ def schedule( total += int(math.ceil(separation_time)) if total < self.max_time: event_schedule.append(total) - # TODO: Why [0]? - event_damage.append(self.damage_distribution.rvs()[0]) + event_damage.append(self.damage_distribution.rvs()) rc_event_schedule.append(event_schedule) rc_event_damage.append(event_damage) From 3a6cacd1e410126c21a8f6012e805d4696f5c472 Mon Sep 17 00:00:00 2001 From: Chris Hughes Date: Tue, 20 Aug 2019 10:18:02 +0100 Subject: [PATCH 099/125] Switch to pickle for storing network data --- ensemble.py | 15 +++++---------- genericclasses.py | 5 +++++ insurancesimulation.py | 4 +--- interactive_visualisation.py | 10 ++++++---- isleconfig.py | 2 +- logger.py | 22 ++++++++++++++-------- 6 files changed, 32 insertions(+), 26 deletions(-) diff --git a/ensemble.py b/ensemble.py index ab319bb..4267d2a 100644 --- a/ensemble.py +++ b/ensemble.py @@ -16,12 +16,6 @@ from setup_simulation import SetupSim -@operation -def agg(*outputs): - # do nothing - return outputs - - def rake(hostname): jobs = [] @@ -29,13 +23,13 @@ def rake(hostname): # Number of replications to be carried out for each configuration. Usually one risk model, two risk models, # three risk models, four risk models. - replications = 70 + replications = 10 model = start.main m = operation(model, include_modules=True) - riskmodels = [1, 2, 3, 4] # The number of risk models that will be used. + riskmodels = [3] # [1, 2, 3, 4] # The number of risk models that will be used. parameters = isleconfig.simulation_parameters @@ -91,14 +85,15 @@ def rake(hostname): "insurance_firms_cash", "reinsurance_firms_cash", "individual_contracts", - "reinsurance_contracts" "unweighted_network_data", + "reinsurance_contracts", + "unweighted_network_data", "network_node_labels", "network_edge_labels", "number_of_agents", ]: del requested_logs[name] - if not isleconfig.save_network: + elif not isleconfig.save_network: for name in [ "unweighted_network_data", "network_node_labels", diff --git a/genericclasses.py b/genericclasses.py index b0d1275..e8c5cc6 100644 --- a/genericclasses.py +++ b/genericclasses.py @@ -32,6 +32,11 @@ ] +@dataclasses.dataclass(order=True) +class RandomNumber: + n: int + + class GenericAgent: def __init__(self): self.cash: float = 0 diff --git a/insurancesimulation.py b/insurancesimulation.py index a8bd728..657200f 100644 --- a/insurancesimulation.py +++ b/insurancesimulation.py @@ -1056,9 +1056,7 @@ def firm_enters_market( "reinsurance_firm_market_entry_probability" ] else: - raise ValueError( - f"Unknown agent type. Simulation requested to create agent of type {agent_type}" - ) + raise ValueError(f"Unknown agent type {agent_type}") return np.random.random() < prob def record_bankruptcy(self): diff --git a/interactive_visualisation.py b/interactive_visualisation.py index 5eb631d..539e53c 100644 --- a/interactive_visualisation.py +++ b/interactive_visualisation.py @@ -19,17 +19,19 @@ from bokeh.layouts import row, WidgetBox from bokeh.server.server import Server +import pickle io_loop = IOLoop.current() -with open("./data/network_data.dat", "r") as rfile: - network_data_dict = [eval(k) for k in rfile] +with open("./data/network_data.pkl", "rb") as rfile: + network_data_dict = pickle.load(rfile) + # network_data_dict = [eval(k) for k in rfile] unweighted_network_data = network_data_dict[0]["unweighted_network_data"] network_edge_labels = network_data_dict[0]["network_edge_labels"] network_node_labels = network_data_dict[0]["network_node_labels"] number_agent_type = network_data_dict[0]["number_of_agents"] - +max_time = len(number_agent_type) # doc = output_file("bokeh/networkx_graph_demo.html") @@ -112,7 +114,7 @@ def update(attr, old, new): timeselect_slider = Slider( start=0, - end=1000, + end=max_time - 1, value=new, value_throttled=new, step=1, diff --git a/isleconfig.py b/isleconfig.py index 37289c7..9333186 100644 --- a/isleconfig.py +++ b/isleconfig.py @@ -33,7 +33,7 @@ "contract_runtime_halfspread": 2, "reinsurance_contract_runtime": 12, "default_contract_payment_period": 3, - "max_time": 1000, + "max_time": 100, "money_supply": 2000000000, "event_time_mean_separation": 100 / 3.0, "expire_immediately": False, diff --git a/logger.py b/logger.py index 4f7258b..5c1c97d 100644 --- a/logger.py +++ b/logger.py @@ -217,20 +217,26 @@ def save_network_data(self, ensemble): Accepts: ensemble: Type Boolean. Saves to files based on number risk models. No return values.""" - if ensemble is True: + import pickle + + if ensemble: filename_prefix = {1: "one", 2: "two", 3: "three", 4: "four"} fpf = filename_prefix[self.number_riskmodels] network_logs = [ - ("data/" + fpf + "_network_data.dat", self.network_data, "a") + ("data/" + fpf + "_network_data.pkl", self.network_data, "r+b") ] - for filename, data, operation_character in network_logs: - with open(filename, operation_character) as wfile: - wfile.write(str(data) + "\n") + with open(filename, operation_character) as file: + list_ = pickle.load(file) + list_.append(data) + file.truncate(0) + pickle.dump(list_, file) + # wfile.write(str(data) + "\n") else: - with open("data/network_data.dat", "w") as wfile: - wfile.write(str(self.network_data) + "\n") - wfile.write(str(self.rc_event_schedule_initial) + "\n") + with open("data/network_data.pkl", "wb") as wfile: + pickle.dump((self.network_data, self.rc_event_schedule_initial), wfile) + # wfile.write(str(self.network_data) + "\n") + # wfile.write(str(self.rc_event_schedule_initial) + "\n") def add_insurance_agent(self): """Method for adding an additional insurer agent to the history log. This is necessary to keep the number From ee0c4469e8f1f10d4c7f6aae4b7a3177a19d0c5c Mon Sep 17 00:00:00 2001 From: Chris Hughes Date: Wed, 21 Aug 2019 11:49:42 +0100 Subject: [PATCH 100/125] Modified the ensemble running functions to simplify changing parameters other than number of riskmodels --- ensemble.py | 152 ++++++++++++++++++++++++++++++-------------------- isleconfig.py | 2 +- logger.py | 27 ++++----- start.py | 6 +- 4 files changed, 112 insertions(+), 75 deletions(-) diff --git a/ensemble.py b/ensemble.py index 4267d2a..c3e42d3 100644 --- a/ensemble.py +++ b/ensemble.py @@ -1,49 +1,73 @@ -# This script allows to launch an ensemble of simulations for different number of risks models. -# It can be run locally if no argument is passed when called from the terminal. -# It can be run in the cloud if it is passed as argument the server that will be used. +""" +This script allows to launch an ensemble of simulations for different number of risks models. +It can be run locally if no argument is passed when called from the terminal. +It can be run in the cloud if it is passed as argument the server that will be used. +""" import sys - -import copy import os +from typing import Dict -# noinspection PyUnresolvedReferences -from sandman2.api import operation, Session +import sandman2.api as sm import isleconfig import listify import logger import start -from setup_simulation import SetupSim +import setup_simulation -def rake(hostname): - jobs = [] +def rake(hostname=None, replications=10): + """ + Uses the sandman2 api to run multiple replications of multiple configurations of the simulation. + If hostname=None, runs locally. Otherwise, make sure environment variable SANDMAN_KEY_ID and SANDMAN_KEY_SECRET + are set. + Args: + hostname: The remote server to run the job on + replications: The number of replications of each parameter set to run + """ - """Configuration of the ensemble""" + """Configure the parameter sets to run""" + default_parameters: Dict = isleconfig.simulation_parameters - # Number of replications to be carried out for each configuration. Usually one risk model, two risk models, - # three risk models, four risk models. - replications = 10 + ################################################################################################################### + # This section should be freely modified to determine the experiment + # The keys of parameter_sets are the prefixes to save logs under, the values are the parameters to run + # The keys should be strings - model = start.main + parameter_sets: Dict[str:Dict] = {} - m = operation(model, include_modules=True) + for prefix in range(1, 5): + # default_parameters is mutable, so should be copied + new_parameters = default_parameters.copy() + new_parameters["no_riskmodels"] = prefix + parameter_sets[str(prefix)] = new_parameters - riskmodels = [3] # [1, 2, 3, 4] # The number of risk models that will be used. + ################################################################################################################### - parameters = isleconfig.simulation_parameters + for name in parameter_sets: + assert isinstance(name, str) - nums = { - "1": "one", - "2": "two", - "3": "three", - "4": "four", - "5": "five", - "6": "six", - "7": "seven", - "8": "eight", - "9": "nine", - } + """Sanity checks""" + + # Check that the necessary env variables are set + if hostname is not None: + if not ("SANDMAN_KEY_ID" in os.environ and "SANDMAN_KEY_SECRET" in os.environ): + print("Warning: Sandman authentication not found in environment variables.") + + # Don't want to use fat logs with lots of time steps/replications + if ( + replications * isleconfig.simulation_parameters["max_time"] > 5000 + and not isleconfig.slim_log + ): + print( + f"Warning: not using slim logs with {replications} replications and " + f"{isleconfig.simulation_parameters['max_time']} timesteps, logs may be very large" + ) + + if hostname is not None and isleconfig.show_network: + print("Warning: can't show network on remote server") + + """Configuration of the ensemble""" """Configure the return values and corresponding file suffixes where they should be saved""" requested_logs = { @@ -102,44 +126,46 @@ def rake(hostname): ]: del requested_logs[name] - assert "number_riskmodels" in requested_logs - """Configure log directory and ensure that the directory exists""" dir_prefix = "/data/" directory = os.getcwd() + dir_prefix - try: # Here it is checked whether the directory to collect the results exists or not. If not it is created. - os.stat(directory) - except FileNotFoundError: - os.mkdir(directory) + if not os.path.isdir(directory): + if os.path.exists(directory.rstrip("/")): + raise Exception( + "./data exists as regular file. " + "This filename is required for the logging and event schedule directory" + ) + os.makedirs("data") """Clear old dict saving files (*_history_logs.dat)""" - for i in riskmodels: - filename = os.getcwd() + dir_prefix + nums[str(i)] + "_history_logs.dat" + for prefix in parameter_sets.keys(): + filename = os.getcwd() + dir_prefix + prefix + "_history_logs.dat" if os.path.exists(filename): os.remove(filename) """Setup of the simulations""" # Here the setup for the simulation is done. # Since this script is used to carry out simulations in the cloud will usually have more than 1 replication. - setup = SetupSim() + setup = setup_simulation.SetupSim() [ general_rc_event_schedule, general_rc_event_damage, np_seeds, random_seeds, ] = setup.obtain_ensemble(replications) + # never save simulation state in ensemble runs (resuming is impossible anyway) - save_iter = isleconfig.simulation_parameters["max_time"] + 2 + save_iter = 0 - for i in riskmodels: + m = sm.operation(start.main, include_modules=True) + + jobs = {} + for prefix in parameter_sets: # In this loop the parameters, schedules and random seeds for every run are prepared. Different risk models will # be run with the same schedule, damage size and random seed for a fair comparison. - # Here the parameters used for the simulation are loaded. Clone is needed otherwise all the runs will be carried - # out with the last number of the loop. - simulation_parameters = copy.copy(parameters) - # Since we want to obtain ensembles for different number of risk models, we vary the number of risks models. - simulation_parameters["no_riskmodels"] = i + simulation_parameters = parameter_sets[prefix] + # Here is assembled each job with the corresponding: simulation parameters, time events, damage events, seeds, # simulation state save interval (never, i.e. longer than max_time), and list of requested logs. job = [ @@ -154,21 +180,20 @@ def rake(hostname): ) for x in range(replications) ] - jobs.append(job) # All jobs are collected in the jobs list. + jobs[prefix] = job """Here the jobs are submitted""" - with Session(host=hostname, default_cb_to_stdout=True) as sess: + with sm.Session(host=hostname, default_cb_to_stdout=True) as sess: - for job in jobs: - # If there are 4 risk models jobs will be a list with 4 elements. + for prefix, job in jobs.items(): + # If there are 4 parameter sets jobs will be a list with 4 elements. """Run simulation and obtain result""" result = sess.submit(job) """Find number of riskmodels from log""" delistified_result = [listify.delistify(list(res)) for res in result] - nrm = delistified_result[0]["number_riskmodels"] """These are the files created to collect the results""" wfiles_dict = {} @@ -181,7 +206,7 @@ def rake(hostname): os.getcwd() + dir_prefix + "check_" - + str(nums[str(nrm)]) + + prefix + requested_logs[name] ) elif "firms_cash" in name: @@ -189,14 +214,15 @@ def rake(hostname): os.getcwd() + dir_prefix + "record_" - + str(nums[str(nrm)]) + + prefix + requested_logs[name] ) else: logfile_dict[name] = ( os.getcwd() + dir_prefix - + str(nums[str(nrm)]) + + "data_" + + prefix + requested_logs[name] ) @@ -210,18 +236,23 @@ def rake(hostname): """Create local object""" log = logger.Logger() - for i in range(len(job)): + for replic in range(len(job)): """Populate logger object with logs obtained from remote simulation run""" - log.restore_logger_object(list(result[i])) + log.restore_logger_object(list(result[replic])) + + """Save logs as dict (to _history_logs.dat)""" + log.save_log(True, prefix=prefix) - """Save logs as dict (to _history_logs.dat)""" - log.save_log(True) + """Save network data""" if isleconfig.save_network: - log.save_network_data(ensemble=True) + log.save_network_data(ensemble=True, prefix=prefix) """Save logs as individual files""" for name in logfile_dict: - wfiles_dict[name].write(str(delistified_result[i][name]) + "\n") + # Append the current replication data to the file + wfiles_dict[name].write( + str(delistified_result[replic][name]) + "\n" + ) finally: """Once the data is stored in disk the files are closed""" @@ -234,5 +265,6 @@ def rake(hostname): if __name__ == "__main__": host = None if len(sys.argv) > 1: - host = sys.argv[1] # The server is passed as an argument. + # The server is passed as an argument. + host = sys.argv[1] rake(host) diff --git a/isleconfig.py b/isleconfig.py index 9333186..342796a 100644 --- a/isleconfig.py +++ b/isleconfig.py @@ -33,7 +33,7 @@ "contract_runtime_halfspread": 2, "reinsurance_contract_runtime": 12, "default_contract_payment_period": 3, - "max_time": 100, + "max_time": 300, "money_supply": 2000000000, "event_time_mean_separation": 100 / 3.0, "expire_immediately": False, diff --git a/logger.py b/logger.py index 5c1c97d..fd83f46 100644 --- a/logger.py +++ b/logger.py @@ -164,16 +164,17 @@ def restore_logger_object(self, log): self.history_logs_to_save.append(log) self.history_logs = log - def save_log(self, background_run): + def save_log(self, background_run, prefix=""): """Method to save log to disk of local machine. Distinguishes single and ensemble runs. Is called at the end of the replication (if at all). Arguments: background_run: Type bool. Is this an ensemble run (true) or not (false). + prefix: Type str. The prefix to prepend to the filename Returns None.""" """Prepare writing tasks""" if background_run: - to_log = self.replication_log_prepare() + to_log = self.replication_log_prepare(prefix) else: to_log = self.single_log_prepare() @@ -182,19 +183,17 @@ def save_log(self, background_run): with open(filename, operation_character) as wfile: wfile.write(str(data) + "\n") - def replication_log_prepare(self): + def replication_log_prepare(self, prefix): """Method to prepare writing tasks for ensemble run saving. No arguments Returns list of tuples with three elements each. Element 1: filename Element 2: data structure to save Element 3: operation parameter (w-write or a-append).""" - filename_prefix = {1: "one", 2: "two", 3: "three", 4: "four"} - fpf = filename_prefix[self.number_riskmodels] - to_log = [("data/" + fpf + "_history_logs.dat", self.history_logs, "a")] + to_log = [("data/" + prefix + "_history_logs.dat", self.history_logs, "a")] return to_log - def single_log_prepare(self): + def single_log_prepare(self, prefix="single"): """Method to prepare writing tasks for single run saving. No arguments Returns list of tuples with three elements each. @@ -202,9 +201,13 @@ def single_log_prepare(self): Element 2: data structure to save Element 3: operation parameter (w-write or a-append).""" to_log = [] - filename = "data/single_history_logs.dat" + filename = "data/" + prefix + "_history_logs.dat" backupfilename = ( - "data/single_history_logs_old_" + time.strftime("%Y_%b_%d_%H_%M") + ".dat" + "data/" + + prefix + + "_history_logs_old_" + + time.strftime("%Y_%b_%d_%H_%M") + + ".dat" ) if os.path.exists(filename): os.rename(filename, backupfilename) @@ -212,7 +215,7 @@ def single_log_prepare(self): to_log.append((filename, history_log, "a")) return to_log - def save_network_data(self, ensemble): + def save_network_data(self, ensemble, prefix=""): """Method to save network data to its own file. Accepts: ensemble: Type Boolean. Saves to files based on number risk models. @@ -220,10 +223,8 @@ def save_network_data(self, ensemble): import pickle if ensemble: - filename_prefix = {1: "one", 2: "two", 3: "three", 4: "four"} - fpf = filename_prefix[self.number_riskmodels] network_logs = [ - ("data/" + fpf + "_network_data.pkl", self.network_data, "r+b") + ("data/" + prefix + "_network_data.pkl", self.network_data, "r+b") ] for filename, data, operation_character in network_logs: with open(filename, operation_character) as file: diff --git a/start.py b/start.py index c6a0b2e..808b2b5 100644 --- a/start.py +++ b/start.py @@ -71,7 +71,11 @@ def main( simulation.save_data() # Don't save at t=0 or if the simulation has just finished - if t % save_iteration == 0 and 0 < t < sim_params["max_time"]: + if ( + save_iteration > 0 + and t % save_iteration == 0 + and 0 < t < sim_params["max_time"] + ): # Need to use t+1 as resume will start at time saved save_simulation(t + 1, simulation, sim_params, exit_now=False) From 16f0b7377e9d43dcec6e22c788490a9a0f3817e9 Mon Sep 17 00:00:00 2001 From: Chris Hughes Date: Wed, 21 Aug 2019 14:35:13 +0100 Subject: [PATCH 101/125] Switch to pickle for storing network data --- genericclasses.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/genericclasses.py b/genericclasses.py index e8c5cc6..dbc86d0 100644 --- a/genericclasses.py +++ b/genericclasses.py @@ -1,7 +1,7 @@ from itertools import chain import dataclasses -from functools import lru_cache +from functools import lru_cache, wraps from sortedcontainers import SortedList import numpy as np from scipy import stats @@ -396,6 +396,7 @@ def weak_lru_cache(maxsize=128, typed=False): def lru_wrapped(user_function): cached_func = lru_cache(maxsize, typed)(user_function) + @wraps(user_function) def func_wrapper(*func_args, **func_kwargs): try: return cached_func(*func_args, **func_kwargs) From 568f315f17720cd7f363acad45b96b5b962b8036 Mon Sep 17 00:00:00 2001 From: Chris Hughes Date: Wed, 21 Aug 2019 16:07:15 +0100 Subject: [PATCH 102/125] Minor tweaks --- ensemble.py | 9 +++++---- logger.py | 10 ++++++---- 2 files changed, 11 insertions(+), 8 deletions(-) diff --git a/ensemble.py b/ensemble.py index c3e42d3..a5226e2 100644 --- a/ensemble.py +++ b/ensemble.py @@ -1,7 +1,7 @@ """ This script allows to launch an ensemble of simulations for different number of risks models. It can be run locally if no argument is passed when called from the terminal. -It can be run in the cloud if it is passed as argument the server that will be used. +It can be run in the cloud if it is passed as argument the sandman2 server that will be used. """ import sys import os @@ -146,6 +146,8 @@ def rake(hostname=None, replications=10): """Setup of the simulations""" # Here the setup for the simulation is done. # Since this script is used to carry out simulations in the cloud will usually have more than 1 replication. + # We don't set filepath=, so the full set of events and seeds will be stored in data/risk_event_schedules.islestore + # If we wished we could replicate by setting isleconfig.replicating = True. setup = setup_simulation.SetupSim() [ general_rc_event_schedule, @@ -167,7 +169,7 @@ def rake(hostname=None, replications=10): simulation_parameters = parameter_sets[prefix] # Here is assembled each job with the corresponding: simulation parameters, time events, damage events, seeds, - # simulation state save interval (never, i.e. longer than max_time), and list of requested logs. + # simulation state save interval (never), and list of requested logs. job = [ m( simulation_parameters, @@ -187,12 +189,11 @@ def rake(hostname=None, replications=10): with sm.Session(host=hostname, default_cb_to_stdout=True) as sess: for prefix, job in jobs.items(): - # If there are 4 parameter sets jobs will be a list with 4 elements. + # If there are 4 parameter sets jobs will be a dict with 4 elements. """Run simulation and obtain result""" result = sess.submit(job) - """Find number of riskmodels from log""" delistified_result = [listify.delistify(list(res)) for res in result] """These are the files created to collect the results""" diff --git a/logger.py b/logger.py index fd83f46..bb4a2cd 100644 --- a/logger.py +++ b/logger.py @@ -164,16 +164,16 @@ def restore_logger_object(self, log): self.history_logs_to_save.append(log) self.history_logs = log - def save_log(self, background_run, prefix=""): + def save_log(self, ensemble_run, prefix=""): """Method to save log to disk of local machine. Distinguishes single and ensemble runs. Is called at the end of the replication (if at all). Arguments: - background_run: Type bool. Is this an ensemble run (true) or not (false). + ensemble_run: Type bool. Is this an ensemble run (true) or not (false). prefix: Type str. The prefix to prepend to the filename Returns None.""" """Prepare writing tasks""" - if background_run: + if ensemble_run: to_log = self.replication_log_prepare(prefix) else: to_log = self.single_log_prepare() @@ -190,7 +190,9 @@ def replication_log_prepare(self, prefix): Element 1: filename Element 2: data structure to save Element 3: operation parameter (w-write or a-append).""" - to_log = [("data/" + prefix + "_history_logs.dat", self.history_logs, "a")] + to_log = [ + ("data/" + "full_" + prefix + "_history_logs.dat", self.history_logs, "a") + ] return to_log def single_log_prepare(self, prefix="single"): From 08beb302c3511634bc98856c6c32a5ce4dc9acf2 Mon Sep 17 00:00:00 2001 From: Chris Hughes Date: Wed, 21 Aug 2019 13:10:54 +0100 Subject: [PATCH 103/125] Investigating the distribution of cat damage for potential implementation --- isleconfig.py | 2 +- reinsurancecontract.py | 50 +++++++++++++++++++++++++++--------------- 2 files changed, 33 insertions(+), 19 deletions(-) diff --git a/isleconfig.py b/isleconfig.py index 7cd23a9..9407176 100644 --- a/isleconfig.py +++ b/isleconfig.py @@ -32,7 +32,7 @@ "mean_contract_runtime": 12, "contract_runtime_halfspread": 2, "default_contract_payment_period": 3, - "max_time": 1000, + "max_time": 300, "money_supply": 2000000000, "event_time_mean_separation": 100 / 3.0, "expire_immediately": False, diff --git a/reinsurancecontract.py b/reinsurancecontract.py index e141efa..989cba6 100644 --- a/reinsurancecontract.py +++ b/reinsurancecontract.py @@ -58,24 +58,38 @@ def __init__( else: assert self.contract is not None - # import distributionaggregate - # if type(self.insurer).__name__ == "CatBond": - # max_claims = 1 - # expected_total_claim, std, var, exposure = distributionaggregate.get_contract_risk( - # risk, params=self.insurer.simulation_parameters, max_claims=max_claims, - # ) - # total_premium = self.insurer.per_period_dividend * self.runtime - # # Initial purchase cost is exposure + 1 - # expected_return = ((exposure + 1) - expected_total_claim + total_premium) / (exposure + 1) - 1 - # print(f"Catbond created with total return of {expected_return:.1%}") - # else: - # max_claims = 0 - # expected_total_claim, std, var, exposure = distributionaggregate.get_contract_risk( - # risk, params=self.insurer.simulation_parameters, max_claims=max_claims, - # ) - # total_premium = self.periodized_premium * self.runtime - # expected_return = total_premium - expected_total_claim - # print(f"Reinsurance contract created with expected return of MU{expected_return:.0f}") + evaluating = False + if evaluating: + import distributionaggregate + import isleconfig + + if type(self.insurer).__name__ == "CatBond": + max_claims = 1 + expected_total_claim, std, var, exposure = distributionaggregate.get_contract_risk( + risk, + params=self.insurer.simulation_parameters, + max_claims=max_claims, + ) + total_premium = self.insurer.per_period_dividend * self.runtime + # Initial purchase cost is exposure + 1 + expected_return = ( + (exposure + 1) - expected_total_claim + total_premium + ) / (exposure + 1) - 1 + if isleconfig.verbose: + print(f"Catbond created with total return of {expected_return:.1%}") + else: + max_claims = 0 + expected_total_claim, std, var, exposure = distributionaggregate.get_contract_risk( + risk, + params=self.insurer.simulation_parameters, + max_claims=max_claims, + ) + total_premium = self.periodized_premium * self.runtime + expected_return = total_premium - expected_total_claim + if isleconfig.verbose: + print( + f"Reinsurance contract created with expected return of MU{expected_return:.0f}" + ) def explode( self, time: int, uniform_value: None = None, damage_extent: float = None From fb1b591eb3c0d5023473d7efe533262f3ebd0d12 Mon Sep 17 00:00:00 2001 From: Chris Hughes Date: Thu, 22 Aug 2019 10:17:17 +0100 Subject: [PATCH 104/125] Tweak to make insurers pay out in the same timestep as reinsurers --- insurancecontract.py | 6 +++--- visualisation.py | 3 +++ 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/insurancecontract.py b/insurancecontract.py index ee789ff..575caae 100644 --- a/insurancecontract.py +++ b/insurancecontract.py @@ -70,12 +70,12 @@ def explode(self, time, uniform_value=None, damage_extent=None): # Every insurance claim made is immediately registered. self.insurer.register_claim(claim) self.current_claim += claim - # Insurer pays one time step after reinsurer to avoid bankruptcy. + + # Reinsurers iterate before insurers, so not delaying the claim is fine (still have to wait one ts) self.insurer.receive_obligation( - claim, self.property_holder, time + 2, "claim" + claim, self.property_holder, time + 1, "claim" ) - # TODO: Is this realistic? Change this? if self.expire_immediately: self.expiration = time # self.terminating = True diff --git a/visualisation.py b/visualisation.py index bb1ed3a..b6792f0 100644 --- a/visualisation.py +++ b/visualisation.py @@ -427,6 +427,8 @@ def reinsurer_time_series( reincash = np.median(reincash_agg, axis=0) catbonds_number = np.median(catbonds_number_agg, axis=0) + ts_length = len(reincash) + self.reins_time_series = TimeSeries( [ ( @@ -467,6 +469,7 @@ def reinsurer_time_series( axlst=axlst, fig=fig, colour=colour, + length=ts_length, ) fig, axlst = self.reins_time_series.plot() return fig, axlst From e411b1c87385a2d3db25d3589594e0f4f3767e5e Mon Sep 17 00:00:00 2001 From: Chris Hughes Date: Thu, 22 Aug 2019 11:28:02 +0100 Subject: [PATCH 105/125] Ensembles now run fully asynchronously --- ensemble.py | 169 ++++++++++++++++++++++++++++---------------------- isleconfig.py | 8 +-- 2 files changed, 98 insertions(+), 79 deletions(-) diff --git a/ensemble.py b/ensemble.py index a5226e2..3107db4 100644 --- a/ensemble.py +++ b/ensemble.py @@ -6,6 +6,7 @@ import sys import os from typing import Dict +import time import sandman2.api as sm @@ -16,7 +17,7 @@ import setup_simulation -def rake(hostname=None, replications=10): +def rake(hostname=None, replications=50): """ Uses the sandman2 api to run multiple replications of multiple configurations of the simulation. If hostname=None, runs locally. Otherwise, make sure environment variable SANDMAN_KEY_ID and SANDMAN_KEY_SECRET @@ -28,19 +29,18 @@ def rake(hostname=None, replications=10): """Configure the parameter sets to run""" default_parameters: Dict = isleconfig.simulation_parameters + parameter_sets: Dict[str:Dict] = {} ################################################################################################################### # This section should be freely modified to determine the experiment # The keys of parameter_sets are the prefixes to save logs under, the values are the parameters to run # The keys should be strings - parameter_sets: Dict[str:Dict] = {} - - for prefix in range(1, 5): + for prefix in range(1, 4): # default_parameters is mutable, so should be copied new_parameters = default_parameters.copy() new_parameters["no_riskmodels"] = prefix - parameter_sets[str(prefix)] = new_parameters + parameter_sets["ensemble" + str(prefix)] = new_parameters ################################################################################################################### @@ -187,80 +187,99 @@ def rake(hostname=None, replications=10): """Here the jobs are submitted""" with sm.Session(host=hostname, default_cb_to_stdout=True) as sess: - + tasks = {} for prefix, job in jobs.items(): # If there are 4 parameter sets jobs will be a dict with 4 elements. """Run simulation and obtain result""" - result = sess.submit(job) - - delistified_result = [listify.delistify(list(res)) for res in result] - - """These are the files created to collect the results""" - wfiles_dict = {} - - logfile_dict = {} - - for name in requested_logs.keys(): - if "rc_event" in name or "number_riskmodels" in name: - logfile_dict[name] = ( - os.getcwd() - + dir_prefix - + "check_" - + prefix - + requested_logs[name] - ) - elif "firms_cash" in name: - logfile_dict[name] = ( - os.getcwd() - + dir_prefix - + "record_" - + prefix - + requested_logs[name] - ) - else: - logfile_dict[name] = ( - os.getcwd() - + dir_prefix - + "data_" - + prefix - + requested_logs[name] - ) - - # with ... as would be awkward here, so use try ... finally - try: - for name in logfile_dict: - wfiles_dict[name] = open(logfile_dict[name], "w") - - """Recreate logger object locally and save logs""" - - """Create local object""" - log = logger.Logger() - - for replic in range(len(job)): - """Populate logger object with logs obtained from remote simulation run""" - log.restore_logger_object(list(result[replic])) - - """Save logs as dict (to _history_logs.dat)""" - log.save_log(True, prefix=prefix) - - """Save network data""" - if isleconfig.save_network: - log.save_network_data(ensemble=True, prefix=prefix) - - """Save logs as individual files""" - for name in logfile_dict: - # Append the current replication data to the file - wfiles_dict[name].write( - str(delistified_result[replic][name]) + "\n" - ) - - finally: - """Once the data is stored in disk the files are closed""" - for name in logfile_dict: - if name in wfiles_dict: - wfiles_dict[name].close() - del wfiles_dict[name] + task = sess.submit_async(job) + print(f"Started job, prefix {prefix}, given ID {task.id}") + tasks[prefix] = task + + print("Now waiting for jobs to complete\033[5m...\033[0m") + # Need to do it this way as dictionary can't change size during iteration + completed_tasks = [] + while len(tasks) > 0: + for prefix in completed_tasks: + del tasks[prefix] + completed_tasks = [] + + time.sleep(0.5) + for prefix, task in tasks.items(): + if task.is_done(): + print(f"Finsihed job, prefix {prefix}, with ID {task.id}") + result = task.results + completed_tasks.append(prefix) + delistified_result = [ + listify.delistify(list(res)) for res in result + ] + + """These are the files created to collect the results""" + wfiles_dict = {} + + logfile_dict = {} + + for name in requested_logs.keys(): + if "rc_event" in name or "number_riskmodels" in name: + logfile_dict[name] = ( + os.getcwd() + + dir_prefix + + "check_" + + prefix + + requested_logs[name] + ) + elif "firms_cash" in name: + logfile_dict[name] = ( + os.getcwd() + + dir_prefix + + "record_" + + prefix + + requested_logs[name] + ) + else: + logfile_dict[name] = ( + os.getcwd() + + dir_prefix + + "data_" + + prefix + + requested_logs[name] + ) + + # with ... as would be awkward here, so use try ... finally + try: + for name in logfile_dict: + wfiles_dict[name] = open(logfile_dict[name], "w") + + """Recreate logger object locally and save logs""" + + """Create local object""" + log = logger.Logger() + + for replic in range(len(job)): + """Populate logger object with logs obtained from remote simulation run""" + log.restore_logger_object(list(result[replic])) + + """Save logs as dict (to _history_logs.dat)""" + log.save_log(True, prefix=prefix) + + """Save network data""" + if isleconfig.save_network: + log.save_network_data(ensemble=True, prefix=prefix) + + """Save logs as individual files""" + for name in logfile_dict: + # Append the current replication data to the file + wfiles_dict[name].write( + str(delistified_result[replic][name]) + "\n" + ) + + finally: + """Once the data is stored in disk the files are closed""" + for name in logfile_dict: + if name in wfiles_dict: + wfiles_dict[name].close() + del wfiles_dict[name] + print(f"Finished writing files for prefix {prefix}") if __name__ == "__main__": diff --git a/isleconfig.py b/isleconfig.py index ecb1f97..a03791b 100644 --- a/isleconfig.py +++ b/isleconfig.py @@ -33,9 +33,9 @@ "contract_runtime_halfspread": 2, "reinsurance_contract_runtime": 12, "default_contract_payment_period": 3, - "max_time": 300, + "max_time": 1000, "money_supply": 2000000000, - "event_time_mean_separation": 100 / 3.0, + "event_time_mean_separation": 100 / 3, "expire_immediately": False, "risk_factors_present": False, "risk_factor_lower_bound": 0.4, @@ -54,8 +54,8 @@ "reinsurance_off": False, "capacity_target_decrement_threshold": 1.8, "capacity_target_increment_threshold": 1.2, - "capacity_target_decrement_factor": 24 / 25.0, - "capacity_target_increment_factor": 25 / 24.0, + "capacity_target_decrement_factor": 24 / 25, + "capacity_target_increment_factor": 25 / 24, # Retention parameters "insurance_retention": 0.85, # Ratio of insurance contracts retained every iteration. "reinsurance_retention": 1, # Ratio of reinsurance contracts retained every iteration. From f01242423a7f519e6a9db4110f3eb0088f6804a7 Mon Sep 17 00:00:00 2001 From: Chris Hughes Date: Fri, 23 Aug 2019 11:11:20 +0100 Subject: [PATCH 106/125] Thinking about storage/RAM --- ensemble.py | 43 +++++++++++++++++++++++++++---------------- isleconfig.py | 4 ++-- logger.py | 12 +++++++----- visualisation.py | 11 +++++++---- 4 files changed, 43 insertions(+), 27 deletions(-) diff --git a/ensemble.py b/ensemble.py index 3107db4..8e70160 100644 --- a/ensemble.py +++ b/ensemble.py @@ -36,14 +36,17 @@ def rake(hostname=None, replications=50): # The keys of parameter_sets are the prefixes to save logs under, the values are the parameters to run # The keys should be strings - for prefix in range(1, 4): + for number_riskmodels in [1, 3]: # default_parameters is mutable, so should be copied new_parameters = default_parameters.copy() - new_parameters["no_riskmodels"] = prefix - parameter_sets["ensemble" + str(prefix)] = new_parameters + new_parameters["no_riskmodels"] = number_riskmodels + parameter_sets["ensemble" + str(number_riskmodels)] = new_parameters ################################################################################################################### - + print( + f"Running {len(parameter_sets)} simulations of {replications} " + f"replications of {default_parameters['max_time']} timesteps" + ) for name in parameter_sets: assert isinstance(name, str) @@ -162,6 +165,7 @@ def rake(hostname=None, replications=50): m = sm.operation(start.main, include_modules=True) jobs = {} + position_maps = {} for prefix in parameter_sets: # In this loop the parameters, schedules and random seeds for every run are prepared. Different risk models will # be run with the same schedule, damage size and random seed for a fair comparison. @@ -183,6 +187,7 @@ def rake(hostname=None, replications=50): for x in range(replications) ] jobs[prefix] = job + position_maps[prefix] = {o.id: p for p, o in enumerate(job)} """Here the jobs are submitted""" @@ -208,17 +213,14 @@ def rake(hostname=None, replications=50): for prefix, task in tasks.items(): if task.is_done(): print(f"Finsihed job, prefix {prefix}, with ID {task.id}") - result = task.results + # These will be disordered, fix? + results_iterator = task.iterresults() completed_tasks.append(prefix) - delistified_result = [ - listify.delistify(list(res)) for res in result - ] """These are the files created to collect the results""" - wfiles_dict = {} + # Construct filenames to write to logfile_dict = {} - for name in requested_logs.keys(): if "rc_event" in name or "number_riskmodels" in name: logfile_dict[name] = ( @@ -246,6 +248,7 @@ def rake(hostname=None, replications=50): ) # with ... as would be awkward here, so use try ... finally + wfiles_dict = {} try: for name in logfile_dict: wfiles_dict[name] = open(logfile_dict[name], "w") @@ -255,11 +258,14 @@ def rake(hostname=None, replications=50): """Create local object""" log = logger.Logger() - for replic in range(len(job)): + for output_id, result in results_iterator: + position = position_maps[prefix][output_id] + delistified_result = listify.delistify(list(result)) + """Populate logger object with logs obtained from remote simulation run""" - log.restore_logger_object(list(result[replic])) + log.restore_logger_object(list(result)) - """Save logs as dict (to _history_logs.dat)""" + """Save logs as dict (to full__history_logs.dat)""" log.save_log(True, prefix=prefix) """Save network data""" @@ -268,10 +274,13 @@ def rake(hostname=None, replications=50): """Save logs as individual files""" for name in logfile_dict: + # Async iteration doesn't preserve order (replications can be reshuffled), so in case + # we want to compare like with like between parameter sets we store the original + # position in the list. + to_write = (position, delistified_result[name]) # Append the current replication data to the file - wfiles_dict[name].write( - str(delistified_result[replic][name]) + "\n" - ) + wfiles_dict[name].write(repr(to_write) + "\n") + # TODO: Use pickle or hickle rather than repr()s. finally: """Once the data is stored in disk the files are closed""" @@ -281,6 +290,8 @@ def rake(hostname=None, replications=50): del wfiles_dict[name] print(f"Finished writing files for prefix {prefix}") + print("Recieved all results and written all files") + if __name__ == "__main__": host = None diff --git a/isleconfig.py b/isleconfig.py index a03791b..4bdd08c 100644 --- a/isleconfig.py +++ b/isleconfig.py @@ -20,7 +20,7 @@ # values >=1; inaccuracy higher with higher values "riskmodel_inaccuracy_parameter": 2, # values >=1; factor of additional liquidity beyond value at risk - "riskmodel_margin_of_safety": 2, + "riskmodel_margin_of_safety": 1.5, "margin_increase": 0, # "margin_increase" modifies the margin of safety depending on the number of risks models available in the market. # When it is 0 all risk models have the same margin of safety. @@ -33,7 +33,7 @@ "contract_runtime_halfspread": 2, "reinsurance_contract_runtime": 12, "default_contract_payment_period": 3, - "max_time": 1000, + "max_time": 4000, "money_supply": 2000000000, "event_time_mean_separation": 100 / 3, "expire_immediately": False, diff --git a/logger.py b/logger.py index bb4a2cd..dd1ed3f 100644 --- a/logger.py +++ b/logger.py @@ -164,7 +164,7 @@ def restore_logger_object(self, log): self.history_logs_to_save.append(log) self.history_logs = log - def save_log(self, ensemble_run, prefix=""): + def save_log(self, ensemble_run: bool, prefix: str = "") -> None: """Method to save log to disk of local machine. Distinguishes single and ensemble runs. Is called at the end of the replication (if at all). Arguments: @@ -183,16 +183,18 @@ def save_log(self, ensemble_run, prefix=""): with open(filename, operation_character) as wfile: wfile.write(str(data) + "\n") - def replication_log_prepare(self, prefix): + def replication_log_prepare(self, prefix, position: int = None): """Method to prepare writing tasks for ensemble run saving. No arguments Returns list of tuples with three elements each. Element 1: filename Element 2: data structure to save Element 3: operation parameter (w-write or a-append).""" - to_log = [ - ("data/" + "full_" + prefix + "_history_logs.dat", self.history_logs, "a") - ] + if position is not None: + data = (position, self.history_logs) + else: + data = self.history_logs + to_log = [("data/" + "full_" + prefix + "_history_logs.dat", data, "a")] return to_log def single_log_prepare(self, prefix="single"): diff --git a/visualisation.py b/visualisation.py index b6792f0..f3d8287 100644 --- a/visualisation.py +++ b/visualisation.py @@ -798,6 +798,7 @@ def generate_plot( """Create figure with correct number of subplots""" self.fig, self.ax = plt.subplots(nrows=len(self.vis_list)) + # self.fig, self.ax = plt.gcf(), plt.gca() """find max and min values""" """combine all data sets""" @@ -1558,9 +1559,10 @@ def stat_tests(self, upper, lower): "Wasserstein distance: ", wasser, ) - except SyntaxError: - # Dud error to track down what we should be excepting - pass + except Exception as ex: + # Dud error to track down what we should be excepting (was previously bare except) + print(f"The exception should be {ex}!") + raise ex if __name__ == "__main__": @@ -1647,7 +1649,8 @@ def stat_tests(self, upper, lower): # Loads all risk model history logs data (very long :'( ) and creates list of visualisation class instances. filenames = [ - "./data/" + x + "_history_logs.dat" for x in ["one", "two", "three", "four"] + "./data/full_" + x + "_history_logs.dat" + for x in ["ensemble1", "ensemble2", "ensemble3"] ] for filename in filenames: with open(filename, "r") as rfile: From 55220a0b49fcd59ead979e7e399d70f9fe416c6e Mon Sep 17 00:00:00 2001 From: Chris Hughes Date: Fri, 23 Aug 2019 15:11:36 +0100 Subject: [PATCH 107/125] Fix issue with requested logs not being passed to simulation --- ensemble.py | 3 ++- logger.py | 26 +++++++++++++++----------- 2 files changed, 17 insertions(+), 12 deletions(-) diff --git a/ensemble.py b/ensemble.py index 3107db4..8cfea2e 100644 --- a/ensemble.py +++ b/ensemble.py @@ -139,7 +139,7 @@ def rake(hostname=None, replications=50): """Clear old dict saving files (*_history_logs.dat)""" for prefix in parameter_sets.keys(): - filename = os.getcwd() + dir_prefix + prefix + "_history_logs.dat" + filename = os.getcwd() + dir_prefix + "full_" + prefix + "_history_logs.dat" if os.path.exists(filename): os.remove(filename) @@ -178,6 +178,7 @@ def rake(hostname=None, replications=50): np_seeds[x], random_seeds[x], save_iter, + 0, list(requested_logs.keys()), ) for x in range(replications) diff --git a/logger.py b/logger.py index bb4a2cd..d4674d2 100644 --- a/logger.py +++ b/logger.py @@ -143,17 +143,21 @@ def restore_logger_object(self, log): """Restore dict""" log = listify.delistify(log) - - self.network_data["unweighted_network_data"] = log["unweighted_network_data"] - self.network_data["network_node_labels"] = log["network_node_labels"] - self.network_data["network_edge_labels"] = log["network_edge_labels"] - self.network_data["number_of_agents"] = log["number_of_agents"] - del ( - log["number_of_agents"], - log["network_edge_labels"], - log["network_node_labels"], - log["unweighted_network_data"], - ) + try: + self.network_data["unweighted_network_data"] = log[ + "unweighted_network_data" + ] + self.network_data["network_node_labels"] = log["network_node_labels"] + self.network_data["network_edge_labels"] = log["network_edge_labels"] + self.network_data["number_of_agents"] = log["number_of_agents"] + del ( + log["number_of_agents"], + log["network_edge_labels"], + log["network_node_labels"], + log["unweighted_network_data"], + ) + except KeyError: + pass """Extract environment variables (number of risk models and risk event schedule)""" self.rc_event_schedule_initial = log["rc_event_schedule_initial"] From 7b9442333a54d7e7bd31e71952269af3c7ff11a4 Mon Sep 17 00:00:00 2001 From: Chris Hughes Date: Tue, 27 Aug 2019 11:12:34 +0100 Subject: [PATCH 108/125] Change log storage to hickle (HDF5), add utility to convert back to old text-based format --- calibration_conditions.py | 21 ++-- ensemble.py | 142 ++++++++++------------- fileconvert.py | 89 +++++++++++++++ insurancesimulation.py | 22 ++-- isleconfig.py | 4 +- logger.py | 68 +++++------ requirements.txt | 3 +- start.py | 148 ++++++++++++++++++++++-- starter_four.sh | 11 -- starter_one.sh | 13 --- starter_three.sh | 11 -- starter_two.sh | 14 --- visualisation.py | 234 ++++++++++++++------------------------ 13 files changed, 425 insertions(+), 355 deletions(-) create mode 100644 fileconvert.py delete mode 100755 starter_four.sh delete mode 100755 starter_one.sh delete mode 100755 starter_three.sh delete mode 100755 starter_two.sh diff --git a/calibration_conditions.py b/calibration_conditions.py index 66828d9..1852182 100644 --- a/calibration_conditions.py +++ b/calibration_conditions.py @@ -106,11 +106,9 @@ def condition_defaults_insurance( """Test for number of insurance bankruptcies (non zero, not all insurers)""" # series = logobj.history_logs['total_operational'] # if series[-1] != 0 and any(series[i]-series[i-1] < 0 for i in range(1,len(series))): - pass - opseries = [ - logobj.history_logs["insurance_firms_cash"][-1][i][2] - for i in range(len(logobj.history_logs["insurance_firms_cash"][-1])) - ] + + # We use cash > 0 as a proxy for operational + opseries = logobj.history_logs["insurance_firms_cash"][-1] if any(opseries) and not all(opseries): return 1 else: @@ -123,10 +121,7 @@ def condition_defaults_reinsurance( """Test for number of reinsurance bankruptcies (non zero, not all reinsurers)""" # series = logobj.history_logs['total_reinoperational'] # if series[-1] != 0 and any(series[i]-series[i-1] < 0 for i in range(1,len(series))): - opseries = [ - logobj.history_logs["reinsurance_firms_cash"][-1][i][2] - for i in range(len(logobj.history_logs["reinsurance_firms_cash"][-1])) - ] + opseries = logobj.history_logs["reinsurance_firms_cash"][-1] if any(opseries) and not all(opseries): return 1 else: @@ -166,9 +161,9 @@ def condition_insurance_firm_dist(logobj): # > isleconfig.simulation_parameters["cash_permanency_limit"] # ] dist = [ - logobj.history_logs["insurance_firms_cash"][-1][i][0] + logobj.history_logs["insurance_firms_cash"][-1][i] for i in range(len(logobj.history_logs["insurance_firms_cash"][-1])) - if logobj.history_logs["insurance_firms_cash"][-1][i][2] + if logobj.history_logs["insurance_firms_cash"][-1][i] > 0 ] """run two-sided KS test""" ks_statistic, p_value = stats.ks_2samp( @@ -188,9 +183,9 @@ def condition_reinsurance_firm_dist(logobj): # > isleconfig.simulation_parameters["cash_permanency_limit"] # ] dist = [ - logobj.history_logs["reinsurance_firms_cash"][-1][i][0] + logobj.history_logs["reinsurance_firms_cash"][-1][i] for i in range(len(logobj.history_logs["reinsurance_firms_cash"][-1])) - if logobj.history_logs["reinsurance_firms_cash"][-1][i][2] + if logobj.history_logs["reinsurance_firms_cash"][-1][i] > 0 ] """run two-sided KS test""" ks_statistic, p_value = stats.ks_2samp( diff --git a/ensemble.py b/ensemble.py index 1628992..0a402bc 100644 --- a/ensemble.py +++ b/ensemble.py @@ -8,16 +8,18 @@ from typing import Dict import time +import pickle +import zlib +import numpy as np import sandman2.api as sm import isleconfig import listify -import logger import start import setup_simulation -def rake(hostname=None, replications=50): +def rake(hostname=None, replications=35): """ Uses the sandman2 api to run multiple replications of multiple configurations of the simulation. If hostname=None, runs locally. Otherwise, make sure environment variable SANDMAN_KEY_ID and SANDMAN_KEY_SECRET @@ -26,7 +28,10 @@ def rake(hostname=None, replications=50): hostname: The remote server to run the job on replications: The number of replications of each parameter set to run """ - + if hostname is None: + print("Running ensemble locally") + else: + print(f"Running ensemble on {hostname}") """Configure the parameter sets to run""" default_parameters: Dict = isleconfig.simulation_parameters parameter_sets: Dict[str:Dict] = {} @@ -58,13 +63,12 @@ def rake(hostname=None, replications=50): print("Warning: Sandman authentication not found in environment variables.") # Don't want to use fat logs with lots of time steps/replications - if ( - replications * isleconfig.simulation_parameters["max_time"] > 5000 - and not isleconfig.slim_log - ): + max_time = isleconfig.simulation_parameters["max_time"] + + if replications * max_time > 5000 and not isleconfig.slim_log: print( f"Warning: not using slim logs with {replications} replications and " - f"{isleconfig.simulation_parameters['max_time']} timesteps, logs may be very large" + f"{max_time} timesteps, logs may be very large" ) if hostname is not None and isleconfig.show_network: @@ -106,6 +110,40 @@ def rake(hostname=None, replications=50): "network_edge_labels": "_network_edge_labels.dat", "number_of_agents": "_number_of_agents", } + """Define the numpy types of the underlying data in each requested log""" + types = { + "total_cash": np.float_, + "total_excess_capital": np.float_, + "total_profitslosses": np.float_, + "total_contracts": np.int_, + "total_operational": np.int_, + "total_reincash": np.float_, + "total_reinexcess_capital": np.float_, + "total_reinprofitslosses": np.float_, + "total_reincontracts": np.int_, + "total_reinoperational": np.int_, + "total_catbondsoperational": np.int_, + "market_premium": np.float_, + "market_reinpremium": np.float_, + "cumulative_bankruptcies": np.int_, + "cumulative_market_exits": np.int_, + "cumulative_unrecovered_claims": np.float_, + "cumulative_claims": np.float_, + "cumulative_bought_firms": np.int_, + "cumulative_nonregulation_firms": np.int_, + "insurance_firms_cash": np.float_, + "reinsurance_firms_cash": np.float_, + "market_diffvar": np.float_, + "rc_event_schedule_initial": np.int_, + "rc_event_damage_initial": np.float_, + "number_riskmodels": np.int_, + "individual_contracts": np.int_, + "reinsurance_contracts": np.int_, + "unweighted_network_data": np.float_, + "network_node_labels": np.float_, + "network_edge_labels": np.float_, + "number_of_agents": np.int_, + } if isleconfig.slim_log: for name in [ @@ -193,6 +231,7 @@ def rake(hostname=None, replications=50): """Here the jobs are submitted""" with sm.Session(host=hostname, default_cb_to_stdout=True) as sess: + # TODO: Allow for resuming a detatched run with task = sess.get(job_id) tasks = {} for prefix, job in jobs.items(): # If there are 4 parameter sets jobs will be a dict with 4 elements. @@ -214,84 +253,21 @@ def rake(hostname=None, replications=50): for prefix, task in tasks.items(): if task.is_done(): print(f"Finsihed job, prefix {prefix}, with ID {task.id}") - # These will be disordered, fix? results_iterator = task.iterresults() completed_tasks.append(prefix) - """These are the files created to collect the results""" - - # Construct filenames to write to - logfile_dict = {} - for name in requested_logs.keys(): - if "rc_event" in name or "number_riskmodels" in name: - logfile_dict[name] = ( - os.getcwd() - + dir_prefix - + "check_" - + prefix - + requested_logs[name] - ) - elif "firms_cash" in name: - logfile_dict[name] = ( - os.getcwd() - + dir_prefix - + "record_" - + prefix - + requested_logs[name] - ) - else: - logfile_dict[name] = ( - os.getcwd() - + dir_prefix - + "data_" - + prefix - + requested_logs[name] - ) - - # with ... as would be awkward here, so use try ... finally - wfiles_dict = {} - try: - for name in logfile_dict: - wfiles_dict[name] = open(logfile_dict[name], "w") - - """Recreate logger object locally and save logs""" - - """Create local object""" - log = logger.Logger() - - for output_id, result in results_iterator: - position = position_maps[prefix][output_id] - delistified_result = listify.delistify(list(result)) - - """Populate logger object with logs obtained from remote simulation run""" - log.restore_logger_object(list(result)) - - """Save logs as dict (to full__history_logs.dat)""" - log.save_log(True, prefix=prefix) - - """Save network data""" - if isleconfig.save_network: - log.save_network_data(ensemble=True, prefix=prefix) - - """Save logs as individual files""" - for name in logfile_dict: - # Async iteration doesn't preserve order (replications can be reshuffled), so in case - # we want to compare like with like between parameter sets we store the original - # position in the list. - to_write = (position, delistified_result[name]) - # Append the current replication data to the file - wfiles_dict[name].write(repr(to_write) + "\n") - # TODO: Use pickle or hickle rather than repr()s. - - finally: - """Once the data is stored in disk the files are closed""" - for name in logfile_dict: - if name in wfiles_dict: - wfiles_dict[name].close() - del wfiles_dict[name] - print(f"Finished writing files for prefix {prefix}") - - print("Recieved all results and written all files") + results_list = [None for _ in range(replications)] + for output_id, result in results_iterator: + position = position_maps[prefix][output_id] + results_list[position] = result + # Note that the results are still compressed and pickled + print( + f"Downloaded compressed results for job {task.id}, writing to " + f"file {'data/' + prefix + '_full_logs.hdf'}" + ) + start.save_results(results_list, prefix) + print(f"Finished writing results for job {task.id}") + print("Recieved all results and written all files, all finished.") if __name__ == "__main__": diff --git a/fileconvert.py b/fileconvert.py new file mode 100644 index 0000000..a95ff58 --- /dev/null +++ b/fileconvert.py @@ -0,0 +1,89 @@ +import sys +import os + +import hickle +import numpy as np + +import logger + +requested_logs = { + "total_cash": "_cash.dat", + "total_excess_capital": "_excess_capital.dat", + "total_profitslosses": "_profitslosses.dat", + "total_contracts": "_contracts.dat", + "total_operational": "_operational.dat", + "total_reincash": "_reincash.dat", + "total_reinexcess_capital": "_reinexcess_capital.dat", + "total_reinprofitslosses": "_reinprofitslosses.dat", + "total_reincontracts": "_reincontracts.dat", + "total_reinoperational": "_reinoperational.dat", + "total_catbondsoperational": "_total_catbondsoperational.dat", + "market_premium": "_premium.dat", + "market_reinpremium": "_reinpremium.dat", + "cumulative_bankruptcies": "_cumulative_bankruptcies.dat", + "cumulative_market_exits": "_cumulative_market_exits.dat", + "cumulative_unrecovered_claims": "_cumulative_unrecovered_claims.dat", + "cumulative_claims": "_cumulative_claims.dat", + "cumulative_bought_firms": "_cumulative_bought_firms.dat", + "cumulative_nonregulation_firms": "_cumulative_nonregulation_firms.dat", + "insurance_firms_cash": "_insurance_firms_cash.dat", + "reinsurance_firms_cash": "_reinsurance_firms_cash.dat", + "market_diffvar": "_market_diffvar.dat", + "rc_event_schedule_initial": "_rc_event_schedule.dat", + "rc_event_damage_initial": "_rc_event_damage.dat", + "number_riskmodels": "_number_riskmodels.dat", + "individual_contracts": "_insurance_contracts.dat", + "reinsurance_contracts": "_reinsurance_contracts.dat", + "unweighted_network_data": "_unweighted_network_data.dat", + "network_node_labels": "_network_node_labels.dat", + "network_edge_labels": "_network_edge_labels.dat", + "number_of_agents": "_number_of_agents", +} + + +def convert(prefix: str): + dir_prefix = "/data/" + if not prefix.endswith("_full_logs.hdf"): + file = "data/" + prefix + "_full_logs.hdf" + else: + file = "data/" + prefix + results_dict = hickle.load(file) + + found_logs = list(results_dict.keys()) + logfile_dict = {} + for name in found_logs: + if "rc_event" in name or "number_riskmodels" in name: + logfile_dict[name] = ( + os.getcwd() + dir_prefix + "check_" + prefix + requested_logs[name] + ) + elif "firms_cash" in name: + logfile_dict[name] = ( + os.getcwd() + dir_prefix + "record_" + prefix + requested_logs[name] + ) + else: + logfile_dict[name] = ( + os.getcwd() + dir_prefix + "data_" + prefix + requested_logs[name] + ) + for name in logfile_dict: + with open(logfile_dict[name], "w") as rfile: + this_data = results_dict[name] + if isinstance(this_data, np.ndarray): + if this_data.ndim == 0: + this_data = [this_data] + for replication_data in this_data: + rfile.write(repr(replication_data.tolist()) + "\n") + else: + assert isinstance(this_data, list) + for replication_data in this_data: + rfile.write(repr(replication_data) + "\n") + + +if __name__ == "__main__": + # filename = None + # if len(sys.argv) > 1: + # # The server is passed as an argument. + # filename = sys.argv[1] + # else: + # raise ValueError("No filename or prefix given") + filename = "ensemble1" + convert(filename) diff --git a/insurancesimulation.py b/insurancesimulation.py index 732dc21..f89757a 100644 --- a/insurancesimulation.py +++ b/insurancesimulation.py @@ -428,7 +428,7 @@ def add_agents( # We've made the agents, add them to the simulation self.insurancefirms += agents for _ in agents: - self.logger.add_insurance_agent() + self.logger.add_firm("insurance") elif agent_class_string == "reinsurancefirm": # Much the same as above @@ -452,7 +452,7 @@ def add_agents( ] self.reinsurancefirms += agents for _ in agents: - self.logger.add_reinsurance_agent() + self.logger.add_firm("reinsurance") elif agent_class_string == "catbond": raise ValueError(f"Catbonds must be built before being added") @@ -650,11 +650,13 @@ def save_data(self): catbondsoperational_no = sum([cb.operational for cb in self.catbonds]) """ collect agent-level data """ - insurance_firms = [ - (firm.cash, firm.id, firm.operational) for firm in self.insurancefirms + insurance_firms = [firm.cash for firm in self.insurancefirms] + reinsurance_firms = [firm.cash for firm in self.reinsurancefirms] + insurance_contracts = [ + len(firm.underwritten_contracts) for firm in self.insurancefirms ] - reinsurance_firms = [ - (firm.cash, firm.id, firm.operational) for firm in self.reinsurancefirms + reinsurance_contracts = [ + len(firm.underwritten_contracts) for firm in self.reinsurancefirms ] """ prepare dict """ @@ -681,12 +683,8 @@ def save_data(self): "insurance_firms_cash": insurance_firms, "reinsurance_firms_cash": reinsurance_firms, "market_diffvar": self.compute_market_diffvar(), - "individual_contracts": [ - len(firm.underwritten_contracts) for firm in self.insurancefirms - ], - "reinsurance_contracts": [ - len(firm.underwritten_contracts) for firm in self.reinsurancefirms - ], + "individual_contracts": insurance_contracts, + "reinsurance_contracts": reinsurance_contracts, } if isleconfig.save_network: diff --git a/isleconfig.py b/isleconfig.py index 4bdd08c..316fcf0 100644 --- a/isleconfig.py +++ b/isleconfig.py @@ -7,7 +7,9 @@ show_network = False save_network = False # Should logs be small in ensemble runs (only aggregated level data)? -slim_log = True +slim_log = False + +# TODO: These should be simulation parameters! buy_bankruptcies = False enforce_regulations = False aid_relief = False diff --git a/logger.py b/logger.py index 2f20f3c..fc5d066 100644 --- a/logger.py +++ b/logger.py @@ -99,18 +99,17 @@ def record_data(self, data_dict): data_dict: Type dict. Data with the same keys as are used in self.history_log(). Returns None.""" for key in data_dict.keys(): - if key not in ["individual_contracts", "reinsurance_contracts"]: + if key in [ + "individual_contracts", + "reinsurance_contracts", + "insurance_firms_cash", + "reinsurance_firms_cash", + ]: + # These four are stored per-firm + for i in range(len(data_dict[key])): + self.history_logs[key][i].append(data_dict[key][i]) + else: self.history_logs[key].append(data_dict[key]) - elif key == "individual_contracts": - for i in range(len(data_dict["individual_contracts"])): - self.history_logs["individual_contracts"][i].append( - data_dict["individual_contracts"][i] - ) - elif key == "reinsurance_contracts": - for i in range(len(data_dict["reinsurance_contracts"])): - self.history_logs["reinsurance_contracts"][i].append( - data_dict["reinsurance_contracts"][i] - ) def obtain_log(self, requested_logs=None): if requested_logs is None: @@ -129,7 +128,8 @@ def obtain_log(self, requested_logs=None): log = {name: self.history_logs[name] for name in requested_logs} """Convert to list and return""" - return listify.listify(log) + # return listify.listify(log) + return log def restore_logger_object(self, log): """Method to restore logger object. A log can be restored later. It can also be restored @@ -142,7 +142,8 @@ def restore_logger_object(self, log): Returns None.""" """Restore dict""" - log = listify.delistify(log) + if not isinstance(log, dict): + log = listify.delistify(log) try: self.network_data["unweighted_network_data"] = log[ "unweighted_network_data" @@ -247,30 +248,19 @@ def save_network_data(self, ensemble, prefix=""): # wfile.write(str(self.network_data) + "\n") # wfile.write(str(self.rc_event_schedule_initial) + "\n") - def add_insurance_agent(self): - """Method for adding an additional insurer agent to the history log. This is necessary to keep the number - of individual insurance firm logs constant in time. - No arguments. - Returns None.""" - # TODO: should this not also be done for self.history_logs['insurance_firms_cash'] and - # self.history_logs['reinsurance_firms_cash'] - if len(self.history_logs["individual_contracts"]) > 0: - zeroes_to_append = list( - np.zeros(len(self.history_logs["individual_contracts"][0]), dtype=int) - ) - else: - zeroes_to_append = [] - self.history_logs["individual_contracts"].append(zeroes_to_append) - - def add_reinsurance_agent(self): - """Method for adding an additional insurer agent to the history log. This is necessary to keep the number - of individual insurance firm logs constant in time. - No arguments. - Returns None.""" - if len(self.history_logs["reinsurance_contracts"]) > 0: - zeroes_to_append = list( - np.zeros(len(self.history_logs["reinsurance_contracts"][0]), dtype=int) - ) + def add_firm(self, firm_type: str): + """Notifies the logger of a new firm, so blank data can be added to firm-level logs""" + if firm_type == "insurance": + keys = ["individual_contracts", "insurance_firms_cash"] + elif firm_type == "reinsurance": + keys = ["reinsurance_contracts", "reinsurance_firms_cash"] else: - zeroes_to_append = [] - self.history_logs["reinsurance_contracts"].append(zeroes_to_append) + raise ValueError + for key in keys: + if len(self.history_logs[key]) > 0: + zeroes_to_append = list( + np.zeros(len(self.history_logs[key][0]), dtype=int) + ) + else: + zeroes_to_append = [] + self.history_logs[key].append(zeroes_to_append) diff --git a/requirements.txt b/requirements.txt index 7a15fbf..7a19bd9 100644 --- a/requirements.txt +++ b/requirements.txt @@ -6,4 +6,5 @@ argparse>=1.1 sortedcontainers>=2.1.0 dataclasses>=0.6 bokeh -tornado \ No newline at end of file +tornado +hickle \ No newline at end of file diff --git a/start.py b/start.py index 808b2b5..225a8be 100644 --- a/start.py +++ b/start.py @@ -5,11 +5,13 @@ import numpy as np import os import pickle +import zlib import random -from typing import MutableMapping, MutableSequence +from typing import MutableMapping, MutableSequence, List import calibrationscore import insurancesimulation +import listify # import config file and apply configuration import isleconfig @@ -20,6 +22,7 @@ overwrite = False override_no_riskmodels = False + """Creates data file for logs if does not exist""" if not os.path.isdir("data"): if os.path.exists("data"): @@ -79,8 +82,9 @@ def main( # Need to use t+1 as resume will start at time saved save_simulation(t + 1, simulation, sim_params, exit_now=False) - # It is required to return this list to download all the data generated by a single run of the model from the cloud. - return simulation.obtain_log(requested_logs) + # We compress the return value for the sake of minimising data transfer over the network + # TODO: Does this help? Locally it reduces the return sizes by a factor of 3, but maybe sandtable already compress? + return zlib.compress(pickle.dumps(simulation.obtain_log(requested_logs))) def save_simulation( @@ -126,6 +130,128 @@ def load_simulation() -> dict: return file_contents +def save_results(results_list: list, prefix: str): + """Saves the results of a simulation run to disk. Takes a list of compressed, pickled dicts.""" + # We decompress the first result so we can infer shape and type information + current_result = pickle.loads(zlib.decompress(results_list[0])) + replications = len(results_list) + + types = { + "total_cash": np.float_, + "total_excess_capital": np.float_, + "total_profitslosses": np.float_, + "total_contracts": np.int_, + "total_operational": np.int_, + "total_reincash": np.float_, + "total_reinexcess_capital": np.float_, + "total_reinprofitslosses": np.float_, + "total_reincontracts": np.int_, + "total_reinoperational": np.int_, + "total_catbondsoperational": np.int_, + "market_premium": np.float_, + "market_reinpremium": np.float_, + "cumulative_bankruptcies": np.int_, + "cumulative_market_exits": np.int_, + "cumulative_unrecovered_claims": np.float_, + "cumulative_claims": np.float_, + "cumulative_bought_firms": np.int_, + "cumulative_nonregulation_firms": np.int_, + "insurance_firms_cash": np.float_, + "reinsurance_firms_cash": np.float_, + "market_diffvar": np.float_, + "rc_event_schedule_initial": np.object, + "rc_event_damage_initial": np.object, + "number_riskmodels": np.int_, + "individual_contracts": np.int_, + "reinsurance_contracts": np.int_, + "unweighted_network_data": np.float_, + "network_node_labels": np.float_, + "network_edge_labels": np.float_, + "number_of_agents": np.int_, + } + # bad_logs are the logs that don't have a consistent size between replications + bad_logs = [ + "rc_event_schedule_initial", + "rc_event_damage_initial", + "individual_contracts", + "insurance_firms_cash", + "reinsurance_contracts", + "reinsurance_firms_cash", + ] + event_info_names = ["rc_event_schedule_initial", "rc_event_damage_initial"] + + logs_found = current_result.keys() + for name in logs_found: + if name not in types: + print(f"Warning: type of log {name} not known, assuming float") + types[name] = np.float_ + shapes = {} + for name in logs_found: + if name not in bad_logs: + # These are mostly standard 1-d timeseries, but may also include stuff like no_riskmodels + shapes[name] = (replications,) + np.asarray( + current_result[name] + ).squeeze().shape + else: + # These are sets of timeseries, where the sets have variable size; or the event schedules + # We have to decompress each replication here, which is annoying + # TODO: find a better way to do this, maybe have the simulation return uncompressed metadata + found_shapes = [ + np.shape(pickle.loads(zlib.decompress(comp_repdata))[name]) + for comp_repdata in results_list + ] + # This only works because the shapes only vary in one dimension + shapes[name] = (replications,) + max(found_shapes) + + # Should make a nice compact data file + results_dict = { + name: np.zeros(shape=shapes[name], dtype=types[name]) + for name in current_result.keys() + } + # Don't need that first result any more, and it could be large in memory + del current_result + # results_dict is a dictionary of numpy arrays, should be efficient to store. + # The event schedules/damages are of differing lengths. Could pad them with NaNs, but probably + # would be more trouble than it's worth + + for i, compressed_result in enumerate(results_list): + result = pickle.loads(zlib.decompress(compressed_result)) + for name in results_dict: + if (name not in event_info_names) and hasattr(result[name], "__len__"): + arr = np.asarray(result[name]) + shape_slice = tuple([slice(i) for i in arr.shape]) + results_dict[name][i][shape_slice] = result[name] + else: + results_dict[name][i] = result[name] + + # Need to do a little pre-processing + for key in list(results_dict.keys()): + assert isinstance(results_dict[key], np.ndarray) + if results_dict[key].size == 0: + del results_dict[key] + continue + if results_dict[key].dtype == np.object: + results_dict[key] = results_dict[key].tolist() + data = results_dict + # data = (True, (results_dict, event_info)) + # We store everything in one file(!) + + filename = "data/" + prefix + "_full_logs.hdf" + + if os.path.exists(filename): + # Don't want to blindly overwrite, so make backups + import time + + backupfilename = filename + "." + time.strftime("%Y-%m-%dT%H%M%S") + os.rename(filename, backupfilename) + # data is a tuple, first element indicating whether the logs are slim, second element being the data + # TODO: Make everything else work with this new format + # Import here so sandman never tries to import + import hickle + + hickle.dump(data, filename, compression="gzip") + + # main entry point if __name__ == "__main__": @@ -247,7 +373,7 @@ def load_simulation() -> dict: # Run the main program # Note that we pass the filepath as the replic_ID - log = main( + comp_result = main( simulation_parameters, general_rc_event_schedule[0], general_rc_event_damage[0], @@ -257,15 +383,15 @@ def load_simulation() -> dict: replic_id=1, resume=args.resume, ) + # result = pickle.loads(zlib.decompress(result)) - replic_ID = 1 - """ Restore the log at the end of the single simulation run for saving and for potential further study """ - is_background = (not isleconfig.force_foreground) and ( - isleconfig.replicating or (replic_ID in locals()) - ) + # save_results([listify.delistify(list(result))], "single") + + save_results([comp_result], "single") + + decomp_result = pickle.loads(zlib.decompress(comp_result)) L = logger.Logger() - L.restore_logger_object(list(log)) - L.save_log(is_background) + L.restore_logger_object(decomp_result) if isleconfig.save_network: L.save_network_data(ensemble=False) diff --git a/starter_four.sh b/starter_four.sh deleted file mode 100755 index 39a25e1..0000000 --- a/starter_four.sh +++ /dev/null @@ -1,11 +0,0 @@ -mv data/four_operational.dat data/four_operational.dat_$(date +%Y_%h_%d_%H_%M) -mv data/four_contracts.dat data/four_contracts.dat_$(date +%Y_%h_%d_%H_%M) -mv data/four_cash.dat data/four_cash.dat_$(date +%Y_%h_%d_%H_%M) -mv data/four_reinoperational.dat data/four_reinoperational.dat_$(date +%Y_%h_%d_%H_%M) -mv data/four_reincontracts.dat data/four_reincontracts.dat_$(date +%Y_%h_%d_%H_%M) -mv data/four_reincash.dat data/four_reincash.dat_$(date +%Y_%h_%d_%H_%M) -mv data/four_premium.dat data/four_premium.dat_$(date +%Y_%h_%d_%H_%M) - -for ((i=0; i<300; i++)) do - python3 start.py --replicid $i --replicating --riskmodels 4 -done diff --git a/starter_one.sh b/starter_one.sh deleted file mode 100755 index 0197cfa..0000000 --- a/starter_one.sh +++ /dev/null @@ -1,13 +0,0 @@ -export timestamp="$(date +%Y_%h_%d_%H_%M)" # to ensure they are identical -mv data/one_operational.dat{,_$timestamp} -mv data/one_contracts.dat{,_$timestamp} -mv data/one_cash.dat{,_$timestamp} -mv data/one_reinoperational.dat{,_$timestamp} -mv data/one_reincontracts.dat{,_$timestamp} -mv data/one_reincash.dat{,_$timestamp} -mv data/one_premium.dat{,_$timestamp} - -for ((i=0; i<300; i++)) do - python3 start.py --replicid $i --replicating --oneriskmodel -done - diff --git a/starter_three.sh b/starter_three.sh deleted file mode 100755 index 104ecd3..0000000 --- a/starter_three.sh +++ /dev/null @@ -1,11 +0,0 @@ -mv data/three_operational.dat data/three_operational.dat_$(date +%Y_%h_%d_%H_%M) -mv data/three_contracts.dat data/three_contracts.dat_$(date +%Y_%h_%d_%H_%M) -mv data/three_cash.dat data/three_cash.dat_$(date +%Y_%h_%d_%H_%M) -mv data/three_reinoperational.dat data/three_reinoperational.dat_$(date +%Y_%h_%d_%H_%M) -mv data/three_reincontracts.dat data/three_reincontracts.dat_$(date +%Y_%h_%d_%H_%M) -mv data/three_reincash.dat data/three_reincash.dat_$(date +%Y_%h_%d_%H_%M) -mv data/three_premium.dat data/three_premium.dat_$(date +%Y_%h_%d_%H_%M) - -for ((i=0; i<300; i++)) do - python3 start.py --replicid $i --replicating --riskmodels 3 -done diff --git a/starter_two.sh b/starter_two.sh deleted file mode 100755 index 0924f07..0000000 --- a/starter_two.sh +++ /dev/null @@ -1,14 +0,0 @@ -export timestamp="$(date +%Y_%h_%d_%H_%M)" # to ensure they are identical -mv data/replication_rc_event_schedule.dat{,_$timestamp} -mv data/replication_randomseed.dat{,_$timestamp} -mv data/two_operational.dat{,_$timestamp} -mv data/two_contracts.dat{,_$timestamp} -mv data/two_cash.dat{,_$timestamp} -mv data/two_reinoperational.dat{,_$timestamp} -mv data/two_reincontracts.dat{,_$timestamp} -mv data/two_reincash.dat{,_$timestamp} -mv data/two_premium.dat{,_$timestamp} - -for ((i=0; i<300; i++)) do - python3 start.py --replicid $i -done diff --git a/visualisation.py b/visualisation.py index f3d8287..caac5ba 100644 --- a/visualisation.py +++ b/visualisation.py @@ -5,6 +5,7 @@ import matplotlib.animation as animation import isleconfig import pickle +import hickle import scipy import scipy.stats from matplotlib.offsetbox import AnchoredText @@ -224,13 +225,13 @@ def save(self): class Visualisation(object): - def __init__(self, history_logs_list): + def __init__(self, history_log): """Initialises visualisation class for all data. Accepts: history_logs_list: Type List of DataDicts. Each element is a different replication/run. Each DataDict contains all info for that run. No return values.""" - self.history_logs_list = history_logs_list + self.history_log = history_log self.scatter_data = {} def insurer_pie_animation(self, run=0): @@ -239,10 +240,10 @@ def insurer_pie_animation(self, run=0): run: Type Integer. Which replication/run is wanted. Allows loops or specific runs. Returns: self.ins_pie_anim: Type animation class instance. Not used outside this method but is saved.""" - data = self.history_logs_list[run] - insurance_cash = np.array(data["insurance_firms_cash"]) - contract_data = data["individual_contracts"] - event_schedule = data["rc_event_schedule_initial"] + data = self.history_log + insurance_cash = np.array(data["insurance_firms_cash"][run]) + contract_data = data["individual_contracts"][run] + event_schedule = data["rc_event_schedule_initial"][run] self.ins_pie_anim = InsuranceFirmAnimation( insurance_cash, contract_data, event_schedule, "Insurance Firm", save=True ) @@ -255,10 +256,10 @@ def reinsurer_pie_animation(self, run=0): run: Type Integer. Which replication/run is wanted. Allows loops or specific runs. Returns: self.reins_pie_anim: Type animation class instance. Not used outside this method but is saved.""" - data = self.history_logs_list[run] - reinsurance_cash = np.array(data["reinsurance_firms_cash"]) - contract_data = data["reinsurance_contracts"] - event_schedule = data["rc_event_schedule_initial"] + data = self.history_log + reinsurance_cash = data["reinsurance_firms_cash"][run] + contract_data = data["reinsurance_contracts"][run] + event_schedule = data["rc_event_schedule_initial"][run] self.reins_pie_anim = InsuranceFirmAnimation( reinsurance_cash, contract_data, @@ -293,29 +294,18 @@ def insurer_time_series( insurance firms from saved data. Also sets event schedule for single run data, to plots event times on timeseries, as this is only helpful in this case.""" if singlerun: - events = self.history_logs_list[0]["rc_event_schedule_initial"] - damages = self.history_logs_list[0]["rc_event_damage_initial"] + events = self.history_log["rc_event_schedule_initial"][0] + damages = self.history_log["rc_event_damage_initial"][0] else: events = None damages = None # Take the element-wise means/medians of the ensemble set (axis=0) - contracts_agg = [ - history_logs["total_contracts"] for history_logs in self.history_logs_list - ] - profitslosses_agg = [ - history_logs["total_profitslosses"] - for history_logs in self.history_logs_list - ] - operational_agg = [ - history_logs["total_operational"] for history_logs in self.history_logs_list - ] - cash_agg = [ - history_logs["total_cash"] for history_logs in self.history_logs_list - ] - premium_agg = [ - history_logs["market_premium"] for history_logs in self.history_logs_list - ] + contracts_agg = self.history_log["total_contracts"] + profitslosses_agg = self.history_log["total_profitslosses"] + operational_agg = self.history_log["total_operational"] + cash_agg = self.history_log["total_cash"] + premium_agg = self.history_log["market_premium"] contracts = np.mean(contracts_agg, axis=0) profitslosses = np.mean(profitslosses_agg, axis=0) @@ -394,33 +384,19 @@ def reinsurer_time_series( reinsurance firms from saved data. Also sets event schedule for single run data, to plots event times on timeseries, as this is only helpful in this case.""" if singlerun: - events = self.history_logs_list[0]["rc_event_schedule_initial"] - damages = self.history_logs_list[0]["rc_event_damage_initial"] + events = self.history_log["rc_event_schedule_initial"][0] + damages = self.history_log["rc_event_damage_initial"][0] else: events = None damages = None - # Take the element-wise means/medians of the ensemble set (axis=0) - reincontracts_agg = [ - history_logs["total_reincontracts"] - for history_logs in self.history_logs_list - ] - reinprofitslosses_agg = [ - history_logs["total_reinprofitslosses"] - for history_logs in self.history_logs_list - ] - reinoperational_agg = [ - history_logs["total_reinoperational"] - for history_logs in self.history_logs_list - ] - reincash_agg = [ - history_logs["total_reincash"] for history_logs in self.history_logs_list - ] - catbonds_number_agg = [ - history_logs["total_catbondsoperational"] - for history_logs in self.history_logs_list - ] + reincontracts_agg = self.history_log["total_reincontracts"] + reinprofitslosses_agg = self.history_log["total_reinprofitslosses"] + reinoperational_agg = self.history_log["total_reinoperational"] + reincash_agg = self.history_log["total_reincash"] + catbonds_number_agg = self.history_log["total_catbondsoperational"] + # Take the element-wise means/medians of the ensemble set (axis=0) reincontracts = np.mean(reincontracts_agg, axis=0) reinprofitslosses = np.mean(reinprofitslosses_agg, axis=0) reinoperational = np.median(reinoperational_agg, axis=0) @@ -482,23 +458,21 @@ def aux_clustered_exit_records(self, exits): exits: numpy ndarray or list - unclustered series Returns: numpy ndarray of the same length as argument "exits": the clustered series.""" - exits2 = [] - ci = False - cidx = 0 - for ee in exits: - if ci: - exits2.append(0) - if ee > 0: - exits2[cidx] += ee + exits = np.asarray(exits) + assert exits.ndim == 2 + clustered = np.zeros_like(exits) + for ts_index, timeseries in enumerate(exits): + # Sadly have to go row-by-row + current_index = -1 + for i, value in enumerate(timeseries): + if value == 0: + current_index = -1 else: - ci = False - else: - exits2.append(ee) - if ee > 0: - ci = True - cidx = len(exits2) - 1 + if current_index == -1: + current_index = i + clustered[ts_index][i] += value - return np.asarray(exits2, dtype=np.float64) + return clustered def populate_scatter_data(self): """Method to generate data samples that do not have a time component (e.g. the size of bankruptcy events, i.e. @@ -508,77 +482,43 @@ def populate_scatter_data(self): Returns: None.""" """Record data on sizes of unrecovered_claims""" - self.scatter_data["unrecovered_claims"] = [] - for hlog in self.history_logs_list: # for each replication - urc = np.diff(np.asarray(hlog["cumulative_unrecovered_claims"])) - self.scatter_data["unrecovered_claims"] = np.hstack( - [self.scatter_data["unrecovered_claims"], np.extract(urc > 0, urc)] - ) + unrecovered_each_step = np.diff( + self.history_log["cumulative_unrecovered_claims"] + ) + total_claims_each_step = np.diff(self.history_log["cumulative_claims"]) + proportion_claims_unrecovered = unrecovered_each_step / total_claims_each_step - """Record data on sizes of unrecovered_claims""" - self.scatter_data["relative_unrecovered_claims"] = [] - for hlog in self.history_logs_list: # for each replication - urc = np.diff(np.asarray(hlog["cumulative_unrecovered_claims"])) - tcl = np.diff(np.asarray(hlog["cumulative_claims"])) - rurc = urc / tcl - self.scatter_data["relative_unrecovered_claims"] = np.hstack( - [self.scatter_data["unrecovered_claims"], np.extract(rurc > 0, rurc)] - ) - try: - assert ( - np.isinf(self.scatter_data["relative_unrecovered_claims"]).any() - is False - ) - except AssertionError: - pass - # pdb.set_trace() + self.scatter_data["unrecovered_claims"] = np.extract( + unrecovered_each_step > 0, unrecovered_each_step + ) + self.scatter_data["relative_unrecovered_claims"] = np.extract( + unrecovered_each_step > 0, proportion_claims_unrecovered + ) """Record data on sizes of bankruptcy_events""" - self.scatter_data["bankruptcy_events"] = [] - self.scatter_data["bankruptcy_events_relative"] = [] - self.scatter_data["bankruptcy_events_clustered"] = [] - self.scatter_data["bankruptcy_events_relative_clustered"] = [] - for hlog in self.history_logs_list: # for each replication - """Obtain numbers of operational firms. This is for computing the relative share of exiting firms.""" - in_op = np.asarray(hlog["total_operational"])[:-1] - rein_op = np.asarray(hlog["total_reinoperational"])[:-1] - op = in_op + rein_op - exits = np.diff( - np.asarray(hlog["cumulative_market_exits"], dtype=np.float64) - ) - assert (exits <= op).all() - op[op == 0] = 1 + operational = ( + self.history_log["total_operational"] + + self.history_log["total_reinoperational"] + ) + exits = np.diff(self.history_log[["cumulative_market_exits"]]) - """Obtain exits and relative exits""" - # exits = np.diff(np.asarray(hlog["cumulative_market_exits"], dtype=np.float64)) # used above already - rel_exits = exits / op + # exists will be zero when operational=0 anyway, so this quickly makes 0/0=0 + operational[operational == 0] = 1 + rel_exits = exits / operational - """Obtain clustered exits (absolute and relative)""" - exits2 = self.aux_clustered_exit_records(exits) - rel_exits2 = exits2 / op + clustered_exits = self.aux_clustered_exit_records(exits) + clustered_rel_exits = clustered_exits / operational - """Record data""" - self.scatter_data["bankruptcy_events"] = np.hstack( - [self.scatter_data["bankruptcy_events"], np.extract(exits > 0, exits)] - ) - self.scatter_data["bankruptcy_events_relative"] = np.hstack( - [ - self.scatter_data["bankruptcy_events_relative"], - np.extract(rel_exits > 0, rel_exits), - ] - ) - self.scatter_data["bankruptcy_events_clustered"] = np.hstack( - [ - self.scatter_data["bankruptcy_events_clustered"], - np.extract(exits2 > 0, exits2), - ] - ) - self.scatter_data["bankruptcy_events_relative_clustered"] = np.hstack( - [ - self.scatter_data["bankruptcy_events_relative_clustered"], - np.extract(rel_exits2 > 0, rel_exits2), - ] - ) + self.scatter_data["bankruptcy_events"] = np.extract(exits > 0, exits) + self.scatter_data["bankruptcy_events_relative"] = np.extract( + rel_exits > 0, rel_exits + ) + self.scatter_data["bankruptcy_events_clustered"] = np.extract( + clustered_exits > 0, clustered_exits + ) + self.scatter_data["bankruptcy_events_relative_clustered"] = np.extract( + clustered_rel_exits > 0, clustered_rel_exits + ) def show(self): plt.show() @@ -1626,12 +1566,13 @@ def stat_tests(self, upper, lower): if args.single: from numpy import array - # load in data from the history_logs dictionary with open("data/history_logs.dat","r") as rfile: - with open("data/single_history_logs.dat", "r") as rfile: - history_logs_list = [eval(k) for k in rfile] # one dict on each line + # load in data from the history_logs dictionary + data = hickle.load("data/single_full_logs.hdf") + assert type(data) is dict + history_log = data # first create visualisation object, then create graph/animation objects as necessary - vis = Visualisation(history_logs_list) + vis = Visualisation(history_log) if args.pie: vis.insurer_pie_animation() vis.reinsurer_pie_animation() @@ -1641,21 +1582,21 @@ def stat_tests(self, upper, lower): insurerfig.savefig("figures/insurer_singlerun_timeseries.png") reinsurerfig.savefig("figures/reinsurer_singlerun_timeseries.png") vis.show() - N = len(history_logs_list) if args.timeseries_comparison or args.bankruptcydistribution: vis_list = [] colour_list = ["red", "blue", "green", "yellow"] - # Loads all risk model history logs data (very long :'( ) and creates list of visualisation class instances. + # Loads all risk model history logs and creates list of visualisation class instances. filenames = [ - "./data/full_" + x + "_history_logs.dat" + "./data/" + x + "_full_logs.hdf" for x in ["ensemble1", "ensemble2", "ensemble3"] ] for filename in filenames: - with open(filename, "r") as rfile: - history_logs_list = [eval(k) for k in rfile] # one dict on each line - vis_list.append(Visualisation(history_logs_list)) + data = hickle.load(filename) + assert type(data) is dict + history_log = data + vis_list.append(Visualisation(history_log)) if args.timeseries_comparison: # Creates time series for all risk models in ensemble data. @@ -1691,15 +1632,16 @@ def stat_tests(self, upper, lower): vis_list = [] colour_list = ["red", "blue", "green", "yellow"] - # Loads all risk model history logs data (very long :'( ) and creates list of visualisation class instances. + # Loads all risk model history logs and creates list of visualisation class instances. filenames = [ - "./data/" + x + "_history_logs_complete.dat" - for x in ["one", "two", "three", "four"] + "./data/" + x + "_full_logs.hdf" + for x in ["ensemble1", "ensemble2", "ensemble3"] ] for filename in filenames: - with open(filename, "r") as rfile: - history_logs_list = [eval(k) for k in rfile] # one dict on each line - vis_list.append(Visualisation(history_logs_list)) + data = hickle.load(filename) + assert type(data) is dict + history_log = data + vis_list.append(Visualisation(history_log)) # Creates CDF for firm size using cash as measure of size. CP = CDFDistributionPlot( From b44c1ca624c441d483009fc05a79b57be468373c Mon Sep 17 00:00:00 2001 From: Chris Hughes Date: Fri, 30 Aug 2019 14:17:25 +0100 Subject: [PATCH 109/125] Document parameters a little more, misc changes --- catbond.py | 1 + compute_profits_losses_from_cash.py | 17 --- ensemble.py | 97 +++++++++------ genericclasses.py | 5 +- insurancefirms.py | 2 +- insurancesimulation.py | 11 +- isleconfig.py | 176 +++++++++++++++++++--------- logger.py | 7 +- metainsuranceorg.py | 8 +- start.py | 73 ++++++------ 10 files changed, 236 insertions(+), 161 deletions(-) delete mode 100644 compute_profits_losses_from_cash.py diff --git a/catbond.py b/catbond.py index d54b14e..2ab4f8c 100644 --- a/catbond.py +++ b/catbond.py @@ -38,6 +38,7 @@ def __init__( self.per_period_dividend: float = per_period_premium self.creditor = self.simulation self.expiration: int = None + self.dividends_paid = 0 # self.simulation_no_risk_categories = self.simulation.simulation_parameters["no_categories"] def iterate(self, time: int): diff --git a/compute_profits_losses_from_cash.py b/compute_profits_losses_from_cash.py deleted file mode 100644 index 865f749..0000000 --- a/compute_profits_losses_from_cash.py +++ /dev/null @@ -1,17 +0,0 @@ -rm = ["one", "two", "three", "four"] -firmtype = ["", "rein"] - -for r in rm: - for ft in firmtype: - filename = "data/" + r + "_" + ft + "cash.dat" - infile = open(filename, "r") - data = [eval(k) for k in infile] - infile.close() - filename = "data/" + r + "_" + ft + "profitslosses.dat" - outfile = open(filename, "w") - - for series in data: - outputdata = [series[i] - series[i - 1] for i in range(1, len(series))] - outfile.write(str(outputdata) + "\n") - - outfile.close() diff --git a/ensemble.py b/ensemble.py index 0a402bc..ebc018b 100644 --- a/ensemble.py +++ b/ensemble.py @@ -8,18 +8,15 @@ from typing import Dict import time -import pickle -import zlib import numpy as np import sandman2.api as sm import isleconfig -import listify import start import setup_simulation -def rake(hostname=None, replications=35): +def rake(hostname=None, replications=9): """ Uses the sandman2 api to run multiple replications of multiple configurations of the simulation. If hostname=None, runs locally. Otherwise, make sure environment variable SANDMAN_KEY_ID and SANDMAN_KEY_SECRET @@ -48,6 +45,7 @@ def rake(hostname=None, replications=35): parameter_sets["ensemble" + str(number_riskmodels)] = new_parameters ################################################################################################################### + print( f"Running {len(parameter_sets)} simulations of {replications} " f"replications of {default_parameters['max_time']} timesteps" @@ -62,17 +60,21 @@ def rake(hostname=None, replications=35): if not ("SANDMAN_KEY_ID" in os.environ and "SANDMAN_KEY_SECRET" in os.environ): print("Warning: Sandman authentication not found in environment variables.") - # Don't want to use fat logs with lots of time steps/replications max_time = isleconfig.simulation_parameters["max_time"] - if replications * max_time > 5000 and not isleconfig.slim_log: - print( - f"Warning: not using slim logs with {replications} replications and " - f"{max_time} timesteps, logs may be very large" - ) + if not isleconfig.slim_log: + # We can estimate the log size per experiment in GB (max_time is squared as number of insurance firms also + # increases with time and per-firm logs are dominating in the limit). The 6 is empirical + # TODO: Is this even vaguely correct? Who knows! + estimated_log_size = max_time ** 2 * replications * 6 / (1000 ** 3) + if estimated_log_size > 1: + print( + "Uncompressed log size estimated to be above 1GB - consider using slim logs" + ) if hostname is not None and isleconfig.show_network: print("Warning: can't show network on remote server") + isleconfig.show_network = False """Configuration of the ensemble""" @@ -229,7 +231,7 @@ def rake(hostname=None, replications=35): position_maps[prefix] = {o.id: p for p, o in enumerate(job)} """Here the jobs are submitted""" - + print("Jobs constructed, submitting") with sm.Session(host=hostname, default_cb_to_stdout=True) as sess: # TODO: Allow for resuming a detatched run with task = sess.get(job_id) tasks = {} @@ -242,37 +244,60 @@ def rake(hostname=None, replications=35): tasks[prefix] = task print("Now waiting for jobs to complete\033[5m...\033[0m") - # Need to do it this way as dictionary can't change size during iteration - completed_tasks = [] - while len(tasks) > 0: - for prefix in completed_tasks: - del tasks[prefix] - completed_tasks = [] - - time.sleep(0.5) - for prefix, task in tasks.items(): - if task.is_done(): - print(f"Finsihed job, prefix {prefix}, with ID {task.id}") - results_iterator = task.iterresults() - completed_tasks.append(prefix) - - results_list = [None for _ in range(replications)] - for output_id, result in results_iterator: - position = position_maps[prefix][output_id] - results_list[position] = result - # Note that the results are still compressed and pickled - print( - f"Downloaded compressed results for job {task.id}, writing to " - f"file {'data/' + prefix + '_full_logs.hdf'}" - ) - start.save_results(results_list, prefix) - print(f"Finished writing results for job {task.id}") + wait_for_tasks(tasks, replications, position_maps) + print("Recieved all results and written all files, all finished.") +def wait_for_tasks(tasks: dict, replications: int, position_maps: dict): + """tasks is a dict mapping prefixes to job objects + position_maps is a dict of dicts: maps prefixes to dicts maping ids to positions + """ + # Need to do it this way as dictionary can't change size during iteration + completed_tasks = [] + while len(tasks) > 0: + for prefix in completed_tasks: + del tasks[prefix] + completed_tasks = [] + + time.sleep(0.5) + for prefix, task in tasks.items(): + if task.is_done(): + print(f"Finsihed job, prefix {prefix}, with ID {task.id}") + results_iterator = task.iterresults() # Could just use .results()? + completed_tasks.append(prefix) + + results_list = [None for _ in range(replications)] + for output_id, result in results_iterator: + position = position_maps[prefix][output_id] + results_list[position] = result + # Note that the results are still compressed and pickled + print( + f"Obtained compressed results for job {task.id}, writing to " + f"file {'data/' + prefix + '_full_logs.hdf'}" + ) + start.save_results(results_list, prefix) + print(f"Finished writing results for job {task.id}") + + +# TODO: Currently broken due to a sandman bug +def restore_jobs(jobs, hostname): + """jobs is a dict mapping prefixes to job ids""" + # Can't restore jobs on a local scheduler + assert hostname is not None + with sm.Session(host=hostname, default_cb_to_stdout=True) as sess: + tasks = {prefix: sess.get(jobs[prefix]) for prefix in jobs} + # Might need to store the position maps - can't test what can be extracted until sandman is fixed + # position_maps = None + # replications = list(tasks.values())[0].f + + if __name__ == "__main__": host = None if len(sys.argv) > 1: # The server is passed as an argument. host = sys.argv[1] rake(host) + # jobs = {"ensemble1" : "23a3f4e1", + # "ensemble2" : "485f7221"} + # restore_jobs(jobs, host) diff --git a/genericclasses.py b/genericclasses.py index dbc86d0..2377643 100644 --- a/genericclasses.py +++ b/genericclasses.py @@ -45,6 +45,7 @@ def __init__(self): self.profits_losses: float = 0 self.creditor = None self.id = -1 + self.dividends_paid = 0 def _pay(self, obligation: "Obligation"): """Method to _pay other class instances. @@ -69,7 +70,9 @@ def _pay(self, obligation: "Obligation"): recipient = recipient.creditor if self.get_operational(): self.cash -= amount - if purpose != "dividend": + if purpose == "dividend": + self.dividends_paid += amount + else: self.profits_losses -= amount recipient.receive(amount) else: diff --git a/insurancefirms.py b/insurancefirms.py index 6641f07..14a79a8 100644 --- a/insurancefirms.py +++ b/insurancefirms.py @@ -35,7 +35,6 @@ def adjust_dividends(self, time: int, actual_capacity: float): No return values. Method is called from MetaInsuranceOrg iterate method between evaluating reinsurance and insurance risks to calculate dividend to be payed if the firm has made profit and has achieved capital targets.""" - profits = self.get_profitslosses() self.per_period_dividend = max(0, self.dividend_share_of_profits * profits) # max function ensures that no negative dividends are paid @@ -389,6 +388,7 @@ def decide_reinsurance_type(self, risk: genericclasses.RiskProperties) -> str: """Decides whether to get catbond or reinsurance for risk with given properties""" # This should be the only place where VaR is evaluated. It should be moved out if we want to use it for # pricing etc. + # TODO: Are we using premium_share correctly here? catbond_price = ( self.get_catbond_price(risk) * risk.value diff --git a/insurancesimulation.py b/insurancesimulation.py index f89757a..769bb27 100644 --- a/insurancesimulation.py +++ b/insurancesimulation.py @@ -105,7 +105,6 @@ def __init__( ) else: self.risk_factor_distribution = Constant(loc=1.0) - # self.risk_value_distribution = scipy.stats.uniform(loc=100, scale=9900) self.risk_value_distribution = Constant( loc=simulation_parameters["value_per_risk"] ) @@ -532,7 +531,7 @@ def iterate(self, t: int): print("Next peril ", self.rc_event_schedule[categ_id]) # Provide government aid if damage severe enough - if isleconfig.aid_relief: + if self.simulation_parameters["aid_relief"]: self.bank.adjust_aid_budget(time=t) if damage_extent is not None: op_firms = [firm for firm in self.insurancefirms if firm.operational] @@ -556,7 +555,7 @@ def iterate(self, t: int): if reinagent.cash < 0: print(f"Reinsurer {reinagent.id} has negative cash") - if isleconfig.buy_bankruptcies: + if self.simulation_parameters["buy_bankruptcies"]: for reinagent in self.reinsurancefirms: if reinagent.operational: reinagent.consider_buyout(firm_type="reinsurer") @@ -575,7 +574,7 @@ def iterate(self, t: int): if agent.cash < 0: print(f"Insurer {agent.id} has negative cash") - if isleconfig.buy_bankruptcies: + if self.simulation_parameters["buy_bankruptcies"]: for agent in self.insurancefirms: if agent.operational: agent.consider_buyout(firm_type="insurer") @@ -658,6 +657,8 @@ def save_data(self): reinsurance_contracts = [ len(firm.underwritten_contracts) for firm in self.reinsurancefirms ] + ins_dividends = sum([firm.dividends_paid for firm in self.insurancefirms]) + re_dividends = sum([firm.dividends_paid for firm in self.reinsurancefirms]) """ prepare dict """ current_log = { @@ -685,6 +686,8 @@ def save_data(self): "market_diffvar": self.compute_market_diffvar(), "individual_contracts": insurance_contracts, "reinsurance_contracts": reinsurance_contracts, + "insurance_cumulative_dividends": ins_dividends, + "reinsurance_cumulative_dividends": re_dividends, } if isleconfig.save_network: diff --git a/isleconfig.py b/isleconfig.py index 316fcf0..037bf63 100644 --- a/isleconfig.py +++ b/isleconfig.py @@ -1,130 +1,192 @@ -oneriskmodel = False +# These should usually be left False by default - they are modified by command line parameters (or similar) replicating = False -force_foreground = False +force_foreground = False # TODO: remove this? verbose = False showprogress = False -# Should network be visualized? This should be False by default, to be overridden by commandline arguments show_network = False save_network = False -# Should logs be small in ensemble runs (only aggregated level data)? slim_log = False -# TODO: These should be simulation parameters! -buy_bankruptcies = False -enforce_regulations = False -aid_relief = False - +# fmt: off simulation_parameters = { + "max_time": 4000, "no_categories": 4, + + # no_[re]insurancefirms are initial conditions only "no_insurancefirms": 20, "no_reinsurancefirms": 4, - "no_riskmodels": 3, + + # Numer of risk models in the market + "no_riskmodels": 1, + # values >=1; inaccuracy higher with higher values "riskmodel_inaccuracy_parameter": 2, # values >=1; factor of additional liquidity beyond value at risk "riskmodel_margin_of_safety": 1.5, - "margin_increase": 0, + # "margin_increase" modifies the margin of safety depending on the number of risks models available in the market. # When it is 0 all risk models have the same margin of safety. - "value_at_risk_tail_probability": 0.02, + "margin_increase": 0, + # values <1, >0, usually close to 0; tail probability at which the value at risk is taken by the risk models + "value_at_risk_tail_probability": 0.02, + + # The markup fims aim to make "norm_profit_markup": 0.15, - "rein_norm_profit_markup": 0.15, + "rein_norm_profit_markup": 0.15, # TODO: UNUSED + + # The share of profits that is given back to investors (the simulation) "dividend_share_of_profits": 0.4, + + # Insurance contracts have a random runtime, "mean_contract_runtime": 12, "contract_runtime_halfspread": 2, + + # Reinsurance contracts have a fixed runtime "reinsurance_contract_runtime": 12, "default_contract_payment_period": 3, - "max_time": 4000, + + # The ammount of money in the economy. Mainly used to check that we aren't losing any anywhere "money_supply": 2000000000, + + # The mean time between catastrophe events "event_time_mean_separation": 100 / 3, + + # Whether contacts expire after the first triggering (only tested on False) "expire_immediately": False, + + # Risk factor provide a per-risk heterogeneity - the risk factor is the probability that a risk is affected when + # a catastrophe occurs in its category. Not properly implemented (insurers can't take into account). If False, all + # risk factors are 1. "risk_factors_present": False, "risk_factor_lower_bound": 0.4, "risk_factor_upper_bound": 0.6, + + # TODO: Acceptance threshold appears UNUSED "initial_acceptance_threshold": 0.5, "acceptance_threshold_friction": 0.9, + + # Each timestep a new [re]insurer enters the market with a given probability "insurance_firm_market_entry_probability": 0.3, # 0.02, "reinsurance_firm_market_entry_probability": 0.05, # 0.004, - # Determines the reinsurance type of the simulation. Should be "non-proportional" or "excess-of-loss" + + # Determines the reinsurance type of the simulation. Should be "non-proportional" or "proportional". Only + # the former actually works "simulation_reinsurance_type": "non-proportional", + + # If True, will use the static deductible (if reinsurance type is non-proportional), otherwise the dynamic + # deductible settings below are used + "static_non-proportional_reinsurance_levels": False, "default_non-proportional_reinsurance_deductible": 0.3, - "default_non-proportional_reinsurance_excess": 1.0, + + # The upper bound of reinsurance that firms can get. Should usually be 1 + "default_non-proportional_reinsurance_limit": 1.0, + + # The share of the premiums that reinsurers take "default_non-proportional_reinsurance_premium_share": 0.3, - "static_non-proportional_reinsurance_levels": False, + + # Insurance and Reinsurance deductible ranges - each firm gets a random value from these ranges + "insurance_reinsurance_levels_lower_bound": 0.25, + "insurance_reinsurance_levels_upper_bound": 0.30, + "reinsurance_reinsurance_levels_lower_bound": 0.5, + "reinsurance_reinsurance_levels_upper_bound": 0.95, + + # Turn catbonds or reinsurance off "catbonds_off": False, "reinsurance_off": False, + + # Firms adjust their capacity target based on the ratio between their risk held and the capacity they could gain + # from reinsurance "capacity_target_decrement_threshold": 1.8, "capacity_target_increment_threshold": 1.2, "capacity_target_decrement_factor": 24 / 25, "capacity_target_increment_factor": 25 / 24, - # Retention parameters - "insurance_retention": 0.85, # Ratio of insurance contracts retained every iteration. - "reinsurance_retention": 1, # Ratio of reinsurance contracts retained every iteration. - # Premium sensitivity parameters + + # When a contract expires it has a chance to be immediatly offered again to the insurer - set that probability + "insurance_retention": 0.85, + "reinsurance_retention": 1, + + # The market premiums are based on the ammount of cash in the market - how sensitive is it? Higher is more sensitive "premium_sensitivity": 5, - # This parameter represents how sensitive is the variation of the insurance premium with respect of the capital - # of the market. Higher means more sensitive. "reinpremium_sensitivity": 6, - # This parameter represents how sensitive is the variation of the reinsurance premium with respect of the capital - # of the market. Higher means more sensitive. - # Balanced portfolio parameters - "insurers_balance_ratio": 0.1, + # This ratio represents how low we want to keep the standard deviation of the cash reserved below the mean for - # insurers. Lower means more balanced. + # [re]insurers. Lower means more balanced. + "insurers_balance_ratio": 0.1, "reinsurers_balance_ratio": 20, - # This ratio represents how low we want to keep the standard deviation of the cash reserved below the mean for - # reinsurers. Lower means more balanced. (Deactivated for the moment) + + + # Intensity of the recursion algorithm to balance the portfolio of risks "insurers_recursion_limit": 50, - # Intensity of the recursion algorithm to balance the portfolio of risks for insurers. "reinsurers_recursion_limit": 10, - # Intensity of the recursion algorithm to balance the portfolio of risks for reinsurers. + # Market permanency parameters - "market_permanency_off": False, # This parameter activates (deactivates) the following market permanency constraints. + "market_permanency_off": False, + # If a firm has cash less that this, they leave the market "cash_permanency_limit": 100, - # This parameter enforces the limit under which the firms leave the market because they cannot underwrite anything. + # If insurers have fewer than this many contracts for the below time period then they leave the market "insurance_permanency_contracts_limit": 4, - # If insurers stay for too long under this limit of contracts they deccide to leave the market. + # Likewise, but regarding the ratio of actual cash to cash being reserved to cover risk "insurance_permanency_ratio_limit": 0.6, - # If insurers stay for too long under this limit they leave the market because they have too much capital. + # The period that the insurers wait before leaving the market in the above situations "insurance_permanency_time_constraint": 24, - # The period that the insurers wait before leaving the market if they have few capital or few contract . + + # Likewise for reinsurers "reinsurance_permanency_contracts_limit": 2, - # If reinsurers stay for too long under this limit of contracts they deccide to leave the market. "reinsurance_permanency_ratio_limit": 0.8, - # If reinsurers stay for too long under this limit they leave the market because they have too much capital. "reinsurance_permanency_time_constraint": 48, - # This parameter defines the period that the reinsurers wait if they have too little capital or too few contracts - # before leaving the market. - # Insurance and Reinsurance deductibles - "insurance_reinsurance_levels_lower_bound": 0.25, - "insurance_reinsurance_levels_upper_bound": 0.30, - "reinsurance_reinsurance_levels_lower_bound": 0.5, - "reinsurance_reinsurance_levels_upper_bound": 0.95, + + # The cash a [re]insurer has when it is created "initial_agent_cash": 80000, "initial_reinagent_cash": 2000000, + + # The per time period bank interest rate (note: time period ~ 1 month) "interest_rate": 0.001, + + # TODO: UNUSED "reinsurance_limit": 0.1, + + # The limits on the adjustment for market price of insurance "upper_price_limit": 1.2, "lower_price_limit": 0.85, + + # The total number of (insurance) risks in the market "no_risks": 20000, + + # The value of each insurance risk "value_per_risk": 1000, - # Determines the maximum upscaling of premiums based on insurer size - set to 1 to disable scaled premiums. - # High values will give bigger insurers more money + + # The maximum upscaling of premiums based on insurer size - set to 1 to disable scaled premiums. + # High values will give bigger insurers more money (we can assume they are more "trusted") # Values between 0 and 1 will make premiums decrease for bigger insurers. - "max_scale_premiums": 1.2, + "max_scale_premiums": 1, + # Determines the minimum fraction of inaccuracy that insurers can achieve - a value of 0 means the biggest insurers - # can be perfectly accurate, a value of 1 disables changes in inaccuracy based on size - "scale_inaccuracy": 0.3, - # The smallest number of tranches that an insurer will issue when asking for reinsurance. Note: even if this is 1, - # insurers may still end up with layered reinsurance to fill gaps - "min_tranches": 1, - "aid_budget": 1000000, + # can be perfectly accurate, a value of 1 disables changes in inaccuracy based on size (bigger insurers have more + # data/resources to model catastrophes) + "scale_inaccuracy": 1, + + # The number of tranches that an insurer will get for reinsurance + "min_tranches": 2, + # If this is true then reinsurance premiums can be adjusted after cat events "adjustable_reinsurance_premiums": False, - # How many cat events are required to adjust the premium + + # How many cat events are required to adjust the premium in the above "reinsurance_premium_adjustment_frequency": 2, - # The amount (as a fraction of the existing premium) to increase by + + # The amount (as a fraction of the existing premium) to increase premimus by in the above "reinsurance_premium_adjustment_amount": 0.1, + + # Whether firms can choose to buy out other bankrupt firms + "buy_bankruptcies": False, + + # Enable or disable the regulator + "enforce_regulations": False, + + # Enable or disable regulator bailouts and set the budget + "aid_relief": False, + "aid_budget": 1000000, } +# fmt: on diff --git a/logger.py b/logger.py index fc5d066..6b5fec2 100644 --- a/logger.py +++ b/logger.py @@ -13,7 +13,8 @@ "cumulative_claims insurance_firms_cash reinsurance_firms_cash market_diffvar " "rc_event_schedule_initial rc_event_damage_initial number_riskmodels individual_contracts reinsurance_contracts " "unweighted_network_data network_node_labels network_edge_labels number_of_agents " - "cumulative_bought_firms cumulative_nonregulation_firms" + "cumulative_bought_firms cumulative_nonregulation_firms insurance_cumulative_dividends " + "reinsurance_cumulative_dividends" ).split(" ") @@ -92,6 +93,8 @@ def __init__( self.history_logs["network_node_labels"] = [] self.history_logs["network_edge_labels"] = [] self.history_logs["number_of_agents"] = [] + self.history_logs["insurance_cumulative_dividends"] = [] + self.history_logs["reinsurance_cumulative_dividends"] = [] def record_data(self, data_dict): """Method to record data for one period @@ -176,7 +179,7 @@ def save_log(self, ensemble_run: bool, prefix: str = "") -> None: ensemble_run: Type bool. Is this an ensemble run (true) or not (false). prefix: Type str. The prefix to prepend to the filename Returns None.""" - + # TODO: remove if not required """Prepare writing tasks""" if ensemble_run: to_log = self.replication_log_prepare(prefix) diff --git a/metainsuranceorg.py b/metainsuranceorg.py index 57dafac..4973329 100644 --- a/metainsuranceorg.py +++ b/metainsuranceorg.py @@ -180,7 +180,7 @@ def __init__(self, simulation_parameters: dict, agent_parameters: AgentPropertie "default_non-proportional_reinsurance_deductible" ] self.np_reinsurance_limit_fraction = simulation_parameters[ - "default_non-proportional_reinsurance_excess" + "default_non-proportional_reinsurance_limit" ] self.np_reinsurance_premium_share = simulation_parameters[ "default_non-proportional_reinsurance_premium_share" @@ -262,7 +262,7 @@ def iterate(self, time: int): if self.operational: # Firms submit cash and var data for regulation every 12 iterations - if time % 12 == 0 and isleconfig.enforce_regulations: + if time % 12 == 0 and self.simulation_parameters["enforce_regulations"]: self.submit_regulator_report(time) if not self.operational: # If not enough average cash then firm is closed and so no underwriting. @@ -412,7 +412,7 @@ def enter_bankruptcy(self, time: int): This method is used when a firm does not have enough cash to pay all its obligations. It is only called from the method self.enter_illiquidity() which is only called from the method self._effect_payments(). This method dissolves the firm through the method self.dissolve().""" - if isleconfig.buy_bankruptcies: + if self.simulation_parameters["buy_bankruptcies"]: if self.is_insurer and self.operational: self.simulation.add_firm_to_be_sold(self, time, "record_bankruptcy") self.operational = False @@ -1335,7 +1335,7 @@ def submit_regulator_report(self, time): if condition == "Warning": self.warning = True if condition == "LoseControl": - if isleconfig.buy_bankruptcies: + if self.simulation_parameters["buy_bankruptcies"]: self.simulation.add_firm_to_be_sold( self, time, "record_nonregulation_firm" ) diff --git a/start.py b/start.py index 225a8be..59380bd 100644 --- a/start.py +++ b/start.py @@ -7,7 +7,7 @@ import pickle import zlib import random -from typing import MutableMapping, MutableSequence, List +from typing import MutableMapping, MutableSequence, List, Tuple import calibrationscore import insurancesimulation @@ -43,7 +43,7 @@ def main( replic_id: int, requested_logs: MutableSequence = None, resume: bool = False, -) -> MutableSequence: +) -> Tuple[bytes, dict]: if not resume: np.random.seed(np_seed) random.seed(random_seed) @@ -82,9 +82,14 @@ def main( # Need to use t+1 as resume will start at time saved save_simulation(t + 1, simulation, sim_params, exit_now=False) - # We compress the return value for the sake of minimising data transfer over the network - # TODO: Does this help? Locally it reduces the return sizes by a factor of 3, but maybe sandtable already compress? - return zlib.compress(pickle.dumps(simulation.obtain_log(requested_logs))) + log = simulation.obtain_log(requested_logs) + + # We compute metadata about the return data that isn't compressed, so a skeleton data structure can be constructed + # before decompression + found_shapes = {name: np.shape(log[name]) for name in log} + + # We compress the return value for the sake of minimising data transfer over the network and RAM usage + return (zlib.compress(pickle.dumps(log)), found_shapes) def save_simulation( @@ -131,9 +136,10 @@ def load_simulation() -> dict: def save_results(results_list: list, prefix: str): - """Saves the results of a simulation run to disk. Takes a list of compressed, pickled dicts.""" - # We decompress the first result so we can infer shape and type information - current_result = pickle.loads(zlib.decompress(results_list[0])) + """Saves the results of a simulation run to disk. results_list is a list of tuples, where each tuple consists of + a compressed, pickled dict and a dict of the shapes of the data in the compressed dict (metadata)""" + # We use the first metadata to infer basic shape information + current_shapes = results_list[0][1] replications = len(results_list) types = { @@ -156,18 +162,22 @@ def save_results(results_list: list, prefix: str): "cumulative_claims": np.float_, "cumulative_bought_firms": np.int_, "cumulative_nonregulation_firms": np.int_, - "insurance_firms_cash": np.float_, - "reinsurance_firms_cash": np.float_, "market_diffvar": np.float_, + # Would store these two as an array of lists, but hdf5 can't do that "rc_event_schedule_initial": np.object, "rc_event_damage_initial": np.object, "number_riskmodels": np.int_, - "individual_contracts": np.int_, - "reinsurance_contracts": np.int_, "unweighted_network_data": np.float_, "network_node_labels": np.float_, "network_edge_labels": np.float_, "number_of_agents": np.int_, + "insurance_cumulative_dividends": np.float_, + "reinsurance_cumulative_dividends": np.float_, + # These are the big ones, so we need to pay attention to data types + "insurance_firms_cash": np.float32, + "reinsurance_firms_cash": np.float32, + "individual_contracts": np.uint16, + "reinsurance_contracts": np.uint16, } # bad_logs are the logs that don't have a consistent size between replications bad_logs = [ @@ -180,7 +190,7 @@ def save_results(results_list: list, prefix: str): ] event_info_names = ["rc_event_schedule_initial", "rc_event_damage_initial"] - logs_found = current_result.keys() + logs_found = current_shapes.keys() for name in logs_found: if name not in types: print(f"Warning: type of log {name} not known, assuming float") @@ -189,33 +199,26 @@ def save_results(results_list: list, prefix: str): for name in logs_found: if name not in bad_logs: # These are mostly standard 1-d timeseries, but may also include stuff like no_riskmodels - shapes[name] = (replications,) + np.asarray( - current_result[name] - ).squeeze().shape + shapes[name] = (replications,) + current_shapes[name] else: - # These are sets of timeseries, where the sets have variable size; or the event schedules - # We have to decompress each replication here, which is annoying - # TODO: find a better way to do this, maybe have the simulation return uncompressed metadata - found_shapes = [ - np.shape(pickle.loads(zlib.decompress(comp_repdata))[name]) - for comp_repdata in results_list - ] - # This only works because the shapes only vary in one dimension + # We could probably do this for all of the data, but this is fine for now. + # These are sets of timeseries: the sets have variable size (also the event schedules) + # We use the uncompressed metadata + found_shapes = [result[1][name] for result in results_list] + # This only works because the shapes only vary in one dimension (tuple comparison is lexicographic) shapes[name] = (replications,) + max(found_shapes) - # Should make a nice compact data file + # Make a skeleton data structure so we only need to have one uncompressed log in memory at a time results_dict = { name: np.zeros(shape=shapes[name], dtype=types[name]) - for name in current_result.keys() + for name in current_shapes.keys() } - # Don't need that first result any more, and it could be large in memory - del current_result # results_dict is a dictionary of numpy arrays, should be efficient to store. # The event schedules/damages are of differing lengths. Could pad them with NaNs, but probably # would be more trouble than it's worth - for i, compressed_result in enumerate(results_list): - result = pickle.loads(zlib.decompress(compressed_result)) + for i, result_tuple in enumerate(results_list): + result = pickle.loads(zlib.decompress(result_tuple[0])) for name in results_dict: if (name not in event_info_names) and hasattr(result[name], "__len__"): arr = np.asarray(result[name]) @@ -290,11 +293,6 @@ def save_results(results_list: list, prefix: str): help="Resume the simulation from a previous save in ./data/simulation_save.pkl. " "All other arguments will be ignored", ) - parser.add_argument( - "--oneriskmodel", - action="store_true", - help="allow overriding the number of riskmodels from the standard config (with 1)", - ) parser.add_argument( "--riskmodels", type=int, @@ -322,9 +320,6 @@ def save_results(results_list: list, prefix: str): ) args = parser.parse_args() - if args.oneriskmodel: - isleconfig.oneriskmodel = True - override_no_riskmodels = 1 if args.riskmodels: override_no_riskmodels = args.riskmodels if args.file: @@ -389,7 +384,7 @@ def save_results(results_list: list, prefix: str): save_results([comp_result], "single") - decomp_result = pickle.loads(zlib.decompress(comp_result)) + decomp_result = pickle.loads(zlib.decompress(comp_result[0])) L = logger.Logger() L.restore_logger_object(decomp_result) if isleconfig.save_network: From 9868f53071e9122b792c602fdb4f86c9fb96a1e0 Mon Sep 17 00:00:00 2001 From: Chris Hughes Date: Tue, 3 Sep 2019 14:14:38 +0100 Subject: [PATCH 110/125] Adding function for sensitivity analysis --- ensemble.py | 44 +++++++--- isleconfig.py | 6 +- sensitivity.py | 234 +++++++++++++++++++++++++++++++++++++++++++++++++ start.py | 32 ++++--- 4 files changed, 292 insertions(+), 24 deletions(-) create mode 100644 sensitivity.py diff --git a/ensemble.py b/ensemble.py index ebc018b..0a47201 100644 --- a/ensemble.py +++ b/ensemble.py @@ -7,6 +7,7 @@ import os from typing import Dict import time +import importlib import numpy as np import sandman2.api as sm @@ -16,7 +17,7 @@ import setup_simulation -def rake(hostname=None, replications=9): +def rake(hostname=None, replications=9, summary: callable = None): """ Uses the sandman2 api to run multiple replications of multiple configurations of the simulation. If hostname=None, runs locally. Otherwise, make sure environment variable SANDMAN_KEY_ID and SANDMAN_KEY_SECRET @@ -24,7 +25,11 @@ def rake(hostname=None, replications=9): Args: hostname: The remote server to run the job on replications: The number of replications of each parameter set to run + summary: The summary statistic (function) to apply to the results """ + if importlib.util.find_spec("hickle") is None: + raise ModuleNotFoundError("hickle not found but required for saving logs") + if hostname is None: print("Running ensemble locally") else: @@ -224,6 +229,7 @@ def rake(hostname=None, replications=9): save_iter, 0, list(requested_logs.keys()), + summary=summary, ) for x in range(replications) ] @@ -244,17 +250,21 @@ def rake(hostname=None, replications=9): tasks[prefix] = task print("Now waiting for jobs to complete\033[5m...\033[0m") - wait_for_tasks(tasks, replications, position_maps) + wait_for_tasks(tasks, replications, position_maps, summary) print("Recieved all results and written all files, all finished.") -def wait_for_tasks(tasks: dict, replications: int, position_maps: dict): +def wait_for_tasks( + tasks: dict, replications: int, position_maps: dict, summary: callable = None +): """tasks is a dict mapping prefixes to job objects position_maps is a dict of dicts: maps prefixes to dicts maping ids to positions """ # Need to do it this way as dictionary can't change size during iteration completed_tasks = [] + if summary is not None: + summary_values = {} while len(tasks) > 0: for prefix in completed_tasks: del tasks[prefix] @@ -271,13 +281,21 @@ def wait_for_tasks(tasks: dict, replications: int, position_maps: dict): for output_id, result in results_iterator: position = position_maps[prefix][output_id] results_list[position] = result - # Note that the results are still compressed and pickled - print( - f"Obtained compressed results for job {task.id}, writing to " - f"file {'data/' + prefix + '_full_logs.hdf'}" - ) - start.save_results(results_list, prefix) - print(f"Finished writing results for job {task.id}") + if summary is None: + # Note that the results are still compressed and pickled + print( + f"Obtained compressed results for job {task.id}, writing to " + f"file {'data/' + prefix + '_full_logs.hdf'}" + ) + start.save_results(results_list, prefix) + print(f"Finished writing results for job {task.id}") + else: + # Should be very small, so no need to worry about RAM etc. + print(f"Obtained summary statistic for job {task.id}") + summary_values[prefix] = results_list + if summary is not None: + print("All tasks complete, writing summary statistics to file") + start.save_summary(summary_values) # TODO: Currently broken due to a sandman bug @@ -292,12 +310,16 @@ def restore_jobs(jobs, hostname): # replications = list(tasks.values())[0].f +def summmmmm(log): + return log["cumulative_bankruptcies"][-1] + + if __name__ == "__main__": host = None if len(sys.argv) > 1: # The server is passed as an argument. host = sys.argv[1] - rake(host) + rake(host, summary=summmmmm) # jobs = {"ensemble1" : "23a3f4e1", # "ensemble2" : "485f7221"} # restore_jobs(jobs, host) diff --git a/isleconfig.py b/isleconfig.py index 037bf63..1ebd76c 100644 --- a/isleconfig.py +++ b/isleconfig.py @@ -9,7 +9,7 @@ # fmt: off simulation_parameters = { - "max_time": 4000, + "max_time": 40, "no_categories": 4, # no_[re]insurancefirms are initial conditions only @@ -97,7 +97,7 @@ # Firms adjust their capacity target based on the ratio between their risk held and the capacity they could gain # from reinsurance - "capacity_target_decrement_threshold": 1.8, + "capacity_target_decrement_threshold": 1.8, # TODO: What? "capacity_target_increment_threshold": 1.2, "capacity_target_decrement_factor": 24 / 25, "capacity_target_increment_factor": 25 / 24, @@ -171,7 +171,7 @@ "min_tranches": 2, # If this is true then reinsurance premiums can be adjusted after cat events - "adjustable_reinsurance_premiums": False, + "adjustable_reinsurance_premiums": True, # How many cat events are required to adjust the premium in the above "reinsurance_premium_adjustment_frequency": 2, diff --git a/sensitivity.py b/sensitivity.py new file mode 100644 index 0000000..9ee5d65 --- /dev/null +++ b/sensitivity.py @@ -0,0 +1,234 @@ +""" +This script allows to launch an ensemble of simulations for different number of risks models. +It can be run locally if no argument is passed when called from the terminal. +It can be run in the cloud if it is passed as argument the sandman2 server that will be used. +""" +import sys +import os +from typing import Dict +import time +import importlib + +import numpy as np +import sandman2.api as sm + +import isleconfig +import start +import setup_simulation + + +def rake(hostname=None, summary: callable = None): + """ + Uses the sandman2 api to run multiple replications of multiple configurations of the simulation. + If hostname=None, runs locally. Otherwise, make sure environment variable SANDMAN_KEY_ID and SANDMAN_KEY_SECRET + are set. + Args: + hostname: The remote server to run the job on + summary: The summary statistic (function) to apply to the results + """ + if importlib.util.find_spec("hickle") is None: + raise ModuleNotFoundError("hickle not found but required for saving logs") + + if hostname is None: + print("Running ensemble locally") + else: + print(f"Running ensemble on {hostname}") + """Configure the parameter sets to run""" + default_parameters: Dict = isleconfig.simulation_parameters + parameter_list = None + + ################################################################################################################### + # This section should be freely modified to determine the experiment + # The keys of parameter_sets are the prefixes to save logs under, the values are the parameters to run + # The keys should be strings + + import SALib.util + import SALib.sample.morris + + problem = SALib.util.read_param_file("isle_all_parameters.txt") + param_values = SALib.sample.morris.sample(problem, N=problem["num_vars"] * 3) + parameters = [tuple(row) for row in param_values] + parameter_list = [ + {problem["names"][i]: row[i] for i in range(len(row))} for row in param_values + ] + for d in parameter_list: + d.update(default_parameters.copy()) + d["max_time"] = 2000 + assert parameter_list[1] != parameter_list[0] + + ################################################################################################################### + + max_time = isleconfig.simulation_parameters["max_time"] + + print(f"Running {len(parameter_list)} simulations of {max_time} timesteps") + + """Sanity checks""" + + # Check that the necessary env variables are set + if hostname is not None: + if not ("SANDMAN_KEY_ID" in os.environ and "SANDMAN_KEY_SECRET" in os.environ): + print("Warning: Sandman authentication not found in environment variables.") + + if hostname is not None and isleconfig.show_network: + print("Warning: can't show network on remote server") + isleconfig.show_network = False + + """Configuration of the ensemble""" + + """Configure the return values and corresponding file suffixes where they should be saved""" + requested_logs = { + "total_cash": "_cash.dat", + "total_excess_capital": "_excess_capital.dat", + "total_profitslosses": "_profitslosses.dat", + "total_contracts": "_contracts.dat", + "total_operational": "_operational.dat", + "total_reincash": "_reincash.dat", + "total_reinexcess_capital": "_reinexcess_capital.dat", + "total_reinprofitslosses": "_reinprofitslosses.dat", + "total_reincontracts": "_reincontracts.dat", + "total_reinoperational": "_reinoperational.dat", + "total_catbondsoperational": "_total_catbondsoperational.dat", + "market_premium": "_premium.dat", + "market_reinpremium": "_reinpremium.dat", + "cumulative_bankruptcies": "_cumulative_bankruptcies.dat", + "cumulative_market_exits": "_cumulative_market_exits.dat", + "cumulative_unrecovered_claims": "_cumulative_unrecovered_claims.dat", + "cumulative_claims": "_cumulative_claims.dat", + "cumulative_bought_firms": "_cumulative_bought_firms.dat", + "cumulative_nonregulation_firms": "_cumulative_nonregulation_firms.dat", + "insurance_firms_cash": "_insurance_firms_cash.dat", + "reinsurance_firms_cash": "_reinsurance_firms_cash.dat", + "market_diffvar": "_market_diffvar.dat", + "rc_event_schedule_initial": "_rc_event_schedule.dat", + "rc_event_damage_initial": "_rc_event_damage.dat", + "number_riskmodels": "_number_riskmodels.dat", + "individual_contracts": "_insurance_contracts.dat", + "reinsurance_contracts": "_reinsurance_contracts.dat", + "unweighted_network_data": "_unweighted_network_data.dat", + "network_node_labels": "_network_node_labels.dat", + "network_edge_labels": "_network_edge_labels.dat", + "number_of_agents": "_number_of_agents", + } + """Define the numpy types of the underlying data in each requested log""" + types = { + "total_cash": np.float_, + "total_excess_capital": np.float_, + "total_profitslosses": np.float_, + "total_contracts": np.int_, + "total_operational": np.int_, + "total_reincash": np.float_, + "total_reinexcess_capital": np.float_, + "total_reinprofitslosses": np.float_, + "total_reincontracts": np.int_, + "total_reinoperational": np.int_, + "total_catbondsoperational": np.int_, + "market_premium": np.float_, + "market_reinpremium": np.float_, + "cumulative_bankruptcies": np.int_, + "cumulative_market_exits": np.int_, + "cumulative_unrecovered_claims": np.float_, + "cumulative_claims": np.float_, + "cumulative_bought_firms": np.int_, + "cumulative_nonregulation_firms": np.int_, + "insurance_firms_cash": np.float_, + "reinsurance_firms_cash": np.float_, + "market_diffvar": np.float_, + "rc_event_schedule_initial": np.int_, + "rc_event_damage_initial": np.float_, + "number_riskmodels": np.int_, + "individual_contracts": np.int_, + "reinsurance_contracts": np.int_, + "unweighted_network_data": np.float_, + "network_node_labels": np.float_, + "network_edge_labels": np.float_, + "number_of_agents": np.int_, + } + + if isleconfig.slim_log: + for name in [ + "insurance_firms_cash", + "reinsurance_firms_cash", + "individual_contracts", + "reinsurance_contracts", + "unweighted_network_data", + "network_node_labels", + "network_edge_labels", + "number_of_agents", + ]: + del requested_logs[name] + + elif not isleconfig.save_network: + for name in [ + "unweighted_network_data", + "network_node_labels", + "network_edge_labels", + "number_of_agents", + ]: + del requested_logs[name] + + """Configure log directory and ensure that the directory exists""" + dir_prefix = "/data/" + directory = os.getcwd() + dir_prefix + if not os.path.isdir(directory): + if os.path.exists(directory.rstrip("/")): + raise Exception( + "./data exists as regular file. " + "This filename is required for the logging and event schedule directory" + ) + os.makedirs("data") + + """Setup of the simulations""" + # Here the setup for the simulation is done. + # Since this script is used to carry out simulations in the cloud will usually have more than 1 replication. + # We don't set filepath=, so the full set of events and seeds will be stored in data/risk_event_schedules.islestore + # If we wished we could replicate by setting isleconfig.replicating = True. + setup = setup_simulation.SetupSim() + [ + general_rc_event_schedule, + general_rc_event_damage, + np_seeds, + random_seeds, + ] = setup.obtain_ensemble(len(parameter_list)) + + m = sm.operation(start.main, include_modules=True) + + # Here is assembled each job with the corresponding: simulation parameters, time events, damage events, seeds, + # simulation state save interval (never), and list of requested logs. + job = [ + m( + parameter_list[x], + general_rc_event_schedule[x], + general_rc_event_damage[x], + np_seeds[x], + random_seeds[x], + 0, + 0, + list(requested_logs.keys()), + summary=summary, + ) + for x in range(len(parameter_list)) + ] + """Here the jobs are submitted""" + print("Jobs constructed, submitting") + with sm.Session(host=hostname, default_cb_to_stdout=True) as sess: + print("Starting job") + # Don't use async here, since there is only one job + result = sess.submit(job) + + result_dict = {t: r for t, r in zip(parameters, result)} + start.save_summary([result_dict]) + + +def get_cumulative_bankruptcies(log): + return log["cumulative_bankruptcies"][-1] + + +if __name__ == "__main__": + host = None + if len(sys.argv) > 1: + # The server is passed as an argument. + host = sys.argv[1] + rake(host, summary=get_cumulative_bankruptcies) + # jobs = {"ensemble1" : "23a3f4e1", + # "ensemble2" : "485f7221"} + # restore_jobs(jobs, host) diff --git a/start.py b/start.py index 59380bd..48e72b4 100644 --- a/start.py +++ b/start.py @@ -43,6 +43,7 @@ def main( replic_id: int, requested_logs: MutableSequence = None, resume: bool = False, + summary: callable = None, ) -> Tuple[bytes, dict]: if not resume: np.random.seed(np_seed) @@ -83,13 +84,15 @@ def main( save_simulation(t + 1, simulation, sim_params, exit_now=False) log = simulation.obtain_log(requested_logs) + if summary is not None: + return summary(log) + else: + # We compute metadata about the return data that isn't compressed, so a skeleton data structure can be + # constructed before decompression + found_shapes = {name: np.shape(log[name]) for name in log} - # We compute metadata about the return data that isn't compressed, so a skeleton data structure can be constructed - # before decompression - found_shapes = {name: np.shape(log[name]) for name in log} - - # We compress the return value for the sake of minimising data transfer over the network and RAM usage - return (zlib.compress(pickle.dumps(log)), found_shapes) + # We compress the return value for the sake of minimising data transfer over the network and RAM usage + return (zlib.compress(pickle.dumps(log)), found_shapes) def save_simulation( @@ -255,6 +258,19 @@ def save_results(results_list: list, prefix: str): hickle.dump(data, filename, compression="gzip") +def save_summary(summary_values: List[dict]): + filename = "data/summary_statistics.hdf" + if os.path.exists(filename): + # Don't want to blindly overwrite, so make backups + import time + + backupfilename = filename + "." + time.strftime("%Y-%m-%dT%H%M%S") + os.rename(filename, backupfilename) + import hickle + + hickle.dump(summary_values, filename, compression="gzip") + + # main entry point if __name__ == "__main__": @@ -367,7 +383,6 @@ def save_results(results_list: list, prefix: str): ) = np_seeds = random_seeds = [None] # Run the main program - # Note that we pass the filepath as the replic_ID comp_result = main( simulation_parameters, general_rc_event_schedule[0], @@ -378,9 +393,6 @@ def save_results(results_list: list, prefix: str): replic_id=1, resume=args.resume, ) - # result = pickle.loads(zlib.decompress(result)) - - # save_results([listify.delistify(list(result))], "single") save_results([comp_result], "single") From a2aad0fdae243ed8da4f92817542ac48e8f6bf96 Mon Sep 17 00:00:00 2001 From: Chris Hughes Date: Wed, 4 Sep 2019 10:44:23 +0100 Subject: [PATCH 111/125] Removed all assert statements (sandman only returns exception, not traceback, so errors should be unique) --- calibrationscore.py | 5 +++-- catbond.py | 3 ++- centralbank.py | 3 ++- condition_aux.py | 3 ++- distributionaggregate.py | 11 ++++++++--- distributionreinsurance.py | 3 ++- distributiontruncated.py | 6 ++++-- ensemble.py | 6 ++++-- fileconvert.py | 3 ++- insurancecontract.py | 5 ++++- insurancefirms.py | 24 ++++++++++++++++++------ insurancesimulation.py | 24 +++++++++++++++++++----- isleconfig.py | 2 +- listify.py | 5 +++-- metainsurancecontract.py | 3 ++- metainsuranceorg.py | 24 +++++++++++++++--------- reinsurancecontract.py | 13 ++++++++++--- riskmodel.py | 25 ++++++++++++++++++------- sensitivity.py | 13 ++++++++----- setup_simulation.py | 11 ++++++----- start.py | 3 ++- sum_distribution.py | 6 ++++-- visualisation.py | 12 ++++++++---- 23 files changed, 147 insertions(+), 66 deletions(-) diff --git a/calibrationscore.py b/calibrationscore.py index 5380004..39403dc 100644 --- a/calibrationscore.py +++ b/calibrationscore.py @@ -9,14 +9,15 @@ class CalibrationScore: - def __init__(self, l): + def __init__(self, l: logger.Logger): """Constructor method. Arguments: l: Type: Logger object. The log of a single simulation run. Returns instance.""" """Assert sanity of log and save log.""" - assert isinstance(l, logger.Logger) + if not isinstance(l, logger.Logger): + raise ValueError("object passed is not a logger") self.logger = l """Prepare list of calibration tests from calibration_conditions.py""" diff --git a/catbond.py b/catbond.py index 2ab4f8c..185cfad 100644 --- a/catbond.py +++ b/catbond.py @@ -48,7 +48,8 @@ def iterate(self, time: int): No return values For each time iteration this is called from insurancesimulation to perform duties: interest payments, _pay obligations, mature the contract if ended, make payments.""" - assert len(self.underwritten_contracts) == 1 + if not len(self.underwritten_contracts) == 1: + raise RuntimeError("Catbond has ended up underwriting multiple contracts") # Interest gets paid directly to the owner of the catbond (i.e. the simulation) self.simulation.bank.award_interest(self.owner, self.cash) self._effect_payments(time) diff --git a/centralbank.py b/centralbank.py index f62d9af..753cdaa 100644 --- a/centralbank.py +++ b/centralbank.py @@ -28,7 +28,8 @@ def update_money_supply(self, amount, reduce=True): self.economy_money -= amount else: self.economy_money += amount - assert self.economy_money > 0 + if not self.economy_money > 0: + raise RuntimeError("Economy just lost all its money") def award_interest(self, firm, total_cash): """Method to award interest. diff --git a/condition_aux.py b/condition_aux.py index 8caae30..3e14bb1 100644 --- a/condition_aux.py +++ b/condition_aux.py @@ -138,7 +138,8 @@ def scaler( Returns: Calibratied series.""" series = np.asarray(series) - assert (series > 1).all() + if not (series > 1).all(): + raise ValueError("Series is not uniformly greater than one") logseries = np.log(series) mean = np.mean(logseries) std = np.std(logseries) diff --git a/distributionaggregate.py b/distributionaggregate.py index bb07f42..9f1647f 100644 --- a/distributionaggregate.py +++ b/distributionaggregate.py @@ -190,7 +190,10 @@ def contract_risk( variance = expected_square_total_claim - expected_total_claim ** 2 std = np.sqrt(variance) - assert max(round(expected_total_claim), var) <= exposure + if not max(round(expected_total_claim), var) <= exposure: + raise RuntimeError( + "var or expected claim greater than exposure, something's wrong" + ) if factor: expected_total_claim *= factor var = round(var * factor) @@ -212,8 +215,10 @@ def get_contract_risk( "limit_fraction", "runtime", ]: - assert risk.__dict__[prop] is not None - assert risk.deductible_fraction < risk.limit_fraction <= 1 + if risk.__dict__[prop] is None: + raise ValueError(f"Risk parameter {prop} no present but required") + if not risk.deductible_fraction < risk.limit_fraction <= 1: + raise ValueError("Invalid deductible/limit passed") return contract_risk( number_risks=risk.number_risks, value_per_risk=int(round(risk.value / risk.number_risks)), diff --git a/distributionreinsurance.py b/distributionreinsurance.py index bf670f7..54fb487 100644 --- a/distributionreinsurance.py +++ b/distributionreinsurance.py @@ -51,7 +51,8 @@ def __init__( if region[0] < value ] for region in self.coverage: - assert 0 <= region[0] < region[1] <= 1 + if not (0 <= region[0] < region[1] <= 1): + raise ValueError(f"coverage contains invalid region {region}") if self.dist.cdf(0) != 0 or self.dist.cdf(1) != 1: raise ValueError( diff --git a/distributiontruncated.py b/distributiontruncated.py index 7de1ad9..bca702f 100644 --- a/distributiontruncated.py +++ b/distributiontruncated.py @@ -11,7 +11,8 @@ def __init__(self, dist, lower_bound=0.0, upper_bound=1.0): self.normalizing_factor = dist.cdf(upper_bound) - dist.cdf(lower_bound) self.lower_bound = lower_bound self.upper_bound = upper_bound - assert self.upper_bound > self.lower_bound + if not self.upper_bound > self.lower_bound: + raise ValueError("upper bound should be above lower bound") @weak_lru_cache(maxsize=1024) def pdf(self, x): @@ -36,7 +37,8 @@ def cdf(self, x): @weak_lru_cache(maxsize=1024) def ppf(self, x): x = np.asarray(x) - assert (x >= 0).all() and (x <= 1).all() + if not ((x >= 0).all() and (x <= 1).all()): + raise ValueError("Probablities not in [0, 1] passed (lol)") return self.dist.ppf( x * self.normalizing_factor + self.dist.cdf(self.lower_bound) ) diff --git a/ensemble.py b/ensemble.py index 0a47201..5afa224 100644 --- a/ensemble.py +++ b/ensemble.py @@ -56,7 +56,8 @@ def rake(hostname=None, replications=9, summary: callable = None): f"replications of {default_parameters['max_time']} timesteps" ) for name in parameter_sets: - assert isinstance(name, str) + if not isinstance(name, str): + raise ValueError("Prefixes must be strings") """Sanity checks""" @@ -302,7 +303,8 @@ def wait_for_tasks( def restore_jobs(jobs, hostname): """jobs is a dict mapping prefixes to job ids""" # Can't restore jobs on a local scheduler - assert hostname is not None + if hostname is None: + raise ValueError("Can't restore from local scheduler") with sm.Session(host=hostname, default_cb_to_stdout=True) as sess: tasks = {prefix: sess.get(jobs[prefix]) for prefix in jobs} # Might need to store the position maps - can't test what can be extracted until sandman is fixed diff --git a/fileconvert.py b/fileconvert.py index a95ff58..1de5ce1 100644 --- a/fileconvert.py +++ b/fileconvert.py @@ -73,7 +73,8 @@ def convert(prefix: str): for replication_data in this_data: rfile.write(repr(replication_data.tolist()) + "\n") else: - assert isinstance(this_data, list) + if not isinstance(this_data, list): + raise ValueError("Data is neither a list nor an array") for replication_data in this_data: rfile.write(repr(replication_data) + "\n") diff --git a/insurancecontract.py b/insurancecontract.py index 575caae..c76bf2d 100644 --- a/insurancecontract.py +++ b/insurancecontract.py @@ -44,7 +44,10 @@ def __init__( limit_fraction, ) # the property holder in an insurance contract should always be the simulation - assert self.property_holder is self.insurer.simulation + if self.property_holder is not self.insurer.simulation: + raise ValueError( + "Only the simulation should be able to take out insurance contracts" + ) self.property_holder: "InsuranceSimulation" def explode(self, time, uniform_value=None, damage_extent=None): diff --git a/insurancefirms.py b/insurancefirms.py index 14a79a8..0f81355 100644 --- a/insurancefirms.py +++ b/insurancefirms.py @@ -131,7 +131,8 @@ def increase_capacity(self, time: int, max_var: float) -> float: market premium is above its average premium, otherwise firm is 'forced' to get a catbond or reinsurance. Only implemented for non-proportional(excess of loss) reinsurance. Only issues one reinsurance or catbond per iteration unless not enough capacity to meet target.""" - assert self.simulation_reinsurance_type == "non-proportional" + if not self.simulation_reinsurance_type == "non-proportional": + raise ValueError("Only non-proportional reinsurance is currently supported") """get prices""" capacity = None @@ -273,7 +274,8 @@ def ask_reinsurance_non_proportional_by_category( tranches = self.reinsurance_profile.split_longest(tranches) risks_to_return = [] for tranche in tranches: - assert tranche[1] > tranche[0] + if not tranche[1] > tranche[0]: + raise ValueError("Ended up with invalid tranche") risk = self.reinsure_tranche( categ_id, tranche[0] / total_value, @@ -318,7 +320,10 @@ def reinsure_tranche( deductible=deductible_fraction * total_value, limit=limit_fraction * total_value, ) - assert risk.deductible_fraction < risk.limit_fraction <= 1 + if not (risk.deductible_fraction < risk.limit_fraction <= 1): + raise ValueError( + "Can't reinsure invalid tranche - deductible must be < limit <= 1" + ) reinsurance_type = self.decide_reinsurance_type(risk) if reinsurance_type == "reinsurance": @@ -420,14 +425,20 @@ def decide_reinsurance_type(self, risk: genericclasses.RiskProperties) -> str: def get_catbond_price(self, risk: genericclasses.RiskProperties) -> float: """Returns the total per-risk premium for a catbond """ - assert risk.deductible_fraction is not None + if risk.deductible_fraction is None: + raise ValueError( + "Risk has no associated deductible fraction, can't price catbond" + ) return self.simulation.get_cat_bond_price( risk.deductible_fraction, risk.limit_fraction ) def get_reinsurance_price(self, risk: genericclasses.RiskProperties) -> float: """Returns the total per-risk premium for reinsurance""" - assert risk.deductible_fraction is not None + if risk.deductible_fraction is None: + raise ValueError( + "Risk has no associated deductible fraction, can't price catbond" + ) return self.simulation.get_reinsurance_premium( risk.deductible_fraction, risk.limit_fraction ) @@ -534,7 +545,8 @@ def refresh_reinrisk( ) if risk.deductible_fraction == risk.limit_fraction == 1: return None - assert risk.deductible_fraction < risk.limit_fraction <= 1 + if not (risk.deductible_fraction < risk.limit_fraction <= 1): + raise ValueError("After refreshing risk has become invalid") return risk diff --git a/insurancesimulation.py b/insurancesimulation.py index 769bb27..39d56b0 100644 --- a/insurancesimulation.py +++ b/insurancesimulation.py @@ -113,7 +113,11 @@ def __init__( "set initial market price (normalized, i.e. must be multiplied by value or excess-deductible)" if self.simulation_parameters["expire_immediately"]: - assert self.cat_separation_distribution.dist.name == "expon" + if not self.cat_separation_distribution.dist.name == "expon": + # TODO: comeon, its geometric really + raise ValueError( + "cat separation distribution must be exponential (for now)" + ) expected_damage_frequency = 1 - scipy.stats.poisson( self.simulation_parameters["mean_contract_runtime"] / self.simulation_parameters["event_time_mean_separation"] @@ -399,7 +403,10 @@ def add_agents( if agents: # We're probably just adding a catbond if agent_class_string == "catbond": - assert len(agents) == n + if not len(agents) == n: + raise ValueError( + "Mismatch in number of agents and claimed number of agents" + ) self.catbonds += agents else: raise ValueError("Only catbonds may be passed directly") @@ -408,7 +415,10 @@ def add_agents( if agent_class_string == "insurancefirm": if not self.insurancefirms: # There aren't any other firms yet, add the first ones - assert len(self.agent_parameters["insurancefirm"]) == n + if not len(self.agent_parameters["insurancefirm"]) == n: + raise ValueError( + "Agent parameters list has incorrect length when adding initial insurers" + ) agent_parameters = self.agent_parameters["insurancefirm"] else: # We are adding new agents to an existing simulation @@ -432,7 +442,10 @@ def add_agents( elif agent_class_string == "reinsurancefirm": # Much the same as above if not self.reinsurancefirms: - assert len(self.agent_parameters["reinsurancefirm"]) == n + if not len(self.agent_parameters["reinsurancefirm"]) == n: + raise ValueError( + "Agent parameters list has incorrect length when adding initial reinsurers" + ) agent_parameters = self.agent_parameters["reinsurancefirm"] else: agent_parameters = [ @@ -740,7 +753,8 @@ def _reduce_money_supply(self, amount: float): amount: Type Integer""" self.cash -= amount self.bank.update_money_supply(amount, reduce=True) - assert self.cash >= 0 + if not self.cash >= 0: + raise RuntimeError("Simulation out of money oh no we're all doomed") def _reset_reinsurance_weights(self): """Method for clearing and setting reinsurance weights dependant on how many reinsurance companies exist and diff --git a/isleconfig.py b/isleconfig.py index 1ebd76c..803bd4b 100644 --- a/isleconfig.py +++ b/isleconfig.py @@ -9,7 +9,7 @@ # fmt: off simulation_parameters = { - "max_time": 40, + "max_time": 2000, "no_categories": 4, # no_[re]insurancefirms are initial conditions only diff --git a/listify.py b/listify.py index fe370c8..998c158 100644 --- a/listify.py +++ b/listify.py @@ -20,7 +20,7 @@ def listify(d): return lst -def delistify(l): +def delistify(l: list) -> dict: """Function to convert listified dict back to dict. Arguments: l: list - input listified dict. This must be a list of dict @@ -31,7 +31,8 @@ def delistify(l): """extract keys""" keys = l.pop() - assert len(keys) == len(l) + if not len(keys) == len(l): + raise ValueError("passed list is not a listified dict") """create dict""" d = {key: l[i] for i, key in enumerate(keys)} diff --git a/metainsurancecontract.py b/metainsurancecontract.py index a41afd2..08a0ab9 100644 --- a/metainsurancecontract.py +++ b/metainsurancecontract.py @@ -174,7 +174,8 @@ def reinsure(self, reinsurer, reinsurance_share, reincontract): self.reinsurer = reinsurer self.reinsurance_share = reinsurance_share self.reincontract = reincontract - assert self.reinsurance_share in [None, 0.0, 1.0] + if self.reinsurance_share not in [None, 0.0, 1.0]: + raise ValueError("Reinsurance share must be 0, 1 or none") def unreinsure(self): """Unreinsurance Method. diff --git a/metainsuranceorg.py b/metainsuranceorg.py index 4973329..555d142 100644 --- a/metainsuranceorg.py +++ b/metainsuranceorg.py @@ -309,7 +309,10 @@ def collect_process_evaluate_risks( new_nonproportional_risks ) - assert self.recursion_limit > 0 + if not self.recursion_limit > 0: + raise ValueError( + "recursion limit is zero, can't evaluate any risks ever" + ) not_accepted_reinrisks = None for repetition in range(self.recursion_limit): # Here we process all the new reinrisks in order to keep the portfolio as balanced as possible. @@ -440,7 +443,10 @@ def market_exit(self, time: int): self.enter_bankruptcy(time) else: for obligation in due: - assert self.cash > obligation.amount + if not self.cash > obligation.amount: + raise RuntimeError( + "Insufficient cash to pay obligations, but in market_exit not enter_illiquidity" + ) self._pay(obligation) self.obligations = [] self.dissolve(time, "record_market_exit") @@ -559,10 +565,7 @@ def underwrite(self, contract: "MetaInsuranceContract"): ].total_exposure + (contract.limit - contract.deductible), ) - # if new_characterisation[1] != 1.0: - # print(new_characterisation[1]) - # assert new_characterisation[:3] == self.risk_char_slow(contract.category)[:3] - # assert np.isclose(new_characterisation[3], self.risk_char_slow(contract.category)[3]) + self.underwritten_risk_characterisation[ contract.category ] = new_characterisation @@ -901,8 +904,10 @@ def process_newrisks_reinsurer( for risk in roundrobin(reinrisks_per_categ): # Here we take only one risk per category at a time to achieve risk[C1], risk[C2], risk[C3], # risk[C4], risk[C1], risk[C2], ... if possible. - assert risk - assert risk.owner.operational + if not risk: + raise ValueError("Empty risk (None) found in risks passed to reinsurer") + if not risk.owner.operational: + raise RuntimeError("Reinsurance risk has non-operational owner") accept, cash_left_by_categ, var_this_risk, self.excess_capital = self.riskmodel.evaluate( self.underwritten_risks, self.cash, risk ) @@ -980,7 +985,8 @@ def process_newrisks_insurer( not_accepted_risks = [[] for _ in range(len(risks_per_categ))] has_accepted_risks = False for risk in roundrobin(risks_per_categ): - assert risk + if not risk: + raise ValueError("Empty risk (None) found in risks passed to insurer") if acceptable_by_category[risk.category] > 0: if risk.contract and risk.contract.expiration > time: # In this case the risk being inspected already has a contract, so we are deciding whether to diff --git a/reinsurancecontract.py b/reinsurancecontract.py index a7760fb..eec0672 100644 --- a/reinsurancecontract.py +++ b/reinsurancecontract.py @@ -55,7 +55,10 @@ def __init__( if self.insurancetype == "excess-of-loss": self.property_holder.add_reinsurance(contract=self) else: - assert self.contract is not None + if self.contract is None: + raise ValueError( + "Proportional reinsurance must have an associated contract" + ) evaluating = False if evaluating: @@ -105,7 +108,10 @@ def explode( # Just a type hint since for a generic insurance contract property_holder can be the simulation self.property_holder: "InsuranceFirm" - assert uniform_value is None + if uniform_value is not None: + raise ValueError( + "uniform value should not be given for reinsurance contract explosion" + ) if damage_extent is None: raise ValueError("Damage extent should be given") if damage_extent > self.deductible: @@ -143,7 +149,8 @@ def explode( # TODO: Allow for catbonds that can pay out multiple times? self.insurer: "CatBond" remaining_cb_cash = self.insurer.get_available_cash(time) - claim - assert remaining_cb_cash >= 0 + if not remaining_cb_cash >= 0: + raise ValueError("Catbond has run out of cash, oh no") if remaining_cb_cash < 2: # If the claim uses up all the catbond's remaining money, the contract ends self.expiration = time diff --git a/riskmodel.py b/riskmodel.py index 847b797..d80fcb4 100644 --- a/riskmodel.py +++ b/riskmodel.py @@ -84,7 +84,8 @@ def compute_expectation( runtimes = np.zeros(len(categ_risks)) for i, risk in enumerate(categ_risks): # TODO: factor in excess instead of value? - assert risk.limit is not None + if risk.limit is None: + raise ValueError("no no, no no no no, no no no no, there's no limit") exposures[i] = risk.value - risk.deductible risk_factors[i] = risk.risk_factor runtimes[i] = risk.runtime @@ -145,7 +146,8 @@ def evaluate_proportional( This method iterates through the risks in each category and calculates the average VaR, how many could be underwritten according to their average VaR, how much cash would be left per category if all risks were underwritten at average VaR, and the total expected profit (currently always None).""" - assert len(cash) == self.category_number + if not len(cash) == self.category_number: + raise ValueError("Cash should be split category-wise") # prepare variables acceptable_by_category = [] @@ -211,7 +213,10 @@ def evaluate_proportional( expected_profits = None else: if necessary_liquidity == 0: - assert expected_profits == 0 + if not expected_profits == 0: + raise ValueError( + "Expected profits should be zero at this point, but isn't" + ) expected_profits = self.init_profit_estimate * cash[0] else: expected_profits /= necessary_liquidity @@ -249,7 +254,8 @@ def evaluate_excess_of_loss( each underwritten contract were to be claimed at expected values. The additional cash required to cover the offered risk (if applicable) is then calculated (should only be one).""" cash_left_by_categ = np.copy(cash) - assert len(cash_left_by_categ) == self.category_number + if not len(cash_left_by_categ) == self.category_number: + raise ValueError("cash left not split by category") # prepare variables additional_required = np.zeros(self.category_number) additional_var_per_categ = np.zeros(self.category_number) @@ -296,7 +302,8 @@ def evaluate_excess_of_loss( additional_var_per_categ[categ_id] += var_claim_total # Additional value at risk should only occur in one category. Assert that this is the case. - assert sum(additional_var_per_categ > 0) <= 1 + if not sum(additional_var_per_categ > 0) <= 1: + raise ValueError("Additional VaR in multiple categories") var_this_risk = max(additional_var_per_categ) return cash_left_by_categ, additional_required, var_this_risk @@ -340,13 +347,17 @@ def evaluate( underwritten or not.""" # TODO: split this into two functions # ensure that any risk to be considered supplied directly as argument is non-proportional/excess-of-loss - assert (offered_risk is None) or offered_risk.insurancetype == "excess-of-loss" + if not ( + (offered_risk is None) or offered_risk.insurancetype == "excess-of-loss" + ): + raise ValueError("proportional risk isn't evaluated like this") # construct cash_left_by_categ as a sequence, defining remaining liquidity by category if not isinstance(cash, (np.ndarray, list)): cash_left_by_categ = np.ones(self.category_number) * cash else: cash_left_by_categ = np.copy(cash) - assert len(cash_left_by_categ) == self.category_number + if not len(cash_left_by_categ) == self.category_number: + raise ValueError("cash left by categ has wrong length") # sort current contracts el_risks = [risk for risk in risks if risk.insurancetype == "excess-of-loss"] diff --git a/sensitivity.py b/sensitivity.py index 9ee5d65..d2f04c2 100644 --- a/sensitivity.py +++ b/sensitivity.py @@ -49,12 +49,15 @@ def rake(hostname=None, summary: callable = None): param_values = SALib.sample.morris.sample(problem, N=problem["num_vars"] * 3) parameters = [tuple(row) for row in param_values] parameter_list = [ - {problem["names"][i]: row[i] for i in range(len(row))} for row in param_values + { + **default_parameters.copy(), + "max_time": 2000, + **{problem["names"][i]: row[i] for i in range(len(row))}, + } + for row in param_values ] - for d in parameter_list: - d.update(default_parameters.copy()) - d["max_time"] = 2000 - assert parameter_list[1] != parameter_list[0] + if parameter_list[1] == parameter_list[0]: + raise RuntimeError("Parameter list appears to be homogenous!") ################################################################################################################### diff --git a/setup_simulation.py b/setup_simulation.py index 293d1cc..180aa2e 100644 --- a/setup_simulation.py +++ b/setup_simulation.py @@ -7,7 +7,6 @@ - np_seed: int - numpy module random seed - random_seed: int - random module random seed A simulation given event schedule dictionary d should be set up like so: - assert isleconfig.simulation_parameters["no_categories"] == d["num_categories"] simulation.rc_event_schedule = d["event_times"] simulation.rc_event_damages = d["event_damages"] np.random.seed(d["np_seed"]) @@ -101,12 +100,13 @@ def store(self): # the model. The number of replications is calculated from the length of the exisiting values. # With the information stored it is possible to replicate the entire behavior of the ensemble at a later time. event_schedules = [] - assert ( + if not ( len(self.np_seed) == len(self.random_seed) == len(self.general_rc_event_damage) == len(self.general_rc_event_schedule) - ) + ): + raise ValueError("Required data not all the same lenght, can't store") replications = len(self.np_seed) for i in range(replications): @@ -140,13 +140,14 @@ def store(self): pickle.dump(event_schedules, wfile, protocol=pickle.HIGHEST_PROTOCOL) def recall(self): - assert ( + if not ( self.np_seed == self.random_seed == self.general_rc_event_schedule == self.general_rc_event_damage == [] - ) + ): + raise ValueError("Some of the data to be recalled already exists") with open("./data/" + self.filepath, "rb") as rfile: event_schedules = pickle.load(rfile) self.replications = len(event_schedules) diff --git a/start.py b/start.py index 48e72b4..2d8d3b7 100644 --- a/start.py +++ b/start.py @@ -232,7 +232,8 @@ def save_results(results_list: list, prefix: str): # Need to do a little pre-processing for key in list(results_dict.keys()): - assert isinstance(results_dict[key], np.ndarray) + if not isinstance(results_dict[key], np.ndarray): + raise ValueError(f"Results_dict[{key}] is not an array") if results_dict[key].size == 0: del results_dict[key] continue diff --git a/sum_distribution.py b/sum_distribution.py index 0849e10..6f9c6e9 100644 --- a/sum_distribution.py +++ b/sum_distribution.py @@ -38,7 +38,8 @@ def sum_beta_pdf(damage, n, resolution=5000): def plot_mc(ns, no_mc_sims=100000, norm_approx=False, kde=True, hist=True): - assert kde or hist + if not kde or hist: + raise ValueError("At least one of kde and hist must be passed") print("Doing MC simulation for n = " + ", ".join([str(n) for n in ns])) for n in ns: results = [] @@ -121,7 +122,8 @@ def plot_3d(pdfs): y = [] z = [] for n in pdfs: - assert len(pdfs[n][0]) == len(pdfs[n][1]) + if not len(pdfs[n][0]) == len(pdfs[n][1]): + raise ValueError("Attempting to ploy pdfs of unequal sizes") x += list(np.array(pdfs[n][0]) / n) z += list(np.array(pdfs[n][1]) * n) y += [n] * len(pdfs[n][0]) diff --git a/visualisation.py b/visualisation.py index caac5ba..752cd48 100644 --- a/visualisation.py +++ b/visualisation.py @@ -459,7 +459,8 @@ def aux_clustered_exit_records(self, exits): Returns: numpy ndarray of the same length as argument "exits": the clustered series.""" exits = np.asarray(exits) - assert exits.ndim == 2 + if not exits.ndim == 2: + raise ValueError("exits should be 2-dimensional") clustered = np.zeros_like(exits) for ts_index, timeseries in enumerate(exits): # Sadly have to go row-by-row @@ -1568,7 +1569,8 @@ def stat_tests(self, upper, lower): # load in data from the history_logs dictionary data = hickle.load("data/single_full_logs.hdf") - assert type(data) is dict + if not type(data) is dict: + raise ValueError("hickle file does not contain a dict") history_log = data # first create visualisation object, then create graph/animation objects as necessary @@ -1594,7 +1596,8 @@ def stat_tests(self, upper, lower): ] for filename in filenames: data = hickle.load(filename) - assert type(data) is dict + if type(data) is not dict: + raise ValueError(f"Hickle file {filename} does not contain a dict") history_log = data vis_list.append(Visualisation(history_log)) @@ -1639,7 +1642,8 @@ def stat_tests(self, upper, lower): ] for filename in filenames: data = hickle.load(filename) - assert type(data) is dict + if type(data) is not dict: + raise ValueError(f"Hickle file, {filename}, contains non-dict data") history_log = data vis_list.append(Visualisation(history_log)) From 32f07292ed724cb5a0fe156d28108d9f704f33b5 Mon Sep 17 00:00:00 2001 From: Chris Hughes Date: Wed, 4 Sep 2019 14:24:53 +0100 Subject: [PATCH 112/125] It turns out the central bank wasn't implemented correctly at all (at least the economy size part of it), so I removed that bit. --- centralbank.py | 24 ++++++++++++++--------- insurancesimulation.py | 19 ++++++++++++++++++- listify.py | 2 +- sensitivity.py | 43 +++++++++++++++++++++--------------------- sum_distribution.py | 2 +- 5 files changed, 57 insertions(+), 33 deletions(-) diff --git a/centralbank.py b/centralbank.py index 753cdaa..f42fb50 100644 --- a/centralbank.py +++ b/centralbank.py @@ -3,7 +3,7 @@ class CentralBank: - def __init__(self, money_supply): + def __init__(self, money_supply, simulation): """Constructor Method. No accepted arguments. Constructs the CentralBank class. This class is currently only used to award interest payments.""" @@ -17,6 +17,7 @@ def __init__(self, money_supply): self.economy_money = money_supply self.warnings = {} self.aid_budget = self.aid_budget_reset = simulation_parameters["aid_budget"] + self.simulation = simulation def update_money_supply(self, amount, reduce=True): """Method to update the current supply of money in the insurance simulation economy. Only used to monitor @@ -24,12 +25,14 @@ def update_money_supply(self, amount, reduce=True): Accepts: amount: Type Integer. reduce: Type Boolean.""" - if reduce: - self.economy_money -= amount - else: - self.economy_money += amount - if not self.economy_money > 0: - raise RuntimeError("Economy just lost all its money") + pass + # if reduce: + # self.economy_money -= amount + # else: + # self.economy_money += amount + # if not self.economy_money >= 0: + # simulation_parameters["simulation"].count_money() + # raise RuntimeError("Economy appears to be out of money") def award_interest(self, firm, total_cash): """Method to award interest. @@ -38,8 +41,11 @@ def award_interest(self, firm, total_cash): total_cash: Type decimal This method takes an agents cash and awards it an interest payment on the cash.""" interest_payment = total_cash * self.interest_rate - firm.receive(interest_payment) - self.update_money_supply(interest_payment, reduce=True) + self.simulation.receive_obligation( + interest_payment, firm, self.simulation._time, "interest" + ) + # firm.receive(interest_payment) + # self.update_money_supply(interest_payment, reduce=True) def set_interest_rate(self): """Method to set the interest rate diff --git a/insurancesimulation.py b/insurancesimulation.py index 39d56b0..c1c77ba 100644 --- a/insurancesimulation.py +++ b/insurancesimulation.py @@ -145,7 +145,7 @@ def __init__( "Set up monetary system (should instead be with the customers, if customers are modeled explicitly)" self.cash: float = self.simulation_parameters["money_supply"] - self.bank = CentralBank(self.cash) + self.bank = CentralBank(self.cash, self) "set up risk categories" self.riskcategories: Sequence[int] = list( @@ -1384,3 +1384,20 @@ def update_network_data(self): """unweighted adjacency matrix""" adj_matrix = np.sign(weights_matrix) return adj_matrix.tolist(), node_labels, edge_labels, num_entities + + def count_money(self): + """Counts all the money in the economy. Used if the economy runs out of money + Just a quick and dirty function to check money doesn't get created or destroyed. + We do get some drift thanks to (I assume) floating point error""" + try: + firms = self.insurancefirms + self.reinsurancefirms + self.catbonds + except AttributeError: + firms = [] + firm_cash = sum(firm.cash for firm in firms) + own_cash = self.cash + bank_cash = 0 # self.bank.economy_money + total = firm_cash + own_cash + bank_cash + # if not total == self.simulation_parameters["money_supply"]: + # raise RuntimeError(f"Money has been lost - {total} is held by all agents, but " + # f"{self.simulation_parameters['money_supply']} is expected") + return self.simulation_parameters["money_supply"] - total diff --git a/listify.py b/listify.py index 998c158..e1c1a82 100644 --- a/listify.py +++ b/listify.py @@ -2,7 +2,7 @@ from cloud (sandman2) to local.""" -def listify(d): +def listify(d: dict) -> list: """Function to convert dict to list with keys in last list element. Arguments: d: dict - input dict diff --git a/sensitivity.py b/sensitivity.py index d2f04c2..f9714e8 100644 --- a/sensitivity.py +++ b/sensitivity.py @@ -1,7 +1,5 @@ """ -This script allows to launch an ensemble of simulations for different number of risks models. -It can be run locally if no argument is passed when called from the terminal. -It can be run in the cloud if it is passed as argument the sandman2 server that will be used. +A modification of ensemble.py to do sensitivity analysis using SALib """ import sys import os @@ -186,38 +184,41 @@ def rake(hostname=None, summary: callable = None): # We don't set filepath=, so the full set of events and seeds will be stored in data/risk_event_schedules.islestore # If we wished we could replicate by setting isleconfig.replicating = True. setup = setup_simulation.SetupSim() + print("Setting up simulation") [ general_rc_event_schedule, general_rc_event_damage, np_seeds, random_seeds, ] = setup.obtain_ensemble(len(parameter_list)) - + print("Constructing sandman operation") m = sm.operation(start.main, include_modules=True) - # Here is assembled each job with the corresponding: simulation parameters, time events, damage events, seeds, # simulation state save interval (never), and list of requested logs. - job = [ - m( - parameter_list[x], - general_rc_event_schedule[x], - general_rc_event_damage[x], - np_seeds[x], - random_seeds[x], - 0, - 0, - list(requested_logs.keys()), - summary=summary, - ) - for x in range(len(parameter_list)) - ] + print("Assembling jobs") + n = len(parameter_list) + m_params = ( + parameter_list, + general_rc_event_schedule, + general_rc_event_damage, + np_seeds, + random_seeds, + [0] * n, + [0] * n, + [list(requested_logs.keys())] * n, + [False] * n, + [summary] * n, + ) + # This is actually quite slow for large sets of jobs. Can't use mp.Pool due to unpickleability + # Could use pathos if we actually end up caring + job = list(map(m, *m_params)) """Here the jobs are submitted""" - print("Jobs constructed, submitting") + print("Jobs created, submitting") with sm.Session(host=hostname, default_cb_to_stdout=True) as sess: print("Starting job") # Don't use async here, since there is only one job result = sess.submit(job) - + print("Job done, saving") result_dict = {t: r for t, r in zip(parameters, result)} start.save_summary([result_dict]) diff --git a/sum_distribution.py b/sum_distribution.py index 6f9c6e9..92582c3 100644 --- a/sum_distribution.py +++ b/sum_distribution.py @@ -38,7 +38,7 @@ def sum_beta_pdf(damage, n, resolution=5000): def plot_mc(ns, no_mc_sims=100000, norm_approx=False, kde=True, hist=True): - if not kde or hist: + if not (kde or hist): raise ValueError("At least one of kde and hist must be passed") print("Doing MC simulation for n = " + ", ".join([str(n) for n in ns])) for n in ns: From 2649eddeed2fc89f6924f89eb115e875f246a1a3 Mon Sep 17 00:00:00 2001 From: Chris Hughes Date: Wed, 4 Sep 2019 14:24:53 +0100 Subject: [PATCH 113/125] Add parameter specification for sensitivity analysis --- centralbank.py | 24 +++++++++++++-------- insurancesimulation.py | 19 ++++++++++++++++- isle_all_parameters.txt | 38 ++++++++++++++++++++++++++++++++++ isleconfig.py | 17 ++++++++------- listify.py | 2 +- sensitivity.py | 46 ++++++++++++++++++++--------------------- sum_distribution.py | 2 +- 7 files changed, 105 insertions(+), 43 deletions(-) create mode 100644 isle_all_parameters.txt diff --git a/centralbank.py b/centralbank.py index 753cdaa..f42fb50 100644 --- a/centralbank.py +++ b/centralbank.py @@ -3,7 +3,7 @@ class CentralBank: - def __init__(self, money_supply): + def __init__(self, money_supply, simulation): """Constructor Method. No accepted arguments. Constructs the CentralBank class. This class is currently only used to award interest payments.""" @@ -17,6 +17,7 @@ def __init__(self, money_supply): self.economy_money = money_supply self.warnings = {} self.aid_budget = self.aid_budget_reset = simulation_parameters["aid_budget"] + self.simulation = simulation def update_money_supply(self, amount, reduce=True): """Method to update the current supply of money in the insurance simulation economy. Only used to monitor @@ -24,12 +25,14 @@ def update_money_supply(self, amount, reduce=True): Accepts: amount: Type Integer. reduce: Type Boolean.""" - if reduce: - self.economy_money -= amount - else: - self.economy_money += amount - if not self.economy_money > 0: - raise RuntimeError("Economy just lost all its money") + pass + # if reduce: + # self.economy_money -= amount + # else: + # self.economy_money += amount + # if not self.economy_money >= 0: + # simulation_parameters["simulation"].count_money() + # raise RuntimeError("Economy appears to be out of money") def award_interest(self, firm, total_cash): """Method to award interest. @@ -38,8 +41,11 @@ def award_interest(self, firm, total_cash): total_cash: Type decimal This method takes an agents cash and awards it an interest payment on the cash.""" interest_payment = total_cash * self.interest_rate - firm.receive(interest_payment) - self.update_money_supply(interest_payment, reduce=True) + self.simulation.receive_obligation( + interest_payment, firm, self.simulation._time, "interest" + ) + # firm.receive(interest_payment) + # self.update_money_supply(interest_payment, reduce=True) def set_interest_rate(self): """Method to set the interest rate diff --git a/insurancesimulation.py b/insurancesimulation.py index 39d56b0..c1c77ba 100644 --- a/insurancesimulation.py +++ b/insurancesimulation.py @@ -145,7 +145,7 @@ def __init__( "Set up monetary system (should instead be with the customers, if customers are modeled explicitly)" self.cash: float = self.simulation_parameters["money_supply"] - self.bank = CentralBank(self.cash) + self.bank = CentralBank(self.cash, self) "set up risk categories" self.riskcategories: Sequence[int] = list( @@ -1384,3 +1384,20 @@ def update_network_data(self): """unweighted adjacency matrix""" adj_matrix = np.sign(weights_matrix) return adj_matrix.tolist(), node_labels, edge_labels, num_entities + + def count_money(self): + """Counts all the money in the economy. Used if the economy runs out of money + Just a quick and dirty function to check money doesn't get created or destroyed. + We do get some drift thanks to (I assume) floating point error""" + try: + firms = self.insurancefirms + self.reinsurancefirms + self.catbonds + except AttributeError: + firms = [] + firm_cash = sum(firm.cash for firm in firms) + own_cash = self.cash + bank_cash = 0 # self.bank.economy_money + total = firm_cash + own_cash + bank_cash + # if not total == self.simulation_parameters["money_supply"]: + # raise RuntimeError(f"Money has been lost - {total} is held by all agents, but " + # f"{self.simulation_parameters['money_supply']} is expected") + return self.simulation_parameters["money_supply"] - total diff --git a/isle_all_parameters.txt b/isle_all_parameters.txt new file mode 100644 index 0000000..966dc47 --- /dev/null +++ b/isle_all_parameters.txt @@ -0,0 +1,38 @@ +riskmodel_inaccuracy_parameter, 1.5, 4 +riskmodel_margin_of_safety, 1, 4 +value_at_risk_tail_probability, 0.0001, 0.1 +norm_profit_markup, 0.1, 1 +dividend_share_of_profits, 0.1, 0.7 +insurance_firm_market_entry_probability, 0.05, 0.95 +reinsurance_firm_market_entry_probability, 0.05, 0.95 +default_non-proportional_reinsurance_limit, 0.5, 1.0 +default_non-proportional_reinsurance_premium_share, 0, 1 +insurance_reinsurance_levels_lower_bound, 0, 0.3 +insurance_reinsurance_levels_upper_bound, 0.3, 0.6 +reinsurance_reinsurance_levels_lower_bound, 0, 0.3 +reinsurance_reinsurance_levels_upper_bound, 0.3, 0.6 +capacity_target_decrement_threshold, 1, 2 +capacity_target_increment_threshold, 1, 2 +capacity_target_decrement_factor, 0.75, 1 +capacity_target_increment_factor, 1, 1.5 +insurance_retention, 0, 1 +reinsurance_retention, 0, 1 +premium_sensitivity, 1, 10 +reinpremium_sensitivity, 1, 10 +insurers_balance_ratio, 0, 30 +reinsurers_balance_ratio, 0, 30 +cash_permanency_limit, 1, 1000 +insurance_permanency_contracts_limit, 1, 100 +insurance_permanency_ratio_limit, 0.4, 1 +insurance_permanency_time_constraint, 2, 96 +reinsurance_permanency_contracts_limit, 1, 100 +reinsurance_permanency_ratio_limit, 0.4, 1 +reinsurance_permanency_time_constraint, 2, 96 +initial_agent_cash, 10000, 100000 +initial_reinagent_cash, 100000, 1000000 +interest_rate, 0, 0.01 +upper_price_limit, 1, 2 +lower_price_limit, 0.5, 1 +max_scale_premiums, 1, 2 +scale_inaccuracy, 0, 1 +reinsurance_premium_adjustment_amount, 0, 1 diff --git a/isleconfig.py b/isleconfig.py index 803bd4b..4c7f31f 100644 --- a/isleconfig.py +++ b/isleconfig.py @@ -17,7 +17,7 @@ "no_reinsurancefirms": 4, # Numer of risk models in the market - "no_riskmodels": 1, + "no_riskmodels": 4, # values >=1; inaccuracy higher with higher values "riskmodel_inaccuracy_parameter": 2, @@ -76,6 +76,7 @@ # If True, will use the static deductible (if reinsurance type is non-proportional), otherwise the dynamic # deductible settings below are used + # TODO: RM Unsused "static_non-proportional_reinsurance_levels": False, "default_non-proportional_reinsurance_deductible": 0.3, @@ -124,7 +125,7 @@ # This parameter activates (deactivates) the following market permanency constraints. "market_permanency_off": False, # If a firm has cash less that this, they leave the market - "cash_permanency_limit": 100, + "cash_permanency_limit": 100, # TODO: Check Morris - don't tweak by less than 1 # If insurers have fewer than this many contracts for the below time period then they leave the market "insurance_permanency_contracts_limit": 4, # Likewise, but regarding the ratio of actual cash to cash being reserved to cover risk @@ -141,6 +142,12 @@ "initial_agent_cash": 80000, "initial_reinagent_cash": 2000000, + # The total number of (insurance) risks in the market + "no_risks": 20000, + + # The value of each insurance risk + "value_per_risk": 1000, + # The per time period bank interest rate (note: time period ~ 1 month) "interest_rate": 0.001, @@ -151,12 +158,6 @@ "upper_price_limit": 1.2, "lower_price_limit": 0.85, - # The total number of (insurance) risks in the market - "no_risks": 20000, - - # The value of each insurance risk - "value_per_risk": 1000, - # The maximum upscaling of premiums based on insurer size - set to 1 to disable scaled premiums. # High values will give bigger insurers more money (we can assume they are more "trusted") # Values between 0 and 1 will make premiums decrease for bigger insurers. diff --git a/listify.py b/listify.py index 998c158..e1c1a82 100644 --- a/listify.py +++ b/listify.py @@ -2,7 +2,7 @@ from cloud (sandman2) to local.""" -def listify(d): +def listify(d: dict) -> list: """Function to convert dict to list with keys in last list element. Arguments: d: dict - input dict diff --git a/sensitivity.py b/sensitivity.py index d2f04c2..9eda062 100644 --- a/sensitivity.py +++ b/sensitivity.py @@ -1,7 +1,5 @@ """ -This script allows to launch an ensemble of simulations for different number of risks models. -It can be run locally if no argument is passed when called from the terminal. -It can be run in the cloud if it is passed as argument the sandman2 server that will be used. +A modification of ensemble.py to do sensitivity analysis using SALib """ import sys import os @@ -39,8 +37,7 @@ def rake(hostname=None, summary: callable = None): ################################################################################################################### # This section should be freely modified to determine the experiment - # The keys of parameter_sets are the prefixes to save logs under, the values are the parameters to run - # The keys should be strings + # parameters should be a list of (hashable) lables for the settings, which parameter_list should be a list of. import SALib.util import SALib.sample.morris @@ -186,38 +183,41 @@ def rake(hostname=None, summary: callable = None): # We don't set filepath=, so the full set of events and seeds will be stored in data/risk_event_schedules.islestore # If we wished we could replicate by setting isleconfig.replicating = True. setup = setup_simulation.SetupSim() + print("Setting up simulation") [ general_rc_event_schedule, general_rc_event_damage, np_seeds, random_seeds, ] = setup.obtain_ensemble(len(parameter_list)) - + print("Constructing sandman operation") m = sm.operation(start.main, include_modules=True) - # Here is assembled each job with the corresponding: simulation parameters, time events, damage events, seeds, # simulation state save interval (never), and list of requested logs. - job = [ - m( - parameter_list[x], - general_rc_event_schedule[x], - general_rc_event_damage[x], - np_seeds[x], - random_seeds[x], - 0, - 0, - list(requested_logs.keys()), - summary=summary, - ) - for x in range(len(parameter_list)) - ] + print("Assembling jobs") + n = len(parameter_list) + m_params = ( + parameter_list, + general_rc_event_schedule, + general_rc_event_damage, + np_seeds, + random_seeds, + [0] * n, + [0] * n, + [list(requested_logs.keys())] * n, + [False] * n, + [summary] * n, + ) + # This is actually quite slow for large sets of jobs. Can't use mp.Pool due to unpickleability + # Could use pathos if we actually end up caring + job = list(map(m, *m_params)) """Here the jobs are submitted""" - print("Jobs constructed, submitting") + print("Jobs created, submitting") with sm.Session(host=hostname, default_cb_to_stdout=True) as sess: print("Starting job") # Don't use async here, since there is only one job result = sess.submit(job) - + print("Job done, saving") result_dict = {t: r for t, r in zip(parameters, result)} start.save_summary([result_dict]) diff --git a/sum_distribution.py b/sum_distribution.py index 6f9c6e9..92582c3 100644 --- a/sum_distribution.py +++ b/sum_distribution.py @@ -38,7 +38,7 @@ def sum_beta_pdf(damage, n, resolution=5000): def plot_mc(ns, no_mc_sims=100000, norm_approx=False, kde=True, hist=True): - if not kde or hist: + if not (kde or hist): raise ValueError("At least one of kde and hist must be passed") print("Doing MC simulation for n = " + ", ".join([str(n) for n in ns])) for n in ns: From 70a6fb17e7e44ab93d53f3e3cd709183f9520259 Mon Sep 17 00:00:00 2001 From: chris Date: Fri, 6 Sep 2019 10:45:01 +0100 Subject: [PATCH 114/125] bug fix --- insurancefirms.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/insurancefirms.py b/insurancefirms.py index 0f81355..241713e 100644 --- a/insurancefirms.py +++ b/insurancefirms.py @@ -220,9 +220,8 @@ def ask_reinsurance_non_proportional_by_category( if number_risks > 0: tranches = self.reinsurance_profile.uncovered(categ_id) - # Don't get reinsurance above maximum limit - while tranches[-1][1] > self.np_reinsurance_limit_fraction * total_value: + while tranches and tranches[-1][1] > self.np_reinsurance_limit_fraction * total_value: if tranches[-1][0] >= self.np_reinsurance_limit_fraction * total_value: tranches.pop() else: @@ -231,7 +230,7 @@ def ask_reinsurance_non_proportional_by_category( self.np_reinsurance_limit_fraction * total_value, ) while ( - tranches[0][0] < self.np_reinsurance_deductible_fraction * total_value + tranches and tranches[0][0] < self.np_reinsurance_deductible_fraction * total_value ): if ( tranches[0][1] From 4dc7ecc5f0ca6a8fef407e8079a3eb165b3439f8 Mon Sep 17 00:00:00 2001 From: chris Date: Fri, 6 Sep 2019 10:45:01 +0100 Subject: [PATCH 115/125] bug fix --- insurancefirms.py | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/insurancefirms.py b/insurancefirms.py index 0f81355..dbb7411 100644 --- a/insurancefirms.py +++ b/insurancefirms.py @@ -220,9 +220,11 @@ def ask_reinsurance_non_proportional_by_category( if number_risks > 0: tranches = self.reinsurance_profile.uncovered(categ_id) - # Don't get reinsurance above maximum limit - while tranches[-1][1] > self.np_reinsurance_limit_fraction * total_value: + while ( + tranches + and tranches[-1][1] > self.np_reinsurance_limit_fraction * total_value + ): if tranches[-1][0] >= self.np_reinsurance_limit_fraction * total_value: tranches.pop() else: @@ -231,7 +233,9 @@ def ask_reinsurance_non_proportional_by_category( self.np_reinsurance_limit_fraction * total_value, ) while ( - tranches[0][0] < self.np_reinsurance_deductible_fraction * total_value + tranches + and tranches[0][0] + < self.np_reinsurance_deductible_fraction * total_value ): if ( tranches[0][1] From fd4b2c82f0849a373ef15d1d58bd705554ef2c34 Mon Sep 17 00:00:00 2001 From: chris Date: Fri, 6 Sep 2019 10:45:01 +0100 Subject: [PATCH 116/125] bug fix --- ensemble.py | 7 ++----- insurancefirms.py | 10 +++++++--- start.py | 4 ++++ 3 files changed, 13 insertions(+), 8 deletions(-) diff --git a/ensemble.py b/ensemble.py index 5afa224..019da5b 100644 --- a/ensemble.py +++ b/ensemble.py @@ -230,6 +230,7 @@ def rake(hostname=None, replications=9, summary: callable = None): save_iter, 0, list(requested_logs.keys()), + resume=False, summary=summary, ) for x in range(replications) @@ -312,16 +313,12 @@ def restore_jobs(jobs, hostname): # replications = list(tasks.values())[0].f -def summmmmm(log): - return log["cumulative_bankruptcies"][-1] - - if __name__ == "__main__": host = None if len(sys.argv) > 1: # The server is passed as an argument. host = sys.argv[1] - rake(host, summary=summmmmm) + rake(host, summary=start.cumulative_bankruptcies) # jobs = {"ensemble1" : "23a3f4e1", # "ensemble2" : "485f7221"} # restore_jobs(jobs, host) diff --git a/insurancefirms.py b/insurancefirms.py index 0f81355..dbb7411 100644 --- a/insurancefirms.py +++ b/insurancefirms.py @@ -220,9 +220,11 @@ def ask_reinsurance_non_proportional_by_category( if number_risks > 0: tranches = self.reinsurance_profile.uncovered(categ_id) - # Don't get reinsurance above maximum limit - while tranches[-1][1] > self.np_reinsurance_limit_fraction * total_value: + while ( + tranches + and tranches[-1][1] > self.np_reinsurance_limit_fraction * total_value + ): if tranches[-1][0] >= self.np_reinsurance_limit_fraction * total_value: tranches.pop() else: @@ -231,7 +233,9 @@ def ask_reinsurance_non_proportional_by_category( self.np_reinsurance_limit_fraction * total_value, ) while ( - tranches[0][0] < self.np_reinsurance_deductible_fraction * total_value + tranches + and tranches[0][0] + < self.np_reinsurance_deductible_fraction * total_value ): if ( tranches[0][1] diff --git a/start.py b/start.py index 2d8d3b7..6f32ff1 100644 --- a/start.py +++ b/start.py @@ -32,6 +32,10 @@ os.makedirs("data") +def cumulative_bankruptcies(log): + return log["cumulative_bankruptcies"][-1] + + # main function def main( sim_params: MutableMapping, From 73ca98eacf4344697f54f24d9c559f3ad86eb66e Mon Sep 17 00:00:00 2001 From: chris Date: Mon, 9 Sep 2019 09:09:53 +0100 Subject: [PATCH 117/125] Add option to use multiprocessing instead of sandman --- insurancesimulation.py | 2 +- logger.py | 2 +- sensitivity.py | 56 ++++++++++++++++++++++++++++-------------- 3 files changed, 39 insertions(+), 21 deletions(-) diff --git a/insurancesimulation.py b/insurancesimulation.py index c1c77ba..ce1774b 100644 --- a/insurancesimulation.py +++ b/insurancesimulation.py @@ -715,7 +715,7 @@ def save_data(self): """ call to Logger object """ self.logger.record_data(current_log) - def obtain_log(self, requested_logs: Mapping = None) -> MutableSequence: + def obtain_log(self, requested_logs: Mapping = None) -> dict: """This function allows to return in a list all the data generated by the model. There is no other way to transfer it back from the cloud.""" return self.logger.obtain_log(requested_logs) diff --git a/logger.py b/logger.py index 6b5fec2..882aded 100644 --- a/logger.py +++ b/logger.py @@ -114,7 +114,7 @@ def record_data(self, data_dict): else: self.history_logs[key].append(data_dict[key]) - def obtain_log(self, requested_logs=None): + def obtain_log(self, requested_logs=None) -> dict: if requested_logs is None: requested_logs = LOG_DEFAULT """Method to transfer entire log (self.history_log as well as risk event schedule). This is diff --git a/sensitivity.py b/sensitivity.py index 9eda062..9f9f48c 100644 --- a/sensitivity.py +++ b/sensitivity.py @@ -8,14 +8,12 @@ import importlib import numpy as np -import sandman2.api as sm - import isleconfig import start import setup_simulation -def rake(hostname=None, summary: callable = None): +def rake(hostname=None, summary: callable = None, use_sandman: bool = False): """ Uses the sandman2 api to run multiple replications of multiple configurations of the simulation. If hostname=None, runs locally. Otherwise, make sure environment variable SANDMAN_KEY_ID and SANDMAN_KEY_SECRET @@ -23,6 +21,8 @@ def rake(hostname=None, summary: callable = None): Args: hostname: The remote server to run the job on summary: The summary statistic (function) to apply to the results + use_sandman: if True, uses sandman, otherwise uses multiprocessing (faster if running very many simulations + locally) """ if importlib.util.find_spec("hickle") is None: raise ModuleNotFoundError("hickle not found but required for saving logs") @@ -30,7 +30,10 @@ def rake(hostname=None, summary: callable = None): if hostname is None: print("Running ensemble locally") else: - print(f"Running ensemble on {hostname}") + if use_sandman: + print(f"Running ensemble on {hostname}") + else: + raise ValueError("use_sandman is False, but hostname is given") """Configure the parameter sets to run""" default_parameters: Dict = isleconfig.simulation_parameters parameter_list = None @@ -190,13 +193,9 @@ def rake(hostname=None, summary: callable = None): np_seeds, random_seeds, ] = setup.obtain_ensemble(len(parameter_list)) - print("Constructing sandman operation") - m = sm.operation(start.main, include_modules=True) - # Here is assembled each job with the corresponding: simulation parameters, time events, damage events, seeds, - # simulation state save interval (never), and list of requested logs. - print("Assembling jobs") + n = len(parameter_list) - m_params = ( + m_params = zip( parameter_list, general_rc_event_schedule, general_rc_event_damage, @@ -208,15 +207,34 @@ def rake(hostname=None, summary: callable = None): [False] * n, [summary] * n, ) - # This is actually quite slow for large sets of jobs. Can't use mp.Pool due to unpickleability - # Could use pathos if we actually end up caring - job = list(map(m, *m_params)) - """Here the jobs are submitted""" - print("Jobs created, submitting") - with sm.Session(host=hostname, default_cb_to_stdout=True) as sess: - print("Starting job") - # Don't use async here, since there is only one job - result = sess.submit(job) + + if use_sandman: + import sandman2.api as sm + from itertools import starmap + + print("Constructing sandman operation") + m = sm.operation(start.main, include_modules=True) + print("Assembling jobs") + + # Here is assembled each job with the corresponding: simulation parameters, time events, damage events, seeds, + # simulation state save interval (never), and list of requested logs. + + # This is actually quite slow for large sets of jobs. Can't use mp.Pool due to unpickleability + # Could use pathos if we actually end up caring + job = list(starmap(m, m_params)) + """Here the jobs are submitted""" + print("Jobs created, submitting") + with sm.Session(host=hostname, default_cb_to_stdout=True) as sess: + print("Starting job") + # Don't use async here, since there is only one job + result = sess.submit(job) + else: + import multiprocessing as mp + + print("Running multiprocessing pool") + with mp.Pool(maxtasksperchild=4) as pool: + result = pool.starmap(start.main, m_params) + print("Job done, saving") result_dict = {t: r for t, r in zip(parameters, result)} start.save_summary([result_dict]) From 2d3029fb8ecbe9caca94d97a92cfbd6e45d0f3c9 Mon Sep 17 00:00:00 2001 From: chris Date: Mon, 9 Sep 2019 18:39:27 +0100 Subject: [PATCH 118/125] Implementing proposed calibration statistic --- calibration_statistic.py | 51 +++++++++++++++++++++++++++++ ensemble.py | 6 ++-- fileconvert.py | 2 +- genericclasses.py | 7 ++++ insurancesimulation.py | 45 +++++++++++++++---------- logger.py | 44 ++++++++++++------------- metainsuranceorg.py | 5 ++- sensitivity.py | 71 ++++------------------------------------ start.py | 34 +++++++++++-------- visualisation.py | 4 +-- 10 files changed, 144 insertions(+), 125 deletions(-) create mode 100644 calibration_statistic.py diff --git a/calibration_statistic.py b/calibration_statistic.py new file mode 100644 index 0000000..5fd9a62 --- /dev/null +++ b/calibration_statistic.py @@ -0,0 +1,51 @@ +import numpy as np +import scipy.stats as stats + + +def calculate_single(log: dict, t: int = -1): + """Takes a dict as returned by Logger.obtainlog() and returns a vector of statistics + We always look at year-long data""" + ins_pls = np.array( + [sum(firm_data[t - 12 : t]) for firm_data in log["insurance_pls"]] + ) + re_pls = np.array( + [sum(firm_data[t - 12 : t]) for firm_data in log["reinsurance_pls"]] + ) + ins_assets = np.array([firm_data[t] for firm_data in log["insurance_firms_cash"]]) + re_assets = np.array([firm_data[t] for firm_data in log["reinsurance_firms_cash"]]) + ins_claims = np.array( + [sum(firm_data[t - 12 : t]) for firm_data in log["insurance_claims"]] + ) + ins_premiums = np.array( + [firm_data[t] for firm_data in log["insurance_cumulative_premiums"]] + ) + + insvars = [ins_pls, ins_assets, ins_claims, ins_premiums] + revars = [re_pls, re_assets] + + for vars_ in (insvars, revars): + if not all([len(x) == len(vars_[0]) for x in vars_]): + raise ValueError("Data are not all same length") + + ins_mask = ins_assets > 0 + re_mask = re_assets > 0 + + ins_pls = ins_pls[ins_mask] + re_pls = re_assets[re_mask] + ins_assets = ins_assets[ins_mask] + re_assets = re_assets[re_mask] + ins_claims = ins_claims[ins_mask] + ins_premiums = ins_premiums[ins_mask] + + output = [] + for data in (ins_pls, ins_assets, ins_claims, ins_premiums): + st = stats.describe(data, nan_policy="raise") + for result in (st.mean, st.variance, st.skewness, st.kurtosis): + output.append(result) + + for data in (re_pls, re_assets): + st = stats.describe(data, nan_policy="raise") + for result in (st.mean, st.variance): + output.append(result) + + return output diff --git a/ensemble.py b/ensemble.py index 019da5b..9547d59 100644 --- a/ensemble.py +++ b/ensemble.py @@ -111,7 +111,7 @@ def rake(hostname=None, replications=9, summary: callable = None): "rc_event_schedule_initial": "_rc_event_schedule.dat", "rc_event_damage_initial": "_rc_event_damage.dat", "number_riskmodels": "_number_riskmodels.dat", - "individual_contracts": "_insurance_contracts.dat", + "insurance_contracts": "_insurance_contracts.dat", "reinsurance_contracts": "_reinsurance_contracts.dat", "unweighted_network_data": "_unweighted_network_data.dat", "network_node_labels": "_network_node_labels.dat", @@ -145,7 +145,7 @@ def rake(hostname=None, replications=9, summary: callable = None): "rc_event_schedule_initial": np.int_, "rc_event_damage_initial": np.float_, "number_riskmodels": np.int_, - "individual_contracts": np.int_, + "insurance_contracts": np.int_, "reinsurance_contracts": np.int_, "unweighted_network_data": np.float_, "network_node_labels": np.float_, @@ -157,7 +157,7 @@ def rake(hostname=None, replications=9, summary: callable = None): for name in [ "insurance_firms_cash", "reinsurance_firms_cash", - "individual_contracts", + "insurance_contracts", "reinsurance_contracts", "unweighted_network_data", "network_node_labels", diff --git a/fileconvert.py b/fileconvert.py index 1de5ce1..da10f1e 100644 --- a/fileconvert.py +++ b/fileconvert.py @@ -32,7 +32,7 @@ "rc_event_schedule_initial": "_rc_event_schedule.dat", "rc_event_damage_initial": "_rc_event_damage.dat", "number_riskmodels": "_number_riskmodels.dat", - "individual_contracts": "_insurance_contracts.dat", + "insurance_contracts": "_insurance_contracts.dat", "reinsurance_contracts": "_reinsurance_contracts.dat", "unweighted_network_data": "_unweighted_network_data.dat", "network_node_labels": "_network_node_labels.dat", diff --git a/genericclasses.py b/genericclasses.py index 2377643..2356413 100644 --- a/genericclasses.py +++ b/genericclasses.py @@ -46,6 +46,7 @@ def __init__(self): self.creditor = None self.id = -1 self.dividends_paid = 0 + self.premiums_recieved = 0 def _pay(self, obligation: "Obligation"): """Method to _pay other class instances. @@ -72,6 +73,8 @@ def _pay(self, obligation: "Obligation"): self.cash -= amount if purpose == "dividend": self.dividends_paid += amount + elif purpose == "premium": + recipient.track_premiums(amount) else: self.profits_losses -= amount recipient.receive(amount) @@ -133,6 +136,10 @@ def receive(self, amount: float): self.cash += amount self.profits_losses += amount + def track_premiums(self, amount: float): + """Tracks total premiums recieved for logging""" + self.premiums_recieved += amount + @dataclasses.dataclass class RiskProperties: diff --git a/insurancesimulation.py b/insurancesimulation.py index ce1774b..3ced847 100644 --- a/insurancesimulation.py +++ b/insurancesimulation.py @@ -660,18 +660,26 @@ def save_data(self): operational_no = sum([firm.operational for firm in self.insurancefirms]) reinoperational_no = sum([firm.operational for firm in self.reinsurancefirms]) catbondsoperational_no = sum([cb.operational for cb in self.catbonds]) + ins_dividends = sum([firm.dividends_paid for firm in self.insurancefirms]) + re_dividends = sum([firm.dividends_paid for firm in self.reinsurancefirms]) """ collect agent-level data """ - insurance_firms = [firm.cash for firm in self.insurancefirms] - reinsurance_firms = [firm.cash for firm in self.reinsurancefirms] - insurance_contracts = [ - len(firm.underwritten_contracts) for firm in self.insurancefirms - ] + insurance_firms_cash = [firm.cash for firm in self.insurancefirms] + reinsurance_firms_cash = [firm.cash for firm in self.reinsurancefirms] + insurance_contracts = [firm.num_underwritten() for firm in self.insurancefirms] reinsurance_contracts = [ - len(firm.underwritten_contracts) for firm in self.reinsurancefirms + firm.num_underwritten() for firm in self.reinsurancefirms + ] + insurance_claims = [firm.claims_this_iteration for firm in self.insurancefirms] + reinsurance_claims = [ + firm.claims_this_iteration for firm in self.reinsurancefirms + ] + insurance_pls = [firm.get_profitslosses() for firm in self.insurancefirms] + reinsurance_pls = [firm.get_profitslosses() for firm in self.reinsurancefirms] + insurance_premiums = [firm.premiums_recieved for firm in self.insurancefirms] + reinsurance_premiums = [ + firm.premiums_recieved for firm in self.reinsurancefirms ] - ins_dividends = sum([firm.dividends_paid for firm in self.insurancefirms]) - re_dividends = sum([firm.dividends_paid for firm in self.reinsurancefirms]) """ prepare dict """ current_log = { @@ -694,13 +702,19 @@ def save_data(self): "cumulative_claims": self.cumulative_claims, "cumulative_bought_firms": self.cumulative_bought_firms, "cumulative_nonregulation_firms": self.cumulative_nonregulation_firms, - "insurance_firms_cash": insurance_firms, - "reinsurance_firms_cash": reinsurance_firms, "market_diffvar": self.compute_market_diffvar(), - "individual_contracts": insurance_contracts, - "reinsurance_contracts": reinsurance_contracts, "insurance_cumulative_dividends": ins_dividends, "reinsurance_cumulative_dividends": re_dividends, + "insurance_firms_cash": insurance_firms_cash, + "reinsurance_firms_cash": reinsurance_firms_cash, + "insurance_contracts": insurance_contracts, + "reinsurance_contracts": reinsurance_contracts, + "insurance_claims": insurance_claims, + "reinsurance_claims": reinsurance_claims, + "insurance_pls": insurance_pls, + "reinsurance_pls": reinsurance_pls, + "insurance_cumulative_premiums": insurance_premiums, + "reinsurance_cumulative_premiums": reinsurance_premiums, } if isleconfig.save_network: @@ -1248,10 +1262,7 @@ def get_risk_share(self, firm: "MetaInsuranceOrg") -> float: total = self.simulation_parameters["no_risks"] elif firm.is_reinsurer: total = sum( - [ - reinfirm.number_underwritten_contracts() - for reinfirm in self.reinsurancefirms - ] + [reinfirm.num_underwritten() for reinfirm in self.reinsurancefirms] + [len(self.reinrisks)] ) else: @@ -1259,7 +1270,7 @@ def get_risk_share(self, firm: "MetaInsuranceOrg") -> float: if total == 0: return 0 else: - return firm.number_underwritten_contracts() / total + return firm.num_underwritten() / total def get_total_firm_cash(self, firm_type: str): """Method to get sum of all cash of firms of a given type. Called from consider_buyout() but could be used for diff --git a/logger.py b/logger.py index 882aded..47a74e7 100644 --- a/logger.py +++ b/logger.py @@ -11,12 +11,25 @@ "total_reincontracts total_reinoperational total_catbondsoperational market_premium " "market_reinpremium cumulative_bankruptcies cumulative_market_exits cumulative_unrecovered_claims " "cumulative_claims insurance_firms_cash reinsurance_firms_cash market_diffvar " - "rc_event_schedule_initial rc_event_damage_initial number_riskmodels individual_contracts reinsurance_contracts " + "rc_event_schedule_initial rc_event_damage_initial number_riskmodels insurance_contracts reinsurance_contracts " "unweighted_network_data network_node_labels network_edge_labels number_of_agents " "cumulative_bought_firms cumulative_nonregulation_firms insurance_cumulative_dividends " "reinsurance_cumulative_dividends" ).split(" ") +firm_level_logs = [ + "insurance_firms_cash", + "reinsurance_firms_cash", + "insurance_contracts", + "reinsurance_contracts", + "insurance_claims", + "reinsurance_claims", + "insurance_pls", + "reinsurance_pls", + "insurance_cumulative_premiums", + "reinsurance_cumulative_premiums", +] + class Logger: def __init__( @@ -59,9 +72,8 @@ def __init__( for _v in insurance_sector: self.history_logs[_v] = [] - """Variables pertaining to individual insurance firms""" - self.history_logs["individual_contracts"] = [] - self.history_logs["insurance_firms_cash"] = [] + for name in firm_level_logs: + self.history_logs[name] = [] """Variables pertaining to reinsurance sector""" self.history_logs["total_reincash"] = [] @@ -70,10 +82,6 @@ def __init__( self.history_logs["total_reincontracts"] = [] self.history_logs["total_reinoperational"] = [] - """Variables pertaining to individual reinsurance firms""" - self.history_logs["reinsurance_firms_cash"] = [] - self.history_logs["reinsurance_contracts"] = [] - """Variables pertaining to cat bonds""" self.history_logs["total_catbondsoperational"] = [] @@ -102,13 +110,8 @@ def record_data(self, data_dict): data_dict: Type dict. Data with the same keys as are used in self.history_log(). Returns None.""" for key in data_dict.keys(): - if key in [ - "individual_contracts", - "reinsurance_contracts", - "insurance_firms_cash", - "reinsurance_firms_cash", - ]: - # These four are stored per-firm + if key in firm_level_logs: + # These are stored per-firm for i in range(len(data_dict[key])): self.history_logs[key][i].append(data_dict[key][i]) else: @@ -116,7 +119,7 @@ def record_data(self, data_dict): def obtain_log(self, requested_logs=None) -> dict: if requested_logs is None: - requested_logs = LOG_DEFAULT + requested_logs = self.history_logs.keys() """Method to transfer entire log (self.history_log as well as risk event schedule). This is used to transfer the log to master core from work cores in ensemble runs in the cloud. No arguments. @@ -253,16 +256,11 @@ def save_network_data(self, ensemble, prefix=""): def add_firm(self, firm_type: str): """Notifies the logger of a new firm, so blank data can be added to firm-level logs""" - if firm_type == "insurance": - keys = ["individual_contracts", "insurance_firms_cash"] - elif firm_type == "reinsurance": - keys = ["reinsurance_contracts", "reinsurance_firms_cash"] - else: - raise ValueError + keys = [key for key in firm_level_logs if key.startswith(firm_type)] for key in keys: if len(self.history_logs[key]) > 0: zeroes_to_append = list( - np.zeros(len(self.history_logs[key][0]), dtype=int) + np.zeros(len(self.history_logs[key][0]), dtype=float) ) else: zeroes_to_append = [] diff --git a/metainsuranceorg.py b/metainsuranceorg.py index 555d142..493b265 100644 --- a/metainsuranceorg.py +++ b/metainsuranceorg.py @@ -222,6 +222,7 @@ def __init__(self, simulation_parameters: dict, agent_parameters: AgentPropertie ] # The share of all risks that this firm holds. Gets updated every timestep self.risk_share = 0 + self.claims_this_iteration = 0 def iterate(self, time: int): """Method that iterates each firm by one time step. @@ -680,7 +681,7 @@ def get_excess_capital(self) -> float: Returns agents excess capital""" return self.excess_capital - def number_underwritten_contracts(self) -> int: + def num_underwritten(self) -> int: return len(self.underwritten_contracts) def get_underwritten_contracts(self) -> Collection["MetaInsuranceContract"]: @@ -1131,6 +1132,7 @@ def register_claim(self, claim: float): No return values. This method records in insurancesimulation.py every claim made. It is called either from insurancecontract.py or reinsurancecontract.py respectively.""" + self.claims_this_iteration += 1 self.simulation.record_claims(claim) def reset_pl(self): @@ -1139,6 +1141,7 @@ def reset_pl(self): No return value. Reset the profits and losses variable of each firm at the beginning of every iteration. It has to be run in insurancesimulation.py at the beginning of the iterate method""" + self.claims_this_iteration = 0 self.profits_losses = 0 def roll_over(self, time: int): diff --git a/sensitivity.py b/sensitivity.py index 9f9f48c..110af0e 100644 --- a/sensitivity.py +++ b/sensitivity.py @@ -11,6 +11,7 @@ import isleconfig import start import setup_simulation +import calibration_statistic def rake(hostname=None, summary: callable = None, use_sandman: bool = False): @@ -46,7 +47,7 @@ def rake(hostname=None, summary: callable = None, use_sandman: bool = False): import SALib.sample.morris problem = SALib.util.read_param_file("isle_all_parameters.txt") - param_values = SALib.sample.morris.sample(problem, N=problem["num_vars"] * 3) + param_values = SALib.sample.morris.sample(problem, N=1) # problem["num_vars"] * 3) parameters = [tuple(row) for row in param_values] parameter_list = [ { @@ -105,69 +106,13 @@ def rake(hostname=None, summary: callable = None, use_sandman: bool = False): "rc_event_schedule_initial": "_rc_event_schedule.dat", "rc_event_damage_initial": "_rc_event_damage.dat", "number_riskmodels": "_number_riskmodels.dat", - "individual_contracts": "_insurance_contracts.dat", + "insurance_contracts": "_insurance_contracts.dat", "reinsurance_contracts": "_reinsurance_contracts.dat", "unweighted_network_data": "_unweighted_network_data.dat", "network_node_labels": "_network_node_labels.dat", "network_edge_labels": "_network_edge_labels.dat", "number_of_agents": "_number_of_agents", } - """Define the numpy types of the underlying data in each requested log""" - types = { - "total_cash": np.float_, - "total_excess_capital": np.float_, - "total_profitslosses": np.float_, - "total_contracts": np.int_, - "total_operational": np.int_, - "total_reincash": np.float_, - "total_reinexcess_capital": np.float_, - "total_reinprofitslosses": np.float_, - "total_reincontracts": np.int_, - "total_reinoperational": np.int_, - "total_catbondsoperational": np.int_, - "market_premium": np.float_, - "market_reinpremium": np.float_, - "cumulative_bankruptcies": np.int_, - "cumulative_market_exits": np.int_, - "cumulative_unrecovered_claims": np.float_, - "cumulative_claims": np.float_, - "cumulative_bought_firms": np.int_, - "cumulative_nonregulation_firms": np.int_, - "insurance_firms_cash": np.float_, - "reinsurance_firms_cash": np.float_, - "market_diffvar": np.float_, - "rc_event_schedule_initial": np.int_, - "rc_event_damage_initial": np.float_, - "number_riskmodels": np.int_, - "individual_contracts": np.int_, - "reinsurance_contracts": np.int_, - "unweighted_network_data": np.float_, - "network_node_labels": np.float_, - "network_edge_labels": np.float_, - "number_of_agents": np.int_, - } - - if isleconfig.slim_log: - for name in [ - "insurance_firms_cash", - "reinsurance_firms_cash", - "individual_contracts", - "reinsurance_contracts", - "unweighted_network_data", - "network_node_labels", - "network_edge_labels", - "number_of_agents", - ]: - del requested_logs[name] - - elif not isleconfig.save_network: - for name in [ - "unweighted_network_data", - "network_node_labels", - "network_edge_labels", - "number_of_agents", - ]: - del requested_logs[name] """Configure log directory and ensure that the directory exists""" dir_prefix = "/data/" @@ -203,7 +148,7 @@ def rake(hostname=None, summary: callable = None, use_sandman: bool = False): random_seeds, [0] * n, [0] * n, - [list(requested_logs.keys())] * n, + [None] * n, [False] * n, [summary] * n, ) @@ -240,16 +185,14 @@ def rake(hostname=None, summary: callable = None, use_sandman: bool = False): start.save_summary([result_dict]) -def get_cumulative_bankruptcies(log): - return log["cumulative_bankruptcies"][-1] - - if __name__ == "__main__": host = None + use_sandman = False if len(sys.argv) > 1: # The server is passed as an argument. host = sys.argv[1] - rake(host, summary=get_cumulative_bankruptcies) + use_sandman = True + rake(host, summary=calibration_statistic.calculate_single, use_sandman=use_sandman) # jobs = {"ensemble1" : "23a3f4e1", # "ensemble2" : "485f7221"} # restore_jobs(jobs, host) diff --git a/start.py b/start.py index 6f32ff1..24c2e7a 100644 --- a/start.py +++ b/start.py @@ -12,6 +12,7 @@ import calibrationscore import insurancesimulation import listify +import calibration_statistic # import config file and apply configuration import isleconfig @@ -183,14 +184,14 @@ def save_results(results_list: list, prefix: str): # These are the big ones, so we need to pay attention to data types "insurance_firms_cash": np.float32, "reinsurance_firms_cash": np.float32, - "individual_contracts": np.uint16, + "insurance_contracts": np.uint16, "reinsurance_contracts": np.uint16, } # bad_logs are the logs that don't have a consistent size between replications bad_logs = [ "rc_event_schedule_initial", "rc_event_damage_initial", - "individual_contracts", + "insurance_contracts", "insurance_firms_cash", "reinsurance_contracts", "reinsurance_firms_cash", @@ -387,6 +388,7 @@ def save_summary(summary_values: List[dict]): general_rc_event_damage ) = np_seeds = random_seeds = [None] + summary = calibration_statistic.calculate_single # Run the main program comp_result = main( simulation_parameters, @@ -397,16 +399,20 @@ def save_summary(summary_values: List[dict]): save_iter, replic_id=1, resume=args.resume, + summary=summary, ) - - save_results([comp_result], "single") - - decomp_result = pickle.loads(zlib.decompress(comp_result[0])) - L = logger.Logger() - L.restore_logger_object(decomp_result) - if isleconfig.save_network: - L.save_network_data(ensemble=False) - - """ Obtain calibration score """ - CS = calibrationscore.CalibrationScore(L) - score = CS.test_all() + if summary is None: + save_results([comp_result], "single") + + decomp_result = pickle.loads(zlib.decompress(comp_result[0])) + L = logger.Logger() + L.restore_logger_object(decomp_result) + if isleconfig.save_network: + L.save_network_data(ensemble=False) + + """ Obtain calibration score """ + CS = calibrationscore.CalibrationScore(L) + score = CS.test_all() + else: + print("\nSummary output:") + print(comp_result) diff --git a/visualisation.py b/visualisation.py index 752cd48..456f58f 100644 --- a/visualisation.py +++ b/visualisation.py @@ -242,7 +242,7 @@ def insurer_pie_animation(self, run=0): self.ins_pie_anim: Type animation class instance. Not used outside this method but is saved.""" data = self.history_log insurance_cash = np.array(data["insurance_firms_cash"][run]) - contract_data = data["individual_contracts"][run] + contract_data = data["insurance_contracts"][run] event_schedule = data["rc_event_schedule_initial"][run] self.ins_pie_anim = InsuranceFirmAnimation( insurance_cash, contract_data, event_schedule, "Insurance Firm", save=True @@ -1302,7 +1302,7 @@ def init_averages(self, avg_dict, data_dict): or "riskmodels" in key ): pass - elif key == "individual_contracts" or key == "reinsurance_contracts": + elif key == "insurance_contracts" or key == "reinsurance_contracts": avg_contract_per_firm = [] for t in range(len(data[key][0])): total_contracts = 0 From 336e4cc933f510557ca9291d1e04702d85fa0ddd Mon Sep 17 00:00:00 2001 From: chris Date: Mon, 9 Sep 2019 19:07:15 +0100 Subject: [PATCH 119/125] (another) bug fix --- ensemble.py | 2 +- start.py | 8 +++++--- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/ensemble.py b/ensemble.py index 019da5b..78a26e6 100644 --- a/ensemble.py +++ b/ensemble.py @@ -318,7 +318,7 @@ def restore_jobs(jobs, hostname): if len(sys.argv) > 1: # The server is passed as an argument. host = sys.argv[1] - rake(host, summary=start.cumulative_bankruptcies) + rake(host, summary="start.cumulative_bankruptcies") # jobs = {"ensemble1" : "23a3f4e1", # "ensemble2" : "485f7221"} # restore_jobs(jobs, host) diff --git a/start.py b/start.py index 6f32ff1..0458bd5 100644 --- a/start.py +++ b/start.py @@ -7,7 +7,7 @@ import pickle import zlib import random -from typing import MutableMapping, MutableSequence, List, Tuple +from typing import MutableMapping, MutableSequence, List, Tuple, Union import calibrationscore import insurancesimulation @@ -47,8 +47,10 @@ def main( replic_id: int, requested_logs: MutableSequence = None, resume: bool = False, - summary: callable = None, + summary: Union[callable, str] = None, ) -> Tuple[bytes, dict]: + if isinstance(summary, str): + summary = eval(summary) if not resume: np.random.seed(np_seed) random.seed(random_seed) @@ -263,7 +265,7 @@ def save_results(results_list: list, prefix: str): hickle.dump(data, filename, compression="gzip") -def save_summary(summary_values: List[dict]): +def save_summary(summary_values): filename = "data/summary_statistics.hdf" if os.path.exists(filename): # Don't want to blindly overwrite, so make backups From 4df8369d259f617b6f2b8cdc6e2257f98af04dd4 Mon Sep 17 00:00:00 2001 From: chris Date: Mon, 9 Sep 2019 19:07:15 +0100 Subject: [PATCH 120/125] (another) bug fix --- ensemble.py | 2 +- isleconfig.py | 2 +- start.py | 8 +++++--- 3 files changed, 7 insertions(+), 5 deletions(-) diff --git a/ensemble.py b/ensemble.py index 019da5b..ae3a40f 100644 --- a/ensemble.py +++ b/ensemble.py @@ -318,7 +318,7 @@ def restore_jobs(jobs, hostname): if len(sys.argv) > 1: # The server is passed as an argument. host = sys.argv[1] - rake(host, summary=start.cumulative_bankruptcies) + rake(host, summary="cumulative_bankruptcies") # jobs = {"ensemble1" : "23a3f4e1", # "ensemble2" : "485f7221"} # restore_jobs(jobs, host) diff --git a/isleconfig.py b/isleconfig.py index 4c7f31f..eefc1e5 100644 --- a/isleconfig.py +++ b/isleconfig.py @@ -9,7 +9,7 @@ # fmt: off simulation_parameters = { - "max_time": 2000, + "max_time": 20, "no_categories": 4, # no_[re]insurancefirms are initial conditions only diff --git a/start.py b/start.py index 6f32ff1..0458bd5 100644 --- a/start.py +++ b/start.py @@ -7,7 +7,7 @@ import pickle import zlib import random -from typing import MutableMapping, MutableSequence, List, Tuple +from typing import MutableMapping, MutableSequence, List, Tuple, Union import calibrationscore import insurancesimulation @@ -47,8 +47,10 @@ def main( replic_id: int, requested_logs: MutableSequence = None, resume: bool = False, - summary: callable = None, + summary: Union[callable, str] = None, ) -> Tuple[bytes, dict]: + if isinstance(summary, str): + summary = eval(summary) if not resume: np.random.seed(np_seed) random.seed(random_seed) @@ -263,7 +265,7 @@ def save_results(results_list: list, prefix: str): hickle.dump(data, filename, compression="gzip") -def save_summary(summary_values: List[dict]): +def save_summary(summary_values): filename = "data/summary_statistics.hdf" if os.path.exists(filename): # Don't want to blindly overwrite, so make backups From 3f853be74ea9b5acc62ec162831f01c682c6341f Mon Sep 17 00:00:00 2001 From: chris Date: Wed, 11 Sep 2019 11:04:59 +0100 Subject: [PATCH 121/125] Multiprocessing changes --- calibration_statistic.py | 5 +++++ isle_all_parameters.txt | 18 +++++++++--------- isleconfig.py | 2 +- metainsuranceorg.py | 4 ++-- sensitivity.py | 9 ++++----- start.py | 25 +++++++++++++++++++------ 6 files changed, 40 insertions(+), 23 deletions(-) diff --git a/calibration_statistic.py b/calibration_statistic.py index 5fd9a62..77b587f 100644 --- a/calibration_statistic.py +++ b/calibration_statistic.py @@ -5,6 +5,8 @@ def calculate_single(log: dict, t: int = -1): """Takes a dict as returned by Logger.obtainlog() and returns a vector of statistics We always look at year-long data""" + + """First do firm-wise timeseries data""" ins_pls = np.array( [sum(firm_data[t - 12 : t]) for firm_data in log["insurance_pls"]] ) @@ -48,4 +50,7 @@ def calculate_single(log: dict, t: int = -1): for result in (st.mean, st.variance): output.append(result) + """Next do market premium""" + premium = np.mean(log["market_premium"][t - 12 : t]) + output.append(premium) return output diff --git a/isle_all_parameters.txt b/isle_all_parameters.txt index 966dc47..5c4de64 100644 --- a/isle_all_parameters.txt +++ b/isle_all_parameters.txt @@ -1,8 +1,8 @@ riskmodel_inaccuracy_parameter, 1.5, 4 riskmodel_margin_of_safety, 1, 4 -value_at_risk_tail_probability, 0.0001, 0.1 -norm_profit_markup, 0.1, 1 -dividend_share_of_profits, 0.1, 0.7 +value_at_risk_tail_probability, 0.0001, 0.05 +norm_profit_markup, 0.1, 0.5 +dividend_share_of_profits, 0.01, 0.5 insurance_firm_market_entry_probability, 0.05, 0.95 reinsurance_firm_market_entry_probability, 0.05, 0.95 default_non-proportional_reinsurance_limit, 0.5, 1.0 @@ -13,10 +13,10 @@ reinsurance_reinsurance_levels_lower_bound, 0, 0.3 reinsurance_reinsurance_levels_upper_bound, 0.3, 0.6 capacity_target_decrement_threshold, 1, 2 capacity_target_increment_threshold, 1, 2 -capacity_target_decrement_factor, 0.75, 1 -capacity_target_increment_factor, 1, 1.5 -insurance_retention, 0, 1 -reinsurance_retention, 0, 1 +capacity_target_decrement_factor, 0.85, 1 +capacity_target_increment_factor, 1, 1.2 +insurance_retention, 0.4, 1 +reinsurance_retention, 0.4, 1 premium_sensitivity, 1, 10 reinpremium_sensitivity, 1, 10 insurers_balance_ratio, 0, 30 @@ -28,8 +28,8 @@ insurance_permanency_time_constraint, 2, 96 reinsurance_permanency_contracts_limit, 1, 100 reinsurance_permanency_ratio_limit, 0.4, 1 reinsurance_permanency_time_constraint, 2, 96 -initial_agent_cash, 10000, 100000 -initial_reinagent_cash, 100000, 1000000 +initial_agent_cash, 10000, 500000 +initial_reinagent_cash, 1000000, 10000000 interest_rate, 0, 0.01 upper_price_limit, 1, 2 lower_price_limit, 0.5, 1 diff --git a/isleconfig.py b/isleconfig.py index eefc1e5..4c7f31f 100644 --- a/isleconfig.py +++ b/isleconfig.py @@ -9,7 +9,7 @@ # fmt: off simulation_parameters = { - "max_time": 20, + "max_time": 2000, "no_categories": 4, # no_[re]insurancefirms are initial conditions only diff --git a/metainsuranceorg.py b/metainsuranceorg.py index 493b265..5b58c67 100644 --- a/metainsuranceorg.py +++ b/metainsuranceorg.py @@ -330,7 +330,7 @@ def collect_process_evaluate_risks( ) """obtain risk model evaluation (VaR) for underwriting decisions and for capacity specific decisions""" - # TODO: Enable reinsurance shares other than 0.0 and 1.0 + # TODO: Enable (proportional) reinsurance shares other than 0.0 and 1.0 [ _, acceptable_by_category, @@ -1029,7 +1029,7 @@ def process_newrisks_insurer( self, risk, time, - self.simulation.get_market_premium(), + self.insurance_premium(), random_runtime, self.default_contract_payment_period, expire_immediately=self.simulation_parameters[ diff --git a/sensitivity.py b/sensitivity.py index 110af0e..145c4a8 100644 --- a/sensitivity.py +++ b/sensitivity.py @@ -47,7 +47,7 @@ def rake(hostname=None, summary: callable = None, use_sandman: bool = False): import SALib.sample.morris problem = SALib.util.read_param_file("isle_all_parameters.txt") - param_values = SALib.sample.morris.sample(problem, N=1) # problem["num_vars"] * 3) + param_values = SALib.sample.morris.sample(problem, N=problem["num_vars"] * 3) parameters = [tuple(row) for row in param_values] parameter_list = [ { @@ -155,7 +155,6 @@ def rake(hostname=None, summary: callable = None, use_sandman: bool = False): if use_sandman: import sandman2.api as sm - from itertools import starmap print("Constructing sandman operation") m = sm.operation(start.main, include_modules=True) @@ -165,8 +164,8 @@ def rake(hostname=None, summary: callable = None, use_sandman: bool = False): # simulation state save interval (never), and list of requested logs. # This is actually quite slow for large sets of jobs. Can't use mp.Pool due to unpickleability - # Could use pathos if we actually end up caring - job = list(starmap(m, m_params)) + # Could use pathos or similar if we actually end up caring + job = list(map(m, m_params)) """Here the jobs are submitted""" print("Jobs created, submitting") with sm.Session(host=hostname, default_cb_to_stdout=True) as sess: @@ -178,7 +177,7 @@ def rake(hostname=None, summary: callable = None, use_sandman: bool = False): print("Running multiprocessing pool") with mp.Pool(maxtasksperchild=4) as pool: - result = pool.starmap(start.main, m_params) + result = pool.map(start.main, m_params, chunksize=4) print("Job done, saving") result_dict = {t: r for t, r in zip(parameters, result)} diff --git a/start.py b/start.py index 4e8d0d6..ddf6646 100644 --- a/start.py +++ b/start.py @@ -40,16 +40,29 @@ def cumulative_bankruptcies(log): # main function def main( sim_params: MutableMapping, - rc_event_schedule: MutableSequence[MutableSequence[int]], - rc_event_damage: MutableSequence[MutableSequence[float]], - np_seed: int, - random_seed: int, - save_iteration: int, - replic_id: int, + rc_event_schedule: MutableSequence[MutableSequence[int]] = None, + rc_event_damage: MutableSequence[MutableSequence[float]] = None, + np_seed: int = None, + random_seed: int = None, + save_iteration: int = None, + replic_id: int = None, requested_logs: MutableSequence = None, resume: bool = False, summary: Union[callable, str] = None, ) -> Tuple[bytes, dict]: + if isinstance(sim_params, tuple): + ( + sim_params, + rc_event_schedule, + rc_event_damage, + np_seed, + random_seed, + save_iteration, + replic_id, + requested_logs, + resume, + summary, + ) = sim_params if isinstance(summary, str): summary = eval(summary) if not resume: From e7957e9381300ff90b00b627c35800f59660c5fb Mon Sep 17 00:00:00 2001 From: Chris Hughes Date: Thu, 12 Sep 2019 16:27:19 +0100 Subject: [PATCH 122/125] Setting up for calibration --- calibrate.py | 124 +++++++++++++++++ calibration_statistic.py | 239 ++++++++++++++++++++++++++++++-- ensemble.py | 4 +- genericclasses.py | 12 +- insurancefirms.py | 8 ++ insurancesimulation.py | 6 + isle_calibration_parameters.txt | 6 + logger.py | 7 + riskmodel.py | 2 + sensitivity.py | 51 ++++--- start.py | 141 ++++++++++--------- 11 files changed, 501 insertions(+), 99 deletions(-) create mode 100644 calibrate.py create mode 100644 isle_calibration_parameters.txt diff --git a/calibrate.py b/calibrate.py new file mode 100644 index 0000000..72a082b --- /dev/null +++ b/calibrate.py @@ -0,0 +1,124 @@ +import sandman2.api as sm +import pyabc +import pyabc.sampler +from typing import Callable, Iterable, TypeVar, List +from functools import partial +import os +import isleconfig +import setup_simulation +import start +import calibration_statistic +import scipy.spatial +import scipy.stats + +T1 = TypeVar("T1") +T2 = TypeVar("T2") + + +def sm_map( + func: Callable[[T1], T2], iter_: Iterable[T1], hostname: str = None +) -> List[T2]: + """Implements a map-like interface using sandman. Should be obtained using get_sm_map to set hostname""" + op = sm.operation(func, include_modules=True) + outputs = [op(arg) for arg in iter_] + with sm.Session(host=hostname) as sess: + result = sess.submit(outputs) + return result + + +def get_sm_map(hostname: str) -> Callable[[Callable[[T1], T2], Iterable[T1]], List[T2]]: + """Returns sm_map with hostname parameter pre-filled, so it can be used as a drop-in repacement for + map (with a single iterable)""" + return partial(sm_map, hostname=hostname) + + +def model(parameters: dict) -> dict: + """Runs the model with random randomness + Args: + parameters (dict): the parameters of this model run. Override those in isleconfig + Returns: + the result as a dictionary + """ + sim_params = isleconfig.simulation_parameters.copy() + sim_params.update(parameters) + setup = setup_simulation.SetupSim() + [event_schedule, event_damage, np_seeds, random_seeds] = setup.obtain_ensemble( + 1, overwrite=True + ) + result = start.main( + sim_params=sim_params, + rc_event_schedule=event_schedule[0], + rc_event_damage=event_damage[0], + np_seed=np_seeds[0], + random_seed=random_seeds[0], + save_iteration=0, + replic_id=0, + requested_logs=None, + resume=False, + summary=calibration_statistic.calculate_single, + ) + return result + + +def single_prior(lower_bound, upper_bound, shape): + if shape not in ("linear", "logarithmic"): + print(f"Warning: shape {shape} not recognised, assuming linear") + shape = "linear" + lower_bound, upper_bound = float(lower_bound), float(upper_bound) + if shape == "linear": + return scipy.stats.uniform(lower_bound, upper_bound - lower_bound) + elif shape == "logarithmic": + return scipy.stats.reciprocal(lower_bound, upper_bound) + + +def get_prior(): + params = {} + param_file_path = os.path.join(os.getcwd(), "isle_calibration_parameters.txt") + with open(param_file_path, "r") as rfile: + for line in rfile: + if line.startswith("#") or line == "\n": + continue + parts = line.strip("\n").replace(" ", "").split(",") + if len(parts) < 4: + continue + params[parts[0]] = single_prior(parts[1], parts[2], parts[3]) + return params + + +def calibrate(observed: dict, hostname: str = None): + """Calibrates. observed is a dictionary with keys as in calibration_statistic.statistics containing the real data""" + db_path = "sqlite:///" + os.path.join(os.getcwd(), "data", "calibration.db") + if hostname is not None: + sampler = pyabc.sampler.MappingSampler( + map_=get_sm_map(hostname), mapper_pickles=True + ) + else: + sampler = pyabc.sampler.MulticoreEvalParallelSampler() + # Adaptive distance based on Prangle (2017) (also acceptor below) + dist = pyabc.distance.AdaptivePNormDistance(p=2, adaptive=True) + prior = pyabc.Distribution(**get_prior()) + + abc = pyabc.ABCSMC( + model, + parameter_priors=prior, + distance_function=dist, + sampler=sampler, + acceptor=pyabc.accept_use_complete_history, + ) + run_id = abc.new(db=db_path, observed_sum_stat=observed) + print(f"Run ID is {run_id}") + history = abc.run(max_nr_populations=10) + df, w = history.get_distribution() + + # TODO: Finish + raise Exception("Should probably finish writing this code") + + +if __name__ == "__main__": + import sys + + host = None + if len(sys.argv) > 1: + # The server is passed as an argument. + host = sys.argv[1] + calibrate(observed=calibration_statistic.observed, hostname=host) diff --git a/calibration_statistic.py b/calibration_statistic.py index 77b587f..599dc21 100644 --- a/calibration_statistic.py +++ b/calibration_statistic.py @@ -1,11 +1,112 @@ import numpy as np import scipy.stats as stats +import scipy.signal as sig +# A list of the statistics that are outputted (for convenience). This is the canonical order. +# statistics = [ +# "ins_profitloss_mean", +# "ins_profitloss_var", +# "ins_profitloss_skew", +# "ins_profitloss_kurt", +# "ins_assets_mean", +# "ins_assets_var", +# "ins_assets_skew", +# "ins_assets_kurt", +# "ins_claims_mean", +# "ins_claims_var", +# "ins_claims_skew", +# "ins_claims_kurt", +# "ins_premiums_mean", +# "ins_premiums_var", +# "ins_premiums_skew", +# "ins_premiums_kurt", +# "ins_solvency_II_mean", +# "ins_solvency_II_var", +# "ins_solvency_II_skew", +# "ins_solvency_II_kurt", +# "re_profitloss_mean", +# "re_profitloss_var", +# "re_assets_mean", +# "re_assets_var", +# "re_solvency_II_mean", +# "re_solvency_II_var", +# "rate_on_line", +# ] -def calculate_single(log: dict, t: int = -1): +# A list of the statistics we actually have data for +statistics = [ + "ins_profitloss_mean", + "ins_profitloss_var", + "ins_profitloss_skew", + "ins_profitloss_kurt", + "ins_assets_mean", + "ins_assets_var", + "ins_assets_skew", + "ins_assets_kurt", + "ins_claims_mean", + "ins_claims_var", + "ins_claims_skew", + "ins_claims_kurt", + "ins_premiums_mean", + "ins_premiums_var", + "ins_premiums_skew", + "ins_premiums_kurt", + "re_profitloss_mean", + "re_profitloss_var", + "re_assets_mean", + "re_assets_var", + "rate_on_line_autocorr_lag_1", + "rate_on_line_autocorr_lag_2", + "rate_on_line_autocorr_lag_3", + "rate_on_line_autocorr_lag_4", + "rate_on_line_autocorr_lag_5", + "rate_on_line_autocorr_lag_6", + "rate_on_line_autocorr_lag_7", + "rate_on_line_autocorr_lag_8", + "rate_on_line_autocorr_lag_9", + "rate_on_line_autocorr_lag_10", +] + +# The observed data +observed = { + "ins_profitloss_mean": 760.8403583654745, + "ins_profitloss_var": 2645861.077652064, + "ins_profitloss_skew": 4.2449962851289325, + "ins_profitloss_kurt": 19.079106920165742, + "ins_assets_mean": 34422.302595397, + "ins_assets_var": 3760288749.1412654, + "ins_assets_skew": 2.493880852639668, + "ins_assets_kurt": 5.3551029266263885, + "ins_claims_mean": 4256.6503666102135, + "ins_claims_var": 116386072.62414846, + "ins_claims_skew": 4.3147875953543515, + "ins_claims_kurt": 17.92245449535894, + "ins_premiums_mean": 3938.0877987962162, + "ins_premiums_var": 24680436.944519438, + "ins_premiums_skew": 1.5042246765191107, + "ins_premiums_kurt": 0.9007423845542157, + "re_profitloss_mean": 604.9962383992475, + "re_profitloss_var": 362488.40656840097, + "re_assets_mean": 30621.369781367932, + "re_assets_var": 2159558210.9914727, + "rate_on_line_autocorr_lag_1": 29.812898800240387, + "rate_on_line_autocorr_lag_2": 28.600956884326997, + "rate_on_line_autocorr_lag_3": 27.489320890771207, + "rate_on_line_autocorr_lag_4": 25.83402370785473, + "rate_on_line_autocorr_lag_5": 24.36635259681405, + "rate_on_line_autocorr_lag_6": 23.029625203121448, + "rate_on_line_autocorr_lag_7": 21.708364947181977, + "rate_on_line_autocorr_lag_8": 20.715011404172028, + "rate_on_line_autocorr_lag_9": 19.940742580799636, + "rate_on_line_autocorr_lag_10": 19.133566632717763, +} + + +def calculate_single(log: dict, t: int = -1) -> dict: """Takes a dict as returned by Logger.obtainlog() and returns a vector of statistics We always look at year-long data""" + print("Calculating calibration statistic...") """First do firm-wise timeseries data""" ins_pls = np.array( [sum(firm_data[t - 12 : t]) for firm_data in log["insurance_pls"]] @@ -21,9 +122,11 @@ def calculate_single(log: dict, t: int = -1): ins_premiums = np.array( [firm_data[t] for firm_data in log["insurance_cumulative_premiums"]] ) + ins_ratios = np.array([firm_data[t] for firm_data in log["insurance_ratios"]]) + re_ratios = np.array([firm_data[t] for firm_data in log["reinsurance_ratios"]]) - insvars = [ins_pls, ins_assets, ins_claims, ins_premiums] - revars = [re_pls, re_assets] + insvars = [ins_pls, ins_assets, ins_claims, ins_premiums, ins_ratios] + revars = [re_pls, re_assets, re_ratios] for vars_ in (insvars, revars): if not all([len(x) == len(vars_[0]) for x in vars_]): @@ -38,19 +141,125 @@ def calculate_single(log: dict, t: int = -1): re_assets = re_assets[re_mask] ins_claims = ins_claims[ins_mask] ins_premiums = ins_premiums[ins_mask] + ins_ratios = ins_ratios[ins_mask] + re_ratios = re_ratios[re_mask] + + ins_data = { + "ins_profitloss": ins_pls, + "ins_assets": ins_assets, + "ins_claims": ins_claims, + "ins_premiums": ins_premiums, + "ins_solvency_II": ins_ratios, + } + re_data = { + "re_profitloss": re_pls, + "re_assets": re_assets, + "re_solvency_II": re_ratios, + } + statistic_names = ["mean", "var", "skew", "kurt"] - output = [] - for data in (ins_pls, ins_assets, ins_claims, ins_premiums): - st = stats.describe(data, nan_policy="raise") - for result in (st.mean, st.variance, st.skewness, st.kurtosis): - output.append(result) + output = {} + for name, data in ins_data.items(): + if len(data) == 0: + if len(ins_mask) == 0: + print(f"Empty ins data found, name = {name}") + for stat_name in statistic_names: + output[name + "_" + stat_name] = float("nan") + else: + for stat_name in statistic_names: + output[name + "_" + stat_name] = 0 + else: + st = stats.describe(data, nan_policy="raise", ddof=0) + for stat, stat_name in zip( + (st.mean, st.variance, st.skewness, st.kurtosis), statistic_names + ): + output[name + "_" + stat_name] = stat - for data in (re_pls, re_assets): - st = stats.describe(data, nan_policy="raise") - for result in (st.mean, st.variance): - output.append(result) + for name, data in re_data.items(): + if len(data) == 0: + if len(re_mask == 0): + print(f"Empty re data found, name = {name}") + for stat_name in statistic_names: + output[name + "_" + stat_name] = float("nan") + else: + for stat_name in statistic_names: + output[name + "_" + stat_name] = 0 + else: + st = stats.describe(data, nan_policy="raise", ddof=0) + for stat, stat_name in zip((st.mean, st.variance), statistic_names): + output[name + "_" + stat_name] = stat - """Next do market premium""" - premium = np.mean(log["market_premium"][t - 12 : t]) - output.append(premium) + """Next do market premium (Rate-On-Line)""" + no_years = len(log["market_premium"]) // 4 // 12 + slices = [slice(-(n + 1) * 12, -n * 12) for n in range(no_years - 1, -1, -1)] + slices[-1] = slice(-12, None, None) + premium_series = np.array([np.mean(log["market_premium"][sl]) for sl in slices]) + premium_series = premium_series / np.mean(premium_series) + ac = sig.correlate(premium_series, premium_series, mode="full") + ac = ac[len(ac) // 2 :] + for lag, c in enumerate(ac[:11]): + if lag >= 1: + output["rate_on_line_autocorr_lag_" + str(lag)] = c return output + + +def make_from_excel(): + import pandas as pd + + data = pd.read_excel("Data.xlsx", sheet_name=None) + transposed_data = {} + for key in data: + df = data[key].T + transposed_data[key.replace("(", " ").replace(")", " ")] = df.rename( + columns=df.iloc[0] + ).drop(df.index[0]) + ins = transposed_data["Insurance remove inflation "].loc[2014] + re = transposed_data["Reinsurance remove inflation "].loc[2014] + oth = transposed_data["Other Data"].loc[2014] + + ins_pls = np.array(ins.iloc[1:37], dtype=np.float_) + re_pls = np.array(re.iloc[1:13], dtype=np.float_) + ins_assets = np.array(ins.iloc[39:75], dtype=np.float_) + re_assets = np.array(re.iloc[15:27], dtype=np.float_) + ins_claims = np.array(ins.iloc[104:130], dtype=np.float_) + ins_premiums = np.array(ins.iloc[77:103], dtype=np.float_) + # ins_ratios = + # re_ratios = + + ins_data = { + "ins_profitloss": ins_pls, + "ins_assets": ins_assets, + "ins_claims": ins_claims, + "ins_premiums": ins_premiums, + } + re_data = {"re_profitloss": re_pls, "re_assets": re_assets} + + statistic_names = ["mean", "var", "skew", "kurt"] + + output = {} + for name, data in ins_data.items(): + st = stats.describe(data, nan_policy="raise", ddof=0) + for stat, stat_name in zip( + (st.mean, st.variance, st.skewness, st.kurtosis), statistic_names + ): + output[name + "_" + stat_name] = stat + + for name, data in re_data.items(): + st = stats.describe(data, nan_policy="raise", ddof=0) + for stat, stat_name in zip((st.mean, st.variance), statistic_names): + output[name + "_" + stat_name] = stat + + rols = list( + transposed_data["Other Data"]["Guy Carpenter U.S. Property Rate on Line Index"] + )[28::-1] + rols = rols / np.mean(rols) + ac = sig.correlate(rols, rols, mode="full") + ac = ac[len(ac) // 2 :] + for lag, c in enumerate(ac[:11]): + if lag >= 1: + output["rate_on_line_autocorr_lag_" + str(lag)] = c + print(repr(output)) + + +if __name__ == "__main__": + make_from_excel() diff --git a/ensemble.py b/ensemble.py index 2a6768c..b0c8b21 100644 --- a/ensemble.py +++ b/ensemble.py @@ -5,7 +5,7 @@ """ import sys import os -from typing import Dict +from typing import Dict, Callable, Iterable import time import importlib @@ -17,7 +17,7 @@ import setup_simulation -def rake(hostname=None, replications=9, summary: callable = None): +def rake(hostname=None, replications=4, summary: callable = None): """ Uses the sandman2 api to run multiple replications of multiple configurations of the simulation. If hostname=None, runs locally. Otherwise, make sure environment variable SANDMAN_KEY_ID and SANDMAN_KEY_SECRET diff --git a/genericclasses.py b/genericclasses.py index 2356413..436f2aa 100644 --- a/genericclasses.py +++ b/genericclasses.py @@ -367,7 +367,11 @@ def __init__(self, seq: Collection[T] = None): self._dict = {} if seq is not None: for item in seq: - self.add(item) + try: + self.add(item) + except ValueError: + # Silently remove duplicates + pass def __hash__(self): return None @@ -381,6 +385,12 @@ def __iter__(self) -> T: def __contains__(self, item: T) -> bool: return id(item) in self._dict + def __repr__(self) -> str: + return "IdSet(" + repr(list(self._dict.values())) + ")" + + def __str__(self) -> str: + return "{" + str(list(self._dict.values()))[1:-1] + "}" + def add(self, item: T) -> None: if item not in self: self._dict[id(item)] = item diff --git a/insurancefirms.py b/insurancefirms.py index dbb7411..b11e49b 100644 --- a/insurancefirms.py +++ b/insurancefirms.py @@ -553,6 +553,14 @@ def refresh_reinrisk( raise ValueError("After refreshing risk has become invalid") return risk + def get_solvency_ratio(self): + solvency = self.cash + var = self.cash - self.excess_capital + if var == 0: + return 1 + else: + return solvency / var + class ReinsuranceFirm(InsuranceFirm): """ReinsuranceFirm class. diff --git a/insurancesimulation.py b/insurancesimulation.py index 3ced847..cdb3f36 100644 --- a/insurancesimulation.py +++ b/insurancesimulation.py @@ -680,6 +680,10 @@ def save_data(self): reinsurance_premiums = [ firm.premiums_recieved for firm in self.reinsurancefirms ] + insurance_ratios = [firm.get_solvency_ratio() for firm in self.insurancefirms] + reinsurance_ratios = [ + firm.get_solvency_ratio() for firm in self.reinsurancefirms + ] """ prepare dict """ current_log = { @@ -715,6 +719,8 @@ def save_data(self): "reinsurance_pls": reinsurance_pls, "insurance_cumulative_premiums": insurance_premiums, "reinsurance_cumulative_premiums": reinsurance_premiums, + "insurance_ratios": insurance_ratios, + "reinsurance_ratios": reinsurance_ratios, } if isleconfig.save_network: diff --git a/isle_calibration_parameters.txt b/isle_calibration_parameters.txt new file mode 100644 index 0000000..547a5fa --- /dev/null +++ b/isle_calibration_parameters.txt @@ -0,0 +1,6 @@ +# This file will hold the parameters to actually calibrate on, as well as info on their priors +# Priors can be uniform linearly or uniform in a logarithmic sense +# Format is: +# , , , (linear|logarithmic) +riskmodel_inaccuracy_parameter, 1.5, 4, linear + diff --git a/logger.py b/logger.py index 47a74e7..c5a04dd 100644 --- a/logger.py +++ b/logger.py @@ -28,6 +28,8 @@ "reinsurance_pls", "insurance_cumulative_premiums", "reinsurance_cumulative_premiums", + "insurance_ratios", + "reinsurance_ratios", ] @@ -112,6 +114,11 @@ def record_data(self, data_dict): for key in data_dict.keys(): if key in firm_level_logs: # These are stored per-firm + if not len(data_dict[key]) == len(self.history_logs[key]): + raise RuntimeError( + f"Log {key} passed to logger has different number of firms to those already in" + f" log - {len(data_dict[key])} passed, {len(self.history_logs[key])} expected" + ) for i in range(len(data_dict[key])): self.history_logs[key][i].append(data_dict[key][i]) else: diff --git a/riskmodel.py b/riskmodel.py index d80fcb4..fd6d9cf 100644 --- a/riskmodel.py +++ b/riskmodel.py @@ -174,6 +174,8 @@ def evaluate_proportional( expected_profits += incr_expected_profits + if average_exposure == 0: + average_exposure = self.init_average_exposure # compute value at risk var_per_risk = ( self.get_ppf(categ_id=categ_id, tail_size=self.var_tail_prob) diff --git a/sensitivity.py b/sensitivity.py index 145c4a8..80e639b 100644 --- a/sensitivity.py +++ b/sensitivity.py @@ -25,6 +25,10 @@ def rake(hostname=None, summary: callable = None, use_sandman: bool = False): use_sandman: if True, uses sandman, otherwise uses multiprocessing (faster if running very many simulations locally) """ + + # TODO: RM + np.seterr(all="raise") + if importlib.util.find_spec("hickle") is None: raise ModuleNotFoundError("hickle not found but required for saving logs") @@ -62,7 +66,7 @@ def rake(hostname=None, summary: callable = None, use_sandman: bool = False): ################################################################################################################### - max_time = isleconfig.simulation_parameters["max_time"] + max_time = parameter_list[0]["max_time"] print(f"Running {len(parameter_list)} simulations of {max_time} timesteps") @@ -140,17 +144,19 @@ def rake(hostname=None, summary: callable = None, use_sandman: bool = False): ] = setup.obtain_ensemble(len(parameter_list)) n = len(parameter_list) - m_params = zip( - parameter_list, - general_rc_event_schedule, - general_rc_event_damage, - np_seeds, - random_seeds, - [0] * n, - [0] * n, - [None] * n, - [False] * n, - [summary] * n, + m_params = list( + zip( + parameter_list, + general_rc_event_schedule, + general_rc_event_damage, + np_seeds, + random_seeds, + [0] * n, + [0] * n, + [None] * n, + [False] * n, + [summary] * n, + ) ) if use_sandman: @@ -166,18 +172,31 @@ def rake(hostname=None, summary: callable = None, use_sandman: bool = False): # This is actually quite slow for large sets of jobs. Can't use mp.Pool due to unpickleability # Could use pathos or similar if we actually end up caring job = list(map(m, m_params)) + # Split up into chunks so sandman server doesn't blow up + max_size = 71 + job_lists = [] + while len(job) > 0: + job_lists.append(job[: min(max_size, len(job))]) + job = job[min(max_size, len(job)) :] """Here the jobs are submitted""" print("Jobs created, submitting") with sm.Session(host=hostname, default_cb_to_stdout=True) as sess: print("Starting job") - # Don't use async here, since there is only one job - result = sess.submit(job) + # Could use async, but, again, that might make the cluster blow up + result = [] + for job in job_lists: + result += sess.submit(job) else: + # result = [] + # m_params.reverse() + # for i, param_set in enumerate(m_params): + # result.append(start.main(param_set)) import multiprocessing as mp print("Running multiprocessing pool") - with mp.Pool(maxtasksperchild=4) as pool: - result = pool.map(start.main, m_params, chunksize=4) + # set maxtasksperchild, otherwise it seems that garbage collection(?) misbehaves and we get huge memory usage + with mp.Pool(maxtasksperchild=1) as pool: + result = pool.map(start.main, m_params, chunksize=1) print("Job done, saving") result_dict = {t: r for t, r in zip(parameters, result)} diff --git a/start.py b/start.py index ddf6646..ef2186f 100644 --- a/start.py +++ b/start.py @@ -4,6 +4,8 @@ import hashlib import numpy as np import os +import sys +import traceback import pickle import zlib import random @@ -39,7 +41,7 @@ def cumulative_bankruptcies(log): # main function def main( - sim_params: MutableMapping, + sim_params: Union[MutableMapping, Tuple], rc_event_schedule: MutableSequence[MutableSequence[int]] = None, rc_event_damage: MutableSequence[MutableSequence[float]] = None, np_seed: int = None, @@ -49,70 +51,79 @@ def main( requested_logs: MutableSequence = None, resume: bool = False, summary: Union[callable, str] = None, -) -> Tuple[bytes, dict]: - if isinstance(sim_params, tuple): - ( - sim_params, - rc_event_schedule, - rc_event_damage, - np_seed, - random_seed, - save_iteration, - replic_id, - requested_logs, - resume, - summary, - ) = sim_params - if isinstance(summary, str): - summary = eval(summary) - if not resume: - np.random.seed(np_seed) - random.seed(random_seed) - - sim_params["simulation"] = simulation = insurancesimulation.InsuranceSimulation( - override_no_riskmodels, - replic_id, - sim_params, - rc_event_schedule, - rc_event_damage, - ) - time = 0 - else: - d = load_simulation() - np.random.set_state(d["np_seed"]) - random.setstate(d["random_seed"]) - time = d["time"] - simulation = d["simulation"] - sim_params = d["simulation_parameters"] - for key in d["isleconfig"]: - isleconfig.__dict__[key] = d["isleconfig"][key] - isleconfig.simulation_parameters = sim_params - for t in range(time, sim_params["max_time"]): - # Main time iteration loop - simulation.iterate(t) - - # log data - simulation.save_data() - - # Don't save at t=0 or if the simulation has just finished - if ( - save_iteration > 0 - and t % save_iteration == 0 - and 0 < t < sim_params["max_time"] - ): - # Need to use t+1 as resume will start at time saved - save_simulation(t + 1, simulation, sim_params, exit_now=False) - - log = simulation.obtain_log(requested_logs) - if summary is not None: - return summary(log) - else: - # We compute metadata about the return data that isn't compressed, so a skeleton data structure can be - # constructed before decompression - found_shapes = {name: np.shape(log[name]) for name in log} - - # We compress the return value for the sake of minimising data transfer over the network and RAM usage - return (zlib.compress(pickle.dumps(log)), found_shapes) +) -> Union[Tuple[bytes, dict], dict]: + try: + if isinstance(sim_params, tuple): + ( + sim_params, + rc_event_schedule, + rc_event_damage, + np_seed, + random_seed, + save_iteration, + replic_id, + requested_logs, + resume, + summary, + ) = sim_params + if isinstance(summary, str): + summary = eval(summary) + if not resume: + np.random.seed(np_seed) + random.seed(random_seed) + + sim_params[ + "simulation" + ] = simulation = insurancesimulation.InsuranceSimulation( + override_no_riskmodels, + replic_id, + sim_params, + rc_event_schedule, + rc_event_damage, + ) + time = 0 + else: + d = load_simulation() + np.random.set_state(d["np_seed"]) + random.setstate(d["random_seed"]) + time = d["time"] + simulation = d["simulation"] + sim_params = d["simulation_parameters"] + for key in d["isleconfig"]: + isleconfig.__dict__[key] = d["isleconfig"][key] + isleconfig.simulation_parameters = sim_params + for t in range(time, sim_params["max_time"]): + # Main time iteration loop + try: + simulation.iterate(t) + except RuntimeError: + print("Simulation encountered error") + return None + + # log data + simulation.save_data() + + # Don't save at t=0 or if the simulation has just finished + if ( + save_iteration > 0 + and t % save_iteration == 0 + and 0 < t < sim_params["max_time"] + ): + # Need to use t+1 as resume will start at time saved + save_simulation(t + 1, simulation, sim_params, exit_now=False) + + log = simulation.obtain_log(requested_logs) + if summary is not None: + return summary(log) + else: + # We compute metadata about the return data that isn't compressed, so a skeleton data structure can be + # constructed before decompression + found_shapes = {name: np.shape(log[name]) for name in log} + + # We compress the return value for the sake of minimising data transfer over the network and RAM usage + return (zlib.compress(pickle.dumps(log)), found_shapes) + except Exception: + raise Exception("".join(traceback.format_exception(*sys.exc_info()))) def save_simulation( From e69086dd620a884a3f20e6b40e8a646d2dff9bce Mon Sep 17 00:00:00 2001 From: Chris Hughes Date: Thu, 12 Sep 2019 16:27:19 +0100 Subject: [PATCH 123/125] Setting up for calibration --- calibrate.py | 136 ++++++++++++++++++ calibration_statistic.py | 239 ++++++++++++++++++++++++++++++-- ensemble.py | 4 +- genericclasses.py | 12 +- insurancefirms.py | 8 ++ insurancesimulation.py | 6 + isle_calibration_parameters.txt | 6 + logger.py | 7 + riskmodel.py | 2 + sensitivity.py | 62 ++++++--- start.py | 141 ++++++++++--------- 11 files changed, 522 insertions(+), 101 deletions(-) create mode 100644 calibrate.py create mode 100644 isle_calibration_parameters.txt diff --git a/calibrate.py b/calibrate.py new file mode 100644 index 0000000..2d515b8 --- /dev/null +++ b/calibrate.py @@ -0,0 +1,136 @@ +import sandman2.api as sm +import pyabc +import pyabc.sampler +from typing import Callable, Iterable, TypeVar, List +from functools import partial +import os +import isleconfig +import setup_simulation +import start +import calibration_statistic +import scipy.spatial +import scipy.stats +import numpy as np + +T1 = TypeVar("T1") +T2 = TypeVar("T2") + + +def sm_map( + func: Callable[[T1], T2], iter_: Iterable[T1], hostname: str = None +) -> List[T2]: + """Implements a map-like interface using sandman. Should be obtained using get_sm_map to set hostname""" + op = sm.operation(func, include_modules=True) + outputs = [op(arg) for arg in iter_] + with sm.Session(host=hostname) as sess: + result = sess.submit(outputs) + return result + + +def get_sm_map(hostname: str) -> Callable[[Callable[[T1], T2], Iterable[T1]], List[T2]]: + """Returns sm_map with hostname parameter pre-filled, so it can be used as a drop-in repacement for + map (with a single iterable)""" + return partial(sm_map, hostname=hostname) + + +def model(parameters: dict) -> dict: + """Runs the model with random randomness + Args: + parameters (dict): the parameters of this model run. Override those in isleconfig + Returns: + the result as a dictionary + """ + sim_params = isleconfig.simulation_parameters.copy() + sim_params.update(parameters) + setup = setup_simulation.SetupSim() + [event_schedule, event_damage, np_seeds, random_seeds] = setup.obtain_ensemble( + 1, overwrite=True + ) + result = start.main( + sim_params=sim_params, + rc_event_schedule=event_schedule[0], + rc_event_damage=event_damage[0], + np_seed=np_seeds[0], + random_seed=random_seeds[0], + save_iteration=0, + replic_id=0, + requested_logs=None, + resume=False, + summary=calibration_statistic.calculate_single, + ) + return result + + +def single_prior(lower_bound, upper_bound, shape): + if shape not in ("linear", "logarithmic"): + print(f"Warning: shape {shape} not recognised, assuming linear") + shape = "linear" + lower_bound, upper_bound = float(lower_bound), float(upper_bound) + if shape == "linear": + return scipy.stats.uniform(lower_bound, upper_bound - lower_bound) + elif shape == "logarithmic": + return scipy.stats.reciprocal(lower_bound, upper_bound) + + +def get_prior(): + params = {} + param_file_path = os.path.join(os.getcwd(), "isle_calibration_parameters.txt") + with open(param_file_path, "r") as rfile: + for line in rfile: + if line.startswith("#") or line == "\n": + continue + parts = line.strip("\n").replace(" ", "").split(",") + if len(parts) < 4: + continue + params[parts[0]] = single_prior(parts[1], parts[2], parts[3]) + return params + + +def calibrate(observed: dict, hostname: str = None): + """Calibrates. observed is a dictionary with keys as in calibration_statistic.statistics containing the real data""" + db_path = "sqlite:///" + os.path.join(os.getcwd(), "data", "calibration.db") + if hostname is not None: + # If we're given a hostname, use the above sandman mapping wrapper + sampler = pyabc.sampler.MappingSampler( + map_=get_sm_map(hostname), mapper_pickles=True + ) + else: + # Otherwise, run locally with the normal sampler + sampler = pyabc.sampler.MulticoreEvalParallelSampler() + # Adaptive distance based on Prangle (2017) (also acceptor below) + dist = pyabc.distance.AdaptivePNormDistance(p=2, adaptive=True) + prior = pyabc.Distribution(**get_prior()) + pop_size = pyabc.populationstrategy.AdaptivePopulationSize( + start_nr_particles=32, max_population_size=256, min_population_size=4 + ) + + abc = pyabc.ABCSMC( + model, + parameter_priors=prior, + distance_function=dist, + population_size=pop_size, + sampler=sampler, + acceptor=pyabc.accept_use_complete_history, + ) + + run_id = abc.new(db=db_path, observed_sum_stat=observed) + print(f"Run ID is {run_id}") + history = abc.run(max_nr_populations=10) + df, w = history.get_distribution() + results = {} + for param in df.columns.values: + # Calculate the posterior mean of each parameter + results[param] = np.dot(list(df[param]), list(w)) + + print("Done! The results are:") + print(results) + + +if __name__ == "__main__": + import sys + + host = None + if len(sys.argv) > 1: + # The server is passed as an argument. + host = sys.argv[1] + calibrate(observed=calibration_statistic.observed, hostname=host) diff --git a/calibration_statistic.py b/calibration_statistic.py index 77b587f..599dc21 100644 --- a/calibration_statistic.py +++ b/calibration_statistic.py @@ -1,11 +1,112 @@ import numpy as np import scipy.stats as stats +import scipy.signal as sig +# A list of the statistics that are outputted (for convenience). This is the canonical order. +# statistics = [ +# "ins_profitloss_mean", +# "ins_profitloss_var", +# "ins_profitloss_skew", +# "ins_profitloss_kurt", +# "ins_assets_mean", +# "ins_assets_var", +# "ins_assets_skew", +# "ins_assets_kurt", +# "ins_claims_mean", +# "ins_claims_var", +# "ins_claims_skew", +# "ins_claims_kurt", +# "ins_premiums_mean", +# "ins_premiums_var", +# "ins_premiums_skew", +# "ins_premiums_kurt", +# "ins_solvency_II_mean", +# "ins_solvency_II_var", +# "ins_solvency_II_skew", +# "ins_solvency_II_kurt", +# "re_profitloss_mean", +# "re_profitloss_var", +# "re_assets_mean", +# "re_assets_var", +# "re_solvency_II_mean", +# "re_solvency_II_var", +# "rate_on_line", +# ] -def calculate_single(log: dict, t: int = -1): +# A list of the statistics we actually have data for +statistics = [ + "ins_profitloss_mean", + "ins_profitloss_var", + "ins_profitloss_skew", + "ins_profitloss_kurt", + "ins_assets_mean", + "ins_assets_var", + "ins_assets_skew", + "ins_assets_kurt", + "ins_claims_mean", + "ins_claims_var", + "ins_claims_skew", + "ins_claims_kurt", + "ins_premiums_mean", + "ins_premiums_var", + "ins_premiums_skew", + "ins_premiums_kurt", + "re_profitloss_mean", + "re_profitloss_var", + "re_assets_mean", + "re_assets_var", + "rate_on_line_autocorr_lag_1", + "rate_on_line_autocorr_lag_2", + "rate_on_line_autocorr_lag_3", + "rate_on_line_autocorr_lag_4", + "rate_on_line_autocorr_lag_5", + "rate_on_line_autocorr_lag_6", + "rate_on_line_autocorr_lag_7", + "rate_on_line_autocorr_lag_8", + "rate_on_line_autocorr_lag_9", + "rate_on_line_autocorr_lag_10", +] + +# The observed data +observed = { + "ins_profitloss_mean": 760.8403583654745, + "ins_profitloss_var": 2645861.077652064, + "ins_profitloss_skew": 4.2449962851289325, + "ins_profitloss_kurt": 19.079106920165742, + "ins_assets_mean": 34422.302595397, + "ins_assets_var": 3760288749.1412654, + "ins_assets_skew": 2.493880852639668, + "ins_assets_kurt": 5.3551029266263885, + "ins_claims_mean": 4256.6503666102135, + "ins_claims_var": 116386072.62414846, + "ins_claims_skew": 4.3147875953543515, + "ins_claims_kurt": 17.92245449535894, + "ins_premiums_mean": 3938.0877987962162, + "ins_premiums_var": 24680436.944519438, + "ins_premiums_skew": 1.5042246765191107, + "ins_premiums_kurt": 0.9007423845542157, + "re_profitloss_mean": 604.9962383992475, + "re_profitloss_var": 362488.40656840097, + "re_assets_mean": 30621.369781367932, + "re_assets_var": 2159558210.9914727, + "rate_on_line_autocorr_lag_1": 29.812898800240387, + "rate_on_line_autocorr_lag_2": 28.600956884326997, + "rate_on_line_autocorr_lag_3": 27.489320890771207, + "rate_on_line_autocorr_lag_4": 25.83402370785473, + "rate_on_line_autocorr_lag_5": 24.36635259681405, + "rate_on_line_autocorr_lag_6": 23.029625203121448, + "rate_on_line_autocorr_lag_7": 21.708364947181977, + "rate_on_line_autocorr_lag_8": 20.715011404172028, + "rate_on_line_autocorr_lag_9": 19.940742580799636, + "rate_on_line_autocorr_lag_10": 19.133566632717763, +} + + +def calculate_single(log: dict, t: int = -1) -> dict: """Takes a dict as returned by Logger.obtainlog() and returns a vector of statistics We always look at year-long data""" + print("Calculating calibration statistic...") """First do firm-wise timeseries data""" ins_pls = np.array( [sum(firm_data[t - 12 : t]) for firm_data in log["insurance_pls"]] @@ -21,9 +122,11 @@ def calculate_single(log: dict, t: int = -1): ins_premiums = np.array( [firm_data[t] for firm_data in log["insurance_cumulative_premiums"]] ) + ins_ratios = np.array([firm_data[t] for firm_data in log["insurance_ratios"]]) + re_ratios = np.array([firm_data[t] for firm_data in log["reinsurance_ratios"]]) - insvars = [ins_pls, ins_assets, ins_claims, ins_premiums] - revars = [re_pls, re_assets] + insvars = [ins_pls, ins_assets, ins_claims, ins_premiums, ins_ratios] + revars = [re_pls, re_assets, re_ratios] for vars_ in (insvars, revars): if not all([len(x) == len(vars_[0]) for x in vars_]): @@ -38,19 +141,125 @@ def calculate_single(log: dict, t: int = -1): re_assets = re_assets[re_mask] ins_claims = ins_claims[ins_mask] ins_premiums = ins_premiums[ins_mask] + ins_ratios = ins_ratios[ins_mask] + re_ratios = re_ratios[re_mask] + + ins_data = { + "ins_profitloss": ins_pls, + "ins_assets": ins_assets, + "ins_claims": ins_claims, + "ins_premiums": ins_premiums, + "ins_solvency_II": ins_ratios, + } + re_data = { + "re_profitloss": re_pls, + "re_assets": re_assets, + "re_solvency_II": re_ratios, + } + statistic_names = ["mean", "var", "skew", "kurt"] - output = [] - for data in (ins_pls, ins_assets, ins_claims, ins_premiums): - st = stats.describe(data, nan_policy="raise") - for result in (st.mean, st.variance, st.skewness, st.kurtosis): - output.append(result) + output = {} + for name, data in ins_data.items(): + if len(data) == 0: + if len(ins_mask) == 0: + print(f"Empty ins data found, name = {name}") + for stat_name in statistic_names: + output[name + "_" + stat_name] = float("nan") + else: + for stat_name in statistic_names: + output[name + "_" + stat_name] = 0 + else: + st = stats.describe(data, nan_policy="raise", ddof=0) + for stat, stat_name in zip( + (st.mean, st.variance, st.skewness, st.kurtosis), statistic_names + ): + output[name + "_" + stat_name] = stat - for data in (re_pls, re_assets): - st = stats.describe(data, nan_policy="raise") - for result in (st.mean, st.variance): - output.append(result) + for name, data in re_data.items(): + if len(data) == 0: + if len(re_mask == 0): + print(f"Empty re data found, name = {name}") + for stat_name in statistic_names: + output[name + "_" + stat_name] = float("nan") + else: + for stat_name in statistic_names: + output[name + "_" + stat_name] = 0 + else: + st = stats.describe(data, nan_policy="raise", ddof=0) + for stat, stat_name in zip((st.mean, st.variance), statistic_names): + output[name + "_" + stat_name] = stat - """Next do market premium""" - premium = np.mean(log["market_premium"][t - 12 : t]) - output.append(premium) + """Next do market premium (Rate-On-Line)""" + no_years = len(log["market_premium"]) // 4 // 12 + slices = [slice(-(n + 1) * 12, -n * 12) for n in range(no_years - 1, -1, -1)] + slices[-1] = slice(-12, None, None) + premium_series = np.array([np.mean(log["market_premium"][sl]) for sl in slices]) + premium_series = premium_series / np.mean(premium_series) + ac = sig.correlate(premium_series, premium_series, mode="full") + ac = ac[len(ac) // 2 :] + for lag, c in enumerate(ac[:11]): + if lag >= 1: + output["rate_on_line_autocorr_lag_" + str(lag)] = c return output + + +def make_from_excel(): + import pandas as pd + + data = pd.read_excel("Data.xlsx", sheet_name=None) + transposed_data = {} + for key in data: + df = data[key].T + transposed_data[key.replace("(", " ").replace(")", " ")] = df.rename( + columns=df.iloc[0] + ).drop(df.index[0]) + ins = transposed_data["Insurance remove inflation "].loc[2014] + re = transposed_data["Reinsurance remove inflation "].loc[2014] + oth = transposed_data["Other Data"].loc[2014] + + ins_pls = np.array(ins.iloc[1:37], dtype=np.float_) + re_pls = np.array(re.iloc[1:13], dtype=np.float_) + ins_assets = np.array(ins.iloc[39:75], dtype=np.float_) + re_assets = np.array(re.iloc[15:27], dtype=np.float_) + ins_claims = np.array(ins.iloc[104:130], dtype=np.float_) + ins_premiums = np.array(ins.iloc[77:103], dtype=np.float_) + # ins_ratios = + # re_ratios = + + ins_data = { + "ins_profitloss": ins_pls, + "ins_assets": ins_assets, + "ins_claims": ins_claims, + "ins_premiums": ins_premiums, + } + re_data = {"re_profitloss": re_pls, "re_assets": re_assets} + + statistic_names = ["mean", "var", "skew", "kurt"] + + output = {} + for name, data in ins_data.items(): + st = stats.describe(data, nan_policy="raise", ddof=0) + for stat, stat_name in zip( + (st.mean, st.variance, st.skewness, st.kurtosis), statistic_names + ): + output[name + "_" + stat_name] = stat + + for name, data in re_data.items(): + st = stats.describe(data, nan_policy="raise", ddof=0) + for stat, stat_name in zip((st.mean, st.variance), statistic_names): + output[name + "_" + stat_name] = stat + + rols = list( + transposed_data["Other Data"]["Guy Carpenter U.S. Property Rate on Line Index"] + )[28::-1] + rols = rols / np.mean(rols) + ac = sig.correlate(rols, rols, mode="full") + ac = ac[len(ac) // 2 :] + for lag, c in enumerate(ac[:11]): + if lag >= 1: + output["rate_on_line_autocorr_lag_" + str(lag)] = c + print(repr(output)) + + +if __name__ == "__main__": + make_from_excel() diff --git a/ensemble.py b/ensemble.py index 2a6768c..b0c8b21 100644 --- a/ensemble.py +++ b/ensemble.py @@ -5,7 +5,7 @@ """ import sys import os -from typing import Dict +from typing import Dict, Callable, Iterable import time import importlib @@ -17,7 +17,7 @@ import setup_simulation -def rake(hostname=None, replications=9, summary: callable = None): +def rake(hostname=None, replications=4, summary: callable = None): """ Uses the sandman2 api to run multiple replications of multiple configurations of the simulation. If hostname=None, runs locally. Otherwise, make sure environment variable SANDMAN_KEY_ID and SANDMAN_KEY_SECRET diff --git a/genericclasses.py b/genericclasses.py index 2356413..436f2aa 100644 --- a/genericclasses.py +++ b/genericclasses.py @@ -367,7 +367,11 @@ def __init__(self, seq: Collection[T] = None): self._dict = {} if seq is not None: for item in seq: - self.add(item) + try: + self.add(item) + except ValueError: + # Silently remove duplicates + pass def __hash__(self): return None @@ -381,6 +385,12 @@ def __iter__(self) -> T: def __contains__(self, item: T) -> bool: return id(item) in self._dict + def __repr__(self) -> str: + return "IdSet(" + repr(list(self._dict.values())) + ")" + + def __str__(self) -> str: + return "{" + str(list(self._dict.values()))[1:-1] + "}" + def add(self, item: T) -> None: if item not in self: self._dict[id(item)] = item diff --git a/insurancefirms.py b/insurancefirms.py index dbb7411..b11e49b 100644 --- a/insurancefirms.py +++ b/insurancefirms.py @@ -553,6 +553,14 @@ def refresh_reinrisk( raise ValueError("After refreshing risk has become invalid") return risk + def get_solvency_ratio(self): + solvency = self.cash + var = self.cash - self.excess_capital + if var == 0: + return 1 + else: + return solvency / var + class ReinsuranceFirm(InsuranceFirm): """ReinsuranceFirm class. diff --git a/insurancesimulation.py b/insurancesimulation.py index 3ced847..cdb3f36 100644 --- a/insurancesimulation.py +++ b/insurancesimulation.py @@ -680,6 +680,10 @@ def save_data(self): reinsurance_premiums = [ firm.premiums_recieved for firm in self.reinsurancefirms ] + insurance_ratios = [firm.get_solvency_ratio() for firm in self.insurancefirms] + reinsurance_ratios = [ + firm.get_solvency_ratio() for firm in self.reinsurancefirms + ] """ prepare dict """ current_log = { @@ -715,6 +719,8 @@ def save_data(self): "reinsurance_pls": reinsurance_pls, "insurance_cumulative_premiums": insurance_premiums, "reinsurance_cumulative_premiums": reinsurance_premiums, + "insurance_ratios": insurance_ratios, + "reinsurance_ratios": reinsurance_ratios, } if isleconfig.save_network: diff --git a/isle_calibration_parameters.txt b/isle_calibration_parameters.txt new file mode 100644 index 0000000..547a5fa --- /dev/null +++ b/isle_calibration_parameters.txt @@ -0,0 +1,6 @@ +# This file will hold the parameters to actually calibrate on, as well as info on their priors +# Priors can be uniform linearly or uniform in a logarithmic sense +# Format is: +# , , , (linear|logarithmic) +riskmodel_inaccuracy_parameter, 1.5, 4, linear + diff --git a/logger.py b/logger.py index 47a74e7..c5a04dd 100644 --- a/logger.py +++ b/logger.py @@ -28,6 +28,8 @@ "reinsurance_pls", "insurance_cumulative_premiums", "reinsurance_cumulative_premiums", + "insurance_ratios", + "reinsurance_ratios", ] @@ -112,6 +114,11 @@ def record_data(self, data_dict): for key in data_dict.keys(): if key in firm_level_logs: # These are stored per-firm + if not len(data_dict[key]) == len(self.history_logs[key]): + raise RuntimeError( + f"Log {key} passed to logger has different number of firms to those already in" + f" log - {len(data_dict[key])} passed, {len(self.history_logs[key])} expected" + ) for i in range(len(data_dict[key])): self.history_logs[key][i].append(data_dict[key][i]) else: diff --git a/riskmodel.py b/riskmodel.py index d80fcb4..fd6d9cf 100644 --- a/riskmodel.py +++ b/riskmodel.py @@ -174,6 +174,8 @@ def evaluate_proportional( expected_profits += incr_expected_profits + if average_exposure == 0: + average_exposure = self.init_average_exposure # compute value at risk var_per_risk = ( self.get_ppf(categ_id=categ_id, tail_size=self.var_tail_prob) diff --git a/sensitivity.py b/sensitivity.py index 145c4a8..6bd4100 100644 --- a/sensitivity.py +++ b/sensitivity.py @@ -4,7 +4,6 @@ import sys import os from typing import Dict -import time import importlib import numpy as np @@ -25,6 +24,10 @@ def rake(hostname=None, summary: callable = None, use_sandman: bool = False): use_sandman: if True, uses sandman, otherwise uses multiprocessing (faster if running very many simulations locally) """ + + # TODO: RM + np.seterr(all="raise") + if importlib.util.find_spec("hickle") is None: raise ModuleNotFoundError("hickle not found but required for saving logs") @@ -62,7 +65,7 @@ def rake(hostname=None, summary: callable = None, use_sandman: bool = False): ################################################################################################################### - max_time = isleconfig.simulation_parameters["max_time"] + max_time = parameter_list[0]["max_time"] print(f"Running {len(parameter_list)} simulations of {max_time} timesteps") @@ -140,17 +143,19 @@ def rake(hostname=None, summary: callable = None, use_sandman: bool = False): ] = setup.obtain_ensemble(len(parameter_list)) n = len(parameter_list) - m_params = zip( - parameter_list, - general_rc_event_schedule, - general_rc_event_damage, - np_seeds, - random_seeds, - [0] * n, - [0] * n, - [None] * n, - [False] * n, - [summary] * n, + m_params = list( + zip( + parameter_list, + general_rc_event_schedule, + general_rc_event_damage, + np_seeds, + random_seeds, + [0] * n, + [0] * n, + [None] * n, + [False] * n, + [summary] * n, + ) ) if use_sandman: @@ -166,18 +171,37 @@ def rake(hostname=None, summary: callable = None, use_sandman: bool = False): # This is actually quite slow for large sets of jobs. Can't use mp.Pool due to unpickleability # Could use pathos or similar if we actually end up caring job = list(map(m, m_params)) + # # Split up into chunks so sandman server doesn't blow up + # max_size = 71 + # job_lists = [] + # while len(job) > 0: + # job_lists.append(job[: min(max_size, len(job))]) + # job = job[min(max_size, len(job)) :] """Here the jobs are submitted""" print("Jobs created, submitting") with sm.Session(host=hostname, default_cb_to_stdout=True) as sess: print("Starting job") - # Don't use async here, since there is only one job - result = sess.submit(job) + # result = [] + # for job in job_lists: + # result += sess.submit(job) + + # Submit async so we can reattach with sess.get if something goes wrong locally + task = sess.submit_async(job) + + task.wait() + result = task.results else: + # result = [] + # m_params.reverse() + # for i, param_set in enumerate(m_params): + # result.append(start.main(param_set)) import multiprocessing as mp print("Running multiprocessing pool") - with mp.Pool(maxtasksperchild=4) as pool: - result = pool.map(start.main, m_params, chunksize=4) + # set maxtasksperchild, otherwise it seems that garbage collection(?) misbehaves and we get huge memory usage + with mp.Pool(maxtasksperchild=1) as pool: + # Since the jobs are so big, chunksize=1 is best + result = pool.map(start.main, m_params, chunksize=1) print("Job done, saving") result_dict = {t: r for t, r in zip(parameters, result)} @@ -191,7 +215,9 @@ def rake(hostname=None, summary: callable = None, use_sandman: bool = False): # The server is passed as an argument. host = sys.argv[1] use_sandman = True - rake(host, summary=calibration_statistic.calculate_single, use_sandman=use_sandman) + rake( + host, summary="calibration_statistic.calculate_single", use_sandman=use_sandman + ) # jobs = {"ensemble1" : "23a3f4e1", # "ensemble2" : "485f7221"} # restore_jobs(jobs, host) diff --git a/start.py b/start.py index ddf6646..25b8cdb 100644 --- a/start.py +++ b/start.py @@ -4,6 +4,8 @@ import hashlib import numpy as np import os +import sys +import traceback import pickle import zlib import random @@ -39,7 +41,7 @@ def cumulative_bankruptcies(log): # main function def main( - sim_params: MutableMapping, + sim_params: Union[MutableMapping, Tuple], rc_event_schedule: MutableSequence[MutableSequence[int]] = None, rc_event_damage: MutableSequence[MutableSequence[float]] = None, np_seed: int = None, @@ -49,70 +51,79 @@ def main( requested_logs: MutableSequence = None, resume: bool = False, summary: Union[callable, str] = None, -) -> Tuple[bytes, dict]: - if isinstance(sim_params, tuple): - ( - sim_params, - rc_event_schedule, - rc_event_damage, - np_seed, - random_seed, - save_iteration, - replic_id, - requested_logs, - resume, - summary, - ) = sim_params - if isinstance(summary, str): - summary = eval(summary) - if not resume: - np.random.seed(np_seed) - random.seed(random_seed) - - sim_params["simulation"] = simulation = insurancesimulation.InsuranceSimulation( - override_no_riskmodels, - replic_id, - sim_params, - rc_event_schedule, - rc_event_damage, - ) - time = 0 - else: - d = load_simulation() - np.random.set_state(d["np_seed"]) - random.setstate(d["random_seed"]) - time = d["time"] - simulation = d["simulation"] - sim_params = d["simulation_parameters"] - for key in d["isleconfig"]: - isleconfig.__dict__[key] = d["isleconfig"][key] - isleconfig.simulation_parameters = sim_params - for t in range(time, sim_params["max_time"]): - # Main time iteration loop - simulation.iterate(t) - - # log data - simulation.save_data() - - # Don't save at t=0 or if the simulation has just finished - if ( - save_iteration > 0 - and t % save_iteration == 0 - and 0 < t < sim_params["max_time"] - ): - # Need to use t+1 as resume will start at time saved - save_simulation(t + 1, simulation, sim_params, exit_now=False) - - log = simulation.obtain_log(requested_logs) - if summary is not None: - return summary(log) - else: - # We compute metadata about the return data that isn't compressed, so a skeleton data structure can be - # constructed before decompression - found_shapes = {name: np.shape(log[name]) for name in log} - - # We compress the return value for the sake of minimising data transfer over the network and RAM usage - return (zlib.compress(pickle.dumps(log)), found_shapes) +) -> Union[Tuple[bytes, dict], dict]: + try: + if isinstance(sim_params, tuple): + ( + sim_params, + rc_event_schedule, + rc_event_damage, + np_seed, + random_seed, + save_iteration, + replic_id, + requested_logs, + resume, + summary, + ) = sim_params + if isinstance(summary, str): + summary = eval(summary) + if not resume: + np.random.seed(np_seed) + random.seed(random_seed) + + sim_params[ + "simulation" + ] = simulation = insurancesimulation.InsuranceSimulation( + override_no_riskmodels, + replic_id, + sim_params, + rc_event_schedule, + rc_event_damage, + ) + time = 0 + else: + d = load_simulation() + np.random.set_state(d["np_seed"]) + random.setstate(d["random_seed"]) + time = d["time"] + simulation = d["simulation"] + sim_params = d["simulation_parameters"] + for key in d["isleconfig"]: + isleconfig.__dict__[key] = d["isleconfig"][key] + isleconfig.simulation_parameters = sim_params + for t in range(time, sim_params["max_time"]): + # Main time iteration loop + try: + simulation.iterate(t) + except RuntimeError: + print("Simulation encountered error") + return None + + # log data + simulation.save_data() + + # Don't save at t=0 or if the simulation has just finished + if ( + save_iteration > 0 + and t % save_iteration == 0 + and 0 < t < sim_params["max_time"] + ): + # Need to use t+1 as resume will start at time saved + save_simulation(t + 1, simulation, sim_params, exit_now=False) + + log = simulation.obtain_log(requested_logs) + if summary is not None: + return summary(log) + else: + # We compute metadata about the return data that isn't compressed, so a skeleton data structure can be + # constructed before decompression + found_shapes = {name: np.shape(log[name]) for name in log} + + # We compress the return value for the sake of minimising data transfer over the network and RAM usage + return (zlib.compress(pickle.dumps(log)), found_shapes) + except Exception as ex: + raise Exception("".join(traceback.format_exception(*sys.exc_info()))[-900:]) def save_simulation( From b3124674208aa77387a03afd840b5bfbd6b4021b Mon Sep 17 00:00:00 2001 From: Chris Hughes Date: Fri, 27 Sep 2019 11:38:17 +0100 Subject: [PATCH 124/125] Code for running sensitivity analysis --- sensitivity.py | 60 ++++++++++++++++++++++++++++++++++++++++---------- 1 file changed, 48 insertions(+), 12 deletions(-) diff --git a/sensitivity.py b/sensitivity.py index 6bd4100..dda89e8 100644 --- a/sensitivity.py +++ b/sensitivity.py @@ -208,16 +208,52 @@ def rake(hostname=None, summary: callable = None, use_sandman: bool = False): start.save_summary([result_dict]) +def analyse(data: dict): + keylist = list(data.keys()) + # SALib expects a matrix, so we give it a matrix + x = np.array(keylist) + outputs = [] + for key_name in keylist: + result_dict = data[key_name] + result = [ + result_dict[name] + for name in calibration_statistic.statistics + if name in result_dict + ] + outputs.append(result) + found_statistics = [ + name for name in calibration_statistic.statistics if name in result_dict + ] + y_full = np.array(outputs) + + import SALib.util + import SALib.analyze.morris + + problem = SALib.util.read_param_file("isle_all_parameters.txt") + outputs = {} + for i, stat in enumerate(found_statistics): + print(stat + ":") + outputs[stat] = SALib.analyze.morris.analyze( + problem, x, y_full[:, i], print_to_console=True + ) + return outputs + + if __name__ == "__main__": - host = None - use_sandman = False - if len(sys.argv) > 1: - # The server is passed as an argument. - host = sys.argv[1] - use_sandman = True - rake( - host, summary="calibration_statistic.calculate_single", use_sandman=use_sandman - ) - # jobs = {"ensemble1" : "23a3f4e1", - # "ensemble2" : "485f7221"} - # restore_jobs(jobs, host) + import hickle + + data = hickle.load("data/summary_statistics.hdf") + result = analyse(data[0]) + hickle.dump(result, "data/sensitivity_analysis_results.hdf") + # host = None + # remote = False + # if len(sys.argv) > 1: + # # The server is passed as an argument. + # host = sys.argv[1] + # remote = True + # rake( + # host, summary="calibration_statistic.calculate_single", use_sandman=remote + # ) + # # jobs = {"ensemble1" : "23a3f4e1", + # # "ensemble2" : "485f7221"} + # # restore_jobs(jobs, host) From a60a8778380006dbe0901f8521630da3e1e1e4a7 Mon Sep 17 00:00:00 2001 From: chris Date: Sun, 29 Sep 2019 17:06:50 +0100 Subject: [PATCH 125/125] sensitivity --- sensitivity.py | 36 +++++++++++++++++++++++------------- 1 file changed, 23 insertions(+), 13 deletions(-) diff --git a/sensitivity.py b/sensitivity.py index dda89e8..3f544ca 100644 --- a/sensitivity.py +++ b/sensitivity.py @@ -210,20 +210,28 @@ def rake(hostname=None, summary: callable = None, use_sandman: bool = False): def analyse(data: dict): keylist = list(data.keys()) - # SALib expects a matrix, so we give it a matrix x = np.array(keylist) outputs = [] + found_statistics = None for key_name in keylist: result_dict = data[key_name] - result = [ - result_dict[name] - for name in calibration_statistic.statistics - if name in result_dict - ] + # If the simulation fails to run due to an exception then it returns None instead of a dict + if result_dict is not None: + result = [ + result_dict[name] + for name in calibration_statistic.statistics + if name in result_dict + ] + + found_statistics = [ + name for name in calibration_statistic.statistics if name in result_dict + ] + else: + if found_statistics is None: + raise ValueError("First data element is None, please fix manually") + # Want to make nan or something, but that breaks analysis + result = [0 for _ in found_statistics] outputs.append(result) - found_statistics = [ - name for name in calibration_statistic.statistics if name in result_dict - ] y_full = np.array(outputs) import SALib.util @@ -234,7 +242,7 @@ def analyse(data: dict): for i, stat in enumerate(found_statistics): print(stat + ":") outputs[stat] = SALib.analyze.morris.analyze( - problem, x, y_full[:, i], print_to_console=True + problem, x, y_full[:, i], print_to_console=False ) return outputs @@ -242,9 +250,11 @@ def analyse(data: dict): if __name__ == "__main__": import hickle - data = hickle.load("data/summary_statistics.hdf") - result = analyse(data[0]) - hickle.dump(result, "data/sensitivity_analysis_results.hdf") + # + # data = hickle.load("data/summary_statistics.hdf") + # results = analyse(data[0]) + # hickle.dump(results, "data/sensitivity_analysis_results.hdf") + results = hickle.load("data/sensitivity_analysis_results.hdf") # host = None # remote = False # if len(sys.argv) > 1: