diff --git a/README.md b/README.md index e402c5a..e16be95 100644 --- a/README.md +++ b/README.md @@ -12,9 +12,12 @@ $ 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 the simulation with this command +Execute a single simulation run with the command: ``` $ python3 start.py @@ -25,28 +28,21 @@ $ 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 [-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 -``` - -## Graphical simulation runs - -abce can be used to run simulations with a graphical interface: - -``` -python3 start.py --abce +$ python3 start.py --help ``` ## 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 @@ -55,7 +51,32 @@ bash starter_four.sh bash starter_three.sh ``` -## Plotting +## Visualisation + +#### 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. 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 +many time iterations you want in the animation. + +#### Ensemble runs +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. + +# Contributing -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. +## 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`. Additionally, it is good to run flake8 over your code. diff --git a/calibration_conditions.py b/calibration_conditions.py index 33bc6f7..7d8c51b 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' @@ -31,100 +31,162 @@ 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))): + pass + 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 + 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 -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..415610a 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,39 @@ 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].""" - + 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)) + print(f"{cond_name:47s}: {score:8f}") """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( + f"\n Total calibration score: {self.calibration_score:8f}" + ) """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..3ee27e3 100644 --- a/catbond.py +++ b/catbond.py @@ -1,251 +1,127 @@ - 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 +from genericclasses import Obligation, GenericAgent -class CatBond(MetaInsuranceOrg): - 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 = [] - 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 # 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 +from typing import MutableSequence +from typing import TYPE_CHECKING - def iterate(self, time): - """obtain investments yield""" - self.obtain_yield(time) +if TYPE_CHECKING: + from insurancesimulation import InsuranceSimulation + from metainsurancecontract import MetaInsuranceContract - """realize due payments""" - self.effect_payments(time) - if isleconfig.verbose: - 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] - 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 - - 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) +# TODO: This and MetaInsuranceOrg should probably both subclass something simple - a MetaAgent, say. MetaInsuranceOrg +# can do more than a CatBond should be able to! - #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) +# noinspection PyAbstractClass +class CatBond(MetaInsuranceOrg): + # noinspection PyMissingConstructor + # TODO inheret GenericAgent instead of MetaInsuranceOrg? + def __init__( + self, + simulation: "InsuranceSimulation", + per_period_premium: float, + owner: GenericAgent, + interest_rate: float = 0, + ): + """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: int = 0 + self.underwritten_contracts: MutableSequence["MetaInsuranceContract"] = [] + self.cash: float = 0 + self.profits_losses: float = 0 + self.obligations: MutableSequence[Obligation] = [] + 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.simulation_no_risk_categories = self.simulation.simulation_parameters["no_categories"] - """mature contracts""" + # 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: + 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, + ) + + """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) - 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 + for contract in self.underwritten_contracts: + contract.check_payment_due(time) - not_accepted_risks += categ_risks[i:] - not_accepted_risks = [risk for risk in not_accepted_risks if risk.get("contract") is None] + 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 - # 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 + # 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() - - def set_owner(self, owner): + def set_owner(self, owner: GenericAgent): + """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): - 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 + def set_contract(self, contract: "MetaInsuranceContract"): + """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): + """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 = Obligation( + amount=self.cash, + recipient=self.simulation, + due_time=1, + purpose="mature", + ) + self._pay(obligation) + self.simulation.delete_agents([self]) + self.operational = False + else: + print("CatBond is not operational so cannot mature") diff --git a/centralbank.py b/centralbank.py new file mode 100644 index 0000000..c6d7fbb --- /dev/null +++ b/centralbank.py @@ -0,0 +1,176 @@ +from isleconfig import simulation_parameters +import numpy as np + + +class CentralBank: + 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.inflation_target = 0.02 + self.actual_inflation = 0 + self.onemonth_CPI = 0 + self.twelvemonth_CPI = 0 + self.feedback_counter = 0 + self.prices_list = [] + self.economy_money = money_supply + self.warnings = {} + 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: + 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 + 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 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 + + 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. + 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 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 + + # 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])): + 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 += 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 = 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 + elif avg_var_coverage >= 0.85: + self.warnings[firm_id] += 1 + 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" + elif self.warnings[firm_id] == 1: + return "Warning" + elif self.warnings[firm_id] >= 2: + 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 = 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: + for insurer in insurance_firms: + 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 + # Give each firm an equal fraction of claims + 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: + 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/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..8caae30 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,37 +106,42 @@ 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 + 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 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 deleted file mode 100644 index b4bfa97..0000000 --- a/distribution_wrapper_test.py +++ /dev/null @@ -1,19 +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., 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) -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 fd85eb3..15640e0 100644 --- a/distributionreinsurance.py +++ b/distributionreinsurance.py @@ -1,73 +1,132 @@ -import scipy.stats +import functools import numpy as np -from math import ceil -import scipy -import pdb +import scipy.stats +import warnings + + +class ReinsuranceDistWrapper: + """ Wrapper for modifying the risk to an insurance company when they have EoL reinsurance -class ReinsuranceDistWrapper(): - def __init__(self, dist, lower_bound=None, upper_bound=None): - assert lower_bound is not None or upper_bound is not None + 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.""" + + 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 - - 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 - + # 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, 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.lower_bound - + + @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 \ - 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 - + + @functools.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 = 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=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(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): + # 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 = 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] - + 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) + mean_estimate, mean_error = scipy.integrate.quad( + lambda x: x * self.pdf(x), 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) + 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/ensemble.py b/ensemble.py index 92c3d49..5ada9e6 100644 --- a/ensemble.py +++ b/ensemble.py @@ -1,93 +1,120 @@ -#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 -import math + import copy -import scipy.stats -import start -import logger -import listify -import isleconfig -from distributiontruncated import TruncatedDistWrapper -from setup import SetupSim +import os + +# noinspection PyUnresolvedReferences from sandman2.api import operation, Session +import isleconfig +import listify +import logger +import start +from setup_simulation import SetupSim @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. + # 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 - 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", + "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']: + 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/" 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: + except FileNotFoundError: os.mkdir(directory) """Clear old dict saving files (*_history_logs.dat)""" @@ -95,78 +122,119 @@ 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""" + # 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) + # never save simulation state in ensemble runs (resuming is impossible anyway) + save_iter = isleconfig.simulation_parameters["max_time"] + 2 - 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. + 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, + 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) + ] + 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""" + + """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(): 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] + ) + + # 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") - """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""" + 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) diff --git a/genericagent.py b/genericagent.py deleted file mode 100644 index 8e58efe..0000000 --- a/genericagent.py +++ /dev/null @@ -1,7 +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/genericagentabce.py b/genericagentabce.py deleted file mode 100644 index c0eb884..0000000 --- a/genericagentabce.py +++ /dev/null @@ -1,4 +0,0 @@ -import abce - -class GenericAgent(abce.Agent): - pass diff --git a/genericclasses.py b/genericclasses.py new file mode 100644 index 0000000..4f918c1 --- /dev/null +++ b/genericclasses.py @@ -0,0 +1,267 @@ +from itertools import chain + +import dataclasses +from sortedcontainers import SortedList +import numpy as np +from scipy import stats + +import isleconfig + +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.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. + 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 + 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 + 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. + No accepted values + Returns Boolean""" + return self.operational + + 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.""" + 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 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 + 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.""" + obligation = 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" = None + insurancetype: str = None + deductible: float = None + runtime: int = None + expiration: int = None + limit_fraction: float = None + deductible_fraction: float = None + reinsurance_share: float = None + periodized_total_premium: float = None + limit: 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 + + +@dataclasses.dataclass +class Obligation: + """Class for holding the properties of an obligation""" + + amount: float + 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.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") + + +class ReinsuranceProfile: + """Class for keeping track of the reinsurance that an insurance firm holds + + 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, 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.limit + 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 \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])}") + + 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 + upper_bound = contract.limit + 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 d331a8d..fa11f09 100644 --- a/insurancecontract.py +++ b/insurancecontract.py @@ -1,25 +1,32 @@ -import numpy as np +import metainsurancecontract -from metainsurancecontract import MetaInsuranceContract +from typing import TYPE_CHECKING +if TYPE_CHECKING: + from metainsuranceorg import MetaInsuranceOrg + from insurancesimulation import InsuranceSimulation + from genericclasses import RiskProperties -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. 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) - - self.risk_data = properties + 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" - 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. @@ -29,17 +36,15 @@ def explode(self, time, uniform_value, damage_extent): 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 np.random.uniform(0, 1) < self.risk_factor: + 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: - # if True: - claim = min(self.excess, damage_extent * self.value) - self.deductible - self.insurer.register_claim(claim) #Every insurance claim made is immediately registered. - + 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') - # 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 + 1, "claim") if self.expire_immediately: self.expiration = time # self.terminating = True @@ -51,10 +56,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]) - + self.property_holder.return_risks([self.risk]) diff --git a/insurancefirm.py b/insurancefirm.py deleted file mode 100644 index 61d5746..0000000 --- a/insurancefirm.py +++ /dev/null @@ -1,283 +0,0 @@ -from metainsuranceorg import MetaInsuranceOrg -from catbond import CatBond -import numpy as np -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 - 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) - self.is_insurer = True - self.is_reinsurer = False - - 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 = 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): - 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) - 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: - self.capacity_target *= self.capacity_target_increment_factor - elif capacity_target_var_ratio_estimate < self.capacity_target_decrement_threshold: - self.capacity_target *= self.capacity_target_decrement_factor - 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 - # 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) - 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 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.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) - # 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, 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)) - 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("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)) - self.ask_reinsurance_non_proportional_by_category(time, categ_id) - return True - - def get_average_premium(self, categ_id): - 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 - if total_weight == 0: - 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': - self.ask_reinsurance_proportional() - elif self.simulation_reinsurance_type == 'non-proportional': - self.ask_reinsurance_non_proportional(time) - else: - assert False, "Undefined reinsurance type" - - 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 - 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): - """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): - self.ask_reinsurance_non_proportional_by_category(time, categ_id) - - def characterize_underwritten_risks_by_category(self, time, 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 - - - 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 - - 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.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 = {"value": contract.value, "category": contract.category, "owner": self, - #"identifier": uuid.uuid1(), - "reinsurance_share": 1., - "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: - break - - 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 - # 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 """ - 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 - _, _, 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""" - 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') - 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.simulation.accept_agents("catbond", [catbond], time=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 - 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() - if is_proportional: - claims_this_turn[categ_id] += claims - 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]) - - 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) - 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 diff --git a/insurancefirms.py b/insurancefirms.py new file mode 100644 index 0000000..5ccf66d --- /dev/null +++ b/insurancefirms.py @@ -0,0 +1,497 @@ +import numpy as np + +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.MetaInsuranceOrg): + """ReinsuranceFirm class. + Inherits from MetaInsuranceFirm.""" + + 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 = True + self.is_reinsurer = False + + def adjust_dividends(self, time: int, actual_capacity: float): + """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.""" + + 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 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: float) -> float: + """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.""" + 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_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_limit_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: + 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) + 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 = ( + (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 + ): + self.capacity_target *= self.capacity_target_decrement_factor + + def get_capacity(self, max_var: float) -> float: + """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: + 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: int, max_var: float) -> float: + """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 + 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) + 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, + ): + categ_ids = [] + else: + 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: + 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: + """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}" + ) + 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) + 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. + 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 + if total_weight == 0: + 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]: + """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. + 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 + 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.""" + # 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] + 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: + 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 = 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]) / 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 and not self.reinsurance_profile.all_contracts(): + tranches = self.reinsurance_profile.split_longest(tranches) + 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": + risks_to_return.append(risk) + if purpose == "rollover": + return risks_to_return + 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. + 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): + """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)]) + # 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) + + """add contract; contract is a quasi-reinsurance contract""" + 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,) + # 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") + 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",) + 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: int): + """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) + for contract in self.underwritten_contracts: + categ_id, claims, is_proportional = contract.get_and_reset_current_claim() + if is_proportional: + claims_this_turn[categ_id] += claims + if contract.reincontract: + contract.reincontract.explode(time, damage_extent=claims) + + 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) + 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 + contract and category. Only used for network visualisation. + No accepted values. + Returns: + reinsurance: Type list of DataDicts.""" + reinsurance = [] + for contract in self.reinsurance_profile.all_contracts(): + 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]: + # 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] + 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, + limit_fraction=old_contract.limit / total_value, + periodized_total_premium=periodized_total_premium, + runtime=12, + expiration=time + 12, + risk_factor=avg_risk_factor,) + return risk + + +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 f7383d8..842bbd9 100644 --- a/insurancesimulation.py +++ b/insurancesimulation.py @@ -1,481 +1,672 @@ -from insurancefirm import InsuranceFirm -#from riskmodel import RiskModel -from reinsurancefirm import ReinsuranceFirm -from distributiontruncated import TruncatedDistWrapper -import numpy as np -import scipy.stats import math -import sys, pdb -import isleconfig import random import copy import logger +import warnings -if isleconfig.show_network: - import visualization_network +import scipy.stats +import numpy as np + +from distributiontruncated import TruncatedDistWrapper +import insurancefirms +from centralbank import CentralBank +import isleconfig +from genericclasses import GenericAgent, RiskProperties, AgentProperties, Constant +import catbond + +from typing import Mapping, MutableMapping, MutableSequence, Sequence, Any, Optional +from typing import TYPE_CHECKING -if isleconfig.use_abce: - import abce - #print("abce imported") -#else: -# print("abce not imported") +if TYPE_CHECKING: + from genericclasses import Distribution + from metainsuranceorg import MetaInsuranceOrg +if isleconfig.show_network or isleconfig.save_network: + import visualization_network -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) +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, + 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: 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 + 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""" + 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 - self.number_riskmodels = simulation_parameters["no_riskmodels"] - - # save parameters - if (replic_ID is None) or (isleconfig.force_foreground): - self.background_run = False + # 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" + 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 - 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.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) - 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) + self.replic_id = replic_id + 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? + 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: float = simulation_parameters[ + "risk_factor_lower_bound" + ] + self.risk_factor_spread: float = ( + simulation_parameters["risk_factor_upper_bound"] + - self.risk_factor_lower_bound + ) + 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 = Constant(loc=1.0) + # self.risk_value_distribution = scipy.stats.uniform(loc=100, scale=9900) + self.risk_value_distribution = Constant(loc=1000) 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() - # 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"] * \ - self.simulation_parameters["mean_contract_runtime"]).pmf(0) + expected_damage_frequency = 1 - scipy.stats.poisson( + self.simulation_parameters["mean_contract_runtime"] + / self.simulation_parameters["event_time_mean_separation"] + ).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"]) - - 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"] - - # 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 - 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. + expected_damage_frequency = ( + self.simulation_parameters["mean_contract_runtime"] + / self.cat_separation_distribution.mean() + ) + self.norm_premium: float = ( + expected_damage_frequency + * self.damage_distribution.mean() + * risk_factor_mean + * (1 + self.simulation_parameters["norm_profit_markup"]) + ) + + 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: int = simulation_parameters["no_risks"] + + "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) + + "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"]) + ) + 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_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. 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. - self.setup_risk_categories_caller() - + 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() - 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] + # 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"] + ) + 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: MutableSequence[RiskProperties] = [ + 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 for _ in range(self.simulation_parameters["no_categories"]) + ] + + 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"]) - 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 = random.sample(self.inaccuracy, self.simulation_parameters["no_riskmodels"]) - self.inaccuracy = self.get_all_riskmodel_combinations(self.simulation_parameters["no_categories"], self.simulation_parameters["riskmodel_inaccuracy_parameter"]) + 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)" + # 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: 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 + ) + self.initialize_agent_parameters( + "reinsurancefirm", simulation_parameters, risk_model_configurations + ) + + "Agent lists" + self.reinsurancefirms: MutableSequence = [] + self.insurancefirms: MutableSequence = [] + self.catbonds: MutableSequence = [] + + "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] = [] + + "Cumulative variables for history and logging" + self.cumulative_bankruptcies: int = 0 + self.cumulative_market_exits: int = 0 + self.cumulative_bought_firms: int = 0 + self.cumulative_nonregulation_firms: int = 0 + self.cumulative_unrecovered_claims: float = 0.0 + self.cumulative_claims: float = 0.0 + + "Lists for firms that are to be sold." + self.selling_insurance_firms = [] + self.selling_reinsurance_firms = [] + + "Lists for logging history" + 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.ndarray = np.zeros( + self.simulation_parameters["no_categories"] + ) + self.reinsurance_models_counter: np.ndarray = np.zeros( + self.simulation_parameters["no_categories"] + ) + "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: 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 + Returns None""" + if firmtype == "insurancefirm": + 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": + 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" + ] + else: + raise ValueError(f"Firm type {firmtype} not recognised") - self.inaccuracy = random.sample(self.inaccuracy, self.simulation_parameters["no_riskmodels"]) + 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() + else: + raise ValueError(f"Firm type {firmtype} not recognised") - 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.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"]: + 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"] + 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( + 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" + ], + 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 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 + 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: - 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 - 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": - try: + 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 = agent_group - 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 - new_agent_cash = sum([agent.cash for agent in agents]) - self.reduce_money_supply(new_agent_cash) - elif agent_class_string == "reinsurance": - try: + for _ 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 = agent_group - 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) - elif agent_class_string == "catbond": - try: - self.catbonds += agents - except: - print(sys.exc_info()) - pdb.set_trace() - else: - assert False, "Error: Unexpected agent class used {0:s}".format(agent_class_string) + for _ in agents: + self.logger.add_reinsurance_agent() - def delete_agents(self, agent_class_string, agents): - if agent_class_string == "catbond": - for agent in agents: + 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) + + 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""" + for agent in agents: + if isinstance(agent, catbond.CatBond): self.catbonds.remove(agent) - else: - assert False, "Trying to remove unremovable agent, type: {0:s}".format(agent_class_string) - - def iterate(self, t): - + else: + raise ValueError( + f"Trying to remove unremovable agent, type: {type(agent)}" + ) + + 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. + Accepts: + t: Integer, current time step + Returns None""" + + self._time = t if isleconfig.verbose: print() print(t, ": ", len(self.risks)) if isleconfig.showprogress: - print("\rTime: {0:4d}".format(t), end="") + print(f"\rTime: {t}", end="") - self.reset_pls() + if self.firm_enters_market(agent_type="InsuranceFirm"): + self.add_agents(insurancefirms.InsuranceFirm, "insurancefirm", n=1) + + if self.firm_enters_market(agent_type="ReinsuranceFirm"): + self.add_agents(insurancefirms.ReinsuranceFirm, "reinsurancefirm", n=1) + self.reset_pls() # 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 - self.effect_payments(t) - + 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]) + 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: - 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: + 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. - 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) 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 + + # Provide government aid if damage severe enough + 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] + 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() + + # 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: - # self.reinsurancefirms_group.iterate(time=t) - #else: - # for reinagent in self.reinsurancefirms: - # reinagent.iterate(t) - - # remove all non-accepted reinsurance risks + 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: + reinagent.consider_buyout(type="reinsurer") + # remove all non-accepted reinsurance risks self.reinrisks = [] - # reset weights - self.reset_insurance_weights() - - # iterate insurance firm agents + # 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: - # self.insurancefirms_group.iterate(time=t) - #else: - # for agent in self.insurancefirms: - # agent.iterate(t) - - # iterate catbonds + 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: + agent.consider_buyout(type="insurer") + + # Reset list of bankrupt insurance firms + self.reset_selling_firms() + + # Iterate catbonds for agent in self.catbonds: agent.iterate(t) self.insurance_models_counter = np.zeros(self.simulation_parameters["no_categories"]) - for insurer in self.insurancefirms: - for i in range(len(self.inaccuracy)): - if insurer.operational: - if insurer.riskmodel.inaccuracy == self.inaccuracy[i]: - self.insurance_models_counter[i] += 1 + self._update_model_counters() - self.reinsurance_models_counter = np.zeros(self.simulation_parameters["no_categories"]) + """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) + + 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): + self.RN.save_network_data() + print("Network data has been saved to data/network_data.dat")""" - 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) - # 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 + """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]) - catbondsoperational_no = sum([catbond.operational for catbond in self.catbonds]) - + 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_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]) + 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 = [(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 = [(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 = {} - 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 = {} # 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] + + 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) - - 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: Mapping = None) -> MutableSequence: + """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. - It may be used to handle e.g. loging by including: - self.log() - but logging has been moved to start.py and ensemble.py - """ - 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] - if isleconfig.verbose: - print("**** PERIL ", damage) - damagevalues = np.random.beta(1, 1./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)] - - def receive_obligation(self, amount, recipient, due_time, 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] - sum_due = sum([item["amount"] for item in due]) - for obligation in due: - self.pay(obligation) - - 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 self.get_operational() and recipient.get_operational(): - self.money_supply -= amount - recipient.receive(amount) - - def receive(self, amount): - """Method to accept cash payments.""" - 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).""" - self.money_supply -= amount - assert self.money_supply >= 0 - - def reset_reinsurance_weights(self): + 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: + 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) + damagevalues = np.random.beta( + a=1, b=1.0 / damage - 1, size=len(affected_contracts) + ) + uniformvalues = np.random.uniform(0, 1, size=len(affected_contracts)) + for i, contract in enumerate(affected_contracts): + contract.explode(t, uniformvalues[i], damagevalues[i]) + + 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 + to compensate for agent endowment). + Accepts: + amount: Type Integer""" + self.cash -= amount + self.bank.update_money_supply(amount, reduce=True) + assert self.cash >= 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] + operational_reinfirms = [ + firm for firm in self.reinsurancefirms if firm.operational + ] operational_no = len(operational_reinfirms) @@ -488,8 +679,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: + weights = reinrisks_no / operational_no for reinsurer in self.reinsurancefirms: self.reinsurers_weights[reinsurer.id] = math.floor(weights) else: @@ -499,11 +690,13 @@ def reset_reinsurance_weights(self): else: self.not_accepted_reinrisks = self.reinrisks - def reset_insurance_weights(self): - - operational_no = sum([insurancefirm.operational for insurancefirm in self.insurancefirms]) + 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([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) @@ -514,8 +707,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: # TODO: as above + weights = risks_no / operational_no for insurer in self.insurancefirms: self.insurers_weights[insurer.id] = math.floor(weights) else: @@ -523,37 +716,80 @@ 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 _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) 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). 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.""" - 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): + 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"] + * capital + / ( + self.simulation_parameters["initial_agent_cash"] + * self.damage_distribution.mean() + * self.simulation_parameters["no_risks"] + ) + ) + self.market_premium = max( + self.market_premium, + self.norm_premium * self.simulation_parameters["lower_price_limit"], + ) + + 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). 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.""" - 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): + 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"] + * capital + / ( + self.simulation_parameters["initial_agent_cash"] + * self.damage_distribution.mean() + * self.simulation_parameters["no_risks"] + ) + ) + self.reinsurance_market_premium = max( + self.reinsurance_market_premium, + self.norm_premium * self.simulation_parameters["lower_price_limit"], + ) + + def get_market_premium(self) -> float: """Get_market_premium Method. Accepts no arguments. Returns: @@ -561,7 +797,7 @@ def get_market_premium(self): This method returns the current insurance market premium.""" return self.market_premium - def get_market_reinpremium(self): + def get_market_reinpremium(self) -> float: """Get_market_reinpremium Method. Accepts no arguments. Returns: @@ -569,50 +805,84 @@ 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): + """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 ? if self.reinsurance_off: - return float('inf') - max_reduction = 0.1 - return self.reinsurance_market_premium * (1. - max_reduction * np_reinsurance_deductible_fraction) - - def get_cat_bond_price(self, np_reinsurance_deductible_fraction): + return float("inf") + 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: 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. + 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 ? + # TODO: make max_reduction and max_cat_bond_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) - - def append_reinrisks(self, item): - if(len(item) > 0): - self.reinrisks.append(item) - - def remove_reinrisks(self,risko): - if(risko != None): + max_cat_bond_surcharge = 0.5 + # QUERY: again, what does max_reduction represent? + return self.reinsurance_market_premium * ( + 1.0 + + max_cat_bond_surcharge + - 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)""" + if reinrisk: + self.reinrisks.append(reinrisk) + + 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, 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]):] + 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 + 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: 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_kept = [] np.random.shuffle(risks_to_be_sent) return risks_to_be_sent - def solicit_reinsurance_requests(self, id, cash, 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 + 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]):] @@ -625,93 +895,52 @@ def solicit_reinsurance_requests(self, id, cash, 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: + not_accepted_risks: Type List + 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: 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: + 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"]) - riskmodel_combination[i] = 1/rm_factor - riskmodels.append(riskmodel_combination.tolist()) + riskmodel_combination[i] = 1 / rm_factor + riskmodels.append(riskmodel_combination) return riskmodels - 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) - - 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.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") - 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): - 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() - 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 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: + agent_type: Type String + 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["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: - return False + raise ValueError(f"Unknown agent type. Simulation requested to create agent of type {agent_type}") + return np.random.random() < prob def record_bankruptcy(self): """Record_bankruptcy Method. @@ -725,110 +954,260 @@ 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): + 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. Called at start of dissolve to catch all instances necessary. + Accepts: + loss: Type integer, value of lost claim + No return value""" 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: 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): + """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): - varsfirms = [] - for firm in self.insurancefirms: - if firm.operational: - varsfirms.append(firm.var_counter_per_risk) - totalina = sum(varsfirms) + 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]) + totalreal = len([firm for firm in self.insurancefirms if firm.operational]) + # Real VaR is 1 for each firm, we think - varsfirms = [] - for firm in self.insurancefirms: - if firm.operational: - varsfirms.append(1) - totalreal = sum(varsfirms) - - varsreinfirms = [] - for reinfirm in self.reinsurancefirms: - if reinfirm.operational: - varsreinfirms.append(reinfirm.var_counter_per_risk) - totalina = totalina + sum(varsreinfirms) - - varsreinfirms = [] - for reinfirm in self.reinsurancefirms: - if reinfirm.operational: - varsreinfirms.append(1) - totalreal = totalreal + sum(varsreinfirms) + 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 count_underwritten_and_reinsured_risks_by_category(self): - 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): + 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: + current_id: Type integer""" current_id = self.insurer_id_counter 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: + current_id: Type integer""" current_id = self.reinsurer_id_counter self.reinsurer_id_counter += 1 return current_id - def insurance_entry_index(self): - return self.insurance_models_counter[0:self.simulation_parameters["no_riskmodels"]].argmin() - - def reinsurance_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: + 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) -> 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() - def get_operational(self): + 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): #This method determines the capital market entry of reinsurers. It is only run in start.py. + 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"]) #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 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?) + 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. 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() - 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. + 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 + + 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 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.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. + 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.selling_insurance_firms = [] + + 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.selling_reinsurance_firms = [] + + 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. Only called if conditions save_network is True and slim logs is False.""" + """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/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() diff --git a/isleconfig.py b/isleconfig.py index 2885c21..f4fbf92 100644 --- a/isleconfig.py +++ b/isleconfig.py @@ -1,79 +1,119 @@ -use_abce = False oneriskmodel = False 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)? - -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} - +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 = True +aid_relief = False +simulation_parameters = { + "no_categories": 4, + "no_insurancefirms": 20, + "no_reinsurancefirms": 4, + "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. + "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, + "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, + # 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, + "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, + # 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, + # 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, + # 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. + "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.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, + "aid_budget": 1000000, +} diff --git a/listify.py b/listify.py index 591e626..fe370c8 100644 --- a/listify.py +++ b/listify.py @@ -1,37 +1,39 @@ """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: 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) - - return l + lst = [d[key] for key in keys] + lst.append(keys) + + 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: 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..bc3e221 100644 --- a/logger.py +++ b/logger.py @@ -1,19 +1,23 @@ """Logging class. Handles records of a single simulation run. Can save and reload. """ import numpy as np -import pdb import listify +import os +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' -).split(' ') - -class Logger(): + "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. @@ -22,103 +26,130 @@ 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 = {} - + 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').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'] = [] # TODO: Should there not be a similar record for reinsurance - 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_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 = {} + 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 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": + 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]) + 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. + 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. 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: + 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""" 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.""" """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"] 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 + 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. @@ -126,18 +157,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 +181,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 @@ -159,19 +190,57 @@ 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 add_insurance_agent(self): - """Method for adding an additional insurer agent to the history log. This is necessary to keep the number + + 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] + 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 + # 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) + ) + else: + zeroes_to_append = [] + self.history_logs["reinsurance_contracts"].append(zeroes_to_append) diff --git a/metainsurancecontract.py b/metainsurancecontract.py index 20d17bb..d12d388 100644 --- a/metainsurancecontract.py +++ b/metainsurancecontract.py @@ -1,103 +1,128 @@ -import numpy as np -import sys, pdb +from typing import TYPE_CHECKING -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): +if TYPE_CHECKING: + from metainsuranceorg import MetaInsuranceOrg + from genericclasses import GenericAgent, RiskProperties + + +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,): """Constructor method. Accepts arguments - insurer: Type InsuranceFirm. - properties: Type dict. + insurer: Type InsuranceFirm. + risk: Type RiskProperties. time: Type integer. The current time. premium: Type float. runtime: Type integer. 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: 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 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.insurer: "MetaInsuranceOrg" = insurer + self.risk_factor = risk.risk_factor + self.category = risk.category + self.property_holder: "GenericAgent" = risk.owner + self.value = risk.value + self.contract = risk.contract # May be None + self.risk = risk + self.insurancetype = insurancetype or risk.insurancetype + self.runtime = runtime self.starttime = time self.expiration = runtime + time self.expire_immediately = expire_immediately 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 + self.initial_VaR = initial_var + + # 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) - self.deductible = self.deductible_fraction * self.value - - # set excess from argument, risk property or default value, whichever first is not None + self.deductible_fraction = ( + deductible_fraction + if deductible_fraction is not None + else risk.deductible_fraction + if risk.deductible_fraction is not None + 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 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) - self.excess = self.excess_fraction * self.value - + 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.limit = round(self.limit_fraction * self.value) + self.reinsurance = reinsurance 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 + # 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') - + + # 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_values = [total_premium / len(self.payment_times)] * len(self.payment_times) + # 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) + if self.contract: + 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 - - - def check_payment_due(self, time): - if len(self.payment_times) > 0 and time >= self.payment_times[0]: + def check_payment_due(self, time: int): + """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[-1]: # 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(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): + """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") - - def terminate_reinsurance(self, time): + def terminate_reinsurance(self, time: int): """Terminate reinsurance method. Accepts arguments time: Type integer. The current time. @@ -105,8 +130,8 @@ 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): + + def dissolve(self, time: int): """Dissolve method. Accepts arguments time: Type integer. The current time. @@ -126,9 +151,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. @@ -137,3 +162,4 @@ def unreinsure(self): self.reincontract = None self.reinsurance = 0 self.reinsurance_share = None + diff --git a/metainsuranceorg.py b/metainsuranceorg.py index c0fa39e..a2aaa3d 100644 --- a/metainsuranceorg.py +++ b/metainsuranceorg.py @@ -1,98 +1,172 @@ +import math +import functools +from itertools import cycle, islice, chain -import isleconfig import numpy as np import scipy.stats -import copy -from insurancecontract import InsuranceContract -from reinsurancecontract import ReinsuranceContract -from riskmodel import RiskModel -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): + +import isleconfig +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: + """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: + """ + Returns the mean of a list + Args: + x: an iterable of numerics + + Returns: + the mean of x + """ return sum(x) / len(x) -def get_mean_std(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: 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 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(GenericAgent): - 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'] + 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.""" + 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.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"] self.dividend_share_of_profits = simulation_parameters["dividend_share_of_profits"] - - self.owner = self.simulation # TODO: Make this into agent_parameter value? + + # 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) - - 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. + """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.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.RiskModel = 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=self.max_inaccuracy,) + + # 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 = ( + 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.obligations = [] - self.underwritten_contracts = [] - self.profits_losses = 0 - #self.reinsurance_contracts = [] - self.operational = True + self.np_reinsurance_deductible_fraction = simulation_parameters[ + "default_non-proportional_reinsurance_deductible" + ] + self.np_reinsurance_limit_fraction = simulation_parameters[ + "default_non-proportional_reinsurance_excess" + ] + self.np_reinsurance_premium_share = simulation_parameters[ + "default_non-proportional_reinsurance_premium_share" + ] + self.underwritten_contracts: MutableSequence["MetaInsuranceContract"] = [] self.is_insurer = True self.is_reinsurer = False - + self.warning = False + self.age = 0 + """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_sum_last_periods = list(np.zeros((12, 4), 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 = [] @@ -102,155 +176,215 @@ def init(self, simulation_parameters, agent_parameters): 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 - - """obtain investments yield""" - self.obtain_yield(time) + # 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 + + 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.""" + + """Obtain interest generated by cash""" + self.simulation.bank.award_interest(self, self.cash) + self.age += 1 """realize due payments""" - self.effect_payments(time) + 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) + contracts_dissolved = self.mature_contracts(time) """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) + + """Check what proportion of the risk market we hold and then update the riskmodel accordingly""" + self.update_risk_share() + self.adjust_riskmodel_inaccuracy() if self.operational: + # 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 - """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) - 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] - - """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. - 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. - 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] + """Collect and process new risks""" + self.collect_process_evaluate_risks(time, contracts_dissolved) - """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). - # 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) - 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) - self.pay_dividends(time) + """adjust liquidity, borrow or invest""" + # Not implemented - """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 + if self.operational and not self.warning: + self.market_permanency(time) - 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)) + self.roll_over(time) - [risks_per_categ, number_risks_categ] = self.risks_reinrisks_organizer(new_risks) #Here the new risks are organized by category. + self.estimate_var() - 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. - break + 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)): + value = self.underwritten_risk_characterisation[categ][0] + self.reinsurance_profile.update_value(value, categ) + + # 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] - # return unacceptables - #print(self.id, " now has ", len(self.underwritten_contracts), " & returns ", len(not_accepted_risks)) - self.simulation.return_risks(not_accepted_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 + [_, 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). - #not implemented - #"""adjust liquidity, borrow or invest""" - #pass + # 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) + + self.update_risk_characterisations() - self.market_permanency(time) + actual_capacity = self.increase_capacity(time, max_var_by_categ) + # TODO: make independent of insurer/reinsurer, but change this to different deductible values - self.roll_over(time) - - self.estimated_var() + """handle capital market interactions: capital history, dividends""" + 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) - def enter_illiquidity(self, 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) + 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)) + + # 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)]: + for repetition in range(self.recursion_limit): + # 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 + if not has_accepted_risks: + break + self.simulation.return_risks(list(chain.from_iterable(not_accepted_risks))) + self.update_risk_characterisations() + + def enter_illiquidity(self, time: int): """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 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 - 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') + if isleconfig.buy_bankruptcies: + if self.is_insurer and self.operational: + self.simulation.add_firm_to_be_sold(self, time, "record_bankruptcy") + self.operational = False + elif self.is_reinsurer and self.operational: + self.simulation.add_firm_to_be_sold(self, time, "record_bankruptcy") + self.operational = False + else: + self.dissolve(time, 'record_bankruptcy') + else: + self.dissolve(time, 'record_bankruptcy') def market_exit(self, time): """Market_exit Method. 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] + 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._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. @@ -260,315 +394,475 @@ 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.""" - [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) + 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(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] 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. - if self.operational: - 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) + + obligation = 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() + for reincontract in self.reinsurance_profile.all_contracts(): + reincontract.dissolve(time) self.operational = False - def receive_obligation(self, amount, recipient, due_time, purpose): - obligation = {"amount": amount, "recipient": recipient, "due_time": due_time, "purpose": purpose} - self.obligations.append(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. + Currently pays to simulation. + """ + 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 + Accepts: + time: Type integer + Returns: + number of contracts maturing: Type integer""" + 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 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] - 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(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': - self.profits_losses -= amount - recipient.receive(amount) - - def receive(self, amount): - """Method to accept cash payments.""" - self.cash += amount - self.profits_losses += amount - - 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 - 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): + 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): + """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 zeros(self): - # return 0 - - def len_underwritten_contracts(self): + def number_underwritten_contracts(self) -> int: return len(self.underwritten_contracts) - def get_operational(self): - return self.operational - - def get_profitslosses(self): - return self.profits_losses - - def get_underwritten_contracts(self): + def get_underwritten_contracts(self) -> Sequence["MetaInsuranceContract"]: return self.underwritten_contracts - - def get_pointer(self): - return self - def estimated_var(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 estimate_var(self) -> None: + """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. 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] = 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_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) + 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 + + # 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 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: - 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) - - 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". + 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[-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]]: + """Method for soliciting new risks from insurance simulation then organising them based if non-proportional + or not. + No accepted Values. + 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) + if self.is_reinsurer: + new_risks += self.simulation.solicit_reinsurance_requests(self) + + new_nonproportional_risks = [ + risk + for risk in new_risks + if risk.insurancetype == "excess-of-loss" and risk.owner is not self + ] + new_risks = [ + risk + for risk in new_risks + if risk.insurancetype in ["proportional", None] and risk.owner is not self + ] + return new_nonproportional_risks, new_risks + + 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]]: + """This method organizes the new risks received by the insurer (or reinsurer) by category. + Accepts: + new_risks: Type list of DataDicts + 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_ + ) 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. - - 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"] + risks_by_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 + + 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. + 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""" + + # Compute the cash already reserved by category + cash_reserved_by_categ = self.cash - cash_left_by_categ + + _, std_pre = get_mean_std(tuple(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.insurancetype == "excess-of-loss": + percentage_value_at_risk = self.riskmodel.get_ppf( + categ_id=risk.category, tail_size=self.riskmodel.var_tail_prob + ) + var_damage = ( + percentage_value_at_risk + * risk.value + * risk.risk_factor + * self.riskmodel.inaccuracy[risk.category] + ) + var_claim = ( + min(var_damage, risk.value * risk.limit_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 + # Compute how the cash reserved by category would change if the new reinsurance risk was accepted + cash_reserved_by_categ_store[risk.category] += ( + var_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(tuple(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) - cash_left_by_categ[i] = self.cash - cash_reserved_by_categ_store[i] - + # 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) + # 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 - 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: - 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) # 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. - 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 - - 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) - - - - 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. - _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: - 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 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"], ) - 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 + 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. + 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.""" + 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. + 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, + 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) + # 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) + 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 + self.underwritten_contracts.append(contract) + has_accepted_risks = True + self.cash_left_by_categ = cash_left_by_categ + else: + not_accepted_reinrisks[risk.category].append(risk) + else: + not_accepted_reinrisks[risk.category].append(risk) + + 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]]]: + """Method to decide if new risks are underwritten for the insurance firm. + Accepts: + risks_per_categ: Type List of lists containing new risks. + 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 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.""" + random_runtime = self.contract_runtime_dist.rvs() + 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: + 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.ReinsuranceContract( + self, + risk, + time, + self.insurance_premium(), + risk.expiration - time, + self.default_contract_payment_period, + 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 + acceptable_by_category[risk.category] -= 1 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. - 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][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) - - 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) - - 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. + 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) + # 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]) + 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[risk.category].append(risk) + else: + 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 has_accepted_risks, not_accepted_risks + + def market_permanency(self, time: int): + """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) 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. + # 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 + 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. + 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. + 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. 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: float): + """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): + def roll_over(self, time: int): """Roll_over Method. Accepts arguments time: Type integer. The current time. No return value. @@ -581,28 +875,140 @@ 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.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) - if np.random.uniform(0,1,1) < self.simulation_parameters["reinsurance_retention"]: - if reinrisk is not None: - self.reinrisks_kept.append(reinrisk) - - - - - + maturing_next = [ + contract + for contract in self.underwritten_contracts + if contract.expiration == time + 1 + ] + # 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: + 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 + else: + 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"]: + if reinrisk is not None: + self.reinrisks_kept.append(reinrisk) + + 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) + + 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"] + premium = self.simulation.get_market_premium() * (1 * (1 - self.risk_share) + max_adjustment * self.risk_share) + return premium + + 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) + 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) + + 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 = self.simulation.get_firms_to_sell(type) + 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]) + total_premium = sum([np.mean(contract.payment_values) for contract in firm.underwritten_contracts if len(contract.payment_values) > 0]) + 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_sell_reason = reason + firms_further_considered.append([firm, firm_likelihood, firm_price, firm_sell_reason]) + + 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] + 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_sold_firm(best_firm, time, best_firm_sell_reason) + + 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 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)) + + for contract in firm.underwritten_contracts: + 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) + + firm.obligations = [] + firm.underwritten_contracts = [] + 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, self.riskmodel.margin_of_safety) + 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.operational = False + else: + self.dissolve(time, "record_nonregulation_firm") + for contract in self.underwritten_contracts: + contract.mature(time) # Mature contracts so they are returned to simulation as firm non-op + self.underwritten_contracts = [] 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/reinsurancecontract.py b/reinsurancecontract.py index c9ced5a..ccfbd7e 100644 --- a/reinsurancecontract.py +++ b/reinsurancecontract.py @@ -1,65 +1,99 @@ -import numpy as np +import metainsurancecontract -from metainsurancecontract import MetaInsuranceContract +from typing import Optional +from typing import TYPE_CHECKING -class ReinsuranceContract(MetaInsuranceContract): +if TYPE_CHECKING: + from insurancefirms import InsuranceFirm + from metainsuranceorg import MetaInsuranceOrg + from genericclasses import RiskProperties + + +class ReinsuranceContract(metainsurancecontract.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.,\ - 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: "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, + time, + premium, + runtime, + payment_period, + expire_immediately, + initial_var, + insurancetype, + deductible_fraction, + limit_fraction, + reinsurance, + ) + # self.is_reinsurancecontract = True + 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, damage_extent=None): + + def explode(self, time: int, uniform_value: None = None, damage_extent: float = 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 - 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. """ + assert uniform_value is None + if damage_extent is None: + raise ValueError("Damage extend 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 + # 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.limit, damage_extent) - self.deductible + self.insurer.receive_obligation( + claim, self.property_holder, time, "claim" + ) + elif self.insurancetype == "proportional": + claim = min(self.limit, damage_extent) - self.deductible + self.insurer.receive_obligation( + claim, self.property_holder, time + 1, "claim" + ) + else: + raise ValueError(f"Unexpected insurance type {self.insurancetype}") + # Every reinsurance claim made is immediately registered. + self.insurer.register_claim(claim) - 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: - 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. + 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.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 - - def mature(self, time): - """Mature method. + def mature(self, time: int): + """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 + 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(contract=self) + else: # TODO: ? Instead: if self.insurancetype == "proportional": self.contract.unreinsure() - diff --git a/reinsurancefirm.py b/reinsurancefirm.py deleted file mode 100644 index 1c1558b..0000000 --- a/reinsurancefirm.py +++ /dev/null @@ -1,15 +0,0 @@ -#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 - 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) - self.is_insurer = False - self.is_reinsurer = True diff --git a/requirements.txt b/requirements.txt index 340f889..7c3d507 100644 --- a/requirements.txt +++ b/requirements.txt @@ -3,4 +3,5 @@ numpy>=1.13.3 matplotlib>=2.1.1 networkx>=2.0 argparse>=1.1 -git+https://github.com/ABC-E/abce +sortedcontainers>=2.1.0 +dataclasses>=0.6 \ No newline at end of file diff --git a/resume.py b/resume.py deleted file mode 100644 index 7c60700..0000000 --- a/resume.py +++ /dev/null @@ -1,220 +0,0 @@ -# import common packages -import numpy as np -import scipy.stats -import math -import sys, pdb -import argparse -import pickle -import hashlib -import random - -# import config file and apply configuration -import isleconfig - -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="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: - isleconfig.use_abce = True -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 and abce modules -if isleconfig.use_abce: - #print("Importing abce") - import abce - from abce import gui - -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: - 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) - - 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 - 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) - 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 - 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) - 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) - - # 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() - - 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 4769032..9b2a4e7 100644 --- a/riskmodel.py +++ b/riskmodel.py @@ -1,88 +1,152 @@ - import math +from copy import deepcopy + import numpy as np -import sys, pdb -import scipy.stats + import isleconfig from distributionreinsurance import ReinsuranceDistWrapper +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, 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: "Distribution", + expire_immediately: bool, + cat_separation_distribution: "Distribution", + 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 - self.var_tail_prob = 0.02 + # 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 self.init_average_exposure = init_average_exposure 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.inaccuracy = inaccuracy - - def getPPF(self, categ_id, tailSize): + 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.inaccuracy: Sequence[float] = inaccuracy + + 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 - tailSize (float >=0, <=1): quantile + categ_id integer: category + tailSize (float 0<=x<=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): - #categ_risks2 = [risk for risk in risks if risk["category"]==categ_id] - categ_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 + Returns: + categ_risks: Type List of DataDicts.""" + risks_by_categ = [[] for _ in range(self.category_number)] for risk in risks: - if risk["category"]==categ_id: - categ_risks.append(risk) - #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]) - # - ##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: + risks_by_categ[risk.category].append(risk) + return risks_by_categ + + 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: + 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 = 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"]) - average_exposure = np.mean(exposures) + assert risk.limit is not None + 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) - #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 # 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 - - 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. + 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 @@ -91,56 +155,61 @@ 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) - + 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] - + # compute number of acceptable risks of this category + 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) + 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 - + # 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.get_ppf(categ_id=categ_id, tail_size=self.var_tail_prob) + * average_risk_factor + * 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 * 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 * 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) - cash_left = cash[categ_id] - len(categ_risks) * var_per_risk - except: - print(sys.exc_info()) - pdb.set_trace() + 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), + ) + 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 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: @@ -149,65 +218,136 @@ 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)) + 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: 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. + 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 - + # prepare variables 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 - percentage_value_at_risk = self.getPPF(categ_id=categ_id, tailSize=self.var_tail_prob) - + # 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 = percentage_value_at_risk * risk["value"] * risk["risk_factor"] \ - * self.inaccuracy[categ_id] - expected_claim = min(expected_damage, risk["excess"]) - risk["deductible"] - + # 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] + ) + + 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] -= 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.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"] - + if (offered_risk is not None) and (offered_risk.category == categ_id): + var_damage_fraction = ( + percentage_value_at_risk + * offered_risk.risk_factor + * self.inaccuracy[categ_id] + ) + var_claim_fraction = ( + min(var_damage_fraction, offered_risk.limit_fraction) + - offered_risk.deductible_fraction + ) + 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 - additional_var_per_categ[categ_id] += expected_claim_total - + 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. - 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): + # noinspection PyUnboundLocalVariable + 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: + 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.""" + # 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.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)): @@ -217,33 +357,57 @@ 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) - - def add_reinsurance(self, categ_id, excess_fraction, deductible_fraction, contract): - 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]) - - 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() - + return ( + (cash_left_by_categ - additional_required > 0).all(), + cash_left_by_categ, + var_this_risk, + min(cash_left_by_categ), + ) + + 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/setup.py b/setup.py deleted file mode 100644 index 65a9fdf..0000000 --- a/setup.py +++ /dev/null @@ -1,126 +0,0 @@ -"""Class to set up event schedules for reproducible simulation replications. - 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 - - 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: - 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"]) - random.random.seed(d["np_seed"]) - """ - -import argparse -import scipy.stats -import pickle -import math -import os -import isleconfig -from distributiontruncated import TruncatedDistWrapper - -class SetupSim(): - - def __init__(self): - - self.simulation_parameters = isleconfig.simulation_parameters - - """parameters of the simulation setup""" - 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. - - """"random seeds""" - self.np_seed = [] - self.random_seed = [] - 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. - - 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]]) - 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. - total = 0 - while (total < self.max_time): - separation_time = self.cat_separation_distribution.rvs() - total += int(math.ceil(separation_time)) - if total < self.max_time: - event_schedule.append(total) - event_damage.append(self.damage_distribution.rvs()) - rc_event_schedule.append(event_schedule) - rc_event_damage.append(event_damage) - - self.general_rc_event_schedule.append(rc_event_schedule) - self.general_rc_event_damage.append(rc_event_damage) - - 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. - """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) - 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 = [] - - for i in range(replications): - - """pack to dict""" - d = {} - d["np_seed"] = self.np_seed[i] - d["random_seed"] = self.random_seed[i] - d["event_times"] = self.general_rc_event_schedule[i] - d["event_damages"] = self.general_rc_event_damage[i] - d["num_categories"] = self.simulation_parameters["no_categories"] - event_schedules.append(d) - - """ 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" - 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") - - - 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 - - - - - - diff --git a/setup_simulation.py b/setup_simulation.py new file mode 100644 index 0000000..1e05079 --- /dev/null +++ b/setup_simulation.py @@ -0,0 +1,205 @@ +"""Class to set up event schedules for reproducible simulation replications. + 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 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 + 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"]) + random.random.seed(d["np_seed"]) + """ + +import math +from typing import MutableSequence, Tuple +import os +import pickle +import scipy.stats + +import isleconfig +from distributiontruncated import TruncatedDistWrapper + + +class SetupSim: + def __init__(self): + + self.simulation_parameters = isleconfig.simulation_parameters + + """parameters of the simulation setup""" + 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. + self.damage_distribution = TruncatedDistWrapper( + 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"] + ) # It is assumed that the time between catastrophes is exponentially distributed. + + """"random seeds""" + self.np_seed = [] + 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: 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]]) + 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): + # 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() + # 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) + event_damage.append(self.damage_distribution.rvs()[0]) + rc_event_schedule.append(event_schedule) + rc_event_damage.append(event_damage) + + self.general_rc_event_schedule.append(rc_event_schedule) + self.general_rc_event_damage.append(rc_event_damage) + + return self.general_rc_event_schedule, self.general_rc_event_damage + + 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""" + for i in range(replications): + 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) + + return self.np_seed, self.random_seed + + 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""" + d = {} + d["np_seed"] = self.np_seed[i] + d["random_seed"] = self.random_seed[i] + d["event_times"] = self.general_rc_event_schedule[i] + d["event_damages"] = self.general_rc_event_damage[i] + d["num_categories"] = self.simulation_parameters["no_categories"] + event_schedules.append(d) + + """ ensure that logging directory exists""" + if not os.path.isdir("data"): + 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") + + # 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." + ) + + """Save the initial values""" + with open("./data/" + self.filepath, "wb") as wfile: + pickle.dump(event_schedules, wfile, protocol=pickle.HIGHEST_PROTOCOL) + + 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: 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. + # 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: + # 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" + ) + self.overwrite = True + self.schedule(replications) + self.seeds(replications) + + self.store() + else: + # Replicating anothe run, so 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 ( + self.general_rc_event_schedule, + self.general_rc_event_damage, + self.np_seed, + self.random_seed, + ) + + @staticmethod + def to_filename(filepath: str) -> str: + if len(filepath) >= 10 and filepath[-10:] == ".islestore": + return filepath + else: + return filepath + ".islestore" diff --git a/start.py b/start.py index b75e0a9..ae74e57 100644 --- a/start.py +++ b/start.py @@ -1,208 +1,134 @@ # 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 copy -import importlib +from typing import MutableMapping, MutableSequence + +import calibrationscore +import insurancesimulation # import config file and apply configuration import isleconfig +import logger simulation_parameters = isleconfig.simulation_parameters -replic_ID = None +filepath = None +overwrite = False 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 +"""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" + if os.path.exists("data"): + raise FileExistsError("./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 +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) + 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) -#@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): + # log data + simulation.save_data() - np.random.seed(np_seed) - random.seed(random_seed) + # 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"]: + # Need to use t+1 as resume will start at time saved + save_simulation(t + 1, simulation, sim_params, exit_now=False) - # create simulation and world objects (identical in non-abce mode) - if isleconfig.use_abce: - simulation = abce.Simulation(processes=1,random_seed = random_seed) + # 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) - 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"]) - - 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(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[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 - 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 - 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, - '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 - else: - new_reinsurancefirm_pointer = new_reinsurance_firm - 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']) - else: - world.save_data() - - 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. - -# 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 +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] + 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() -> 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()) + rfile.seek(0) + file_contents = pickle.load(rfile) + return file_contents + + # 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], - 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 = 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)") + 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 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.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) @@ -213,11 +139,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: @@ -225,27 +146,35 @@ def save_simulation(t, sim, sim_param, exit_now=False): if args.save_iterations: save_iter = args.save_iterations else: - save_iter = 200 + # Disable saving unless save_iter is given. It doesn't work anyway # TODO + save_iter = isleconfig.simulation_parameters["max_time"] + 2 - """ 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. - [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). + if not args.resume: + from setup_simulation import SetupSim - 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. + + # 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 + 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 """ 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 2ce6814..55614a6 100644 --- a/visualisation.py +++ b/visualisation.py @@ -1,13 +1,35 @@ # 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 +import isleconfig +import pickle +import scipy +import scipy.stats +from matplotlib.offsetbox import AnchoredText +import time +import os +if not os.path.isdir("figures"): + os.makedirs("figures") 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): + """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 @@ -16,99 +38,203 @@ 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): + """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) 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. + 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) 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() - self.stream = self.data_stream() - self.ani = animation.FuncAnimation(self.fig, self.update, repeat=False, interval=100,) - #init_func=self.setup_plot) + 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 = [] + 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 - 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, + # If animation is saved or not + self.save_condition = save + 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() + self.animate = animation.FuncAnimation(self.fig, self.update, repeat=False, interval=20, save_count=998) + 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 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. + 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.suptitle('EVENT AT TIME %i!' % i) + self.all_event_times = self.all_event_times[1:] + 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("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) + else: + print("Incorrect Type for Saving") - def show(self): - plt.show() 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 - # 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 + 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']) - self.ins_pie_anim = InsuranceFirmAnimation(insurance_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.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']) - self.reins_pie_anim = InsuranceFirmAnimation(reinsurance_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.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] profitslosses_agg = [history_logs['total_profitslosses'] for history_logs in self.history_logs_list] @@ -127,16 +253,31 @@ def insurer_time_series(self, runs=None, axlst=None, fig=None, title="Insurer", (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]): - # 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] + (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]): + """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] @@ -157,96 +298,898 @@ def reinsurer_time_series(self, runs=None, axlst=None, fig=None, title="Reinsure (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 + ], events, damages, title=title, xlabel="Time", axlst=axlst, fig=fig, colour=colour) + fig, axlst = self.reins_time_series.plot() + return fig, axlst - 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 + 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 + 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") + print("Saved " + str(risk_model) + " risk_model(s)_insurer_ensemble_timeseries") 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") + print("Saved " + str(risk_model) + " risk_model(s)_reinsurer_ensemble_timeseries") def show(self): plt.show() - def save(self): - # logic to save plots - pass - -if __name__ == "__main__": - # use argparse to handle command line arguments +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. + 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. + 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 "figures/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) + 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) + print("Saved " + self.variable + " CDF") + + +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: + 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 "figures/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) + print("Saved " + self.variable + " histogram") + + +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): + """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, histtype='step') + + """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() + + +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. + 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"] = {} + + # 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() + + 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])): + 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 + 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): + """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)", + "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" + 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). + 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) + 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') - 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.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.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 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 + # 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 + # 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.show() + if args.pie: + vis.insurer_pie_animation() + vis.reinsurer_pie_animation() + if args.timeseries: + 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.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. + 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)) + + 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]) + cmp_rsk.create_reinsurer_timeseries(percentiles=[10, 90]) + # cmp_rsk.show() + + 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_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 - if args.comparison: + 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 - # 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() + if args.firmdistribution: vis_list = [] - filenames = ["./data/"+x+"_history_logs.dat" for x in ["one","two","three","four"]] + 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 + 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'] - 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() + # 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"] + 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]) + + 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") + # 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) + 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") + +# āĻģ diff --git a/visualization_network.py b/visualization_network.py index 82d9129..6d91a77 100644 --- a/visualization_network.py +++ b/visualization_network.py @@ -1,14 +1,69 @@ import networkx as nx import matplotlib.pyplot as plt +import matplotlib.animation as animation import numpy as np +import argparse +import os -class ReinsuranceNetwork(): - def __init__(self, insurancefirms, reinsurancefirms, catbonds): - """save entities""" + +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. + 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) + + # Figure out what the except can be and then specify + node_centralities = nx.eigenvector_centrality(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 +71,204 @@ 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 = {} + 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() - idx_from = self.num_entities["insurers"] + (op_entities["reinsurers"] + op_entities["catbonds"]).index(eolr["reinsurer"]) - weights_matrix[idx_from][idx_to] = eolr["value"] - + 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) - def visualize(self): - """visualize""" - plt.figure() + """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) + + 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"]) + 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) + # 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) + 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""" + self.figure.canvas.flush_events() + self.figure.clear() + + +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_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] + 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.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,) + + 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") + + 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) + + def save_network_animation(self): + """Method to save animation as MP4. + No accepted values. + 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) + + +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() + args.save = True + + if args.number_iterations: + num_iter = args.number_iterations + else: + num_iter = 999 + + # 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()