Coverage for HARK/ConsumptionSaving/ConsPortfolioModel.py: 94%
336 statements
« prev ^ index » next coverage.py v7.11.0, created at 2025-11-02 05:14 +0000
« prev ^ index » next coverage.py v7.11.0, created at 2025-11-02 05:14 +0000
1"""
2This file contains classes and functions for representing, solving, and simulating
3agents who must allocate their resources among consumption, saving in a risk-free
4asset (with a low return), and saving in a risky asset (with higher average return).
5"""
7from copy import deepcopy
9import numpy as np
11from HARK import NullFunc
12from HARK.ConsumptionSaving.ConsIndShockModel import (
13 IndShockConsumerType,
14 make_lognormal_pLvl_init_dstn,
15 make_lognormal_kNrm_init_dstn,
16)
17from HARK.Calibration.Assets.AssetProcesses import (
18 make_lognormal_RiskyDstn,
19 combine_IncShkDstn_and_RiskyDstn,
20 calc_ShareLimit_for_CRRA,
21)
22from HARK.Calibration.Income.IncomeProcesses import (
23 construct_lognormal_income_process_unemployment,
24 get_PermShkDstn_from_IncShkDstn,
25 get_TranShkDstn_from_IncShkDstn,
26)
27from HARK.ConsumptionSaving.ConsRiskyAssetModel import (
28 RiskyAssetConsumerType,
29 make_simple_ShareGrid,
30 make_AdjustDstn,
31)
32from HARK.distributions import expected
33from HARK.interpolation import (
34 BilinearInterp,
35 ConstantFunction,
36 CubicInterp,
37 IdentityFunction,
38 LinearInterp,
39 LinearInterpOnInterp1D,
40 MargValueFuncCRRA,
41 ValueFuncCRRA,
42)
43from HARK.metric import MetricObject
44from HARK.rewards import UtilityFuncCRRA
45from HARK.utilities import make_assets_grid
47__all__ = [
48 "PortfolioSolution",
49 "PortfolioConsumerType",
50]
53# Define a class to represent the single period solution of the portfolio choice problem
54class PortfolioSolution(MetricObject):
55 r"""
56 A class for representing the single period solution of the portfolio choice model.
58 Parameters
59 ----------
60 cFuncAdj : Interp1D
61 Consumption function over normalized market resources when the agent is able
62 to adjust their portfolio shares: :math:`c_t=\text{cFuncAdj} (m_t)`.
63 ShareFuncAdj : Interp1D
64 Risky share function over normalized market resources when the agent is able
65 to adjust their portfolio shares: :math:`S_t=\text{ShareFuncAdj} (m_t)`.
66 vFuncAdj : ValueFuncCRRA
67 Value function over normalized market resources when the agent is able to
68 adjust their portfolio shares: :math:`v_t=\text{vFuncAdj} (m_t)`.
69 vPfuncAdj : MargValueFuncCRRA
70 Marginal value function over normalized market resources when the agent is able
71 to adjust their portfolio shares: :math:`v'_t=\text{vPFuncAdj} (m_t)`.
72 cFuncFxd : Interp2D
73 Consumption function over normalized market resources and risky portfolio share
74 when the agent is NOT able to adjust their portfolio shares, so they are fixed:
75 :math:`c_t=\text{cFuncFxd} (m_t,S_t)`.
76 ShareFuncFxd : Interp2D
77 Risky share function over normalized market resources and risky portfolio share
78 when the agent is NOT able to adjust their portfolio shares, so they are fixed.
79 This should always be an IdentityFunc, by definition.
80 vFuncFxd : ValueFuncCRRA
81 Value function over normalized market resources and risky portfolio share when
82 the agent is NOT able to adjust their portfolio shares, so they are fixed:
83 :math:`v_t=\text{vFuncFxd}(m_t,S_t)`.
84 dvdmFuncFxd : MargValueFuncCRRA
85 The derivative of the value function with respect to normalized market
86 resources when the agent is Not able to adjust their portfolio shares,
87 so they are fixed: :math:`\frac{dv_t}{dm_t}=\text{vFuncFxd}(m_t,S_t)`.
88 dvdsFuncFxd : MargValueFuncCRRA
89 The derivative of the value function with respect to risky asset share
90 when the agent is Not able to adjust their portfolio shares,so they are
91 fixed: :math:`\frac{dv_t}{dS_t}=\text{vFuncFxd}(m_t,S_t)`.
92 aGrid: np.array
93 End-of-period-assets grid used to find the solution.
94 Share_adj: np.array
95 Optimal portfolio share associated with each aGrid point: :math:`S^{*}_t=\text{vFuncFxd}(m_t)`.
96 EndOfPrddvda_adj: np.array
97 Marginal value of end-of-period resources associated with each aGrid
98 point.
99 ShareGrid: np.array
100 Grid for the portfolio share that is used to solve the model.
101 EndOfPrddvda_fxd: np.array
102 Marginal value of end-of-period resources associated with each
103 (aGrid x sharegrid) combination, for the agent who can not adjust his
104 portfolio.
105 AdjustPrb: float
106 Probability that the agent will be able to adjust his portfolio
107 next period.
108 """
110 distance_criteria = ["vPfuncAdj"]
112 def __init__(
113 self,
114 cFuncAdj=None,
115 ShareFuncAdj=None,
116 vFuncAdj=None,
117 vPfuncAdj=None,
118 cFuncFxd=None,
119 ShareFuncFxd=None,
120 vFuncFxd=None,
121 dvdmFuncFxd=None,
122 dvdsFuncFxd=None,
123 aGrid=None,
124 Share_adj=None,
125 EndOfPrddvda_adj=None,
126 ShareGrid=None,
127 EndOfPrddvda_fxd=None,
128 EndOfPrddvds_fxd=None,
129 AdjPrb=None,
130 ):
131 # Change any missing function inputs to NullFunc
132 if cFuncAdj is None:
133 cFuncAdj = NullFunc()
134 if cFuncFxd is None:
135 cFuncFxd = NullFunc()
136 if ShareFuncAdj is None:
137 ShareFuncAdj = NullFunc()
138 if ShareFuncFxd is None:
139 ShareFuncFxd = NullFunc()
140 if vFuncAdj is None:
141 vFuncAdj = NullFunc()
142 if vFuncFxd is None:
143 vFuncFxd = NullFunc()
144 if vPfuncAdj is None:
145 vPfuncAdj = NullFunc()
146 if dvdmFuncFxd is None:
147 dvdmFuncFxd = NullFunc()
148 if dvdsFuncFxd is None:
149 dvdsFuncFxd = NullFunc()
151 # Set attributes of self
152 self.cFuncAdj = cFuncAdj
153 self.cFuncFxd = cFuncFxd
154 self.ShareFuncAdj = ShareFuncAdj
155 self.ShareFuncFxd = ShareFuncFxd
156 self.vFuncAdj = vFuncAdj
157 self.vFuncFxd = vFuncFxd
158 self.vPfuncAdj = vPfuncAdj
159 self.dvdmFuncFxd = dvdmFuncFxd
160 self.dvdsFuncFxd = dvdsFuncFxd
161 self.aGrid = aGrid
162 self.Share_adj = Share_adj
163 self.EndOfPrddvda_adj = EndOfPrddvda_adj
164 self.ShareGrid = ShareGrid
165 self.EndOfPrddvda_fxd = EndOfPrddvda_fxd
166 self.EndOfPrddvds_fxd = EndOfPrddvds_fxd
167 self.AdjPrb = AdjPrb
170###############################################################################
173def make_portfolio_solution_terminal(CRRA):
174 """
175 Solves the terminal period of the portfolio choice problem. The solution is
176 trivial, as usual: consume all market resources, and put nothing in the risky
177 asset (because you have nothing anyway).
179 Parameters
180 ----------
181 CRRA : float
182 Coefficient of relative risk aversion.
184 Returns
185 -------
186 solution_terminal : PortfolioSolution
187 Terminal period solution for a consumption-saving problem with portfolio
188 choice and CRRA utility.
189 """
190 # Consume all market resources: c_T = m_T
191 cFuncAdj_terminal = IdentityFunction()
192 cFuncFxd_terminal = IdentityFunction(i_dim=0, n_dims=2)
194 # Risky share is irrelevant-- no end-of-period assets; set to zero
195 ShareFuncAdj_terminal = ConstantFunction(0.0)
196 ShareFuncFxd_terminal = IdentityFunction(i_dim=1, n_dims=2)
198 # Value function is simply utility from consuming market resources
199 vFuncAdj_terminal = ValueFuncCRRA(cFuncAdj_terminal, CRRA)
200 vFuncFxd_terminal = ValueFuncCRRA(cFuncFxd_terminal, CRRA)
202 # Marginal value of market resources is marg utility at the consumption function
203 vPfuncAdj_terminal = MargValueFuncCRRA(cFuncAdj_terminal, CRRA)
204 dvdmFuncFxd_terminal = MargValueFuncCRRA(cFuncFxd_terminal, CRRA)
205 dvdsFuncFxd_terminal = ConstantFunction(0.0) # No future, no marg value of Share
207 # Construct the terminal period solution
208 solution_terminal = PortfolioSolution(
209 cFuncAdj=cFuncAdj_terminal,
210 ShareFuncAdj=ShareFuncAdj_terminal,
211 vFuncAdj=vFuncAdj_terminal,
212 vPfuncAdj=vPfuncAdj_terminal,
213 cFuncFxd=cFuncFxd_terminal,
214 ShareFuncFxd=ShareFuncFxd_terminal,
215 vFuncFxd=vFuncFxd_terminal,
216 dvdmFuncFxd=dvdmFuncFxd_terminal,
217 dvdsFuncFxd=dvdsFuncFxd_terminal,
218 )
219 solution_terminal.hNrm = 0.0
220 solution_terminal.MPCmin = 1.0
221 return solution_terminal
224def calc_radj(shock, share_limit, rfree, crra):
225 """Expected rate of return adjusted by CRRA
227 Args:
228 shock (DiscreteDistribution): Distribution of risky asset returns
229 share_limit (float): limiting lower bound of risky portfolio share
230 rfree (float): Risk free interest rate
231 crra (float): Coefficient of relative risk aversion
232 """
233 rport = share_limit * shock + (1.0 - share_limit) * rfree
234 return rport ** (1.0 - crra)
237def calc_human_wealth(shocks, perm_gro_fac, share_limit, rfree, crra, h_nrm_next):
238 """Calculate human wealth this period given human wealth next period.
240 Args:
241 shocks (DiscreteDistribution): Joint distribution of shocks to income and returns.
242 perm_gro_fac (float): Permanent income growth factor
243 share_limit (float): limiting lower bound of risky portfolio share
244 rfree (float): Risk free interest rate
245 crra (float): Coefficient of relative risk aversion
246 h_nrm_next (float): Human wealth next period
247 """
248 perm_shk_fac = perm_gro_fac * shocks["PermShk"]
249 rport = share_limit * shocks["Risky"] + (1.0 - share_limit) * rfree
250 hNrm = (perm_shk_fac / rport**crra) * (shocks["TranShk"] + h_nrm_next)
251 return hNrm
254def calc_m_nrm_next(shocks, b_nrm, perm_gro_fac):
255 """
256 Calculate future realizations of market resources mNrm from the income
257 shock distribution "shocks" and normalized bank balances b.
258 """
259 return b_nrm / (shocks["PermShk"] * perm_gro_fac) + shocks["TranShk"]
262def calc_dvdx_next(
263 shocks,
264 b_nrm,
265 share,
266 adjust_prob,
267 perm_gro_fac,
268 crra,
269 vp_func_adj,
270 dvdm_func_fxd,
271 dvds_func_fxd,
272):
273 """
274 Evaluate realizations of marginal values next period, based
275 on the income distribution "shocks", values of bank balances bNrm, and values of
276 the risky share z.
277 """
278 m_nrm = calc_m_nrm_next(shocks, b_nrm, perm_gro_fac)
279 dvdm_adj = vp_func_adj(m_nrm)
280 # No marginal value of shockshare if it's a free choice!
281 dvds_adj = np.zeros_like(m_nrm)
283 if adjust_prob < 1.0:
284 # Expand to the same dimensions as mNrm
285 share_exp = np.full_like(m_nrm, share)
286 dvdm_fxd = dvdm_func_fxd(m_nrm, share_exp)
287 dvds_fxd = dvds_func_fxd(m_nrm, share_exp)
288 # Combine by adjustment probability
289 dvdm = adjust_prob * dvdm_adj + (1.0 - adjust_prob) * dvdm_fxd
290 dvds = adjust_prob * dvds_adj + (1.0 - adjust_prob) * dvds_fxd
291 else: # Don't bother evaluating if there's no chance that portfolio share is fixed
292 dvdm = dvdm_adj
293 dvds = dvds_adj
295 perm_shk_fac = shocks["PermShk"] * perm_gro_fac
296 dvdm = perm_shk_fac ** (-crra) * dvdm
297 dvds = perm_shk_fac ** (1.0 - crra) * dvds
299 return dvdm, dvds
302def calc_end_of_prd_dvdx(shocks, a_nrm, share, rfree, dvdb_func, dvds_func):
303 """
304 Compute end-of-period marginal values at values a, conditional
305 on risky asset return shocks and risky share z.
306 """
307 # Calculate future realizations of bank balances bNrm
308 ex_ret = shocks - rfree # Excess returns
309 r_port = rfree + share * ex_ret # Portfolio return
310 b_nrm = r_port * a_nrm
311 # Ensure shape concordance
312 share_exp = np.full_like(b_nrm, share)
314 # Calculate and return dvda, dvds
315 dvda = r_port * dvdb_func(b_nrm, share_exp)
316 dvds = ex_ret * a_nrm * dvdb_func(b_nrm, share_exp) + dvds_func(b_nrm, share_exp)
317 return dvda, dvds
320def calc_v_intermed(
321 shocks, b_nrm, share, adjust_prob, perm_gro_fac, crra, v_func_adj, v_func_fxd
322):
323 """
324 Calculate "intermediate" value from next period's bank balances, the
325 income shocks shocks, and the risky asset share.
326 """
327 m_nrm = calc_m_nrm_next(shocks, b_nrm, perm_gro_fac)
329 v_adj = v_func_adj(m_nrm)
330 if adjust_prob < 1.0:
331 v_fxd = v_func_fxd(m_nrm, share)
332 # Combine by adjustment probability
333 v_next = adjust_prob * v_adj + (1.0 - adjust_prob) * v_fxd
334 else: # Don't bother evaluating if there's no chance that portfolio share is fixed
335 v_next = v_adj
337 v_intermed = (shocks["PermShk"] * perm_gro_fac) ** (1.0 - crra) * v_next
338 return v_intermed
341def calc_end_of_prd_v(shocks, a_nrm, share, rfree, v_func):
342 """Compute end-of-period values."""
343 # Calculate future realizations of bank balances bNrm
344 ex_ret = shocks - rfree
345 r_port = rfree + share * ex_ret
346 b_rnm = r_port * a_nrm
348 # Make an extended share_next of the same dimension as b_nrm so
349 # that the function can be vectorized
350 share_exp = np.full_like(b_rnm, share)
352 return v_func(b_rnm, share_exp)
355def calc_m_nrm_next_joint(shocks, a_nrm, share, rfree, perm_gro_fac):
356 """
357 Calculate future realizations of market resources mNrm from the shock
358 distribution shocks, normalized end-of-period assets a, and risky share z.
359 """
360 # Calculate future realizations of bank balances bNrm
361 ex_ret = shocks["Risky"] - rfree
362 r_port = rfree + share * ex_ret
363 b_nrm = r_port * a_nrm
364 return b_nrm / (shocks["PermShk"] * perm_gro_fac) + shocks["TranShk"]
367def calc_end_of_prd_dvdx_joint(
368 shocks,
369 a_nrm,
370 share,
371 rfree,
372 adjust_prob,
373 perm_gro_fac,
374 crra,
375 vp_func_adj,
376 dvdm_func_fxd,
377 dvds_func_fxd,
378):
379 """
380 Evaluate end-of-period marginal value of assets and risky share based
381 on the shock distribution S, values of bend of period assets a, and
382 risky share z.
383 """
384 m_nrm = calc_m_nrm_next_joint(shocks, a_nrm, share, rfree, perm_gro_fac)
385 ex_ret = shocks["Risky"] - rfree
386 r_port = rfree + share * ex_ret
387 dvdm_adj = vp_func_adj(m_nrm)
388 # No marginal value of Share if it's a free choice!
389 dvds_adj = np.zeros_like(m_nrm)
391 if adjust_prob < 1.0:
392 # Expand to the same dimensions as mNrm
393 share_exp = np.full_like(m_nrm, share)
394 dvdm_fxd = dvdm_func_fxd(m_nrm, share_exp)
395 dvds_fxd = dvds_func_fxd(m_nrm, share_exp)
396 # Combine by adjustment probability
397 dvdm_next = adjust_prob * dvdm_adj + (1.0 - adjust_prob) * dvdm_fxd
398 dvds_next = adjust_prob * dvds_adj + (1.0 - adjust_prob) * dvds_fxd
399 else: # Don't bother evaluating if there's no chance that portfolio share is fixed
400 dvdm_next = dvdm_adj
401 dvds_next = dvds_adj
403 perm_shk_fac = shocks["PermShk"] * perm_gro_fac
404 temp_fac = perm_shk_fac ** (-crra) * dvdm_next
405 eop_dvda = r_port * temp_fac
406 eop_dvds = ex_ret * a_nrm * temp_fac + perm_shk_fac ** (1 - crra) * dvds_next
408 return eop_dvda, eop_dvds
411def calc_end_of_prd_v_joint(
412 shocks, a_nrm, share, rfree, adjust_prob, perm_gro_fac, crra, v_func_adj, v_func_fxd
413):
414 """
415 Evaluate end-of-period value, based on the shock distribution S, values
416 of bank balances bNrm, and values of the risky share z.
417 """
418 m_nrm = calc_m_nrm_next_joint(shocks, a_nrm, share, rfree, perm_gro_fac)
419 v_adj = v_func_adj(m_nrm)
421 if adjust_prob < 1.0:
422 # Expand to the same dimensions as mNrm
423 share_exp = np.full_like(m_nrm, share)
424 v_fxd = v_func_fxd(m_nrm, share_exp)
425 # Combine by adjustment probability
426 v_next = adjust_prob * v_adj + (1.0 - adjust_prob) * v_fxd
427 else: # Don't bother evaluating if there's no chance that portfolio share is fixed
428 v_next = v_adj
430 return (shocks["PermShk"] * perm_gro_fac) ** (1.0 - crra) * v_next
433def solve_one_period_ConsPortfolio(
434 solution_next,
435 ShockDstn,
436 IncShkDstn,
437 RiskyDstn,
438 LivPrb,
439 DiscFac,
440 CRRA,
441 Rfree,
442 PermGroFac,
443 BoroCnstArt,
444 aXtraGrid,
445 ShareGrid,
446 AdjustPrb,
447 ShareLimit,
448 vFuncBool,
449 DiscreteShareBool,
450 IndepDstnBool,
451):
452 """
453 Solve one period of a consumption-saving problem with portfolio allocation
454 between a riskless and risky asset. This function handles various sub-cases
455 or variations on the problem, including the possibility that the agent does
456 not necessarily get to update their portfolio share in every period, or that
457 they must choose a discrete rather than continuous risky share.
459 Parameters
460 ----------
461 solution_next : PortfolioSolution
462 Solution to next period's problem.
463 ShockDstn : Distribution
464 Joint distribution of permanent income shocks, transitory income shocks,
465 and risky returns. This is only used if the input IndepDstnBool is False,
466 indicating that income and return distributions can't be assumed to be
467 independent.
468 IncShkDstn : Distribution
469 Discrete distribution of permanent income shocks and transitory income
470 shocks. This is only used if the input IndepDstnBool is True, indicating
471 that income and return distributions are independent.
472 RiskyDstn : Distribution
473 Distribution of risky asset returns. This is only used if the input
474 IndepDstnBool is True, indicating that income and return distributions
475 are independent.
476 LivPrb : float
477 Survival probability; likelihood of being alive at the beginning of
478 the succeeding period.
479 DiscFac : float
480 Intertemporal discount factor for future utility.
481 CRRA : float
482 Coefficient of relative risk aversion.
483 Rfree : float
484 Risk free interest factor on end-of-period assets.
485 PermGroFac : float
486 Expected permanent income growth factor at the end of this period.
487 BoroCnstArt: float or None
488 Borrowing constraint for the minimum allowable assets to end the
489 period with. In this model, it is *required* to be zero.
490 aXtraGrid: np.array
491 Array of "extra" end-of-period asset values-- assets above the
492 absolute minimum acceptable level.
493 ShareGrid : np.array
494 Array of risky portfolio shares on which to define the interpolation
495 of the consumption function when Share is fixed. Also used when the
496 risky share choice is specified as discrete rather than continuous.
497 AdjustPrb : float
498 Probability that the agent will be able to update his portfolio share.
499 ShareLimit : float
500 Limiting lower bound of risky portfolio share as mNrm approaches infinity.
501 vFuncBool: boolean
502 An indicator for whether the value function should be computed and
503 included in the reported solution.
504 DiscreteShareBool : bool
505 Indicator for whether risky portfolio share should be optimized on the
506 continuous [0,1] interval using the FOC (False), or instead only selected
507 from the discrete set of values in ShareGrid (True). If True, then
508 vFuncBool must also be True.
509 IndepDstnBool : bool
510 Indicator for whether the income and risky return distributions are in-
511 dependent of each other, which can speed up the expectations step.
513 Returns
514 -------
515 solution_now : PortfolioSolution
516 Solution to this period's problem.
517 """
518 # Make sure the individual is liquidity constrained. Allowing a consumer to
519 # borrow *and* invest in an asset with unbounded (negative) returns is a bad mix.
520 if BoroCnstArt != 0.0:
521 raise ValueError("PortfolioConsumerType must have BoroCnstArt=0.0!")
523 # Make sure that if risky portfolio share is optimized only discretely, then
524 # the value function is also constructed (else this task would be impossible).
525 if DiscreteShareBool and (not vFuncBool):
526 raise ValueError(
527 "PortfolioConsumerType requires vFuncBool to be True when DiscreteShareBool is True!"
528 )
530 # Define the current period utility function and effective discount factor
531 uFunc = UtilityFuncCRRA(CRRA)
532 DiscFacEff = DiscFac * LivPrb # "effective" discount factor
534 # Unpack next period's solution for easier access
535 vPfuncAdj_next = solution_next.vPfuncAdj
536 dvdmFuncFxd_next = solution_next.dvdmFuncFxd
537 dvdsFuncFxd_next = solution_next.dvdsFuncFxd
538 vFuncAdj_next = solution_next.vFuncAdj
539 vFuncFxd_next = solution_next.vFuncFxd
541 # Set a flag for whether the natural borrowing constraint is zero, which
542 # depends on whether the smallest transitory income shock is zero
543 BoroCnstNat_iszero = np.min(IncShkDstn.atoms[1]) == 0.0
545 # Prepare to calculate end-of-period marginal values by creating an array
546 # of market resources that the agent could have next period, considering
547 # the grid of end-of-period assets and the distribution of shocks he might
548 # experience next period.
550 # Unpack the risky return shock distribution
551 Risky_next = RiskyDstn.atoms
552 RiskyMax = np.max(Risky_next)
553 RiskyMin = np.min(Risky_next)
555 # Perform an alternate calculation of the absolute patience factor when
556 # returns are risky. This uses the Merton-Samuelson limiting risky share,
557 # which is what's relevant as mNrm goes to infinity.
559 R_adj = expected(calc_radj, RiskyDstn, args=(ShareLimit, Rfree, CRRA))[0]
560 PatFac = (DiscFacEff * R_adj) ** (1.0 / CRRA)
561 MPCminNow = 1.0 / (1.0 + PatFac / solution_next.MPCmin)
563 # Also perform an alternate calculation for human wealth under risky returns
565 # This correctly accounts for risky returns and risk aversion
566 hNrmNow = (
567 expected(
568 calc_human_wealth,
569 ShockDstn,
570 args=(PermGroFac, ShareLimit, Rfree, CRRA, solution_next.hNrm),
571 )
572 / R_adj
573 )
575 # Set the terms of the limiting linear consumption function as mNrm goes to infinity
576 cFuncLimitIntercept = MPCminNow * hNrmNow
577 cFuncLimitSlope = MPCminNow
579 # bNrm represents R*a, balances after asset return shocks but before income.
580 # This just uses the highest risky return as a rough shifter for the aXtraGrid.
581 if BoroCnstNat_iszero:
582 aNrmGrid = aXtraGrid
583 bNrmGrid = np.insert(RiskyMax * aXtraGrid, 0, RiskyMin * aXtraGrid[0])
584 else:
585 # Add an asset point at exactly zero
586 aNrmGrid = np.insert(aXtraGrid, 0, 0.0)
587 bNrmGrid = RiskyMax * np.insert(aXtraGrid, 0, 0.0)
589 # Get grid and shock sizes, for easier indexing
590 aNrmCount = aNrmGrid.size
591 ShareCount = ShareGrid.size
593 # If the income shock distribution is independent from the risky return distribution,
594 # then taking end-of-period expectations can proceed in a two part process: First,
595 # construct an "intermediate" value function by integrating out next period's income
596 # shocks, *then* compute end-of-period expectations by integrating out return shocks.
597 # This method is lengthy to code, but can be significantly faster.
598 if IndepDstnBool:
599 # Make tiled arrays to calculate future realizations of mNrm and Share when integrating over IncShkDstn
600 bNrmNext, ShareNext = np.meshgrid(bNrmGrid, ShareGrid, indexing="ij")
602 # Define functions that are used internally to evaluate future realizations
604 # Calculate end-of-period marginal value of assets and shares at each point
605 # in aNrm and ShareGrid. Does so by taking expectation of next period marginal
606 # values across income and risky return shocks.
608 # Calculate intermediate marginal value of bank balances and risky portfolio share
609 # by taking expectations over income shocks
611 dvdb_intermed, dvds_intermed = expected(
612 calc_dvdx_next,
613 IncShkDstn,
614 args=(
615 bNrmNext,
616 ShareNext,
617 AdjustPrb,
618 PermGroFac,
619 CRRA,
620 vPfuncAdj_next,
621 dvdmFuncFxd_next,
622 dvdsFuncFxd_next,
623 ),
624 )
626 dvdbNvrs_intermed = uFunc.derinv(dvdb_intermed, order=(1, 0))
627 dvdbNvrsFunc_intermed = BilinearInterp(dvdbNvrs_intermed, bNrmGrid, ShareGrid)
628 dvdbFunc_intermed = MargValueFuncCRRA(dvdbNvrsFunc_intermed, CRRA)
630 dvdsFunc_intermed = BilinearInterp(dvds_intermed, bNrmGrid, ShareGrid)
632 # Make tiled arrays to calculate future realizations of bNrm and Share when integrating over RiskyDstn
633 aNrmNow, ShareNext = np.meshgrid(aNrmGrid, ShareGrid, indexing="ij")
635 # Define functions for calculating end-of-period marginal value
637 # Evaluate realizations of value and marginal value after asset returns are realized
639 # Calculate end-of-period marginal value of assets and risky portfolio share
640 # by taking expectations
642 EndOfPrd_dvda, EndOfPrd_dvds = DiscFacEff * expected(
643 calc_end_of_prd_dvdx,
644 RiskyDstn,
645 args=(aNrmNow, ShareNext, Rfree, dvdbFunc_intermed, dvdsFunc_intermed),
646 )
648 EndOfPrd_dvdaNvrs = uFunc.derinv(EndOfPrd_dvda)
650 # Make the end-of-period value function if the value function is requested
651 if vFuncBool:
652 # Calculate intermediate value by taking expectations over income shocks
653 v_intermed = expected(
654 calc_v_intermed,
655 IncShkDstn,
656 args=(
657 bNrmNext,
658 ShareNext,
659 AdjustPrb,
660 PermGroFac,
661 CRRA,
662 vFuncAdj_next,
663 vFuncFxd_next,
664 ),
665 )
667 # Construct the "intermediate value function" for this period
668 vNvrs_intermed = uFunc.inv(v_intermed)
669 vNvrsFunc_intermed = BilinearInterp(vNvrs_intermed, bNrmGrid, ShareGrid)
670 vFunc_intermed = ValueFuncCRRA(vNvrsFunc_intermed, CRRA)
672 # Calculate end-of-period value by taking expectations
673 EndOfPrd_v = DiscFacEff * expected(
674 calc_end_of_prd_v,
675 RiskyDstn,
676 args=(aNrmNow, ShareNext, Rfree, vFunc_intermed),
677 )
678 EndOfPrd_vNvrs = uFunc.inv(EndOfPrd_v)
680 # Now make an end-of-period value function over aNrm and Share
681 EndOfPrd_vNvrsFunc = BilinearInterp(EndOfPrd_vNvrs, aNrmGrid, ShareGrid)
682 EndOfPrd_vFunc = ValueFuncCRRA(EndOfPrd_vNvrsFunc, CRRA)
683 # This will be used later to make the value function for this period
685 # If the income shock distribution and risky return distribution are *NOT*
686 # independent, then computation of end-of-period expectations are simpler in
687 # code, but might take longer to execute
688 else:
689 # Make tiled arrays to calculate future realizations of mNrm and Share when integrating over IncShkDstn
690 aNrmNow, ShareNext = np.meshgrid(aNrmGrid, ShareGrid, indexing="ij")
692 # Define functions that are used internally to evaluate future realizations
694 # Evaluate realizations of value and marginal value after asset returns are realized
696 # Calculate end-of-period marginal value of assets and risky share by taking expectations
697 EndOfPrd_dvda, EndOfPrd_dvds = DiscFacEff * expected(
698 calc_end_of_prd_dvdx_joint,
699 ShockDstn,
700 args=(
701 aNrmNow,
702 ShareNext,
703 Rfree,
704 AdjustPrb,
705 PermGroFac,
706 CRRA,
707 vPfuncAdj_next,
708 dvdmFuncFxd_next,
709 dvdsFuncFxd_next,
710 ),
711 )
712 EndOfPrd_dvdaNvrs = uFunc.derinv(EndOfPrd_dvda)
714 # Construct the end-of-period value function if requested
715 if vFuncBool:
716 # Calculate end-of-period value, its derivative, and their pseudo-inverse
717 EndOfPrd_v = DiscFacEff * expected(
718 calc_end_of_prd_v_joint,
719 ShockDstn,
720 args=(
721 aNrmNow,
722 ShareNext,
723 Rfree,
724 AdjustPrb,
725 PermGroFac,
726 CRRA,
727 vFuncAdj_next,
728 vFuncFxd_next,
729 ),
730 )
731 EndOfPrd_vNvrs = uFunc.inv(EndOfPrd_v)
733 # value transformed through inverse utility
734 EndOfPrd_vNvrsP = EndOfPrd_dvda * uFunc.derinv(EndOfPrd_v, order=(0, 1))
736 # Construct the end-of-period value function
737 EndOfPrd_vNvrsFunc_by_Share = []
738 for j in range(ShareCount):
739 EndOfPrd_vNvrsFunc_by_Share.append(
740 CubicInterp(
741 aNrmNow[:, j], EndOfPrd_vNvrs[:, j], EndOfPrd_vNvrsP[:, j]
742 )
743 )
744 EndOfPrd_vNvrsFunc = LinearInterpOnInterp1D(
745 EndOfPrd_vNvrsFunc_by_Share, ShareGrid
746 )
747 EndOfPrd_vFunc = ValueFuncCRRA(EndOfPrd_vNvrsFunc, CRRA)
749 # Find the optimal risky asset share either by choosing the best value among
750 # the discrete grid choices, or by satisfying the FOC with equality (continuous)
751 if DiscreteShareBool:
752 # If we're restricted to discrete choices, then portfolio share is
753 # the one with highest value for each aNrm gridpoint
754 opt_idx = np.argmax(EndOfPrd_v, axis=1)
755 ShareAdj_now = ShareGrid[opt_idx]
757 # Take cNrm at that index as well... and that's it!
758 cNrmAdj_now = EndOfPrd_dvdaNvrs[np.arange(aNrmCount), opt_idx]
760 else:
761 # Now find the optimal (continuous) risky share on [0,1] by solving the first
762 # order condition EndOfPrd_dvds == 0.
763 FOC_s = EndOfPrd_dvds # Relabel for convenient typing
765 # For each value of aNrm, find the value of Share such that FOC_s == 0
766 crossing = np.logical_and(FOC_s[:, 1:] <= 0.0, FOC_s[:, :-1] >= 0.0)
767 share_idx = np.argmax(crossing, axis=1)
768 # This represents the index of the segment of the share grid where dvds flips
769 # from positive to negative, indicating that there's a zero *on* the segment
771 # Calculate the fractional distance between those share gridpoints where the
772 # zero should be found, assuming a linear function; call it alpha
773 a_idx = np.arange(aNrmCount)
774 bot_s = ShareGrid[share_idx]
775 top_s = ShareGrid[share_idx + 1]
776 bot_f = FOC_s[a_idx, share_idx]
777 top_f = FOC_s[a_idx, share_idx + 1]
778 bot_c = EndOfPrd_dvdaNvrs[a_idx, share_idx]
779 top_c = EndOfPrd_dvdaNvrs[a_idx, share_idx + 1]
780 alpha = 1.0 - top_f / (top_f - bot_f)
782 # Calculate the continuous optimal risky share and optimal consumption
783 ShareAdj_now = (1.0 - alpha) * bot_s + alpha * top_s
784 cNrmAdj_now = (1.0 - alpha) * bot_c + alpha * top_c
786 # If agent wants to put more than 100% into risky asset, he is constrained.
787 # Likewise if he wants to put less than 0% into risky asset, he is constrained.
788 constrained_top = FOC_s[:, -1] > 0.0
789 constrained_bot = FOC_s[:, 0] < 0.0
791 # Apply those constraints to both risky share and consumption (but lower
792 # constraint should never be relevant)
793 ShareAdj_now[constrained_top] = 1.0
794 ShareAdj_now[constrained_bot] = 0.0
795 cNrmAdj_now[constrained_top] = EndOfPrd_dvdaNvrs[constrained_top, -1]
796 cNrmAdj_now[constrained_bot] = EndOfPrd_dvdaNvrs[constrained_bot, 0]
798 # When the natural borrowing constraint is *not* zero, then aNrm=0 is in the
799 # grid, but there's no way to "optimize" the portfolio if a=0, and consumption
800 # can't depend on the risky share if it doesn't meaningfully exist. Apply
801 # a small fix to the bottom gridpoint (aNrm=0) when this happens.
802 if not BoroCnstNat_iszero:
803 ShareAdj_now[0] = 1.0
804 cNrmAdj_now[0] = EndOfPrd_dvdaNvrs[0, -1]
806 # Construct functions characterizing the solution for this period
808 # Calculate the endogenous mNrm gridpoints when the agent adjusts his portfolio,
809 # then construct the consumption function when the agent can adjust his share
810 mNrmAdj_now = np.insert(aNrmGrid + cNrmAdj_now, 0, 0.0)
811 cNrmAdj_now = np.insert(cNrmAdj_now, 0, 0.0)
812 cFuncAdj_now = LinearInterp(mNrmAdj_now, cNrmAdj_now)
814 # Construct the marginal value (of mNrm) function when the agent can adjust
815 vPfuncAdj_now = MargValueFuncCRRA(cFuncAdj_now, CRRA)
817 # Construct the consumption function when the agent *can't* adjust the risky
818 # share, as well as the marginal value of Share function
819 cFuncFxd_by_Share = []
820 dvdsFuncFxd_by_Share = []
821 for j in range(ShareCount):
822 cNrmFxd_temp = np.insert(EndOfPrd_dvdaNvrs[:, j], 0, 0.0)
823 mNrmFxd_temp = np.insert(aNrmGrid + cNrmFxd_temp[1:], 0, 0.0)
824 dvdsFxd_temp = np.insert(EndOfPrd_dvds[:, j], 0, EndOfPrd_dvds[0, j])
825 cFuncFxd_by_Share.append(LinearInterp(mNrmFxd_temp, cNrmFxd_temp))
826 dvdsFuncFxd_by_Share.append(LinearInterp(mNrmFxd_temp, dvdsFxd_temp))
827 cFuncFxd_now = LinearInterpOnInterp1D(cFuncFxd_by_Share, ShareGrid)
828 dvdsFuncFxd_now = LinearInterpOnInterp1D(dvdsFuncFxd_by_Share, ShareGrid)
830 # The share function when the agent can't adjust his portfolio is trivial
831 ShareFuncFxd_now = IdentityFunction(i_dim=1, n_dims=2)
833 # Construct the marginal value of mNrm function when the agent can't adjust his share
834 dvdmFuncFxd_now = MargValueFuncCRRA(cFuncFxd_now, CRRA)
836 # Construct the optimal risky share function when adjusting is possible.
837 # The interpolation method depends on whether the choice is discrete or continuous.
838 if DiscreteShareBool:
839 # If the share choice is discrete, the "interpolated" share function acts
840 # like a step function, with jumps at the midpoints of mNrm gridpoints.
841 # Because an actual step function would break our (assumed continuous) linear
842 # interpolator, there's a *tiny* region with extremely high slope.
843 mNrmAdj_mid = (mNrmAdj_now[2:] + mNrmAdj_now[1:-1]) / 2
844 mNrmAdj_plus = mNrmAdj_mid * (1.0 + 1e-12)
845 mNrmAdj_comb = (np.transpose(np.vstack((mNrmAdj_mid, mNrmAdj_plus)))).flatten()
846 mNrmAdj_comb = np.append(np.insert(mNrmAdj_comb, 0, 0.0), mNrmAdj_now[-1])
847 Share_comb = (np.transpose(np.vstack((ShareAdj_now, ShareAdj_now)))).flatten()
848 ShareFuncAdj_now = LinearInterp(mNrmAdj_comb, Share_comb)
850 else:
851 # If the share choice is continuous, just make an ordinary interpolating function
852 if BoroCnstNat_iszero:
853 Share_lower_bound = ShareLimit
854 else:
855 Share_lower_bound = 1.0
856 ShareAdj_now = np.insert(ShareAdj_now, 0, Share_lower_bound)
857 ShareFuncAdj_now = LinearInterp(mNrmAdj_now, ShareAdj_now, ShareLimit, 0.0)
859 # Add the value function if requested
860 if vFuncBool:
861 # Create the value functions for this period, defined over market resources
862 # mNrm when agent can adjust his portfolio, and over market resources and
863 # fixed share when agent can not adjust his portfolio.
865 # Construct the value function when the agent can adjust his portfolio
866 mNrm_temp = aXtraGrid # Just use aXtraGrid as our grid of mNrm values
867 cNrm_temp = cFuncAdj_now(mNrm_temp)
868 aNrm_temp = np.maximum(mNrm_temp - cNrm_temp, 0.0) # Fix tiny violations
869 Share_temp = ShareFuncAdj_now(mNrm_temp)
870 v_temp = uFunc(cNrm_temp) + EndOfPrd_vFunc(aNrm_temp, Share_temp)
871 vNvrs_temp = uFunc.inv(v_temp)
872 vNvrsP_temp = uFunc.der(cNrm_temp) * uFunc.inverse(v_temp, order=(0, 1))
873 vNvrsFuncAdj = CubicInterp(
874 np.insert(mNrm_temp, 0, 0.0), # x_list
875 np.insert(vNvrs_temp, 0, 0.0), # f_list
876 np.insert(vNvrsP_temp, 0, vNvrsP_temp[0]), # dfdx_list
877 )
878 # Re-curve the pseudo-inverse value function
879 vFuncAdj_now = ValueFuncCRRA(vNvrsFuncAdj, CRRA)
881 # Construct the value function when the agent *can't* adjust his portfolio
882 mNrm_temp, Share_temp = np.meshgrid(aXtraGrid, ShareGrid)
883 cNrm_temp = cFuncFxd_now(mNrm_temp, Share_temp)
884 aNrm_temp = mNrm_temp - cNrm_temp
885 v_temp = uFunc(cNrm_temp) + EndOfPrd_vFunc(aNrm_temp, Share_temp)
886 vNvrs_temp = uFunc.inv(v_temp)
887 vNvrsP_temp = uFunc.der(cNrm_temp) * uFunc.inverse(v_temp, order=(0, 1))
888 vNvrsFuncFxd_by_Share = []
889 for j in range(ShareCount):
890 vNvrsFuncFxd_by_Share.append(
891 CubicInterp(
892 np.insert(mNrm_temp[:, 0], 0, 0.0), # x_list
893 np.insert(vNvrs_temp[:, j], 0, 0.0), # f_list
894 np.insert(vNvrsP_temp[:, j], 0, vNvrsP_temp[j, 0]), # dfdx_list
895 )
896 )
897 vNvrsFuncFxd = LinearInterpOnInterp1D(vNvrsFuncFxd_by_Share, ShareGrid)
898 vFuncFxd_now = ValueFuncCRRA(vNvrsFuncFxd, CRRA)
900 else: # If vFuncBool is False, fill in dummy values
901 vFuncAdj_now = NullFunc()
902 vFuncFxd_now = NullFunc()
904 # Package and return the solution
905 solution_now = PortfolioSolution(
906 cFuncAdj=cFuncAdj_now,
907 ShareFuncAdj=ShareFuncAdj_now,
908 vPfuncAdj=vPfuncAdj_now,
909 vFuncAdj=vFuncAdj_now,
910 cFuncFxd=cFuncFxd_now,
911 ShareFuncFxd=ShareFuncFxd_now,
912 dvdmFuncFxd=dvdmFuncFxd_now,
913 dvdsFuncFxd=dvdsFuncFxd_now,
914 vFuncFxd=vFuncFxd_now,
915 AdjPrb=AdjustPrb,
916 )
917 solution_now.hNrm = hNrmNow
918 solution_now.MPCmin = MPCminNow
919 return solution_now
922###############################################################################
924# Make a dictionary of constructors for the portfolio choice consumer type
925PortfolioConsumerType_constructors_default = {
926 "IncShkDstn": construct_lognormal_income_process_unemployment,
927 "PermShkDstn": get_PermShkDstn_from_IncShkDstn,
928 "TranShkDstn": get_TranShkDstn_from_IncShkDstn,
929 "aXtraGrid": make_assets_grid,
930 "RiskyDstn": make_lognormal_RiskyDstn,
931 "ShockDstn": combine_IncShkDstn_and_RiskyDstn,
932 "ShareLimit": calc_ShareLimit_for_CRRA,
933 "ShareGrid": make_simple_ShareGrid,
934 "AdjustDstn": make_AdjustDstn,
935 "kNrmInitDstn": make_lognormal_kNrm_init_dstn,
936 "pLvlInitDstn": make_lognormal_pLvl_init_dstn,
937 "solution_terminal": make_portfolio_solution_terminal,
938}
940# Make a dictionary with parameters for the default constructor for kNrmInitDstn
941PortfolioConsumerType_kNrmInitDstn_default = {
942 "kLogInitMean": -12.0, # Mean of log initial capital
943 "kLogInitStd": 0.0, # Stdev of log initial capital
944 "kNrmInitCount": 15, # Number of points in initial capital discretization
945}
947# Make a dictionary with parameters for the default constructor for pLvlInitDstn
948PortfolioConsumerType_pLvlInitDstn_default = {
949 "pLogInitMean": 0.0, # Mean of log permanent income
950 "pLogInitStd": 0.0, # Stdev of log permanent income
951 "pLvlInitCount": 15, # Number of points in initial capital discretization
952}
954# Default parameters to make IncShkDstn using construct_lognormal_income_process_unemployment
955PortfolioConsumerType_IncShkDstn_default = {
956 "PermShkStd": [0.1], # Standard deviation of log permanent income shocks
957 "PermShkCount": 7, # Number of points in discrete approximation to permanent income shocks
958 "TranShkStd": [0.1], # Standard deviation of log transitory income shocks
959 "TranShkCount": 7, # Number of points in discrete approximation to transitory income shocks
960 "UnempPrb": 0.05, # Probability of unemployment while working
961 "IncUnemp": 0.3, # Unemployment benefits replacement rate while working
962 "T_retire": 0, # Period of retirement (0 --> no retirement)
963 "UnempPrbRet": 0.005, # Probability of "unemployment" while retired
964 "IncUnempRet": 0.0, # "Unemployment" benefits when retired
965}
967# Default parameters to make aXtraGrid using make_assets_grid
968PortfolioConsumerType_aXtraGrid_default = {
969 "aXtraMin": 0.001, # Minimum end-of-period "assets above minimum" value
970 "aXtraMax": 100, # Maximum end-of-period "assets above minimum" value
971 "aXtraNestFac": 1, # Exponential nesting factor for aXtraGrid
972 "aXtraCount": 200, # Number of points in the grid of "assets above minimum"
973 "aXtraExtra": None, # Additional other values to add in grid (optional)
974}
976# Default parameters to make RiskyDstn with make_lognormal_RiskyDstn (and uniform ShareGrid)
977PortfolioConsumerType_RiskyDstn_default = {
978 "RiskyAvg": 1.08, # Mean return factor of risky asset
979 "RiskyStd": 0.18362634887, # Stdev of log returns on risky asset
980 "RiskyCount": 5, # Number of integration nodes to use in approximation of risky returns
981}
982PortfolioConsumerType_ShareGrid_default = {
983 "ShareCount": 25 # Number of discrete points in the risky share approximation
984}
986# Make a dictionary to specify a risky asset consumer type
987PortfolioConsumerType_solving_default = {
988 # BASIC HARK PARAMETERS REQUIRED TO SOLVE THE MODEL
989 "cycles": 1, # Finite, non-cyclic model
990 "T_cycle": 1, # Number of periods in the cycle for this agent type
991 "constructors": PortfolioConsumerType_constructors_default, # See dictionary above
992 # PRIMITIVE RAW PARAMETERS REQUIRED TO SOLVE THE MODEL
993 "CRRA": 5.0, # Coefficient of relative risk aversion
994 "Rfree": [1.03], # Return factor on risk free asset
995 "DiscFac": 0.90, # Intertemporal discount factor
996 "LivPrb": [0.98], # Survival probability after each period
997 "PermGroFac": [1.01], # Permanent income growth factor
998 "BoroCnstArt": 0.0, # Artificial borrowing constraint
999 "DiscreteShareBool": False, # Whether risky asset share is restricted to discrete values
1000 "PortfolioBool": True, # This *must* be set to True; only exists because of inheritance
1001 "PortfolioBisect": False, # What does this do?
1002 "IndepDstnBool": True, # Whether return and income shocks are independent
1003 "vFuncBool": False, # Whether to calculate the value function during solution
1004 "CubicBool": False, # Whether to use cubic spline interpolation when True
1005 # (Uses linear spline interpolation for cFunc when False)
1006 "AdjustPrb": 1.0, # Probability that the agent can update their risky portfolio share each period
1007 "RiskyShareFixed": None, # This does nothing in this model; only exists because of inheritance
1008 "sim_common_Rrisky": True, # Whether risky returns have a shared/common value across agents
1009}
1010PortfolioConsumerType_simulation_default = {
1011 # PARAMETERS REQUIRED TO SIMULATE THE MODEL
1012 "AgentCount": 10000, # Number of agents of this type
1013 "T_age": None, # Age after which simulated agents are automatically killed
1014 "PermGroFacAgg": 1.0, # Aggregate permanent income growth factor
1015 # (The portion of PermGroFac attributable to aggregate productivity growth)
1016 "NewbornTransShk": False, # Whether Newborns have transitory shock
1017 # ADDITIONAL OPTIONAL PARAMETERS
1018 "PerfMITShk": False, # Do Perfect Foresight MIT Shock
1019 # (Forces Newborns to follow solution path of the agent they replaced if True)
1020 "neutral_measure": False, # Whether to use permanent income neutral measure (see Harmenberg 2021)
1021}
1022PortfolioConsumerType_default = {}
1023PortfolioConsumerType_default.update(PortfolioConsumerType_solving_default)
1024PortfolioConsumerType_default.update(PortfolioConsumerType_simulation_default)
1025PortfolioConsumerType_default.update(PortfolioConsumerType_kNrmInitDstn_default)
1026PortfolioConsumerType_default.update(PortfolioConsumerType_pLvlInitDstn_default)
1027PortfolioConsumerType_default.update(PortfolioConsumerType_aXtraGrid_default)
1028PortfolioConsumerType_default.update(PortfolioConsumerType_ShareGrid_default)
1029PortfolioConsumerType_default.update(PortfolioConsumerType_IncShkDstn_default)
1030PortfolioConsumerType_default.update(PortfolioConsumerType_RiskyDstn_default)
1031init_portfolio = PortfolioConsumerType_default
1034class PortfolioConsumerType(RiskyAssetConsumerType):
1035 r"""
1036 A consumer type based on IndShockRiskyAssetConsumerType, with portfolio optimization.
1037 The agent is only able to change their risky asset share with a certain probability.
1039 .. math::
1040 \newcommand{\CRRA}{\rho}
1041 \newcommand{\DiePrb}{\mathsf{D}}
1042 \newcommand{\PermGroFac}{\Gamma}
1043 \newcommand{\Rfree}{\mathsf{R}}
1044 \newcommand{\DiscFac}{\beta}
1045 \begin{align*}
1046 v_t(m_t,S_t) &= \max_{c_t,S^{*}_t} u(c_t) + \DiscFac (1-\DiePrb_{t+1}) \mathbb{E}_{t} \left[(\PermGroFac_{t+1}\psi_{t+1})^{1-\CRRA} v_{t+1}(m_{t+1},S_{t+1}) \right], \\
1047 & \text{s.t.} \\
1048 a_t &= m_t - c_t, \\
1049 a_t &\geq \underline{a}, \\
1050 m_{t+1} &= \mathsf{R}_{t+1}/(\PermGroFac_{t+1} \psi_{t+1}) a_t + \theta_{t+1}, \\
1051 \mathsf{R}_{t+1} &=S_t\phi_{t+1}\mathbf{R}_{t+1}+ (1-S_t)\mathsf{R}_{t+1}, \\
1052 S_{t+1} &= \begin{cases}
1053 S^{*}_t & \text{if } p_t < \wp\\
1054 S_t & \text{if } p_t \geq \wp,
1055 \end{cases}\\
1056 (\psi_{t+1},\theta_{t+1},\phi_{t+1},p_t) &\sim F_{t+1}, \\
1057 \mathbb{E}[\psi]=\mathbb{E}[\theta] &= 1.\\
1058 u(c) &= \frac{c^{1-\CRRA}}{1-\CRRA} \\
1059 \end{align*}
1062 Constructors
1063 ------------
1064 IncShkDstn: Constructor, :math:`\psi`, :math:`\theta`
1065 The agent's income shock distributions.
1067 It's default constructor is :func:`HARK.Calibration.Income.IncomeProcesses.construct_lognormal_income_process_unemployment`
1068 aXtraGrid: Constructor
1069 The agent's asset grid.
1071 It's default constructor is :func:`HARK.utilities.make_assets_grid`
1072 ShareGrid: Constructor
1073 The agent's risky asset share grid
1075 It's default constructor is :func:`HARK.ConsumptionSaving.ConsRiskyAssetModel.make_simple_ShareGrid`
1076 RiskyDstn: Constructor, :math:`\phi`
1077 The agent's asset shock distribution for risky assets.
1079 It's default constructor is :func:`HARK.Calibration.Assets.AssetProcesses.make_lognormal_RiskyDstn`
1081 Solving Parameters
1082 ------------------
1083 cycles: int
1084 0 specifies an infinite horizon model, 1 specifies a finite model.
1085 T_cycle: int
1086 Number of periods in the cycle for this agent type.
1087 CRRA: float, :math:`\rho`
1088 Coefficient of Relative Risk Aversion.
1089 Rfree: float or list[float], time varying, :math:`\mathsf{R}`
1090 Risk Free interest rate. Pass a list of floats to make Rfree time varying.
1091 DiscFac: float, :math:`\beta`
1092 Intertemporal discount factor.
1093 LivPrb: list[float], time varying, :math:`1-\mathsf{D}`
1094 Survival probability after each period.
1095 PermGroFac: list[float], time varying, :math:`\Gamma`
1096 Permanent income growth factor.
1097 BoroCnstArt: float, default=0.0, :math:`\underline{a}`
1098 The minimum Asset/Perminant Income ratio. for this agent, BoroCnstArt must be 0.
1099 vFuncBool: bool
1100 Whether to calculate the value function during solution.
1101 CubicBool: bool
1102 Whether to use cubic spline interpoliation.
1103 AdjustPrb: float or list[float], time varying
1104 Must be between 0 and 1. Probability that the agent can update their risky portfolio share each period. Pass a list of floats to make AdjustPrb time varying.
1106 Simulation Parameters
1107 ---------------------
1108 sim_common_Rrisky: Boolean
1109 Whether risky returns have a shared/common value across agents. If True, Risky return's can't be time varying.
1110 AgentCount: int
1111 Number of agents of this kind that are created during simulations.
1112 T_age: int
1113 Age after which to automatically kill agents, None to ignore.
1114 T_sim: int, required for simulation
1115 Number of periods to simulate.
1116 track_vars: list[strings]
1117 List of variables that should be tracked when running the simulation.
1118 For this agent, the options are 'Adjust', 'PermShk', 'Risky', 'TranShk', 'aLvl', 'aNrm', 'bNrm', 'cNrm', 'mNrm', 'pLvl', and 'who_dies'.
1120 Adjust is the array of which agents can adjust
1122 PermShk is the agent's permanent income shock
1124 Risky is the agent's risky asset shock
1126 TranShk is the agent's transitory income shock
1128 aLvl is the nominal asset level
1130 aNrm is the normalized assets
1132 bNrm is the normalized resources without this period's labor income
1134 cNrm is the normalized consumption
1136 mNrm is the normalized market resources
1138 pLvl is the permanent income level
1140 who_dies is the array of which agents died
1141 aNrmInitMean: float
1142 Mean of Log initial Normalized Assets.
1143 aNrmInitStd: float
1144 Std of Log initial Normalized Assets.
1145 pLvlInitMean: float
1146 Mean of Log initial permanent income.
1147 pLvlInitStd: float
1148 Std of Log initial permanent income.
1149 PermGroFacAgg: float
1150 Aggregate permanent income growth factor (The portion of PermGroFac attributable to aggregate productivity growth).
1151 PerfMITShk: boolean
1152 Do Perfect Foresight MIT Shock (Forces Newborns to follow solution path of the agent they replaced if True).
1153 NewbornTransShk: boolean
1154 Whether Newborns have transitory shock.
1156 Attributes
1157 ----------
1158 solution: list[Consumer solution object]
1159 Created by the :func:`.solve` method. Finite horizon models create a list with T_cycle+1 elements, for each period in the solution.
1160 Infinite horizon solutions return a list with T_cycle elements for each period in the cycle.
1162 Visit :class:`HARK.ConsumptionSaving.ConsPortfolioModel.PortfolioSolution` for more information about the solution.
1164 history: Dict[Array]
1165 Created by running the :func:`.simulate()` method.
1166 Contains the variables in track_vars. Each item in the dictionary is an array with the shape (T_sim,AgentCount).
1167 Visit :class:`HARK.core.AgentType.simulate` for more information.
1168 """
1170 IncShkDstn_default = PortfolioConsumerType_IncShkDstn_default
1171 aXtraGrid_default = PortfolioConsumerType_aXtraGrid_default
1172 ShareGrid_default = PortfolioConsumerType_ShareGrid_default
1173 RiskyDstn_default = PortfolioConsumerType_RiskyDstn_default
1174 solving_default = PortfolioConsumerType_solving_default
1175 simulation_default = PortfolioConsumerType_simulation_default
1177 default_ = {
1178 "params": PortfolioConsumerType_default,
1179 "solver": solve_one_period_ConsPortfolio,
1180 "model": "ConsPortfolio.yaml",
1181 }
1183 time_inv_ = deepcopy(RiskyAssetConsumerType.time_inv_)
1184 time_inv_ = time_inv_ + ["DiscreteShareBool"]
1186 def initialize_sim(self):
1187 """
1188 Initialize the state of simulation attributes. Simply calls the same method
1189 for IndShockConsumerType, then sets the type of AdjustNow to bool.
1191 Parameters
1192 ----------
1193 None
1195 Returns
1196 -------
1197 None
1198 """
1199 # these need to be set because "post states",
1200 # but are a control variable and shock, respectively
1201 self.controls["Share"] = np.zeros(self.AgentCount)
1202 RiskyAssetConsumerType.initialize_sim(self)
1204 def sim_birth(self, which_agents):
1205 """
1206 Create new agents to replace ones who have recently died; takes draws of
1207 initial aNrm and pLvl, as in ConsIndShockModel, then sets Share and Adjust
1208 to zero as initial values.
1209 Parameters
1210 ----------
1211 which_agents : np.array
1212 Boolean array of size AgentCount indicating which agents should be "born".
1214 Returns
1215 -------
1216 None
1217 """
1218 IndShockConsumerType.sim_birth(self, which_agents)
1220 self.controls["Share"][which_agents] = 0.0
1221 # here a shock is being used as a 'post state'
1222 self.shocks["Adjust"][which_agents] = False
1224 def get_controls(self):
1225 """
1226 Calculates consumption cNrmNow and risky portfolio share ShareNow using
1227 the policy functions in the attribute solution. These are stored as attributes.
1229 Parameters
1230 ----------
1231 None
1233 Returns
1234 -------
1235 None
1236 """
1237 cNrmNow = np.zeros(self.AgentCount) + np.nan
1238 ShareNow = np.zeros(self.AgentCount) + np.nan
1240 # Loop over each period of the cycle, getting controls separately depending on "age"
1241 for t in range(self.T_cycle):
1242 these = t == self.t_cycle
1244 # Get controls for agents who *can* adjust their portfolio share
1245 those = np.logical_and(these, self.shocks["Adjust"])
1246 cNrmNow[those] = self.solution[t].cFuncAdj(self.state_now["mNrm"][those])
1247 ShareNow[those] = self.solution[t].ShareFuncAdj(
1248 self.state_now["mNrm"][those]
1249 )
1251 # Get controls for agents who *can't* adjust their portfolio share
1252 those = np.logical_and(these, np.logical_not(self.shocks["Adjust"]))
1253 cNrmNow[those] = self.solution[t].cFuncFxd(
1254 self.state_now["mNrm"][those], self.controls["Share"][those]
1255 )
1256 ShareNow[those] = self.solution[t].ShareFuncFxd(
1257 self.state_now["mNrm"][those], self.controls["Share"][those]
1258 ) # this just returns same share as before
1260 # Store controls as attributes of self
1261 self.controls["cNrm"] = cNrmNow
1262 self.controls["Share"] = ShareNow