From 792e76b663f8e6abec0cf494ea3aba22e3b57fd5 Mon Sep 17 00:00:00 2001 From: Mateo Velasquez-Giraldo Date: Tue, 25 May 2021 14:15:13 -0500 Subject: [PATCH 1/3] Create class --- HARK/ConsumptionSaving/ConsRiskyAssetModel.py | 270 ++++++++++++++++++ 1 file changed, 270 insertions(+) create mode 100644 HARK/ConsumptionSaving/ConsRiskyAssetModel.py diff --git a/HARK/ConsumptionSaving/ConsRiskyAssetModel.py b/HARK/ConsumptionSaving/ConsRiskyAssetModel.py new file mode 100644 index 000000000..6d9f69f7d --- /dev/null +++ b/HARK/ConsumptionSaving/ConsRiskyAssetModel.py @@ -0,0 +1,270 @@ +""" +This file contains classes and functions for representing, solving, and simulating +agents who must allocate their resources among consumption, saving in a risk-free +asset (with a low return), and saving in a risky asset (with higher average return). +""" +import numpy as np +from copy import deepcopy +from HARK.ConsumptionSaving.ConsIndShockModel import ( + IndShockConsumerType, # PortfolioConsumerType inherits from it + init_idiosyncratic_shocks, # Baseline dictionary to build on +) + +from HARK.distribution import ( + combine_indep_dstns, + Lognormal, + Bernoulli, +) + +class RiskyAssetConsumerType(IndShockConsumerType): + """ + A consumer type that has access to a risky asset for his savings. The + risky asset has lognormal returns that are possibly correlated with his + income shocks. + + There is a friction that prevents the agent from adjusting his portfolio + at any given period with an exogenously given probability. + The meaning of "adjusting his portfolio" depends on the particular model. + """ + + time_inv_ = deepcopy(IndShockConsumerType.time_inv_) + + shock_vars_ = IndShockConsumerType.shock_vars_ + ["Adjust", "Risky"] + + def __init__(self, cycles=1, verbose=False, quiet=False, **kwds): + params = init_risky_asset.copy() + params.update(kwds) + kwds = params + + # Initialize a basic consumer type + IndShockConsumerType.__init__( + self, cycles=cycles, verbose=verbose, quiet=quiet, **kwds + ) + + def pre_solve(self): + self.update_solution_terminal() + + def update(self): + + IndShockConsumerType.update(self) + self.update_AdjustPrb() + self.update_RiskyDstn() + self.update_ShockDstn() + + def update_RiskyDstn(self): + """ + Creates the attributes RiskyDstn from the primitive attributes RiskyAvg, + RiskyStd, and RiskyCount, approximating the (perceived) distribution of + returns in each period of the cycle. + + Parameters + ---------- + None + + Returns + ------- + None + """ + # Determine whether this instance has time-varying risk perceptions + if ( + (type(self.RiskyAvg) is list) + and (type(self.RiskyStd) is list) + and (len(self.RiskyAvg) == len(self.RiskyStd)) + and (len(self.RiskyAvg) == self.T_cycle) + ): + self.add_to_time_vary("RiskyAvg", "RiskyStd") + elif (type(self.RiskyStd) is list) or (type(self.RiskyAvg) is list): + raise AttributeError( + "If RiskyAvg is time-varying, then RiskyStd must be as well, and they must both have length of T_cycle!" + ) + else: + self.add_to_time_inv("RiskyAvg", "RiskyStd") + + # Generate a discrete approximation to the risky return distribution if the + # agent has age-varying beliefs about the risky asset + if "RiskyAvg" in self.time_vary: + self.RiskyDstn = [] + for t in range(self.T_cycle): + self.RiskyDstn.append( + Lognormal.from_mean_std(self.RiskyAvg[t], self.RiskyStd[t]).approx( + self.RiskyCount + ) + ) + self.add_to_time_vary("RiskyDstn") + + # Generate a discrete approximation to the risky return distribution if the + # agent does *not* have age-varying beliefs about the risky asset (base case) + else: + self.RiskyDstn = Lognormal.from_mean_std( + self.RiskyAvg, self.RiskyStd, + ).approx(self.RiskyCount) + self.add_to_time_inv("RiskyDstn") + + def update_ShockDstn(self): + """ + Combine the income shock distribution (over PermShk and TranShk) with the + risky return distribution (RiskyDstn) to make a new attribute called ShockDstn. + + Parameters + ---------- + None + + Returns + ------- + None + """ + if "RiskyDstn" in self.time_vary: + self.ShockDstn = [ + combine_indep_dstns(self.IncShkDstn[t], self.RiskyDstn[t]) + for t in range(self.T_cycle) + ] + else: + self.ShockDstn = [ + combine_indep_dstns(self.IncShkDstn[t], self.RiskyDstn) + for t in range(self.T_cycle) + ] + self.add_to_time_vary("ShockDstn") + + # Mark whether the risky returns and income shocks are independent (they are) + self.IndepDstnBool = True + self.add_to_time_inv("IndepDstnBool") + + def update_AdjustPrb(self): + """ + Checks and updates the exogenous probability of the agent being allowed + to rebalance his portfolio/contribution scheme. It can be time varying. + + Parameters + ------ + None. + + Returns + ------- + None. + + """ + if type(self.AdjustPrb) is list and (len(self.AdjustPrb) == self.T_cycle): + self.add_to_time_vary("AdjustPrb") + elif type(self.AdjustPrb) is list: + raise AttributeError( + "If AdjustPrb is time-varying, it must have length of T_cycle!" + ) + else: + self.add_to_time_inv("AdjustPrb") + + def get_Risky(self): + """ + Sets the attribute Risky as a single draw from a lognormal distribution. + Uses the attributes RiskyAvgTrue and RiskyStdTrue if RiskyAvg is time-varying, + else just uses the single values from RiskyAvg and RiskyStd. + + Parameters + ---------- + None + + Returns + ------- + None + """ + if "RiskyDstn" in self.time_vary: + RiskyAvg = self.RiskyAvgTrue + RiskyStd = self.RiskyStdTrue + else: + RiskyAvg = self.RiskyAvg + RiskyStd = self.RiskyStd + RiskyAvgSqrd = RiskyAvg ** 2 + RiskyVar = RiskyStd ** 2 + + mu = np.log(RiskyAvg / (np.sqrt(1.0 + RiskyVar / RiskyAvgSqrd))) + sigma = np.sqrt(np.log(1.0 + RiskyVar / RiskyAvgSqrd)) + self.shocks["Risky"] = Lognormal( + mu, sigma, seed=self.RNG.randint(0, 2 ** 31 - 1) + ).draw(1) + + def get_Adjust(self): + """ + Sets the attribute Adjust as a boolean array of size AgentCount, indicating + whether each agent is able to adjust their risky portfolio share this period. + Uses the attribute AdjustPrb to draw from a Bernoulli distribution. + + Parameters + ---------- + None + + Returns + ------- + None + """ + if not ("AdjustPrb" in self.time_vary): + + self.shocks["Adjust"] = Bernoulli( + self.AdjustPrb, seed=self.RNG.randint(0, 2 ** 31 - 1) + ).draw(self.AgentCount) + + else: + + Adjust = np.zeros(self.AgentCount) # Initialize shock array + for t in range(self.T_cycle): + these = t == self.t_cycle + N = np.sum(these) + if N > 0: + AdjustPrb = self.AdjustPrb[t - 1] + Adjust[these] = Bernoulli( + AdjustPrb, seed=self.RNG.randint(0, 2 ** 31 - 1) + ).draw(N) + + self.shocks["Adjust"] = Adjust + + def initialize_sim(self): + """ + Initialize the state of simulation attributes. Simply calls the same + method for IndShockConsumerType, then initializes the new states/shocks + Adjust and Share. + + Parameters + ---------- + None + + Returns + ------- + None + """ + self.shocks["Adjust"] = np.zeros(self.AgentCount, dtype=bool) + IndShockConsumerType.initialize_sim(self) + + def get_shocks(self): + """ + Draw idiosyncratic income shocks, just as for IndShockConsumerType, then draw + a single common value for the risky asset return. Also draws whether each + agent is able to adjust their portfolio this period. + + Parameters + ---------- + None + + Returns + ------- + None + """ + IndShockConsumerType.get_shocks(self) + self.get_Risky() + self.get_Adjust() + +# %% Initial parameter sets + +# %% Base risky asset dictionary + +risky_asset_parms = { + # Risky return factor moments. Based on SP500 real returns from Shiller's + # "chapter 26" data, which can be found at http://www.econ.yale.edu/~shiller/data.htm + "RiskyAvg": 1.080370891, + "RiskyStd": 0.177196585, + # Number of integration nodes to use in approximation of risky returns + "RiskyCount": 5, + # Probability that the agent can adjust their portfolio each period + "AdjustPrb": 1.0 +} + +# Make a dictionary to specify a risky asset consumer type +init_risky_asset = init_idiosyncratic_shocks.copy() +init_risky_asset.update(risky_asset_parms) \ No newline at end of file From 13cd47d840d58e9aa82b5120f34cfd86598e2fb3 Mon Sep 17 00:00:00 2001 From: Mateo Velasquez-Giraldo Date: Tue, 25 May 2021 14:16:06 -0500 Subject: [PATCH 2/3] Apply class to ConsPortfolioModel --- HARK/ConsumptionSaving/ConsPortfolioModel.py | 163 +------------------ 1 file changed, 8 insertions(+), 155 deletions(-) diff --git a/HARK/ConsumptionSaving/ConsPortfolioModel.py b/HARK/ConsumptionSaving/ConsPortfolioModel.py index 3e868e57a..6cee892f0 100644 --- a/HARK/ConsumptionSaving/ConsPortfolioModel.py +++ b/HARK/ConsumptionSaving/ConsPortfolioModel.py @@ -17,6 +17,8 @@ init_idiosyncratic_shocks # Baseline dictionary to build on ) +from HARK.ConsumptionSaving.ConsRiskyAssetModel import RiskyAssetConsumerType + from HARK.distribution import combine_indep_dstns from HARK.distribution import Lognormal, Bernoulli # Random draws for simulating agents from HARK.interpolation import ( @@ -118,7 +120,7 @@ def __init__( self.dvdsFuncFxd = dvdsFuncFxd -class PortfolioConsumerType(IndShockConsumerType): +class PortfolioConsumerType(RiskyAssetConsumerType): """ A consumer type with a portfolio choice. This agent type has log-normal return factors. Their problem is defined by a coefficient of relative risk aversion, @@ -130,7 +132,7 @@ class PortfolioConsumerType(IndShockConsumerType): of the risky asset's return distribution must also be specified. """ - time_inv_ = deepcopy(IndShockConsumerType.time_inv_) + time_inv_ = deepcopy(RiskyAssetConsumerType.time_inv_) time_inv_ = time_inv_ + ["AdjustPrb", "DiscreteShareBool"] def __init__(self, cycles=1, verbose=False, quiet=False, **kwds): @@ -139,12 +141,10 @@ def __init__(self, cycles=1, verbose=False, quiet=False, **kwds): kwds = params # Initialize a basic consumer type - IndShockConsumerType.__init__( + RiskyAssetConsumerType.__init__( self, cycles=cycles, verbose=verbose, quiet=quiet, **kwds ) - shock_vars = ['PermShk', 'TranShk','Adjust','Risky'] - # Set the solver for the portfolio model, and update various constructed attributes self.solve_one_period = solveConsPortfolio self.update() @@ -154,9 +154,8 @@ def pre_solve(self): self.update_solution_terminal() def update(self): - IndShockConsumerType.update(self) - self.update_RiskyDstn() - self.update_ShockDstn() + + RiskyAssetConsumerType.update(self) self.update_ShareGrid() self.update_ShareLimit() @@ -206,86 +205,6 @@ def update_solution_terminal(self): dvdsFuncFxd=dvdsFuncFxd_terminal, ) - def update_RiskyDstn(self): - """ - Creates the attributes RiskyDstn from the primitive attributes RiskyAvg, - RiskyStd, and RiskyCount, approximating the (perceived) distribution of - returns in each period of the cycle. - - Parameters - ---------- - None - - Returns - ------- - None - """ - # Determine whether this instance has time-varying risk perceptions - if ( - (type(self.RiskyAvg) is list) - and (type(self.RiskyStd) is list) - and (len(self.RiskyAvg) == len(self.RiskyStd)) - and (len(self.RiskyAvg) == self.T_cycle) - ): - self.add_to_time_vary("RiskyAvg", "RiskyStd") - elif (type(self.RiskyStd) is list) or (type(self.RiskyAvg) is list): - raise AttributeError( - "If RiskyAvg is time-varying, then RiskyStd must be as well, and they must both have length of T_cycle!" - ) - else: - self.add_to_time_inv("RiskyAvg", "RiskyStd") - - # Generate a discrete approximation to the risky return distribution if the - # agent has age-varying beliefs about the risky asset - if "RiskyAvg" in self.time_vary: - self.RiskyDstn = [] - for t in range(self.T_cycle): - self.RiskyDstn.append( - Lognormal.from_mean_std( - self.RiskyAvg[t], - self.RiskyStd[t] - ).approx(self.RiskyCount) - ) - self.add_to_time_vary("RiskyDstn") - - # Generate a discrete approximation to the risky return distribution if the - # agent does *not* have age-varying beliefs about the risky asset (base case) - else: - self.RiskyDstn = Lognormal.from_mean_std( - self.RiskyAvg, - self.RiskyStd, - ).approx(self.RiskyCount) - self.add_to_time_inv("RiskyDstn") - - def update_ShockDstn(self): - """ - Combine the income shock distribution (over PermShk and TranShk) with the - risky return distribution (RiskyDstn) to make a new attribute called ShockDstn. - - Parameters - ---------- - None - - Returns - ------- - None - """ - if "RiskyDstn" in self.time_vary: - self.ShockDstn = [ - combine_indep_dstns(self.IncShkDstn[t], self.RiskyDstn[t]) - for t in range(self.T_cycle) - ] - else: - self.ShockDstn = [ - combine_indep_dstns(self.IncShkDstn[t], self.RiskyDstn) - for t in range(self.T_cycle) - ] - self.add_to_time_vary("ShockDstn") - - # Mark whether the risky returns and income shocks are independent (they are) - self.IndepDstnBool = True - self.add_to_time_inv("IndepDstnBool") - def update_ShareGrid(self): """ Creates the attribute ShareGrid as an evenly spaced grid on [0.,1.], using @@ -337,53 +256,6 @@ def update_ShareLimit(self): self.ShareLimit = SharePF self.add_to_time_inv("ShareLimit") - def get_Risky(self): - """ - Sets the shock RiskyNow as a single draw from a lognormal distribution. - Uses the attributes RiskyAvgTrue and RiskyStdTrue if RiskyAvg is time-varying, - else just uses the single values from RiskyAvg and RiskyStd. - - Parameters - ---------- - None - - Returns - ------- - None - """ - if "RiskyDstn" in self.time_vary: - RiskyAvg = self.RiskyAvgTrue - RiskyStd = self.RiskyStdTrue - else: - RiskyAvg = self.RiskyAvg - RiskyStd = self.RiskyStd - RiskyAvgSqrd = RiskyAvg ** 2 - RiskyVar = RiskyStd ** 2 - - mu = np.log(RiskyAvg / (np.sqrt(1.0 + RiskyVar / RiskyAvgSqrd))) - sigma = np.sqrt(np.log(1.0 + RiskyVar / RiskyAvgSqrd)) - self.shocks['Risky'] = Lognormal( - mu, sigma, seed=self.RNG.randint(0, 2 ** 31 - 1) - ).draw(1) - - def get_Adjust(self): - """ - Sets the attribute AdjustNow as a boolean array of size AgentCount, indicating - whether each agent is able to adjust their risky portfolio share this period. - Uses the attribute AdjustPrb to draw from a Bernoulli distribution. - - Parameters - ---------- - None - - Returns - ------- - None - """ - self.shocks['Adjust'] = Bernoulli( - self.AdjustPrb, seed=self.RNG.randint(0, 2 ** 31 - 1) - ).draw(self.AgentCount) - def get_Rfree(self): """ Calculates realized return factor for each agent, using the attributes Rfree, @@ -422,8 +294,7 @@ def initialize_sim(self): # these need to be set because "post states", # but are a control variable and shock, respectively self.controls["Share"] = np.zeros(self.AgentCount) - self.shocks['Adjust'] = np.zeros(self.AgentCount, dtype=bool) - IndShockConsumerType.initialize_sim(self) + RiskyAssetConsumerType.initialize_sim(self) def sim_birth(self, which_agents): """ @@ -445,24 +316,6 @@ def sim_birth(self, which_agents): # here a shock is being used as a 'post state' self.shocks['Adjust'][which_agents] = False - def get_shocks(self): - """ - Draw idiosyncratic income shocks, just as for IndShockConsumerType, then draw - a single common value for the risky asset return. Also draws whether each - agent is able to update their risky asset share this period. - - Parameters - ---------- - None - - Returns - ------- - None - """ - IndShockConsumerType.get_shocks(self) - self.get_Risky() - self.get_Adjust() - def get_controls(self): """ Calculates consumption cNrmNow and risky portfolio share ShareNow using From 5274471a1e810b8f2086bd20a3af8ee63a637009 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mateo=20Vel=C3=A1squez-Giraldo?= Date: Thu, 3 Jun 2021 17:16:40 -0500 Subject: [PATCH 3/3] Update CHANGELOG.md --- Documentation/CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/Documentation/CHANGELOG.md b/Documentation/CHANGELOG.md index addf7065e..eace21ebe 100644 --- a/Documentation/CHANGELOG.md +++ b/Documentation/CHANGELOG.md @@ -17,6 +17,7 @@ Release Data: TBD #### Minor Changes * Fix bug in DCEGM's primary kink finder due to numpy no longer accepting NaN in integer arrays [#990](https://github.com/econ-ark/HARK/pull/990). +* Add a general class for consumers who can save using a risky asset [#1012](https://github.com/econ-ark/HARK/pull/1012/). ### 0.11.0