Coverage for HARK / ConsumptionSaving / ConsRiskyAssetModel.py: 95%

494 statements  

« prev     ^ index     » next       coverage.py v7.12.0, created at 2025-12-07 05:16 +0000

1""" 

2This file contains a class that adds a risky asset with a log-normal return 

3factor to IndShockConsumerType. It is meant as a container of methods for dealing 

4with risky assets that will be useful to models what will inherit from it. 

5""" 

6 

7import numpy as np 

8 

9from HARK import NullFunc 

10from HARK.ConsumptionSaving.ConsIndShockModel import ( 

11 ConsumerSolution, 

12 IndShockConsumerType, 

13 make_basic_CRRA_solution_terminal, 

14 make_lognormal_kNrm_init_dstn, 

15 make_lognormal_pLvl_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.distributions import ( 

28 Bernoulli, 

29 expected, 

30 IndexDistribution, 

31) 

32from HARK.interpolation import ( 

33 BilinearInterp, 

34 ConstantFunction, 

35 LinearInterp, 

36 LinearInterpOnInterp1D, 

37 LowerEnvelope, 

38 CubicInterp, 

39 MargMargValueFuncCRRA, 

40 MargValueFuncCRRA, 

41 ValueFuncCRRA, 

42) 

43from HARK.rewards import UtilityFuncCRRA 

44from HARK.utilities import make_assets_grid 

45 

46############################################################################### 

47 

48 

49def make_simple_ShareGrid(ShareCount): 

50 """ 

51 Make a uniformly spaced grid on the unit interval, representing risky asset shares. 

52 

53 Parameters 

54 ---------- 

55 ShareCount : int 

56 Number of points in the grid. 

57 

58 Returns 

59 ------- 

60 ShareGrid : np.array 

61 """ 

62 ShareGrid = np.linspace(0.0, 1.0, ShareCount) 

63 return ShareGrid 

64 

65 

66def select_risky_solver(PortfolioBool): 

67 """ 

68 Trivial constructor function that chooses between two solvers. 

69 """ 

70 if PortfolioBool: 

71 solve_one_period = solve_one_period_ConsPortChoice 

72 else: 

73 solve_one_period = solve_one_period_ConsIndShockRiskyAsset 

74 return solve_one_period 

75 

76 

77def make_AdjustDstn(AdjustPrb, T_cycle, RNG): 

78 """ 

79 Make the distribution of "allowed to adjust" outcomes (a Bernoulli dstn) that 

80 could depend on age. 

81 

82 Parameters 

83 ---------- 

84 AdjustPrb : float or [float] 

85 Probability of being allowed to adjust portfolio allocation, by period of cycle. 

86 T_cycle : int 

87 Number of periods in the cycle. 

88 RNG : RandomState 

89 Instance's own random number generator. 

90 

91 Returns 

92 ------- 

93 AdjustDstn : BernoulliDistribution or IndexDistribution 

94 Distribution object for whether agents can update their portfolios. 

95 """ 

96 if type(AdjustPrb) is list and (len(AdjustPrb) == T_cycle): 

97 AdjustDstn = IndexDistribution( 

98 Bernoulli, {"p": AdjustPrb}, seed=RNG.integers(0, 2**31 - 1) 

99 ) 

100 elif type(AdjustPrb) is list: 

101 raise AttributeError( 

102 "If AdjustPrb is time-varying, it must have length of T_cycle!" 

103 ) 

104 else: 

105 AdjustDstn = Bernoulli(p=AdjustPrb, seed=RNG.integers(0, 2**31 - 1)) 

106 return AdjustDstn 

107 

108 

109############################################################################### 

110 

111# Make a dictionary of constructors for the risky asset model 

112IndShockRiskyAssetConsumerType_constructor_default = { 

113 "IncShkDstn": construct_lognormal_income_process_unemployment, 

114 "PermShkDstn": get_PermShkDstn_from_IncShkDstn, 

115 "TranShkDstn": get_TranShkDstn_from_IncShkDstn, 

116 "aXtraGrid": make_assets_grid, 

117 "RiskyDstn": make_lognormal_RiskyDstn, 

118 "ShockDstn": combine_IncShkDstn_and_RiskyDstn, 

119 "ShareLimit": calc_ShareLimit_for_CRRA, 

120 "ShareGrid": make_simple_ShareGrid, 

121 "AdjustDstn": make_AdjustDstn, 

122 "kNrmInitDstn": make_lognormal_kNrm_init_dstn, 

123 "pLvlInitDstn": make_lognormal_pLvl_init_dstn, 

124 "solution_terminal": make_basic_CRRA_solution_terminal, 

125 "solve_one_period": select_risky_solver, 

126} 

127 

128# Make a dictionary with parameters for the default constructor for kNrmInitDstn 

129IndShockRiskyAssetConsumerType_kNrmInitDstn_default = { 

130 "kLogInitMean": -12.0, # Mean of log initial capital 

131 "kLogInitStd": 0.0, # Stdev of log initial capital 

132 "kNrmInitCount": 15, # Number of points in initial capital discretization 

133} 

134 

135# Make a dictionary with parameters for the default constructor for pLvlInitDstn 

136IndShockRiskyAssetConsumerType_pLvlInitDstn_default = { 

137 "pLogInitMean": 0.0, # Mean of log permanent income 

138 "pLogInitStd": 0.0, # Stdev of log permanent income 

139 "pLvlInitCount": 15, # Number of points in initial capital discretization 

140} 

141 

142# Default parameters to make IncShkDstn using construct_lognormal_income_process_unemployment 

143IndShockRiskyAssetConsumerType_IncShkDstn_default = { 

144 "PermShkStd": [0.1], # Standard deviation of log permanent income shocks 

145 "PermShkCount": 7, # Number of points in discrete approximation to permanent income shocks 

146 "TranShkStd": [0.1], # Standard deviation of log transitory income shocks 

147 "TranShkCount": 7, # Number of points in discrete approximation to transitory income shocks 

148 "UnempPrb": 0.05, # Probability of unemployment while working 

149 "IncUnemp": 0.3, # Unemployment benefits replacement rate while working 

150 "T_retire": 0, # Period of retirement (0 --> no retirement) 

151 "UnempPrbRet": 0.005, # Probability of "unemployment" while retired 

152 "IncUnempRet": 0.0, # "Unemployment" benefits when retired 

153} 

154 

155# Default parameters to make aXtraGrid using make_assets_grid 

156IndShockRiskyAssetConsumerType_aXtraGrid_default = { 

157 "aXtraMin": 0.001, # Minimum end-of-period "assets above minimum" value 

158 "aXtraMax": 20, # Maximum end-of-period "assets above minimum" value 

159 "aXtraNestFac": 3, # Exponential nesting factor for aXtraGrid 

160 "aXtraCount": 48, # Number of points in the grid of "assets above minimum" 

161 "aXtraExtra": None, # Additional other values to add in grid (optional) 

162} 

163 

164# Default parameters to make RiskyDstn with make_lognormal_RiskyDstn 

165IndShockRiskyAssetConsumerType_RiskyDstn_default = { 

166 "RiskyAvg": 1.0803701891, # Mean return factor of risky asset 

167 "RiskyStd": 0.162927447983, # Stdev of log returns on risky asset 

168 "RiskyCount": 5, # Number of integration nodes to use in approximation of risky returns 

169} 

170# Risky return factor moments are based on SP500 real returns from Shiller's 

171# "chapter 26" data, which can be found at https://www.econ.yale.edu/~shiller/data.htm 

172# Access it through the internet archive 

173# We've (will) rounded them to the nearest .01 

174 

175# Default parameters to make RiskyDstn with make_simple_ShareGrid 

176IndShockRiskyAssetConsumerType_ShareGrid_default = { 

177 "ShareCount": 25, # Number of discrete points in the risky share approximation 

178} 

179 

180# Make a dictionary to specify a risky asset consumer type 

181IndShockRiskyAssetConsumerType_solving_default = { 

182 # BASIC HARK PARAMETERS REQUIRED TO SOLVE THE MODEL 

183 "cycles": 1, # Finite, non-cyclic model 

184 "T_cycle": 1, # Number of periods in the cycle for this agent type 

185 "constructors": IndShockRiskyAssetConsumerType_constructor_default, # See dictionary above 

186 # PRIMITIVE RAW PARAMETERS REQUIRED TO SOLVE THE MODEL 

187 "CRRA": 2.0, # Coefficient of relative risk aversion 

188 "Rfree": [1.03], # Return factor on risk free asset (not used by this type) 

189 "DiscFac": 0.96, # Intertemporal discount factor 

190 "LivPrb": [0.98], # Survival probability after each period 

191 "PermGroFac": [1.01], # Permanent income growth factor 

192 "BoroCnstArt": 0.0, # Artificial borrowing constraint 

193 "vFuncBool": False, # Whether to calculate the value function during solution 

194 "CubicBool": False, # Whether to use cubic spline interpolation when True 

195 # (Uses linear spline interpolation for cFunc when False) 

196 "RiskyShareFixed": 1.0, # Fixed share of risky asset when PortfolioBool is False 

197 "AdjustPrb": 1.0, # Probability that the agent can update their risky portfolio share each period 

198 "IndepDstnBool": True, # Whether return and income shocks are independent 

199 "PortfolioBool": False, # Whether this instance can choose portfolio shares 

200 "PortfolioBisect": False, # What does this do? 

201 "pseudo_terminal": False, 

202} 

203IndShockRiskyAssetConsumerType_simulation_default = { 

204 # PARAMETERS REQUIRED TO SIMULATE THE MODEL 

205 "AgentCount": 10000, # Number of agents of this type 

206 "T_age": None, # Age after which simulated agents are automatically killed 

207 "PermGroFacAgg": 1.0, # Aggregate permanent income growth factor 

208 # (The portion of PermGroFac attributable to aggregate productivity growth) 

209 "NewbornTransShk": False, # Whether Newborns have transitory shock 

210 # ADDITIONAL OPTIONAL PARAMETERS 

211 "PerfMITShk": False, # Do Perfect Foresight MIT Shock 

212 # (Forces Newborns to follow solution path of the agent they replaced if True) 

213 "neutral_measure": False, # Whether to use permanent income neutral measure (see Harmenberg 2021) 

214 "sim_common_Rrisky": True, # Whether risky returns have a shared/common value across agents 

215} 

216IndShockRiskyAssetConsumerType_default = {} 

217IndShockRiskyAssetConsumerType_default.update( 

218 IndShockRiskyAssetConsumerType_IncShkDstn_default 

219) 

220IndShockRiskyAssetConsumerType_default.update( 

221 IndShockRiskyAssetConsumerType_RiskyDstn_default 

222) 

223IndShockRiskyAssetConsumerType_default.update( 

224 IndShockRiskyAssetConsumerType_aXtraGrid_default 

225) 

226IndShockRiskyAssetConsumerType_default.update( 

227 IndShockRiskyAssetConsumerType_ShareGrid_default 

228) 

229IndShockRiskyAssetConsumerType_default.update( 

230 IndShockRiskyAssetConsumerType_solving_default 

231) 

232IndShockRiskyAssetConsumerType_default.update( 

233 IndShockRiskyAssetConsumerType_simulation_default 

234) 

235IndShockRiskyAssetConsumerType_default.update( 

236 IndShockRiskyAssetConsumerType_kNrmInitDstn_default 

237) 

238IndShockRiskyAssetConsumerType_default.update( 

239 IndShockRiskyAssetConsumerType_pLvlInitDstn_default 

240) 

241init_risky_asset = IndShockRiskyAssetConsumerType_default 

242 

243 

244class IndShockRiskyAssetConsumerType(IndShockConsumerType): 

245 r""" 

246 A consumer type based on IndShockConsumerType, that has access to a risky asset for their savings. The 

247 risky asset has lognormal returns that are possibly correlated with his 

248 income shocks. 

249 

250 If PortfolioBool is False, then the risky asset share is always one. 

251 Otherwise the agent can optimize their risky asset share. 

252 

253 .. math:: 

254 \newcommand{\CRRA}{\rho} 

255 \newcommand{\DiePrb}{\mathsf{D}} 

256 \newcommand{\PermGroFac}{\Gamma} 

257 \newcommand{\Rfree}{\mathsf{R}} 

258 \newcommand{\DiscFac}{\beta} 

259 \begin{align*} 

260 v_t(m_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}) \right], \\ 

261 & \text{s.t.} \\ 

262 a_t &= m_t - c_t, \\ 

263 a_t &\geq \underline{a}, \\ 

264 m_{t+1} &= \mathsf{R}_{t+1}/(\PermGroFac_{t+1} \psi_{t+1}) a_t + \theta_{t+1}, \\ 

265 \mathsf{R}_{t+1} &=S_t\phi_{t+1}\mathbf{R}_{t+1}+ (1-S_t)\mathsf{R}_{t+1}, \\ 

266 (\psi_{t+1},\theta_{t+1},\phi_{t+1}) &\sim F_{t+1}, \\ 

267 \mathbb{E}[\psi]=\mathbb{E}[\theta] &= 1. \\ 

268 u(c) &= \frac{c^{1-\CRRA}}{1-\CRRA} \\ 

269 \end{align*} 

270 

271 

272 Constructors 

273 ------------ 

274 IncShkDstn: Constructor, :math:`\psi`, :math:`\theta` 

275 The agent's income shock distributions. 

276 

277 Its default constructor is :func:`HARK.Calibration.Income.IncomeProcesses.construct_lognormal_income_process_unemployment` 

278 aXtraGrid: Constructor 

279 The agent's asset grid. 

280 

281 Its default constructor is :func:`HARK.utilities.make_assets_grid` 

282 ShareGrid: Constructor 

283 The agent's risky asset share grid 

284 

285 Its default constructor is :func:`HARK.ConsumptionSaving.ConsRiskyAssetModel.make_simple_ShareGrid` 

286 RiskyDstn: Constructor, :math:`\phi` 

287 The agent's asset shock distribution for risky assets. 

288 

289 Its default constructor is :func:`HARK.Calibration.Assets.AssetProcesses.make_lognormal_RiskyDstn` 

290 

291 Solving Parameters 

292 ------------------ 

293 cycles: int 

294 0 specifies an infinite horizon model, 1 specifies a finite model. 

295 T_cycle: int 

296 Number of periods in the cycle for this agent type. 

297 CRRA: float, :math:`\rho` 

298 Coefficient of Relative Risk Aversion. 

299 Rfree: float or list[float], time varying, :math:`\mathsf{R}` 

300 Risk Free interest rate. Pass a list of floats to make Rfree time varying. 

301 DiscFac: float, :math:`\beta` 

302 Intertemporal discount factor. 

303 LivPrb: list[float], time varying, :math:`1-\mathsf{D}` 

304 Survival probability after each period. 

305 PermGroFac: list[float], time varying, :math:`\Gamma` 

306 Permanent income growth factor. 

307 BoroCnstArt: float, default=0.0, :math:`\underline{a}` 

308 The minimum Asset/Perminant Income ratio. for this agent, BoroCnstArt must be 0. 

309 vFuncBool: bool 

310 Whether to calculate the value function during solution. 

311 CubicBool: bool 

312 Whether to use cubic spline interpoliation. 

313 PortfolioBool: Boolean 

314 Determines whether agent will use portfolio optimization or they only have access to risky assets. If false, the risky share is always one. 

315 

316 Simulation Parameters 

317 --------------------- 

318 sim_common_Rrisky: Boolean 

319 Whether risky returns have a shared/common value across agents. If True, Risky return's can't be time varying. 

320 AgentCount: int 

321 Number of agents of this kind that are created during simulations. 

322 T_age: int 

323 Age after which to automatically kill agents, None to ignore. 

324 T_sim: int, required for simulation 

325 Number of periods to simulate. 

326 track_vars: list[strings] 

327 List of variables that should be tracked when running the simulation. 

328 For this agent, the options are 'Adjust', 'PermShk', 'Risky', 'TranShk', 'aLvl', 'aNrm', 'bNrm', 'cNrm', 'mNrm', 'pLvl', and 'who_dies'. 

329 

330 Adjust is the array of which agents can adjust 

331 

332 PermShk is the agent's permanent income shock 

333 

334 Risky is the agent's risky asset shock 

335 

336 TranShk is the agent's transitory income shock 

337 

338 aLvl is the nominal asset level 

339 

340 aNrm is the normalized assets 

341 

342 bNrm is the normalized resources without this period's labor income 

343 

344 cNrm is the normalized consumption 

345 

346 mNrm is the normalized market resources 

347 

348 pLvl is the permanent income level 

349 

350 who_dies is the array of which agents died 

351 aNrmInitMean: float 

352 Mean of Log initial Normalized Assets. 

353 aNrmInitStd: float 

354 Std of Log initial Normalized Assets. 

355 pLvlInitMean: float 

356 Mean of Log initial permanent income. 

357 pLvlInitStd: float 

358 Std of Log initial permanent income. 

359 PermGroFacAgg: float 

360 Aggregate permanent income growth factor (The portion of PermGroFac attributable to aggregate productivity growth). 

361 PerfMITShk: boolean 

362 Do Perfect Foresight MIT Shock (Forces Newborns to follow solution path of the agent they replaced if True). 

363 NewbornTransShk: boolean 

364 Whether Newborns have transitory shock. 

365 

366 Attributes 

367 ---------- 

368 solution: list[Consumer solution object] 

369 Created by the :func:`.solve` method. Finite horizon models create a list with T_cycle+1 elements, for each period in the solution. 

370 Infinite horizon solutions return a list with T_cycle elements for each period in the cycle. If PortfolioBool is True, the solution also contains ShareFunc. 

371 

372 If PortfolioBool is True, the solution also contains: 

373 ShareFunc - The asset share function for this period, defined over normalized market resources :math:`S=ShareFunc(mNrm)`. 

374 

375 Visit :class:`HARK.ConsumptionSaving.ConsIndShockModel.ConsumerSolution` for more information about the solution. 

376 history: Dict[Array] 

377 Created by running the :func:`.simulate()` method. 

378 Contains the variables in track_vars. Each item in the dictionary is an array with the shape (T_sim,AgentCount). 

379 Visit :class:`HARK.core.AgentType.simulate` for more information. 

380 """ 

381 

382 IncShkDstn_default = IndShockRiskyAssetConsumerType_IncShkDstn_default 

383 RiskyDstn_default = IndShockRiskyAssetConsumerType_RiskyDstn_default 

384 aXtraGrid_default = IndShockRiskyAssetConsumerType_aXtraGrid_default 

385 ShareGrid_default = IndShockRiskyAssetConsumerType_ShareGrid_default 

386 solving_default = IndShockRiskyAssetConsumerType_solving_default 

387 simulation_default = IndShockRiskyAssetConsumerType_simulation_default # So sphinx documents defaults 

388 default_ = { 

389 "params": IndShockRiskyAssetConsumerType_default, 

390 "solver": NullFunc(), 

391 "model": "ConsRiskyAsset.yaml", 

392 } 

393 

394 time_inv_ = IndShockConsumerType.time_inv_ + [ 

395 "PortfolioBisect", 

396 "ShareGrid", 

397 "PortfolioBool", 

398 "IndepDstnBool", 

399 "RiskyShareFixed", 

400 ] 

401 time_vary_ = IndShockConsumerType.time_vary_ + ["ShockDstn", "ShareLimit"] 

402 shock_vars_ = IndShockConsumerType.shock_vars_ + ["Adjust", "Risky"] 

403 distributions = [ 

404 "IncShkDstn", 

405 "PermShkDstn", 

406 "TranShkDstn", 

407 "RiskyDstn", 

408 "ShockDstn", 

409 "kNrmInitDstn", 

410 "pLvlInitDstn", 

411 "RiskyDstn", 

412 ] 

413 

414 def pre_solve(self): 

415 self.construct("solution_terminal") 

416 self.update_timing() 

417 self.solution_terminal.ShareFunc = ConstantFunction(1.0) 

418 

419 def update_timing(self): 

420 """ 

421 This method simply ensures that a few attributes that could be in either 

422 time_inv or time_vary are appropriately labeled. 

423 """ 

424 if type(self.AdjustDstn) is IndexDistribution: 

425 self.add_to_time_vary("AdjustPrb") 

426 self.del_from_time_inv("AdjustPrb") 

427 else: 

428 self.add_to_time_inv("AdjustPrb") 

429 self.del_from_time_vary("AdjustPrb") 

430 if hasattr(self.RiskyDstn, "__getitem__"): 

431 self.add_to_time_vary("RiskyDstn") 

432 else: 

433 self.add_to_time_inv("RiskyDstn") 

434 if type(self.ShareLimit) is list: 

435 self.add_to_time_vary("ShareLimit") 

436 self.del_from_time_inv("ShareLimit") 

437 else: 

438 self.add_to_time_inv("ShareLimit") 

439 self.del_from_time_vary("ShareLimit") 

440 

441 def get_Rfree(self): 

442 """ 

443 Calculates realized return factor for each agent, using the attributes Rfree, 

444 RiskyNow, and ShareNow. This method is a bit of a misnomer, as the return 

445 factor is not riskless, but would more accurately be labeled as Rport. However, 

446 this method makes the portfolio model compatible with its parent class. 

447 

448 Parameters 

449 ---------- 

450 None 

451 

452 Returns 

453 ------- 

454 Rport : np.array 

455 Array of size AgentCount with each simulated agent's realized portfolio 

456 return factor. Will be used by get_states() to calculate mNrmNow, where it 

457 will be mislabeled as "Rfree". 

458 """ 

459 

460 RfreeNow = super().get_Rfree() 

461 RiskyNow = self.shocks["Risky"] 

462 if self.PortfolioBool: 

463 ShareNow = self.controls["Share"] 

464 else: 

465 ShareNow = np.ones_like(RiskyNow) # Only asset is risky asset 

466 

467 Rport = ShareNow * RiskyNow + (1.0 - ShareNow) * RfreeNow 

468 self.Rport = Rport 

469 return Rport 

470 

471 def get_Risky(self): 

472 """ 

473 Draws a new risky return factor. 

474 

475 Parameters 

476 ---------- 

477 None 

478 

479 Returns 

480 ------- 

481 None 

482 """ 

483 

484 # How we draw the shocks depends on whether their distribution is time-varying 

485 if "RiskyDstn" in self.time_vary: 

486 if self.sim_common_Rrisky: 

487 raise AttributeError( 

488 "If sim_common_Rrisky is True, RiskyDstn cannot be time-varying!" 

489 ) 

490 

491 else: 

492 # Make use of the IndexDistribution.draw() method 

493 self.shocks["Risky"] = self.RiskyDstn.draw( 

494 np.maximum(self.t_cycle - 1, 0) 

495 if self.cycles == 1 

496 else self.t_cycle 

497 ) 

498 

499 else: 

500 # Draw either a common economy-wide return, or one for each agent 

501 if self.sim_common_Rrisky: 

502 self.shocks["Risky"] = self.RiskyDstn.draw(1) 

503 else: 

504 self.shocks["Risky"] = self.RiskyDstn.draw(self.AgentCount) 

505 

506 def get_Adjust(self): 

507 """ 

508 Sets the attribute Adjust as a boolean array of size AgentCount, indicating 

509 whether each agent is able to adjust their risky portfolio share this period. 

510 Uses the attribute AdjustPrb to draw from a Bernoulli distribution. 

511 

512 Parameters 

513 ---------- 

514 None 

515 

516 Returns 

517 ------- 

518 None 

519 """ 

520 if "AdjustPrb" in self.time_vary: 

521 self.shocks["Adjust"] = self.AdjustDstn.draw( 

522 np.maximum(self.t_cycle - 1, 0) if self.cycles == 1 else self.t_cycle 

523 ) 

524 else: 

525 self.shocks["Adjust"] = self.AdjustDstn.draw(self.AgentCount) 

526 

527 def initialize_sim(self): 

528 """ 

529 Initialize the state of simulation attributes. Simply calls the same 

530 method for IndShockConsumerType, then initializes the new states/shocks 

531 Adjust and Share. 

532 

533 Parameters 

534 ---------- 

535 None 

536 

537 Returns 

538 ------- 

539 None 

540 """ 

541 self.shocks["Adjust"] = np.zeros(self.AgentCount, dtype=bool) 

542 # Initialize Share to default value; will be updated in get_controls() 

543 self.controls["Share"] = np.ones(self.AgentCount) 

544 IndShockConsumerType.initialize_sim(self) 

545 

546 def get_shocks(self): 

547 """ 

548 Draw idiosyncratic income shocks, just as for IndShockConsumerType, then draw 

549 a single common value for the risky asset return. Also draws whether each 

550 agent is able to adjust their portfolio this period. 

551 

552 Parameters 

553 ---------- 

554 None 

555 

556 Returns 

557 ------- 

558 None 

559 """ 

560 IndShockConsumerType.get_shocks(self) 

561 self.get_Risky() 

562 self.get_Adjust() 

563 

564 def get_controls(self): 

565 """ 

566 Calculates consumption for each consumer of this type using the consumption functions; 

567 also calculates risky asset share when PortfolioBool=True 

568 

569 Parameters 

570 ---------- 

571 None 

572 

573 Returns 

574 ------- 

575 None 

576 """ 

577 cNrmNow = np.full(self.AgentCount, np.nan) 

578 MPCnow = np.full(self.AgentCount, np.nan) 

579 ShareNow = np.full(self.AgentCount, np.nan) 

580 for t in np.unique(self.t_cycle): 

581 idx = self.t_cycle == t 

582 if np.any(idx): 

583 mNrm = self.state_now["mNrm"][idx] 

584 cNrmNow[idx], MPCnow[idx] = self.solution[t].cFunc.eval_with_derivative( 

585 mNrm 

586 ) 

587 if self.PortfolioBool: 

588 ShareNow[idx] = self.solution[t].ShareFunc(mNrm) 

589 else: 

590 ShareNow[idx] = self.RiskyShareFixed 

591 self.controls["cNrm"] = cNrmNow 

592 self.controls["Share"] = ShareNow 

593 self.MPCnow = MPCnow 

594 

595 def check_conditions(self, verbose=None): 

596 raise NotImplementedError() 

597 

598 def calc_limiting_values(self): 

599 raise NotImplementedError() 

600 

601 

602# This is to preserve compatibility with old name 

603RiskyAssetConsumerType = IndShockRiskyAssetConsumerType 

604 

605 

606############################################################################### 

607 

608 

609def solve_one_period_ConsPortChoice( 

610 solution_next, 

611 ShockDstn, 

612 IncShkDstn, 

613 RiskyDstn, 

614 LivPrb, 

615 DiscFac, 

616 CRRA, 

617 Rfree, 

618 PermGroFac, 

619 BoroCnstArt, 

620 aXtraGrid, 

621 ShareGrid, 

622 ShareLimit, 

623 vFuncBool, 

624 IndepDstnBool, 

625): 

626 """ 

627 Solve one period of a consumption-saving problem with portfolio allocation 

628 between a riskless and risky asset. This function handles only the most 

629 fundamental portfolio choice problem: frictionless reallocation of the 

630 portfolio each period as a continuous choice. 

631 

632 Parameters 

633 ---------- 

634 solution_next : PortfolioSolution 

635 Solution to next period's problem. 

636 ShockDstn : Distribution 

637 Joint distribution of permanent income shocks, transitory income shocks, 

638 and risky returns. This is only used if the input IndepDstnBool is False, 

639 indicating that income and return distributions can't be assumed to be 

640 independent. 

641 IncShkDstn : Distribution 

642 Discrete distribution of permanent income shocks and transitory income 

643 shocks. This is only used if the input IndepDstnBool is True, indicating 

644 that income and return distributions are independent. 

645 RiskyDstn : Distribution 

646 Distribution of risky asset returns. This is only used if the input 

647 IndepDstnBool is True, indicating that income and return distributions 

648 are independent. 

649 LivPrb : float 

650 Survival probability; likelihood of being alive at the beginning of 

651 the succeeding period. 

652 DiscFac : float 

653 Intertemporal discount factor for future utility. 

654 CRRA : float 

655 Coefficient of relative risk aversion. 

656 Rfree : float 

657 Risk free interest factor on end-of-period assets. 

658 PermGroFac : float 

659 Expected permanent income growth factor at the end of this period. 

660 BoroCnstArt: float or None 

661 Borrowing constraint for the minimum allowable assets to end the 

662 period with. In this model, it is *required* to be zero. 

663 aXtraGrid: np.array 

664 Array of "extra" end-of-period asset values-- assets above the 

665 absolute minimum acceptable level. 

666 ShareGrid : np.array 

667 Array of risky portfolio shares on which to define the interpolation 

668 of the consumption function when Share is fixed. Also used when the 

669 risky share choice is specified as discrete rather than continuous. 

670 ShareLimit : float 

671 Limiting lower bound of risky portfolio share as mNrm approaches infinity. 

672 vFuncBool: boolean 

673 An indicator for whether the value function should be computed and 

674 included in the reported solution. 

675 IndepDstnBool : bool 

676 Indicator for whether the income and risky return distributions are in- 

677 dependent of each other, which can speed up the expectations step. 

678 

679 Returns 

680 ------- 

681 solution_now : PortfolioSolution 

682 Solution to this period's problem. 

683 

684 :meta private: 

685 """ 

686 # Make sure the individual is liquidity constrained. Allowing a consumer to 

687 # borrow *and* invest in an asset with unbounded (negative) returns is a bad mix. 

688 if BoroCnstArt != 0.0: 

689 raise ValueError("PortfolioConsumerType must have BoroCnstArt=0.0!") 

690 

691 # Define the current period utility function and effective discount factor 

692 uFunc = UtilityFuncCRRA(CRRA) 

693 DiscFacEff = DiscFac * LivPrb # "effective" discount factor 

694 

695 # Unpack next period's solution for easier access 

696 vPfunc_next = solution_next.vPfunc 

697 vFunc_next = solution_next.vFunc 

698 

699 # Set a flag for whether the natural borrowing constraint is zero, which 

700 # depends on whether the smallest transitory income shock is zero 

701 BoroCnstNat_iszero = np.min(IncShkDstn.atoms[1]) == 0.0 

702 

703 # Prepare to calculate end-of-period marginal values by creating an array 

704 # of market resources that the agent could have next period, considering 

705 # the grid of end-of-period assets and the distribution of shocks he might 

706 # experience next period. 

707 

708 # Unpack the risky return shock distribution 

709 Risky_next = RiskyDstn.atoms 

710 RiskyMax = np.max(Risky_next) 

711 RiskyMin = np.min(Risky_next) 

712 

713 # Perform an alternate calculation of the absolute patience factor when 

714 # returns are risky. This uses the Merton-Samuelson limiting risky share, 

715 # which is what's relevant as mNrm goes to infinity. 

716 def calc_Radj(R): 

717 Rport = ShareLimit * R + (1.0 - ShareLimit) * Rfree 

718 return Rport ** (1.0 - CRRA) 

719 

720 R_adj = expected(calc_Radj, RiskyDstn)[0] 

721 PatFac = (DiscFacEff * R_adj) ** (1.0 / CRRA) 

722 MPCminNow = 1.0 / (1.0 + PatFac / solution_next.MPCmin) 

723 

724 # Also perform an alternate calculation for human wealth under risky returns 

725 def calc_hNrm(S): 

726 Risky = S["Risky"] 

727 PermShk = S["PermShk"] 

728 TranShk = S["TranShk"] 

729 G = PermGroFac * PermShk 

730 Rport = ShareLimit * Risky + (1.0 - ShareLimit) * Rfree 

731 hNrm = (G / Rport**CRRA) * (TranShk + solution_next.hNrm) 

732 return hNrm 

733 

734 # This correctly accounts for risky returns and risk aversion 

735 hNrmNow = expected(calc_hNrm, ShockDstn) / R_adj 

736 

737 # This basic equation works if there's no correlation among shocks 

738 # hNrmNow = (PermGroFac/Rfree)*(1 + solution_next.hNrm) 

739 

740 # Define the terms for the limiting linear consumption function as m gets very big 

741 cFuncLimitIntercept = MPCminNow * hNrmNow 

742 cFuncLimitSlope = MPCminNow 

743 

744 # bNrm represents R*a, balances after asset return shocks but before income. 

745 # This just uses the highest risky return as a rough shifter for the aXtraGrid. 

746 if BoroCnstNat_iszero: 

747 aNrmGrid = aXtraGrid 

748 bNrmGrid = np.insert(RiskyMax * aXtraGrid, 0, RiskyMin * aXtraGrid[0]) 

749 else: 

750 # Add an asset point at exactly zero 

751 aNrmGrid = np.insert(aXtraGrid, 0, 0.0) 

752 bNrmGrid = RiskyMax * np.insert(aXtraGrid, 0, 0.0) 

753 

754 # Get grid and shock sizes, for easier indexing 

755 aNrmCount = aNrmGrid.size 

756 ShareCount = ShareGrid.size 

757 

758 # If the income shock distribution is independent from the risky return distribution, 

759 # then taking end-of-period expectations can proceed in a two part process: First, 

760 # construct an "intermediate" value function by integrating out next period's income 

761 # shocks, *then* compute end-of-period expectations by integrating out return shocks. 

762 # This method is lengthy to code, but can be significantly faster. 

763 if IndepDstnBool: 

764 # Make tiled arrays to calculate future realizations of mNrm and Share when integrating over IncShkDstn 

765 bNrmNext = bNrmGrid 

766 

767 # Define functions that are used internally to evaluate future realizations 

768 def calc_mNrm_next(S, b): 

769 """ 

770 Calculate future realizations of market resources mNrm from the income 

771 shock distribution S and normalized bank balances b. 

772 """ 

773 return b / (S["PermShk"] * PermGroFac) + S["TranShk"] 

774 

775 def calc_dvdm_next(S, b): 

776 """ 

777 Evaluate realizations of marginal value of market resources next period, 

778 based on the income distribution S and values of bank balances bNrm 

779 """ 

780 mNrm_next = calc_mNrm_next(S, b) 

781 G = S["PermShk"] * PermGroFac 

782 dvdm_next = G ** (-CRRA) * vPfunc_next(mNrm_next) 

783 return dvdm_next 

784 

785 # Calculate end-of-period marginal value of assets and shares at each point 

786 # in aNrm and ShareGrid. Does so by taking expectation of next period marginal 

787 # values across income and risky return shocks. 

788 

789 # Calculate intermediate marginal value of bank balances by taking expectations over income shocks 

790 dvdb_intermed = expected(calc_dvdm_next, IncShkDstn, args=(bNrmNext)) 

791 dvdbNvrs_intermed = uFunc.derinv(dvdb_intermed, order=(1, 0)) 

792 

793 dvdbNvrsFunc_intermed = LinearInterp(bNrmGrid, dvdbNvrs_intermed) 

794 dvdbFunc_intermed = MargValueFuncCRRA(dvdbNvrsFunc_intermed, CRRA) 

795 

796 # The intermediate marginal value of risky portfolio share is zero in this 

797 # model because risky share is flexible: we can just change it next period, 

798 # so there is no marginal value of Share once the return is realized. 

799 dvdsFunc_intermed = ConstantFunction(0.0) # all zeros 

800 

801 # Make tiled arrays to calculate future realizations of bNrm and Share when integrating over RiskyDstn 

802 aNrmNow, ShareNext = np.meshgrid(aNrmGrid, ShareGrid, indexing="ij") 

803 

804 # Define functions for calculating end-of-period marginal value 

805 def calc_EndOfPrd_dvda(R, a, z): 

806 """ 

807 Compute end-of-period marginal value of assets at values a, conditional 

808 on risky asset return R and risky share z. 

809 """ 

810 # Calculate future realizations of bank balances bNrm 

811 Rxs = R - Rfree # Excess returns 

812 Rport = Rfree + z * Rxs # Portfolio return 

813 bNrm_next = Rport * a 

814 

815 # Calculate and return dvda 

816 EndOfPrd_dvda = Rport * dvdbFunc_intermed(bNrm_next) 

817 return EndOfPrd_dvda 

818 

819 def calc_EndOfPrd_dvds(R, a, z): 

820 """ 

821 Compute end-of-period marginal value of risky share at values a, conditional 

822 on risky asset return S and risky share z. 

823 """ 

824 # Calculate future realizations of bank balances bNrm 

825 Rxs = R - Rfree # Excess returns 

826 Rport = Rfree + z * Rxs # Portfolio return 

827 bNrm_next = Rport * a 

828 

829 # Calculate and return dvds (second term is all zeros) 

830 EndOfPrd_dvds = Rxs * a * dvdbFunc_intermed(bNrm_next) + dvdsFunc_intermed( 

831 bNrm_next 

832 ) 

833 return EndOfPrd_dvds 

834 

835 TempDstn = RiskyDstn # relabeling for below 

836 

837 # Evaluate realizations of value and marginal value after asset returns are realized 

838 

839 # Calculate end-of-period marginal value of risky portfolio share by taking expectations 

840 EndOfPrd_dvds = DiscFacEff * expected( 

841 calc_EndOfPrd_dvds, RiskyDstn, args=(aNrmNow, ShareNext) 

842 ) 

843 

844 # Make the end-of-period value function if the value function is requested 

845 if vFuncBool: 

846 

847 def calc_v_intermed(S, b): 

848 """ 

849 Calculate "intermediate" value from next period's bank balances, the 

850 income shocks S, and the risky asset share. 

851 """ 

852 mNrm_next = calc_mNrm_next(S, b) 

853 v_next = vFunc_next(mNrm_next) 

854 v_intermed = (S["PermShk"] * PermGroFac) ** (1.0 - CRRA) * v_next 

855 return v_intermed 

856 

857 # Calculate intermediate value by taking expectations over income shocks 

858 v_intermed = expected(calc_v_intermed, IncShkDstn, args=(bNrmNext)) 

859 

860 # Construct the "intermediate value function" for this period 

861 vNvrs_intermed = uFunc.inv(v_intermed) 

862 vNvrsFunc_intermed = LinearInterp(bNrmGrid, vNvrs_intermed) 

863 vFunc_intermed = ValueFuncCRRA(vNvrsFunc_intermed, CRRA) 

864 

865 def calc_EndOfPrd_v(S, a, z): 

866 # Calculate future realizations of bank balances bNrm 

867 Rxs = S - Rfree 

868 Rport = Rfree + z * Rxs 

869 bNrm_next = Rport * a 

870 

871 EndOfPrd_v = vFunc_intermed(bNrm_next) 

872 return EndOfPrd_v 

873 

874 # Calculate end-of-period value by taking expectations 

875 EndOfPrd_v = DiscFacEff * expected( 

876 calc_EndOfPrd_v, RiskyDstn, args=(aNrmNow, ShareNext) 

877 ) 

878 EndOfPrd_vNvrs = uFunc.inv(EndOfPrd_v) 

879 

880 # Now make an end-of-period value function over aNrm and Share 

881 EndOfPrd_vNvrsFunc = BilinearInterp(EndOfPrd_vNvrs, aNrmGrid, ShareGrid) 

882 EndOfPrd_vFunc = ValueFuncCRRA(EndOfPrd_vNvrsFunc, CRRA) 

883 # This will be used later to make the value function for this period 

884 

885 # If the income shock distribution and risky return distribution are *NOT* 

886 # independent, then computation of end-of-period expectations are simpler in 

887 # code, but might take longer to execute 

888 else: 

889 # Make tiled arrays to calculate future realizations of mNrm and Share when integrating over IncShkDstn 

890 aNrmNow, ShareNext = np.meshgrid(aNrmGrid, ShareGrid, indexing="ij") 

891 

892 # Define functions that are used internally to evaluate future realizations 

893 def calc_mNrm_next(S, a, z): 

894 """ 

895 Calculate future realizations of market resources mNrm from the shock 

896 distribution S, normalized end-of-period assets a, and risky share z. 

897 """ 

898 # Calculate future realizations of bank balances bNrm 

899 Rxs = S["Risky"] - Rfree 

900 Rport = Rfree + z * Rxs 

901 bNrm_next = Rport * a 

902 mNrm_next = bNrm_next / (S["PermShk"] * PermGroFac) + S["TranShk"] 

903 return mNrm_next 

904 

905 def calc_EndOfPrd_dvdx(S, a, z): 

906 """ 

907 Evaluate end-of-period marginal value of assets and risky share based 

908 on the shock distribution S, normalized end-of-period assets a, and 

909 risky share z. 

910 """ 

911 mNrm_next = calc_mNrm_next(S, a, z) 

912 Rxs = S["Risky"] - Rfree 

913 Rport = Rfree + z * Rxs 

914 dvdm_next = vPfunc_next(mNrm_next) 

915 # No marginal value of Share if it's a free choice! 

916 dvds_next = np.zeros_like(mNrm_next) 

917 

918 EndOfPrd_dvda = Rport * (S["PermShk"] * PermGroFac) ** (-CRRA) * dvdm_next 

919 EndOfPrd_dvds = ( 

920 Rxs * a * (S["PermShk"] * PermGroFac) ** (-CRRA) * dvdm_next 

921 + (S["PermShk"] * PermGroFac) ** (1 - CRRA) * dvds_next 

922 ) 

923 

924 return EndOfPrd_dvda, EndOfPrd_dvds 

925 

926 def calc_EndOfPrd_v(S, a, z): 

927 """ 

928 Evaluate end-of-period value, based on the shock distribution S, values 

929 of bank balances bNrm, and values of the risky share z. 

930 """ 

931 mNrm_next = calc_mNrm_next(S, a, z) 

932 v_next = vFunc_next(mNrm_next) 

933 EndOfPrd_v = (S["PermShk"] * PermGroFac) ** (1.0 - CRRA) * v_next 

934 return EndOfPrd_v 

935 

936 calc_EndOfPrd_dvda = lambda S, a, z: calc_EndOfPrd_dvdx(S, a, z)[0] 

937 calc_EndOfPrd_dvds = lambda S, a, z: calc_EndOfPrd_dvdx(S, a, z)[1] 

938 TempDstn = ShockDstn 

939 

940 # Evaluate realizations of value and marginal value after asset returns are realized 

941 

942 # Calculate end-of-period marginal value of assets and risky share by taking expectations 

943 EndOfPrd_dvda, EndOfPrd_dvds = DiscFacEff * expected( 

944 calc_EndOfPrd_dvdx, ShockDstn, args=(aNrmNow, ShareNext) 

945 ) 

946 EndOfPrd_dvdaNvrs = uFunc.derinv(EndOfPrd_dvda) 

947 

948 # Construct the end-of-period value function if requested 

949 if vFuncBool: 

950 # Calculate end-of-period value, its derivative, and their pseudo-inverse 

951 EndOfPrd_v = DiscFacEff * expected( 

952 calc_EndOfPrd_v, ShockDstn, args=(aNrmNow, ShareNext) 

953 ) 

954 EndOfPrd_vNvrs = uFunc.inv(EndOfPrd_v) 

955 

956 # value transformed through inverse utility 

957 EndOfPrd_vNvrsP = EndOfPrd_dvda * uFunc.derinv(EndOfPrd_v, order=(0, 1)) 

958 

959 # Construct the end-of-period value function 

960 EndOfPrd_vNvrsFunc_by_Share = [] 

961 for j in range(ShareCount): 

962 EndOfPrd_vNvrsFunc_by_Share.append( 

963 CubicInterp( 

964 aNrmNow[:, j], EndOfPrd_vNvrs[:, j], EndOfPrd_vNvrsP[:, j] 

965 ) 

966 ) 

967 EndOfPrd_vNvrsFunc = LinearInterpOnInterp1D( 

968 EndOfPrd_vNvrsFunc_by_Share, ShareGrid 

969 ) 

970 EndOfPrd_vFunc = ValueFuncCRRA(EndOfPrd_vNvrsFunc, CRRA) 

971 

972 # Now find the optimal (continuous) risky share on [0,1] by solving the first 

973 # order condition EndOfPrd_dvds == 0. 

974 FOC_s = EndOfPrd_dvds # Relabel for convenient typing 

975 

976 # If agent wants to put more than 100% into risky asset, he is constrained. 

977 # Likewise if he wants to put less than 0% into risky asset, he is constrained. 

978 constrained_top = FOC_s[:, -1] > 0.0 

979 constrained_bot = FOC_s[:, 0] < 0.0 

980 constrained = np.logical_or(constrained_top, constrained_bot) 

981 a_idx = np.arange(aNrmCount) 

982 

983 # For each value of aNrm, find the value of Share such that FOC_s == 0 

984 crossing = np.logical_and(FOC_s[:, 1:] <= 0.0, FOC_s[:, :-1] >= 0.0) 

985 share_idx = np.argmax(crossing, axis=1) 

986 

987 for k in range(3): 

988 # This represents the index of the segment of the share grid where dvds flips 

989 # from positive to negative, indicating that there's a zero *on* the segment. 

990 # The exception is for aNrm values that are flagged as constrained, for which 

991 # there will be no crossing point and we can just use the boundary value. 

992 

993 # Now that we have a *range* for the location of the optimal share, we can 

994 # do a refined search for the optimal share at each aNrm value where there 

995 # is an interior solution (not constrained). We now make a refined ShareGrid 

996 # that has *different* values on it for each aNrm value. 

997 bot_s = ShareNext[a_idx, share_idx] 

998 top_s = ShareNext[a_idx, share_idx + 1] 

999 for j in range(aNrmCount): 

1000 if constrained[j]: 

1001 continue 

1002 ShareNext[j, :] = np.linspace(bot_s[j], top_s[j], ShareCount) 

1003 

1004 # Now evaluate end-of-period marginal value of risky share on the refined grid 

1005 EndOfPrd_dvds = DiscFacEff * expected( 

1006 calc_EndOfPrd_dvds, TempDstn, args=(aNrmNow, ShareNext) 

1007 ) 

1008 these = np.logical_not(constrained) 

1009 FOC_s[these, :] = EndOfPrd_dvds[these, :] # Relabel for convenient typing 

1010 

1011 # Look for "crossing points" again 

1012 crossing = np.logical_and(FOC_s[these, 1:] <= 0.0, FOC_s[these, :-1] >= 0.0) 

1013 share_idx[these] = np.argmax(crossing, axis=1) 

1014 

1015 # Calculate end-of-period marginal value of assets on the refined grid 

1016 EndOfPrd_dvda = DiscFacEff * expected( 

1017 calc_EndOfPrd_dvda, TempDstn, args=(aNrmNow, ShareNext) 

1018 ) 

1019 EndOfPrd_dvdaNvrs = uFunc.derinv(EndOfPrd_dvda) 

1020 

1021 # Calculate the fractional distance between those share gridpoints where the 

1022 # zero should be found, assuming a linear function; call it alpha 

1023 bot_s = ShareNext[a_idx, share_idx] 

1024 top_s = ShareNext[a_idx, share_idx + 1] 

1025 bot_f = FOC_s[a_idx, share_idx] 

1026 top_f = FOC_s[a_idx, share_idx + 1] 

1027 bot_c = EndOfPrd_dvdaNvrs[a_idx, share_idx] 

1028 top_c = EndOfPrd_dvdaNvrs[a_idx, share_idx + 1] 

1029 alpha = 1.0 - top_f / (top_f - bot_f) 

1030 

1031 # Calculate the continuous optimal risky share and optimal consumption 

1032 Share_now = (1.0 - alpha) * bot_s + alpha * top_s 

1033 cNrm_now = (1.0 - alpha) * bot_c + alpha * top_c 

1034 

1035 # If agent wants to put more than 100% into risky asset, he is constrained. 

1036 # Likewise if he wants to put less than 0% into risky asset, he is constrained. 

1037 constrained_top = FOC_s[:, -1] > 0.0 

1038 constrained_bot = FOC_s[:, 0] < 0.0 

1039 

1040 # Apply the constraints to both risky share and consumption (but lower 

1041 # constraint should never be relevant) 

1042 Share_now[constrained_top] = 1.0 

1043 Share_now[constrained_bot] = 0.0 

1044 cNrm_now[constrained_top] = EndOfPrd_dvdaNvrs[constrained_top, -1] 

1045 cNrm_now[constrained_bot] = EndOfPrd_dvdaNvrs[constrained_bot, 0] 

1046 

1047 # When the natural borrowing constraint is *not* zero, then aNrm=0 is in the 

1048 # grid, but there's no way to "optimize" the portfolio if a=0, and consumption 

1049 # can't depend on the risky share if it doesn't meaningfully exist. Apply 

1050 # a small fix to the bottom gridpoint (aNrm=0) when this happens. 

1051 if not BoroCnstNat_iszero: 

1052 Share_now[0] = 1.0 

1053 cNrm_now[0] = EndOfPrd_dvdaNvrs[0, -1] 

1054 

1055 # Construct functions characterizing the solution for this period 

1056 

1057 # Calculate the endogenous mNrm gridpoints when the agent adjusts his portfolio, 

1058 # then construct the consumption function when the agent can adjust his share 

1059 mNrm_now = np.insert(aNrmGrid + cNrm_now, 0, 0.0) 

1060 cNrm_now = np.insert(cNrm_now, 0, 0.0) 

1061 cFunc_now = LinearInterp(mNrm_now, cNrm_now, cFuncLimitIntercept, cFuncLimitSlope) 

1062 

1063 # Construct the marginal value (of mNrm) function 

1064 vPfunc_now = MargValueFuncCRRA(cFunc_now, CRRA) 

1065 

1066 # If the share choice is continuous, just make an ordinary interpolating function 

1067 if BoroCnstNat_iszero: 

1068 Share_lower_bound = ShareLimit 

1069 else: 

1070 Share_lower_bound = 1.0 

1071 Share_now = np.insert(Share_now, 0, Share_lower_bound) 

1072 ShareFunc_now = LinearInterp(mNrm_now, Share_now, ShareLimit, 0.0) 

1073 

1074 # Add the value function if requested 

1075 if vFuncBool: 

1076 # Create the value functions for this period, defined over market resources 

1077 # mNrm when agent can adjust his portfolio, and over market resources and 

1078 # fixed share when agent can not adjust his portfolio. 

1079 

1080 # Construct the value function 

1081 mNrm_temp = aXtraGrid # Just use aXtraGrid as our grid of mNrm values 

1082 cNrm_temp = cFunc_now(mNrm_temp) 

1083 aNrm_temp = np.maximum(mNrm_temp - cNrm_temp, 0.0) # Fix tiny violations 

1084 Share_temp = ShareFunc_now(mNrm_temp) 

1085 v_temp = uFunc(cNrm_temp) + EndOfPrd_vFunc(aNrm_temp, Share_temp) 

1086 vNvrs_temp = uFunc.inv(v_temp) 

1087 vNvrsP_temp = uFunc.der(cNrm_temp) * uFunc.inverse(v_temp, order=(0, 1)) 

1088 vNvrsFunc = CubicInterp( 

1089 np.insert(mNrm_temp, 0, 0.0), # x_list 

1090 np.insert(vNvrs_temp, 0, 0.0), # f_list 

1091 np.insert(vNvrsP_temp, 0, vNvrsP_temp[0]), # dfdx_list 

1092 ) 

1093 # Re-curve the pseudo-inverse value function 

1094 vFunc_now = ValueFuncCRRA(vNvrsFunc, CRRA) 

1095 

1096 else: # If vFuncBool is False, fill in dummy values 

1097 vFunc_now = NullFunc() 

1098 

1099 # Package and return the solution 

1100 solution_now = ConsumerSolution( 

1101 cFunc=cFunc_now, 

1102 vPfunc=vPfunc_now, 

1103 vFunc=vFunc_now, 

1104 hNrm=hNrmNow, 

1105 MPCmin=MPCminNow, 

1106 ) 

1107 solution_now.ShareFunc = ShareFunc_now 

1108 return solution_now 

1109 

1110 

1111############################################################################### 

1112 

1113 

1114def solve_one_period_ConsIndShockRiskyAsset( 

1115 solution_next, 

1116 IncShkDstn, 

1117 RiskyDstn, 

1118 ShockDstn, 

1119 LivPrb, 

1120 DiscFac, 

1121 Rfree, 

1122 CRRA, 

1123 PermGroFac, 

1124 BoroCnstArt, 

1125 aXtraGrid, 

1126 RiskyShareFixed, 

1127 vFuncBool, 

1128 CubicBool, 

1129 IndepDstnBool, 

1130): 

1131 """ 

1132 Solves one period of a consumption-saving model with idiosyncratic shocks to 

1133 permanent and transitory income, with one risky asset and CRRA utility. 

1134 

1135 Parameters 

1136 ---------- 

1137 solution_next : ConsumerSolution 

1138 The solution to next period's one period problem. 

1139 IncShkDstn : Distribution 

1140 Discrete distribution of permanent income shocks and transitory income 

1141 shocks. This is only used if the input IndepDstnBool is True, indicating 

1142 that income and return distributions are independent. 

1143 RiskyDstn : Distribution 

1144 Distribution of risky asset returns. This is only used if the input 

1145 IndepDstnBool is True, indicating that income and return distributions 

1146 are independent. 

1147 ShockDstn : Distribution 

1148 Joint distribution of permanent income shocks, transitory income shocks, 

1149 and risky returns. This is only used if the input IndepDstnBool is False, 

1150 indicating that income and return distributions can't be assumed to be 

1151 independent. 

1152 LivPrb : float 

1153 Survival probability; likelihood of being alive at the beginning of 

1154 the succeeding period. 

1155 DiscFac : float 

1156 Intertemporal discount factor for future utility. 

1157 Rfree : float 

1158 Risk free interest factor on end-of-period assets. 

1159 CRRA : float 

1160 Coefficient of relative risk aversion. 

1161 PermGroFac : float 

1162 Expected permanent income growth factor at the end of this period. 

1163 BoroCnstArt: float or None 

1164 Borrowing constraint for the minimum allowable assets to end the 

1165 period with. If it is less than the natural borrowing constraint, 

1166 then it is irrelevant; BoroCnstArt=None indicates no artificial bor- 

1167 rowing constraint. 

1168 aXtraGrid: np.array 

1169 Array of "extra" end-of-period asset values-- assets above the 

1170 absolute minimum acceptable level. 

1171 RiskyShareFixed : float 

1172 Fixed fraction of end-of-period assets that are allocated to the risky asset. 

1173 vFuncBool: boolean 

1174 An indicator for whether the value function should be computed and 

1175 included in the reported solution. 

1176 CubicBool: boolean 

1177 An indicator for whether the solver should use cubic or linear interpolation. 

1178 IndepDstnBool : bool 

1179 Indicator for whether the income and risky return distributions are in- 

1180 dependent of each other, which can speed up the expectations step. 

1181 

1182 Returns 

1183 ------- 

1184 solution_now : ConsumerSolution 

1185 Solution to this period's consumption-saving problem with income risk. 

1186 

1187 :meta private: 

1188 """ 

1189 # Do a quick validity check; don't want to allow borrowing with risky returns 

1190 if BoroCnstArt != 0.0: 

1191 raise ValueError("RiskyAssetConsumerType must have BoroCnstArt=0.0!") 

1192 

1193 # Define the current period utility function and effective discount factor 

1194 uFunc = UtilityFuncCRRA(CRRA) 

1195 DiscFacEff = DiscFac * LivPrb # "effective" discount factor 

1196 

1197 # Unpack next period's income shock distribution 

1198 ShkPrbsNext = ShockDstn.pmv 

1199 PermShkValsNext = ShockDstn.atoms[0] 

1200 TranShkValsNext = ShockDstn.atoms[1] 

1201 RiskyValsNext = ShockDstn.atoms[2] 

1202 PermShkMinNext = np.min(PermShkValsNext) 

1203 TranShkMinNext = np.min(TranShkValsNext) 

1204 RiskyMinNext = np.min(RiskyValsNext) 

1205 RiskyMaxNext = np.max(RiskyValsNext) 

1206 

1207 # Unpack next period's (marginal) value function 

1208 vFuncNext = solution_next.vFunc # This is None when vFuncBool is False 

1209 vPfuncNext = solution_next.vPfunc 

1210 vPPfuncNext = solution_next.vPPfunc # This is None when CubicBool is False 

1211 

1212 # Perform an alternate calculation of the absolute patience factor when returns are risky 

1213 def calc_Radj(R): 

1214 Rport = RiskyShareFixed * R + (1.0 - RiskyShareFixed) * Rfree 

1215 return Rport ** (1.0 - CRRA) 

1216 

1217 R_adj = expected(calc_Radj, RiskyDstn) 

1218 PatFac = (DiscFacEff * R_adj) ** (1.0 / CRRA) 

1219 MPCminNow = 1.0 / (1.0 + PatFac / solution_next.MPCmin) 

1220 MPCminNow = MPCminNow[0] 

1221 

1222 # Also perform an alternate calculation for human wealth under risky returns 

1223 def calc_hNrm(S): 

1224 Risky = S["Risky"] 

1225 PermShk = S["PermShk"] 

1226 TranShk = S["TranShk"] 

1227 G = PermGroFac * PermShk 

1228 Rport = RiskyShareFixed * Risky + (1.0 - RiskyShareFixed) * Rfree 

1229 hNrm = (G / Rport**CRRA) * (TranShk + solution_next.hNrm) 

1230 return hNrm 

1231 

1232 # This correctly accounts for risky returns and risk aversion 

1233 hNrmNow = expected(calc_hNrm, ShockDstn) / R_adj 

1234 hNrmNow = hNrmNow[0] 

1235 

1236 # The above attempts to pin down the limiting consumption function for this 

1237 # model, however it is not clear why it creates bugs, so for now we allow 

1238 # for a linear extrapolation beyond the last asset point 

1239 cFuncLimitIntercept = MPCminNow * hNrmNow 

1240 cFuncLimitSlope = MPCminNow 

1241 

1242 # Calculate the minimum allowable value of market resources in this period 

1243 BoroCnstNat_cand = ( 

1244 (solution_next.mNrmMin - TranShkValsNext) 

1245 * (PermGroFac * PermShkValsNext) 

1246 / RiskyValsNext 

1247 ) 

1248 BoroCnstNat = np.max(BoroCnstNat_cand) # Must be at least this 

1249 

1250 # Set a flag for whether the natural borrowing constraint is zero, which 

1251 # depends on whether the smallest transitory income shock is zero 

1252 BoroCnstNat_iszero = np.min(IncShkDstn.atoms[1]) == 0.0 

1253 

1254 # Set the minimum allowable (normalized) market resources based on the natural 

1255 # and artificial borrowing constraints 

1256 if BoroCnstArt is None: 

1257 mNrmMinNow = BoroCnstNat 

1258 else: 

1259 mNrmMinNow = np.max([BoroCnstNat, BoroCnstArt]) 

1260 

1261 # The MPCmax code is a bit unusual here, and possibly "harmlessly wrong". 

1262 # The "worst event" should depend on the risky return factor as well as 

1263 # income shocks. However, the natural borrowing constraint is only ever 

1264 # relevant in this model when it's zero, so the MPC at mNrm is only relevant 

1265 # in the case where risky returns don't matter at all (because a=0). 

1266 

1267 # Calculate the probability that we get the worst possible income draw 

1268 IncNext = PermShkValsNext * TranShkValsNext 

1269 WorstIncNext = PermShkMinNext * TranShkMinNext 

1270 WorstIncPrb = np.sum(ShkPrbsNext[IncNext == WorstIncNext]) 

1271 # WorstIncPrb is the "Weierstrass p" concept: the odds we get the WORST thing 

1272 

1273 # Update the upper bounding MPC as market resources approach the lower bound 

1274 temp_fac = (WorstIncPrb ** (1.0 / CRRA)) * PatFac 

1275 MPCmaxNow = 1.0 / (1.0 + temp_fac / solution_next.MPCmax) 

1276 

1277 # Set the upper limit of the MPC (at mNrmMinNow) based on whether the natural 

1278 # or artificial borrowing constraint actually binds 

1279 if BoroCnstNat < mNrmMinNow: 

1280 MPCmaxEff = 1.0 # If actually constrained, MPC near limit is 1 

1281 else: 

1282 MPCmaxEff = MPCmaxNow # Otherwise, it's the MPC calculated above 

1283 

1284 # Define the borrowing-constrained consumption function 

1285 cFuncNowCnst = LinearInterp( 

1286 np.array([mNrmMinNow, mNrmMinNow + 1.0]), np.array([0.0, 1.0]) 

1287 ) 

1288 

1289 # Big methodological split here: whether the income and return distributions are independent. 

1290 # Calculation of end-of-period marginal (marginal) value uses different approaches 

1291 if IndepDstnBool: 

1292 # bNrm represents R*a, balances after asset return shocks but before income. 

1293 # This just uses the highest risky return as a rough shifter for the aXtraGrid. 

1294 if BoroCnstNat_iszero: 

1295 bNrmNow = np.insert( 

1296 RiskyMaxNext * aXtraGrid, 0, RiskyMinNext * aXtraGrid[0] 

1297 ) 

1298 aNrmNow = aXtraGrid.copy() 

1299 else: 

1300 # Add a bank balances point at exactly zero 

1301 bNrmNow = RiskyMaxNext * np.insert(aXtraGrid, 0, 0.0) 

1302 aNrmNow = np.insert(aXtraGrid, 0, 0.0) 

1303 

1304 # Define local functions for taking future expectations when the interest 

1305 # factor *is* independent from the income shock distribution. These go 

1306 # from "bank balances" bNrm = R * aNrm to t+1 realizations. 

1307 def calc_mNrmNext(S, b): 

1308 return b / (PermGroFac * S["PermShk"]) + S["TranShk"] 

1309 

1310 def calc_vNext(S, b): 

1311 return S["PermShk"] ** (1.0 - CRRA) * vFuncNext(calc_mNrmNext(S, b)) 

1312 

1313 def calc_vPnext(S, b): 

1314 return S["PermShk"] ** (-CRRA) * vPfuncNext(calc_mNrmNext(S, b)) 

1315 

1316 def calc_vPPnext(S, b): 

1317 return S["PermShk"] ** (-CRRA - 1.0) * vPPfuncNext(calc_mNrmNext(S, b)) 

1318 

1319 # Calculate marginal value of bank balances at each gridpoint 

1320 vPfacEff = PermGroFac ** (-CRRA) 

1321 Intermed_vP = vPfacEff * expected(calc_vPnext, IncShkDstn, args=(bNrmNow)) 

1322 Intermed_vPnvrs = uFunc.derinv(Intermed_vP, order=(1, 0)) 

1323 

1324 if BoroCnstNat_iszero: 

1325 Intermed_vPnvrs = np.insert(Intermed_vPnvrs, 0, 0.0) 

1326 bNrm_temp = np.insert(bNrmNow, 0, 0.0) 

1327 else: 

1328 bNrm_temp = bNrmNow.copy() 

1329 

1330 # If using cubic spline interpolation, also calculate "intermediate" 

1331 # marginal marginal value of bank balances 

1332 if CubicBool: 

1333 vPPfacEff = PermGroFac ** (-CRRA - 1.0) 

1334 Intermed_vPP = vPPfacEff * expected( 

1335 calc_vPPnext, IncShkDstn, args=(bNrmNow) 

1336 ) 

1337 Intermed_vPnvrsP = Intermed_vPP * uFunc.derinv(Intermed_vP, order=(1, 1)) 

1338 if BoroCnstNat_iszero: 

1339 Intermed_vPnvrsP = np.insert(Intermed_vPnvrsP, 0, Intermed_vPnvrsP[0]) 

1340 

1341 # Make a cubic spline intermediate pseudo-inverse marginal value function 

1342 Intermed_vPnvrsFunc = CubicInterp( 

1343 bNrm_temp, 

1344 Intermed_vPnvrs, 

1345 Intermed_vPnvrsP, 

1346 lower_extrap=True, 

1347 ) 

1348 Intermed_vPPfunc = MargMargValueFuncCRRA(Intermed_vPnvrsFunc, CRRA) 

1349 else: 

1350 # Make a linear interpolation intermediate pseudo-inverse marginal value function 

1351 Intermed_vPnvrsFunc = LinearInterp( 

1352 bNrm_temp, Intermed_vPnvrs, lower_extrap=True 

1353 ) 

1354 

1355 # "Recurve" the intermediate pseudo-inverse marginal value function 

1356 Intermed_vPfunc = MargValueFuncCRRA(Intermed_vPnvrsFunc, CRRA) 

1357 

1358 # If the value function is requested, calculate "intermediate" value 

1359 if vFuncBool: 

1360 vFacEff = PermGroFac ** (1.0 - CRRA) 

1361 Intermed_v = vFacEff * expected(calc_vNext, IncShkDstn, args=(bNrmNow)) 

1362 Intermed_vNvrs = uFunc.inv(Intermed_v) 

1363 # value transformed through inverse utility 

1364 Intermed_vNvrsP = Intermed_vP * uFunc.derinv(Intermed_v, order=(0, 1)) 

1365 if BoroCnstNat_iszero: 

1366 Intermed_vNvrs = np.insert(Intermed_vNvrs, 0, 0.0) 

1367 Intermed_vNvrsP = np.insert(Intermed_vNvrsP, 0, Intermed_vNvrsP[0]) 

1368 # This is a very good approximation, vNvrsPP = 0 at the asset minimum 

1369 

1370 # Make a cubic spline intermediate pseudo-inverse value function 

1371 Intermed_vNvrsFunc = CubicInterp(bNrm_temp, Intermed_vNvrs, Intermed_vNvrsP) 

1372 

1373 # "Recurve" the intermediate pseudo-inverse value function 

1374 Intermed_vFunc = ValueFuncCRRA(Intermed_vNvrsFunc, CRRA) 

1375 

1376 # We have "intermediate" (marginal) value functions defined over bNrm, 

1377 # so now we want to take expectations over Risky realizations at each aNrm. 

1378 

1379 # Begin by re-defining transition functions for taking expectations, which are all very simple! 

1380 Z = RiskyShareFixed # for shorter notation 

1381 

1382 def calc_bNrmNext(R, a): 

1383 Rport = Z * R + (1 - Z) * Rfree 

1384 return Rport * a 

1385 

1386 def calc_vNext(R, a): 

1387 return Intermed_vFunc(calc_bNrmNext(R, a)) 

1388 

1389 def calc_vPnext(R, a): 

1390 Rport = Z * R + (1 - Z) * Rfree 

1391 return Rport * Intermed_vPfunc(calc_bNrmNext(R, a)) 

1392 

1393 def calc_vPPnext(R, a): 

1394 Rport = Z * R + (1 - Z) * Rfree 

1395 return Rport * Rport * Intermed_vPPfunc(calc_bNrmNext(R, a)) 

1396 

1397 # Calculate end-of-period marginal value of assets at each gridpoint 

1398 EndOfPrdvP = DiscFacEff * expected(calc_vPnext, RiskyDstn, args=(aNrmNow)) 

1399 

1400 # Invert the first order condition to find optimal cNrm from each aNrm gridpoint 

1401 cNrmNow = uFunc.derinv(EndOfPrdvP, order=(1, 0)) 

1402 mNrmNow = cNrmNow + aNrmNow # Endogenous mNrm gridpoints 

1403 

1404 # Calculate the MPC at each gridpoint if using cubic spline interpolation 

1405 if CubicBool: 

1406 # Calculate end-of-period marginal marginal value of assets at each gridpoint 

1407 EndOfPrdvPP = DiscFacEff * expected(calc_vPPnext, RiskyDstn, args=(aNrmNow)) 

1408 dcda = EndOfPrdvPP / uFunc.der(np.array(cNrmNow), order=2) 

1409 MPC = dcda / (dcda + 1.0) 

1410 MPC_for_interpolation = np.insert(MPC, 0, MPCmaxNow) 

1411 

1412 # Limiting consumption is zero as m approaches mNrmMin 

1413 c_for_interpolation = np.insert(cNrmNow, 0, 0.0) 

1414 m_for_interpolation = np.insert(mNrmNow, 0, BoroCnstNat) 

1415 

1416 # Construct the end-of-period value function if requested 

1417 if vFuncBool: 

1418 # Calculate end-of-period value, its derivative, and their pseudo-inverse 

1419 EndOfPrdv = DiscFacEff * expected(calc_vNext, RiskyDstn, args=(aNrmNow)) 

1420 EndOfPrdvNvrs = uFunc.inv(EndOfPrdv) 

1421 # value transformed through inverse utility 

1422 EndOfPrdvNvrsP = EndOfPrdvP * uFunc.derinv(EndOfPrdv, order=(0, 1)) 

1423 

1424 # Construct the end-of-period value function 

1425 if BoroCnstNat_iszero: 

1426 EndOfPrdvNvrs = np.insert(EndOfPrdvNvrs, 0, 0.0) 

1427 EndOfPrdvNvrsP = np.insert(EndOfPrdvNvrsP, 0, EndOfPrdvNvrsP[0]) 

1428 # This is a very good approximation, vNvrsPP = 0 at the asset minimum 

1429 aNrm_temp = np.insert(aNrmNow, 0, BoroCnstNat) 

1430 else: 

1431 aNrm_temp = aNrmNow.copy() 

1432 

1433 EndOfPrd_vNvrsFunc = CubicInterp(aNrm_temp, EndOfPrdvNvrs, EndOfPrdvNvrsP) 

1434 EndOfPrd_vFunc = ValueFuncCRRA(EndOfPrd_vNvrsFunc, CRRA) 

1435 

1436 # NON-INDEPENDENT METHOD BEGINS HERE 

1437 else: 

1438 # Construct the assets grid by adjusting aXtra by the natural borrowing constraint 

1439 # aNrmNow = np.asarray(aXtraGrid) + BoroCnstNat 

1440 if BoroCnstNat_iszero: 

1441 aNrmNow = aXtraGrid 

1442 else: 

1443 # Add an asset point at exactly zero 

1444 aNrmNow = np.insert(aXtraGrid, 0, 0.0) 

1445 

1446 # Define local functions for taking future expectations when the interest 

1447 # factor is *not* independent from the income shock distribution 

1448 Z = RiskyShareFixed # for shorter notation 

1449 

1450 def calc_mNrmNext(S, a): 

1451 Risky = S["Risky"] 

1452 Rport = Z * Risky + (1 - Z) * Rfree 

1453 return Rport / (PermGroFac * S["PermShk"]) * a + S["TranShk"] 

1454 

1455 def calc_vNext(S, a): 

1456 return S["PermShk"] ** (1.0 - CRRA) * vFuncNext(calc_mNrmNext(S, a)) 

1457 

1458 def calc_vPnext(S, a): 

1459 Risky = S["Risky"] 

1460 Rport = Z * Risky + (1 - Z) * Rfree 

1461 return Rport * S["PermShk"] ** (-CRRA) * vPfuncNext(calc_mNrmNext(S, a)) 

1462 

1463 def calc_vPPnext(S, a): 

1464 Risky = S["Risky"] 

1465 Rport = Z * Risky + (1 - Z) * Rfree 

1466 return ( 

1467 (Rport**2) 

1468 * S["PermShk"] ** (-CRRA - 1.0) 

1469 * vPPfuncNext(calc_mNrmNext(S, a)) 

1470 ) 

1471 

1472 # Calculate end-of-period marginal value of assets at each gridpoint 

1473 vPfacEff = DiscFacEff * PermGroFac ** (-CRRA) 

1474 EndOfPrdvP = vPfacEff * expected(calc_vPnext, ShockDstn, args=(aNrmNow)) 

1475 

1476 # Invert the first order condition to find optimal cNrm from each aNrm gridpoint 

1477 cNrmNow = uFunc.derinv(EndOfPrdvP, order=(1, 0)) 

1478 mNrmNow = cNrmNow + aNrmNow # Endogenous mNrm gridpoints 

1479 

1480 # Calculate the MPC at each gridpoint if using cubic spline interpolation 

1481 if CubicBool: 

1482 # Calculate end-of-period marginal marginal value of assets at each gridpoint 

1483 vPPfacEff = DiscFacEff * PermGroFac ** (-CRRA - 1.0) 

1484 EndOfPrdvPP = vPPfacEff * expected(calc_vPPnext, ShockDstn, args=(aNrmNow)) 

1485 dcda = EndOfPrdvPP / uFunc.der(np.array(cNrmNow), order=2) 

1486 MPC = dcda / (dcda + 1.0) 

1487 MPC_for_interpolation = np.insert(MPC, 0, MPCmaxNow) 

1488 

1489 # Limiting consumption is zero as m approaches mNrmMin 

1490 c_for_interpolation = np.insert(cNrmNow, 0, 0.0) 

1491 m_for_interpolation = np.insert(mNrmNow, 0, BoroCnstNat) 

1492 

1493 # Construct the end-of-period value function if requested 

1494 if vFuncBool: 

1495 # Calculate end-of-period value, its derivative, and their pseudo-inverse 

1496 vFacEff = DiscFacEff * PermGroFac ** (1.0 - CRRA) 

1497 EndOfPrdv = vFacEff * expected(calc_vNext, ShockDstn, args=(aNrmNow)) 

1498 EndOfPrdvNvrs = uFunc.inv(EndOfPrdv) 

1499 # value transformed through inverse utility 

1500 EndOfPrdvNvrsP = EndOfPrdvP * uFunc.derinv(EndOfPrdv, order=(0, 1)) 

1501 

1502 # Construct the end-of-period value function 

1503 if BoroCnstNat_iszero: 

1504 EndOfPrdvNvrs = np.insert(EndOfPrdvNvrs, 0, 0.0) 

1505 EndOfPrdvNvrsP = np.insert(EndOfPrdvNvrsP, 0, EndOfPrdvNvrsP[0]) 

1506 # This is a very good approximation, vNvrsPP = 0 at the asset minimum 

1507 aNrm_temp = np.insert(aNrmNow, 0, BoroCnstNat) 

1508 else: 

1509 aNrm_temp = aNrmNow.copy() 

1510 

1511 EndOfPrd_vNvrsFunc = CubicInterp(aNrm_temp, EndOfPrdvNvrs, EndOfPrdvNvrsP) 

1512 EndOfPrd_vFunc = ValueFuncCRRA(EndOfPrd_vNvrsFunc, CRRA) 

1513 

1514 # Construct the consumption function; this uses the same method whether the 

1515 # income distribution is independent from the return distribution 

1516 if CubicBool: 

1517 # Construct the unconstrained consumption function as a cubic interpolation 

1518 cFuncNowUnc = CubicInterp( 

1519 m_for_interpolation, 

1520 c_for_interpolation, 

1521 MPC_for_interpolation, 

1522 cFuncLimitIntercept, 

1523 cFuncLimitSlope, 

1524 ) 

1525 else: 

1526 # Construct the unconstrained consumption function as a linear interpolation 

1527 cFuncNowUnc = LinearInterp( 

1528 m_for_interpolation, 

1529 c_for_interpolation, 

1530 cFuncLimitIntercept, 

1531 cFuncLimitSlope, 

1532 ) 

1533 

1534 # Combine the constrained and unconstrained functions into the true consumption function. 

1535 # LowerEnvelope should only be used when BoroCnstArt is True 

1536 cFuncNow = LowerEnvelope(cFuncNowUnc, cFuncNowCnst, nan_bool=False) 

1537 

1538 # Make the marginal value function and the marginal marginal value function 

1539 vPfuncNow = MargValueFuncCRRA(cFuncNow, CRRA) 

1540 

1541 # Define this period's marginal marginal value function 

1542 if CubicBool: 

1543 vPPfuncNow = MargMargValueFuncCRRA(cFuncNow, CRRA) 

1544 else: 

1545 vPPfuncNow = NullFunc() # Dummy object 

1546 

1547 # Construct this period's value function if requested. This version is set 

1548 # up for the non-independent distributions, need to write a faster version. 

1549 if vFuncBool: 

1550 # Compute expected value and marginal value on a grid of market resources 

1551 mNrm_temp = mNrmMinNow + aXtraGrid 

1552 cNrm_temp = cFuncNow(mNrm_temp) 

1553 aNrm_temp = np.maximum(mNrm_temp - cNrm_temp, 0.0) # fix tiny errors 

1554 v_temp = uFunc(cNrm_temp) + EndOfPrd_vFunc(aNrm_temp) 

1555 vP_temp = uFunc.der(cNrm_temp) 

1556 

1557 # Construct the beginning-of-period value function 

1558 vNvrs_temp = uFunc.inv(v_temp) # value transformed through inv utility 

1559 vNvrsP_temp = vP_temp * uFunc.derinv(v_temp, order=(0, 1)) 

1560 mNrm_temp = np.insert(mNrm_temp, 0, mNrmMinNow) 

1561 vNvrs_temp = np.insert(vNvrs_temp, 0, 0.0) 

1562 vNvrsP_temp = np.insert(vNvrsP_temp, 0, MPCmaxEff ** (-CRRA / (1.0 - CRRA))) 

1563 # MPCminNvrs = MPCminNow ** (-CRRA / (1.0 - CRRA)) 

1564 vNvrsFuncNow = CubicInterp(mNrm_temp, vNvrs_temp, vNvrsP_temp) 

1565 vFuncNow = ValueFuncCRRA(vNvrsFuncNow, CRRA) 

1566 else: 

1567 vFuncNow = NullFunc() # Dummy object 

1568 

1569 # Create and return this period's solution 

1570 solution_now = ConsumerSolution( 

1571 cFunc=cFuncNow, 

1572 vFunc=vFuncNow, 

1573 vPfunc=vPfuncNow, 

1574 vPPfunc=vPPfuncNow, 

1575 mNrmMin=mNrmMinNow, 

1576 hNrm=hNrmNow, 

1577 MPCmin=MPCminNow, 

1578 MPCmax=MPCmaxEff, 

1579 ) 

1580 solution_now.ShareFunc = ConstantFunction(RiskyShareFixed) 

1581 return solution_now