Coverage for HARK/ConsumptionSaving/ConsRiskyContribModel.py: 98%

506 statements  

« 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 

3a consumer type with idiosyncratic shocks to permanent and transitory income, 

4who can save in both a risk-free and a risky asset but faces frictions to 

5moving funds between them. The agent can only consume out of his risk-free 

6asset. 

7 

8The model is described in detail in the REMARK: 

9https://econ-ark.org/materials/riskycontrib 

10 

11.. code:: bibtex 

12 

13 @software{mateo_velasquez_giraldo_2021_4977915, 

14 author = {Mateo Velásquez-Giraldo}, 

15 title = {{Mv77/RiskyContrib: A Two-Asset Savings Model with 

16 an Income-Contribution Scheme}}, 

17 month = jun, 

18 year = 2021, 

19 publisher = {Zenodo}, 

20 version = {v1.0.1}, 

21 doi = {10.5281/zenodo.4977915}, 

22 url = {https://doi.org/10.5281/zenodo.4977915} 

23 } 

24 

25""" 

26 

27import numpy as np 

28 

29from HARK import NullFunc # Basic HARK features 

30from HARK.ConsumptionSaving.ConsIndShockModel import utility # CRRA utility function 

31from HARK.ConsumptionSaving.ConsIndShockModel import ( 

32 utility_inv, # Inverse CRRA utility function 

33) 

34from HARK.ConsumptionSaving.ConsIndShockModel import ( 

35 utilityP, # CRRA marginal utility function 

36) 

37from HARK.ConsumptionSaving.ConsIndShockModel import ( 

38 utilityP_inv, # Inverse CRRA marginal utility function 

39) 

40from HARK.Calibration.Income.IncomeProcesses import ( 

41 construct_lognormal_income_process_unemployment, 

42 get_PermShkDstn_from_IncShkDstn, 

43 get_TranShkDstn_from_IncShkDstn, 

44) 

45from HARK.Calibration.Assets.AssetProcesses import ( 

46 make_lognormal_RiskyDstn, 

47 combine_IncShkDstn_and_RiskyDstn, 

48 calc_ShareLimit_for_CRRA, 

49) 

50from HARK.ConsumptionSaving.ConsIndShockModel import ( 

51 init_lifecycle, 

52 make_lognormal_kNrm_init_dstn, 

53 make_lognormal_pLvl_init_dstn, 

54) 

55from HARK.ConsumptionSaving.ConsRiskyAssetModel import ( 

56 RiskyAssetConsumerType, 

57 init_risky_asset, 

58 make_AdjustDstn, 

59) 

60from HARK.distributions import calc_expectation 

61from HARK.interpolation import BilinearInterp # 2D interpolator 

62from HARK.interpolation import ( 

63 ConstantFunction, # Interpolator-like class that returns constant value 

64) 

65from HARK.interpolation import ( 

66 IdentityFunction, # Interpolator-like class that returns one of its arguments 

67) 

68from HARK.interpolation import LinearInterp # Piecewise linear interpolation 

69from HARK.interpolation import TrilinearInterp # 3D interpolator 

70from HARK.interpolation import DiscreteInterp, MargValueFuncCRRA, ValueFuncCRRA 

71from HARK.metric import MetricObject 

72from HARK.utilities import make_grid_exp_mult, make_assets_grid 

73 

74############################################################################### 

75 

76 

77def make_bounded_ShareGrid(ShareCount, ShareMax): 

78 """ 

79 Make a uniformly spaced grid on the unit interval, representing shares 

80 contributed toward the risky asset. 

81 

82 Parameters 

83 ---------- 

84 ShareCount : int 

85 Number of points in the grid. 

86 ShareMax : float 

87 Highest risky fraction allowed. 

88 

89 Returns 

90 ------- 

91 ShareGrid : np.array 

92 """ 

93 ShareGrid = np.linspace(0.0, ShareMax, ShareCount) 

94 return ShareGrid 

95 

96 

97def make_simple_dGrid(dCount): 

98 """ 

99 Make a uniformly spaced grid on the unit interval, representing rebalancing rates. 

100 

101 Parameters 

102 ---------- 

103 dCount : int 

104 Number of points in the grid. 

105 

106 Returns 

107 ------- 

108 dGrid : np.array 

109 """ 

110 dGrid = np.linspace(0.0, 1.0, dCount) 

111 return dGrid 

112 

113 

114def make_nNrm_grid(nNrmMin, nNrmMax, nNrmCount, nNrmNestFac): 

115 """ 

116 Creates the agent's illiquid assets grid by constructing a multi-exponentially 

117 spaced grid of nNrm values. 

118 

119 Parameters 

120 ---------- 

121 nNrmMin : float 

122 Minimum value in the illiquid assets grid. 

123 nNrmMax : float 

124 Maximum value in the illiquid assets grid. 

125 nNrmCount : float 

126 Number of gridpoints in the illiquid assets grid. 

127 nNrmNestFac : int 

128 Degree of exponential nesting for illiquid assets. 

129 

130 Returns 

131 ------- 

132 nNrmGrid : np.array 

133 Constructed grid of illiquid assets. 

134 """ 

135 nNrmGrid = make_grid_exp_mult( 

136 ming=nNrmMin, maxg=nNrmMax, ng=nNrmCount, timestonest=nNrmNestFac 

137 ) 

138 return nNrmGrid 

139 

140 

141def make_mNrm_grid(mNrmMin, mNrmMax, mNrmCount, mNrmNestFac): 

142 """ 

143 Creates the agent's liquid assets grid by constructing a multi-exponentially 

144 spaced grid of mNrm values. 

145 

146 Parameters 

147 ---------- 

148 mNrmMin : float 

149 Minimum value in the liquid assets grid. 

150 mNrmMax : float 

151 Maximum value in the liquid assets grid. 

152 mNrmCount : float 

153 Number of gridpoints in the liquid assets grid. 

154 mNrmNestFac : int 

155 Degree of exponential nesting for liquid assets. 

156 

157 Returns 

158 ------- 

159 mNrmGrid : np.array 

160 Constructed grid of liquid assets. 

161 """ 

162 mNrmGrid = make_grid_exp_mult( 

163 ming=mNrmMin, maxg=mNrmMax, ng=mNrmCount, timestonest=mNrmNestFac 

164 ) 

165 return mNrmGrid 

166 

167 

168def make_solution_terminal_risky_contrib(CRRA, tau): 

169 """ 

170 Solves the terminal period. The solution is trivial. 

171 Cns: agent will consume all of his liquid resources. 

172 Sha: irrelevant as there is no "next" period. 

173 Reb: agent will shift all of his resources to the risk-free asset. 

174 

175 Parameters 

176 ---------- 

177 CRRA : float 

178 Coefficient of relative risk aversion. 

179 tau : float 

180 Tax rate of some kind. 

181 

182 Returns 

183 ------- 

184 solution_terminal : RiskyContribSolution 

185 Terminal period solution object 

186 """ 

187 

188 # Construct the terminal solution backwards. 

189 

190 # Start with the consumption stage. All liquid resources are consumed. 

191 cFunc_term = IdentityFunction(i_dim=0, n_dims=3) 

192 vFunc_Cns_term = ValueFuncCRRA(cFunc_term, CRRA=CRRA) 

193 # Marginal values 

194 dvdmFunc_Cns_term = MargValueFuncCRRA(cFunc_term, CRRA=CRRA) 

195 dvdnFunc_Cns_term = ConstantFunction(0.0) 

196 dvdsFunc_Cns_term = ConstantFunction(0.0) 

197 

198 Cns_stage_sol = RiskyContribCnsSolution( 

199 # Consumption stage 

200 vFunc=vFunc_Cns_term, 

201 cFunc=cFunc_term, 

202 dvdmFunc=dvdmFunc_Cns_term, 

203 dvdnFunc=dvdnFunc_Cns_term, 

204 dvdsFunc=dvdsFunc_Cns_term, 

205 ) 

206 

207 # Share stage 

208 

209 # It's irrelevant because there is no future period. Set share to 0. 

210 # Create a dummy 2-d consumption function to get value function and marginal 

211 c2d = IdentityFunction(i_dim=0, n_dims=2) 

212 Sha_stage_sol = RiskyContribShaSolution( 

213 # Adjust 

214 vFunc_Adj=ValueFuncCRRA(c2d, CRRA=CRRA), 

215 ShareFunc_Adj=ConstantFunction(0.0), 

216 dvdmFunc_Adj=MargValueFuncCRRA(c2d, CRRA=CRRA), 

217 dvdnFunc_Adj=ConstantFunction(0.0), 

218 # Fixed 

219 vFunc_Fxd=vFunc_Cns_term, 

220 ShareFunc_Fxd=IdentityFunction(i_dim=2, n_dims=3), 

221 dvdmFunc_Fxd=dvdmFunc_Cns_term, 

222 dvdnFunc_Fxd=dvdnFunc_Cns_term, 

223 dvdsFunc_Fxd=dvdsFunc_Cns_term, 

224 ) 

225 

226 # Rebalancing stage 

227 

228 # Adjusting agent: 

229 # Withdraw everything from the pension fund and consume everything 

230 dfracFunc_Adj_term = ConstantFunction(-1.0) 

231 

232 # Find the withdrawal penalty. If it is time-varying, assume it takes 

233 # the same value as in the last non-terminal period 

234 if type(tau) is list: 

235 tau = tau[-1] 

236 else: 

237 tau = tau 

238 

239 # Value and marginal value function of the adjusting agent 

240 vFunc_Reb_Adj_term = ValueFuncCRRA(lambda m, n: m + n / (1 + tau), CRRA) 

241 dvdmFunc_Reb_Adj_term = MargValueFuncCRRA(lambda m, n: m + n / (1 + tau), CRRA) 

242 # A marginal unit of n will be withdrawn and put into m. Then consumed. 

243 dvdnFunc_Reb_Adj_term = lambda m, n: dvdmFunc_Reb_Adj_term(m, n) / (1 + tau) 

244 

245 Reb_stage_sol = RiskyContribRebSolution( 

246 # Rebalancing stage 

247 vFunc_Adj=vFunc_Reb_Adj_term, 

248 dfracFunc_Adj=dfracFunc_Adj_term, 

249 dvdmFunc_Adj=dvdmFunc_Reb_Adj_term, 

250 dvdnFunc_Adj=dvdnFunc_Reb_Adj_term, 

251 # Adjusting stage 

252 vFunc_Fxd=vFunc_Cns_term, 

253 dfracFunc_Fxd=ConstantFunction(0.0), 

254 dvdmFunc_Fxd=dvdmFunc_Cns_term, 

255 dvdnFunc_Fxd=dvdnFunc_Cns_term, 

256 dvdsFunc_Fxd=dvdsFunc_Cns_term, 

257 ) 

258 

259 # Construct the terminal period solution 

260 solution_terminal = RiskyContribSolution( 

261 Reb_stage_sol, Sha_stage_sol, Cns_stage_sol 

262 ) 

263 return solution_terminal 

264 

265 

266############################################################################### 

267 

268# %% Classes for RiskyContrib type solution objects 

269 

270 

271# Class for asset adjustment stage solution 

272class RiskyContribRebSolution(MetricObject): 

273 """ 

274 A class for representing the solution to the asset-rebalancing stage of 

275 the 'RiskyContrib' model. 

276 

277 Parameters 

278 ---------- 

279 vFunc_Adj : ValueFunc2D 

280 Stage value function over normalized liquid resources and normalized 

281 iliquid resources when the agent is able to adjust his portfolio. 

282 dfracFunc_Adj : Interp2D 

283 Deposit function over normalized liquid resources and normalized 

284 iliquid resources when the agent is able to adjust his portfolio. 

285 dvdmFunc_Adj : MargValueFunc2D 

286 Marginal value over normalized liquid resources when the agent is able 

287 to adjust his portfolio. 

288 dvdnFunc_Adj : MargValueFunc2D 

289 Marginal value over normalized liquid resources when the agent is able 

290 to adjust his portfolio. 

291 vFunc_Fxd : ValueFunc3D 

292 Stage value function over normalized liquid resources, normalized 

293 iliquid resources, and income contribution share when the agent is 

294 not able to adjust his portfolio. 

295 dfracFunc_Fxd : Interp2D 

296 Deposit function over normalized liquid resources, normalized iliquid 

297 resources, and income contribution share when the agent is not able to 

298 adjust his portfolio. 

299 Must be ConstantFunction(0.0) 

300 dvdmFunc_Fxd : MargValueFunc3D 

301 Marginal value over normalized liquid resources when the agent is not 

302 able to adjust his portfolio. 

303 dvdnFunc_Fxd : MargValueFunc3D 

304 Marginal value over normalized iliquid resources when the agent is not 

305 able to adjust his portfolio. 

306 dvdsFunc_Fxd : Interp3D 

307 Marginal value function over income contribution share when the agent 

308 is not able to ajust his portfolio. 

309 """ 

310 

311 distance_criteria = ["dvdmFunc_Adj", "dvdnFunc_Adj"] 

312 

313 def __init__( 

314 self, 

315 # Rebalancing stage, adjusting 

316 vFunc_Adj=None, 

317 dfracFunc_Adj=None, 

318 dvdmFunc_Adj=None, 

319 dvdnFunc_Adj=None, 

320 # Rebalancing stage, fixed 

321 vFunc_Fxd=None, 

322 dfracFunc_Fxd=None, 

323 dvdmFunc_Fxd=None, 

324 dvdnFunc_Fxd=None, 

325 dvdsFunc_Fxd=None, 

326 ): 

327 # Rebalancing stage 

328 if vFunc_Adj is None: 

329 vFunc_Adj = NullFunc() 

330 if dfracFunc_Adj is None: 

331 dfracFunc_Adj = NullFunc() 

332 if dvdmFunc_Adj is None: 

333 dvdmFunc_Adj = NullFunc() 

334 if dvdnFunc_Adj is None: 

335 dvdnFunc_Adj = NullFunc() 

336 

337 if vFunc_Fxd is None: 

338 vFunc_Fxd = NullFunc() 

339 if dfracFunc_Fxd is None: 

340 dfracFunc_Fxd = NullFunc() 

341 if dvdmFunc_Fxd is None: 

342 dvdmFunc_Fxd = NullFunc() 

343 if dvdnFunc_Fxd is None: 

344 dvdnFunc_Fxd = NullFunc() 

345 if dvdsFunc_Fxd is None: 

346 dvdsFunc_Fxd = NullFunc() 

347 

348 # Components of the adjusting problem 

349 self.vFunc_Adj = vFunc_Adj 

350 self.dfracFunc_Adj = dfracFunc_Adj 

351 self.dvdmFunc_Adj = dvdmFunc_Adj 

352 self.dvdnFunc_Adj = dvdnFunc_Adj 

353 

354 # Components of the fixed problem 

355 self.vFunc_Fxd = vFunc_Fxd 

356 self.dfracFunc_Fxd = dfracFunc_Fxd 

357 self.dvdmFunc_Fxd = dvdmFunc_Fxd 

358 self.dvdnFunc_Fxd = dvdnFunc_Fxd 

359 self.dvdsFunc_Fxd = dvdsFunc_Fxd 

360 

361 

362# Class for the contribution share stage solution 

363class RiskyContribShaSolution(MetricObject): 

364 """ 

365 A class for representing the solution to the contribution-share stage of 

366 the 'RiskyContrib' model. 

367 

368 Parameters 

369 ---------- 

370 vFunc_Adj : ValueFunc2D 

371 Stage value function over normalized liquid resources and normalized 

372 iliquid resources when the agent is able to adjust his portfolio. 

373 ShareFunc_Adj : Interp2D 

374 Income contribution share function over normalized liquid resources 

375 and normalized iliquid resources when the agent is able to adjust his 

376 portfolio. 

377 dvdmFunc_Adj : MargValueFunc2D 

378 Marginal value function over normalized liquid resources when the agent 

379 is able to adjust his portfolio. 

380 dvdnFunc_Adj : MargValueFunc2D 

381 Marginal value function over normalized iliquid resources when the 

382 agent is able to adjust his portfolio. 

383 vFunc_Fxd : ValueFunc3D 

384 Stage value function over normalized liquid resources, normalized 

385 iliquid resources, and income contribution share when the agent is not 

386 able to adjust his portfolio. 

387 ShareFunc_Fxd : Interp3D 

388 Income contribution share function over normalized liquid resources, 

389 iliquid resources, and income contribution share when the agent is not 

390 able to adjust his portfolio. 

391 Should be an IdentityFunc. 

392 dvdmFunc_Fxd : MargValueFunc3D 

393 Marginal value function over normalized liquid resources when the agent 

394 is not able to adjust his portfolio. 

395 dvdnFunc_Fxd : MargValueFunc3D 

396 Marginal value function over normalized iliquid resources when the 

397 agent is not able to adjust his portfolio. 

398 dvdsFunc_Fxd : Interp3D 

399 Marginal value function over income contribution share when the agent 

400 is not able to adjust his portfolio 

401 """ 

402 

403 distance_criteria = ["dvdmFunc_Adj", "dvdnFunc_Adj"] 

404 

405 def __init__( 

406 self, 

407 # Contribution stage, adjust 

408 vFunc_Adj=None, 

409 ShareFunc_Adj=None, 

410 dvdmFunc_Adj=None, 

411 dvdnFunc_Adj=None, 

412 # Contribution stage, fixed 

413 vFunc_Fxd=None, 

414 ShareFunc_Fxd=None, 

415 dvdmFunc_Fxd=None, 

416 dvdnFunc_Fxd=None, 

417 dvdsFunc_Fxd=None, 

418 ): 

419 # Contribution stage, adjust 

420 if vFunc_Adj is None: 

421 vFunc_Adj = NullFunc() 

422 if ShareFunc_Adj is None: 

423 ShareFunc_Adj = NullFunc() 

424 if dvdmFunc_Adj is None: 

425 dvdmFunc_Adj = NullFunc() 

426 if dvdnFunc_Adj is None: 

427 dvdnFunc_Adj = NullFunc() 

428 

429 # Contribution stage, fixed 

430 if vFunc_Fxd is None: 

431 vFunc_Fxd = NullFunc() 

432 if ShareFunc_Fxd is None: 

433 ShareFunc_Fxd = NullFunc() 

434 if dvdmFunc_Fxd is None: 

435 dvdmFunc_Fxd = NullFunc() 

436 if dvdnFunc_Fxd is None: 

437 dvdnFunc_Fxd = NullFunc() 

438 if dvdsFunc_Fxd is None: 

439 dvdsFunc_Fxd = NullFunc() 

440 

441 # Set attributes of self 

442 self.vFunc_Adj = vFunc_Adj 

443 self.ShareFunc_Adj = ShareFunc_Adj 

444 self.dvdmFunc_Adj = dvdmFunc_Adj 

445 self.dvdnFunc_Adj = dvdnFunc_Adj 

446 

447 self.vFunc_Fxd = vFunc_Fxd 

448 self.ShareFunc_Fxd = ShareFunc_Fxd 

449 self.dvdmFunc_Fxd = dvdmFunc_Fxd 

450 self.dvdnFunc_Fxd = dvdnFunc_Fxd 

451 self.dvdsFunc_Fxd = dvdsFunc_Fxd 

452 

453 

454# Class for the consumption stage solution 

455class RiskyContribCnsSolution(MetricObject): 

456 """ 

457 A class for representing the solution to the consumption stage of the 

458 'RiskyContrib' model. 

459 

460 Parameters 

461 ---------- 

462 vFunc : ValueFunc3D 

463 Stage-value function over normalized liquid resources, normalized 

464 iliquid resources, and income contribution share. 

465 cFunc : Interp3D 

466 Consumption function over normalized liquid resources, normalized 

467 iliquid resources, and income contribution share. 

468 dvdmFunc : MargValueFunc3D 

469 Marginal value function over normalized liquid resources. 

470 dvdnFunc : MargValueFunc3D 

471 Marginal value function over normalized iliquid resources. 

472 dvdsFunc : Interp3D 

473 Marginal value function over income contribution share. 

474 """ 

475 

476 distance_criteria = ["dvdmFunc", "dvdnFunc"] 

477 

478 def __init__( 

479 self, 

480 # Consumption stage 

481 vFunc=None, 

482 cFunc=None, 

483 dvdmFunc=None, 

484 dvdnFunc=None, 

485 dvdsFunc=None, 

486 ): 

487 if vFunc is None: 

488 vFunc = NullFunc() 

489 if cFunc is None: 

490 cFunc = NullFunc() 

491 if dvdmFunc is None: 

492 dvdmFunc = NullFunc() 

493 if dvdnFunc is None: 

494 dvdmFunc = NullFunc() 

495 if dvdsFunc is None: 

496 dvdsFunc = NullFunc() 

497 

498 self.vFunc = vFunc 

499 self.cFunc = cFunc 

500 self.dvdmFunc = dvdmFunc 

501 self.dvdnFunc = dvdnFunc 

502 self.dvdsFunc = dvdsFunc 

503 

504 

505# Class for the solution of a whole period 

506class RiskyContribSolution(MetricObject): 

507 """ 

508 A class for representing the solution to a full time-period of the 

509 'RiskyContrib' agent type's problem. 

510 

511 Parameters 

512 ---------- 

513 Reb : RiskyContribRebSolution 

514 Solution to the period's rebalancing stage. 

515 Sha : RiskyContribShaSolution 

516 Solution to the period's contribution-share stage. 

517 Cns : RiskyContribCnsSolution 

518 Solution to the period's consumption stage. 

519 """ 

520 

521 # Solutions are to be compared on the basis of their sub-period solutions 

522 distance_criteria = ["stage_sols"] 

523 

524 def __init__(self, Reb, Sha, Cns): 

525 # Dictionary of stage solutions 

526 self.stage_sols = {"Reb": Reb, "Sha": Sha, "Cns": Cns} 

527 

528 

529# %% Auxiliary functions and transition equations for the RiskyContrib model. 

530 

531 

532def rebalance_assets(d, m, n, tau): 

533 """ 

534 A function that produces post-rebalancing assets for given initial assets, 

535 rebalancing action, and tax rate. 

536 

537 Parameters 

538 ---------- 

539 d : np.array 

540 Array with rebalancing decisions. d > 0 represents depositing d*m into 

541 the risky asset account. d<0 represents withdrawing ``|d|*n`` (pre-tax) 

542 from the risky account into the risky account. 

543 m : np.array 

544 Initial risk-free assets. 

545 n : np.array 

546 Initial risky assets. 

547 tau : float 

548 Tax rate on flows from the risky to the risk-free asset. 

549 

550 Returns 

551 ------- 

552 mTil : np.array 

553 Post-rebalancing risk-free assets. 

554 nTil : np.arrat 

555 Post-rebalancing risky assets. 

556 

557 """ 

558 # Initialize 

559 mTil = np.zeros_like(m) + np.nan 

560 nTil = np.zeros_like(m) + np.nan 

561 

562 # Contributions 

563 inds = d >= 0 

564 mTil[inds] = m[inds] * (1 - d[inds]) 

565 nTil[inds] = n[inds] + m[inds] * d[inds] 

566 

567 # Withdrawals 

568 inds = d < 0 

569 mTil[inds] = m[inds] - d[inds] * n[inds] * (1 - tau) 

570 nTil[inds] = n[inds] * (1 + d[inds]) 

571 

572 return (mTil, nTil) 

573 

574 

575# Transition equations for the consumption stage 

576def m_nrm_next(shocks, aNrm, Share, Rfree, PermGroFac): 

577 """ 

578 Given end-of-period balances and contribution share and the 

579 start-of-next-period shocks, figure out next period's normalized riskless 

580 assets 

581 

582 Parameters 

583 ---------- 

584 shocks : np.array 

585 Length-3 array with the stochastic shocks that get realized between the 

586 end of the current period and the start of next period. Their order is 

587 (0) permanent income shock, (1) transitory income shock, (2) risky 

588 asset return. 

589 aNrm : float 

590 End-of-period risk-free asset balances. 

591 Share : float 

592 End-of-period income deduction share. 

593 Rfree : float 

594 Risk-free return factor. 

595 PermGroFac : float 

596 Permanent income growth factor. 

597 

598 Returns 

599 ------- 

600 m_nrm_tp1 : float 

601 Next-period normalized riskless balance. 

602 

603 """ 

604 # Extract shocks 

605 perm_shk = shocks[0] 

606 tran_shk = shocks[1] 

607 

608 m_nrm_tp1 = Rfree * aNrm / (perm_shk * PermGroFac) + (1.0 - Share) * tran_shk 

609 

610 return m_nrm_tp1 

611 

612 

613def n_nrm_next(shocks, nNrm, Share, PermGroFac): 

614 """ 

615 Given end-of-period balances and contribution share and the 

616 start-of-next-period shocks, figure out next period's normalized risky 

617 assets 

618 

619 Parameters 

620 ---------- 

621 shocks : np.array 

622 Length-3 array with the stochastic shocks that get realized between the 

623 end of the current period and the start of next period. Their order is 

624 (0) permanent income shock, (1) transitory income shock, (2) risky 

625 asset return. 

626 nNrm : float 

627 End-of-period risky asset balances. 

628 Share : float 

629 End-of-period income deduction share. 

630 PermGroFac : float 

631 Permanent income growth factor. 

632 

633 Returns 

634 ------- 

635 n_nrm_tp1 : float 

636 Next-period normalized risky balance. 

637 

638 """ 

639 

640 # Extract shocks 

641 perm_shk = shocks[0] 

642 tran_shk = shocks[1] 

643 R_risky = shocks[2] 

644 

645 n_nrm_tp1 = R_risky * nNrm / (perm_shk * PermGroFac) + Share * tran_shk 

646 

647 return n_nrm_tp1 

648 

649 

650# %% RiskyContrib solvers 

651 

652 

653# Consumption stage solver 

654def solve_RiskyContrib_Cns( 

655 solution_next, 

656 ShockDstn, 

657 IncShkDstn, 

658 RiskyDstn, 

659 IndepDstnBool, 

660 LivPrb, 

661 DiscFac, 

662 CRRA, 

663 Rfree, 

664 PermGroFac, 

665 BoroCnstArt, 

666 aXtraGrid, 

667 nNrmGrid, 

668 mNrmGrid, 

669 ShareGrid, 

670 vFuncBool, 

671 AdjustPrb, 

672 DiscreteShareBool, 

673 joint_dist_solver, 

674 **unused_params, 

675): 

676 """ 

677 Solves the consumption stage of the agent's problem 

678 

679 Parameters 

680 ---------- 

681 solution_next : RiskyContribRebSolution 

682 Solution to the first stage of the next period in the agent's problem. 

683 ShockDstn : DiscreteDistribution 

684 Joint distribution of next period's (0) permanent income shock, (1) 

685 transitory income shock, and (2) risky asset return factor. 

686 IncShkDstn : DiscreteDistribution 

687 Joint distribution of next period's (0) permanent income shock and (1) 

688 transitory income shock. 

689 RiskyDstn : DiscreteDistribution 

690 Distribution of next period's risky asset return factor. 

691 IndepDstnBool : bool 

692 Indicates whether the income and risky return distributions are 

693 independent. 

694 LivPrb : float 

695 Probability of surviving until next period. 

696 DiscFac : float 

697 Time-preference discount factor. 

698 CRRA : float 

699 Coefficient of relative risk aversion. 

700 Rfree : float 

701 Risk-free return factor. 

702 PermGroFac : float 

703 Deterministic permanent income growth factor. 

704 BoroCnstArt : float 

705 Minimum allowed market resources (must be 0). 

706 aXtraGrid : numpy array 

707 Exogenous grid for end-of-period risk free resources. 

708 nNrmGrid : numpy array 

709 Exogenous grid for risky resources. 

710 mNrmGrid : numpy array 

711 Exogenous grid for risk-free resources. 

712 ShareGrid : numpt array 

713 Exogenous grid for the income contribution share. 

714 vFuncBool : bool 

715 Boolean that determines wether the value function's level needs to be 

716 computed. 

717 AdjustPrb : float 

718 Probability thet the agent will be able to adjust his portfolio next 

719 period. 

720 DiscreteShareBool : bool 

721 Boolean that determines whether only a discrete set of contribution 

722 shares (ShareGrid) is allowed. 

723 joint_dist_solver: bool 

724 Should the general solver be used even if income and returns are 

725 independent? 

726 

727 Returns 

728 ------- 

729 solution : RiskyContribCnsSolution 

730 Solution to the agent's consumption stage problem. 

731 

732 """ 

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

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

735 if BoroCnstArt != 0.0: 

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

737 

738 # Make sure that if risky portfolio share is optimized only discretely, then 

739 # the value function is also constructed (else this task would be impossible). 

740 if DiscreteShareBool and (not vFuncBool): 

741 raise ValueError( 

742 "PortfolioConsumerType requires vFuncBool to be True when DiscreteShareBool is True!" 

743 ) 

744 

745 # Define temporary functions for utility and its derivative and inverse 

746 u = lambda x: utility(x, CRRA) 

747 uPinv = lambda x: utilityP_inv(x, CRRA) 

748 uInv = lambda x: utility_inv(x, CRRA) 

749 

750 # Unpack next period's solution 

751 vFunc_Reb_Adj_next = solution_next.vFunc_Adj 

752 dvdmFunc_Reb_Adj_next = solution_next.dvdmFunc_Adj 

753 dvdnFunc_Reb_Adj_next = solution_next.dvdnFunc_Adj 

754 

755 vFunc_Reb_Fxd_next = solution_next.vFunc_Fxd 

756 dvdmFunc_Reb_Fxd_next = solution_next.dvdmFunc_Fxd 

757 dvdnFunc_Reb_Fxd_next = solution_next.dvdnFunc_Fxd 

758 dvdsFunc_Reb_Fxd_next = solution_next.dvdsFunc_Fxd 

759 

760 # STEP ONE 

761 # Find end-of-period (continuation) value function and its derivatives. 

762 

763 # Start by constructing functions for next-period's pre-adjustment-shock 

764 # expected value functions 

765 if AdjustPrb < 1.0: 

766 dvdm_next = lambda m, n, s: AdjustPrb * dvdmFunc_Reb_Adj_next(m, n) + ( 

767 1.0 - AdjustPrb 

768 ) * dvdmFunc_Reb_Fxd_next(m, n, s) 

769 dvdn_next = lambda m, n, s: AdjustPrb * dvdnFunc_Reb_Adj_next(m, n) + ( 

770 1.0 - AdjustPrb 

771 ) * dvdnFunc_Reb_Fxd_next(m, n, s) 

772 dvds_next = lambda m, n, s: (1.0 - AdjustPrb) * dvdsFunc_Reb_Fxd_next(m, n, s) 

773 

774 # Value function if needed 

775 if vFuncBool: 

776 v_next = lambda m, n, s: AdjustPrb * vFunc_Reb_Adj_next(m, n) + ( 

777 1.0 - AdjustPrb 

778 ) * vFunc_Reb_Fxd_next(m, n, s) 

779 

780 else: 

781 dvdm_next = lambda m, n, s: dvdmFunc_Reb_Adj_next(m, n) 

782 dvdn_next = lambda m, n, s: dvdnFunc_Reb_Adj_next(m, n) 

783 dvds_next = ConstantFunction(0.0) 

784 

785 if vFuncBool: 

786 v_next = lambda m, n, s: vFunc_Reb_Adj_next(m, n) 

787 

788 if IndepDstnBool and not joint_dist_solver: 

789 # If income and returns are independent we can use the law of iterated 

790 # expectations to speed up the computation of end-of-period derivatives 

791 

792 # Define "post-return variables" 

793 # b_aux = aNrm * R 

794 # g_aux = nNrmTilde * Rtilde 

795 # and create a function that interpolates end-of-period marginal values 

796 # as functions of those and the contribution share 

797 

798 def post_return_derivs(inc_shocks, b_aux, g_aux, s): 

799 perm_shk = inc_shocks[0] 

800 tran_shk = inc_shocks[1] 

801 

802 temp_fac_A = utilityP(perm_shk * PermGroFac, CRRA) 

803 temp_fac_B = (perm_shk * PermGroFac) ** (1.0 - CRRA) 

804 

805 # Find next-period asset balances 

806 m_next = b_aux / (perm_shk * PermGroFac) + (1.0 - s) * tran_shk 

807 n_next = g_aux / (perm_shk * PermGroFac) + s * tran_shk 

808 

809 # Interpolate next-period-value derivatives 

810 dvdm_tp1 = dvdm_next(m_next, n_next, s) 

811 dvdn_tp1 = dvdn_next(m_next, n_next, s) 

812 if tran_shk == 0: 

813 dvds_tp1 = dvds_next(m_next, n_next, s) 

814 else: 

815 dvds_tp1 = tran_shk * (dvdn_tp1 - dvdm_tp1) + dvds_next( 

816 m_next, n_next, s 

817 ) 

818 

819 # Discount next-period-value derivatives to current period 

820 

821 # Liquid resources 

822 pr_dvda = temp_fac_A * dvdm_tp1 

823 # Iliquid resources 

824 pr_dvdn = temp_fac_A * dvdn_tp1 

825 # Contribution share 

826 pr_dvds = temp_fac_B * dvds_tp1 

827 

828 # End of period value function, if needed 

829 if vFuncBool: 

830 pr_v = temp_fac_B * v_next(m_next, n_next, s) 

831 return np.stack([pr_dvda, pr_dvdn, pr_dvds, pr_v]) 

832 else: 

833 return np.stack([pr_dvda, pr_dvdn, pr_dvds]) 

834 

835 # Define grids 

836 b_aux_grid = np.concatenate([np.array([0.0]), Rfree * aXtraGrid]) 

837 g_aux_grid = np.concatenate( 

838 [np.array([0.0]), max(RiskyDstn.atoms.flatten()) * nNrmGrid] 

839 ) 

840 

841 # Create tiled arrays with conforming dimensions. 

842 b_aux_tiled, g_aux_tiled, Share_tiled = np.meshgrid( 

843 b_aux_grid, g_aux_grid, ShareGrid, indexing="ij" 

844 ) 

845 

846 # Find end of period derivatives and value as expectations of (discounted) 

847 # next period's derivatives and value. 

848 pr_derivs = calc_expectation( 

849 IncShkDstn, post_return_derivs, b_aux_tiled, g_aux_tiled, Share_tiled 

850 ) 

851 

852 # Unpack results and create interpolators 

853 pr_dvdb_func = MargValueFuncCRRA( 

854 TrilinearInterp(uPinv(pr_derivs[0]), b_aux_grid, g_aux_grid, ShareGrid), 

855 CRRA, 

856 ) 

857 pr_dvdg_func = MargValueFuncCRRA( 

858 TrilinearInterp(uPinv(pr_derivs[1]), b_aux_grid, g_aux_grid, ShareGrid), 

859 CRRA, 

860 ) 

861 pr_dvds_func = TrilinearInterp(pr_derivs[2], b_aux_grid, g_aux_grid, ShareGrid) 

862 

863 if vFuncBool: 

864 pr_vFunc = ValueFuncCRRA( 

865 TrilinearInterp(uInv(pr_derivs[3]), b_aux_grid, g_aux_grid, ShareGrid), 

866 CRRA, 

867 ) 

868 

869 # Now construct a function that produces end-of-period derivatives 

870 # given the risky return draw 

871 def end_of_period_derivs(risky_ret, a, nTil, s): 

872 """ 

873 Computes the end-of-period derivatives (and optionally the value) of the 

874 continuation value function, conditional on risky returns. This is so that the 

875 expectations can be calculated by integrating over risky returns. 

876 

877 Parameters 

878 ---------- 

879 risky_ret : float 

880 Risky return factor 

881 a : float 

882 end-of-period risk-free assets. 

883 nTil : float 

884 end-of-period risky assets. 

885 s : float 

886 end-of-period income deduction share. 

887 """ 

888 

889 # Find next-period asset balances 

890 b_aux = a * Rfree 

891 g_aux = nTil * risky_ret 

892 

893 # Interpolate post-return derivatives 

894 pr_dvdb = pr_dvdb_func(b_aux, g_aux, s) 

895 pr_dvdg = pr_dvdg_func(b_aux, g_aux, s) 

896 pr_dvds = pr_dvds_func(b_aux, g_aux, s) 

897 

898 # Discount 

899 

900 # Liquid resources 

901 end_of_prd_dvda = DiscFac * Rfree * LivPrb * pr_dvdb 

902 # Iliquid resources 

903 end_of_prd_dvdn = DiscFac * risky_ret * LivPrb * pr_dvdg 

904 # Contribution share 

905 end_of_prd_dvds = DiscFac * LivPrb * pr_dvds 

906 

907 # End of period value function, i11f needed 

908 if vFuncBool: 

909 end_of_prd_v = DiscFac * LivPrb * pr_vFunc(b_aux, g_aux, s) 

910 return np.stack( 

911 [end_of_prd_dvda, end_of_prd_dvdn, end_of_prd_dvds, end_of_prd_v] 

912 ) 

913 else: 

914 return np.stack([end_of_prd_dvda, end_of_prd_dvdn, end_of_prd_dvds]) 

915 

916 else: 

917 # If income and returns are not independent, we just integrate over 

918 # them jointly. 

919 

920 # Construct a function that evaluates and discounts them given a 

921 # vector of return and income shocks and an end-of-period state 

922 def end_of_period_derivs(shocks, a, nTil, s): 

923 """ 

924 Computes the end-of-period derivatives (and optionally the value) of the 

925 continuation value function, conditional on shocks. This is so that the 

926 expectations can be calculated by integrating over shocks. 

927 

928 Parameters 

929 ---------- 

930 shocks : np.array 

931 Length-3 array with the stochastic shocks that get realized between the 

932 end of the current period and the start of next period. Their order is 

933 (0) permanent income shock, (1) transitory income shock, (2) risky 

934 asset return. 

935 a : float 

936 end-of-period risk-free assets. 

937 nTil : float 

938 end-of-period risky assets. 

939 s : float 

940 end-of-period income deduction share. 

941 """ 

942 temp_fac_A = utilityP(shocks[0] * PermGroFac, CRRA) 

943 temp_fac_B = (shocks[0] * PermGroFac) ** (1.0 - CRRA) 

944 

945 # Find next-period asset balances 

946 m_next = m_nrm_next(shocks, a, s, Rfree, PermGroFac) 

947 n_next = n_nrm_next(shocks, nTil, s, PermGroFac) 

948 

949 # Interpolate next-period-value derivatives 

950 dvdm_tp1 = dvdm_next(m_next, n_next, s) 

951 dvdn_tp1 = dvdn_next(m_next, n_next, s) 

952 if shocks[1] == 0: 

953 dvds_tp1 = dvds_next(m_next, n_next, s) 

954 else: 

955 dvds_tp1 = shocks[1] * (dvdn_tp1 - dvdm_tp1) + dvds_next( 

956 m_next, n_next, s 

957 ) 

958 

959 # Discount next-period-value derivatives to current period 

960 

961 # Liquid resources 

962 end_of_prd_dvda = DiscFac * Rfree * LivPrb * temp_fac_A * dvdm_tp1 

963 # Iliquid resources 

964 end_of_prd_dvdn = DiscFac * shocks[2] * LivPrb * temp_fac_A * dvdn_tp1 

965 # Contribution share 

966 end_of_prd_dvds = DiscFac * LivPrb * temp_fac_B * dvds_tp1 

967 

968 # End of period value function, i11f needed 

969 if vFuncBool: 

970 end_of_prd_v = DiscFac * LivPrb * temp_fac_B * v_next(m_next, n_next, s) 

971 return np.stack( 

972 [end_of_prd_dvda, end_of_prd_dvdn, end_of_prd_dvds, end_of_prd_v] 

973 ) 

974 else: 

975 return np.stack([end_of_prd_dvda, end_of_prd_dvdn, end_of_prd_dvds]) 

976 

977 # Now find the expected values on a (a, nTil, s) grid 

978 

979 # The "inversion" machinery can deal with assets of 0 even if there is a 

980 # natural borrowing constraint, so include zeros. 

981 nNrmGrid = np.concatenate([np.array([0.0]), nNrmGrid]) 

982 aNrmGrid = np.concatenate([np.array([0.0]), aXtraGrid]) 

983 

984 # Create tiled arrays with conforming dimensions. 

985 aNrm_tiled, nNrm_tiled, Share_tiled = np.meshgrid( 

986 aNrmGrid, nNrmGrid, ShareGrid, indexing="ij" 

987 ) 

988 

989 # Find end of period derivatives and value as expectations of (discounted) 

990 # next period's derivatives and value. 

991 eop_derivs = calc_expectation( 

992 RiskyDstn if IndepDstnBool and not joint_dist_solver else ShockDstn, 

993 end_of_period_derivs, 

994 aNrm_tiled, 

995 nNrm_tiled, 

996 Share_tiled, 

997 ) 

998 

999 # Unpack results 

1000 eop_dvdaNvrs = uPinv(eop_derivs[0]) 

1001 eop_dvdnNvrs = uPinv(eop_derivs[1]) 

1002 eop_dvds = eop_derivs[2] 

1003 if vFuncBool: 

1004 eop_vNvrs = uInv(eop_derivs[3]) 

1005 

1006 # Construct an interpolator for eop_V. It will be used later. 

1007 eop_vFunc = ValueFuncCRRA( 

1008 TrilinearInterp(eop_vNvrs, aNrmGrid, nNrmGrid, ShareGrid), CRRA 

1009 ) 

1010 

1011 # STEP TWO: 

1012 # Solve the consumption problem and create interpolators for c, vCns, 

1013 # and its derivatives. 

1014 

1015 # Apply EGM over liquid resources at every (n,s) to find consumption. 

1016 c_end = eop_dvdaNvrs 

1017 mNrm_end = aNrm_tiled + c_end 

1018 

1019 # Now construct interpolators for c and the derivatives of vCns. 

1020 # The m grid is different for every (n,s). We interpolate the object of 

1021 # interest on the regular m grid for every (n,s). At the end we will have 

1022 # values of the functions of interest on a regular (m,n,s) grid. We use 

1023 # trilinear interpolation on those points. 

1024 

1025 # Expand the exogenous m grid to contain 0. 

1026 mNrmGrid = np.insert(mNrmGrid, 0, 0) 

1027 

1028 # Dimensions might have changed, so re-create tiled arrays 

1029 mNrm_tiled, nNrm_tiled, Share_tiled = np.meshgrid( 

1030 mNrmGrid, nNrmGrid, ShareGrid, indexing="ij" 

1031 ) 

1032 

1033 # Initialize arrays 

1034 c_vals = np.zeros_like(mNrm_tiled) 

1035 dvdnNvrs_vals = np.zeros_like(mNrm_tiled) 

1036 dvds_vals = np.zeros_like(mNrm_tiled) 

1037 

1038 nNrm_N = nNrmGrid.size 

1039 Share_N = ShareGrid.size 

1040 for nInd in range(nNrm_N): 

1041 for sInd in range(Share_N): 

1042 # Extract the endogenous m grid for particular (n,s). 

1043 m_ns = mNrm_end[:, nInd, sInd] 

1044 

1045 # Check if there is a natural constraint 

1046 if m_ns[0] == 0.0: 

1047 # There's no need to insert points since we have m==0.0 

1048 

1049 # c 

1050 c_vals[:, nInd, sInd] = LinearInterp(m_ns, c_end[:, nInd, sInd])( 

1051 mNrmGrid 

1052 ) 

1053 

1054 # dvdnNvrs 

1055 dvdnNvrs_vals[:, nInd, sInd] = LinearInterp( 

1056 m_ns, eop_dvdnNvrs[:, nInd, sInd] 

1057 )(mNrmGrid) 

1058 

1059 # dvds 

1060 dvds_vals[:, nInd, sInd] = LinearInterp(m_ns, eop_dvds[:, nInd, sInd])( 

1061 mNrmGrid 

1062 ) 

1063 

1064 else: 

1065 # We know that: 

1066 # -The lowest gridpoints of both a and n are 0. 

1067 # -Consumption at m < m0 is m. 

1068 # -dvdn_Fxd at (m,n) for m < m0(n) is dvdn_Fxd(m0,n) 

1069 # -Same is true for dvds_Fxd 

1070 

1071 m_ns = np.concatenate([np.array([0]), m_ns]) 

1072 

1073 # c 

1074 c_vals[:, nInd, sInd] = LinearInterp( 

1075 m_ns, np.concatenate([np.array([0]), c_end[:, nInd, sInd]]) 

1076 )(mNrmGrid) 

1077 

1078 # dvdnNvrs 

1079 dvdnNvrs_vals[:, nInd, sInd] = LinearInterp( 

1080 m_ns, 

1081 np.concatenate( 

1082 [ 

1083 np.array([eop_dvdnNvrs[0, nInd, sInd]]), 

1084 eop_dvdnNvrs[:, nInd, sInd], 

1085 ] 

1086 ), 

1087 )(mNrmGrid) 

1088 

1089 # dvds 

1090 dvds_vals[:, nInd, sInd] = LinearInterp( 

1091 m_ns, 

1092 np.concatenate( 

1093 [ 

1094 np.array([eop_dvds[0, nInd, sInd]]), 

1095 eop_dvds[:, nInd, sInd], 

1096 ] 

1097 ), 

1098 )(mNrmGrid) 

1099 

1100 # With the arrays filled, create 3D interpolators 

1101 

1102 # Consumption interpolator 

1103 cFunc = TrilinearInterp(c_vals, mNrmGrid, nNrmGrid, ShareGrid) 

1104 # dvdmCns interpolator 

1105 dvdmFunc_Cns = MargValueFuncCRRA(cFunc, CRRA) 

1106 # dvdnCns interpolator 

1107 dvdnNvrsFunc = TrilinearInterp(dvdnNvrs_vals, mNrmGrid, nNrmGrid, ShareGrid) 

1108 dvdnFunc_Cns = MargValueFuncCRRA(dvdnNvrsFunc, CRRA) 

1109 # dvdsCns interpolator 

1110 dvdsFunc_Cns = TrilinearInterp(dvds_vals, mNrmGrid, nNrmGrid, ShareGrid) 

1111 

1112 # Compute value function if needed 

1113 if vFuncBool: 

1114 # Consumption in the regular grid 

1115 aNrm_reg = mNrm_tiled - c_vals 

1116 vCns = u(c_vals) + eop_vFunc(aNrm_reg, nNrm_tiled, Share_tiled) 

1117 vNvrsCns = uInv(vCns) 

1118 vNvrsFunc_Cns = TrilinearInterp(vNvrsCns, mNrmGrid, nNrmGrid, ShareGrid) 

1119 vFunc_Cns = ValueFuncCRRA(vNvrsFunc_Cns, CRRA) 

1120 else: 

1121 vFunc_Cns = NullFunc() 

1122 

1123 # Assemble solution 

1124 solution = RiskyContribCnsSolution( 

1125 vFunc=vFunc_Cns, 

1126 cFunc=cFunc, 

1127 dvdmFunc=dvdmFunc_Cns, 

1128 dvdnFunc=dvdnFunc_Cns, 

1129 dvdsFunc=dvdsFunc_Cns, 

1130 ) 

1131 

1132 return solution 

1133 

1134 

1135# Solver for the contribution stage 

1136def solve_RiskyContrib_Sha( 

1137 solution_next, 

1138 CRRA, 

1139 AdjustPrb, 

1140 mNrmGrid, 

1141 nNrmGrid, 

1142 ShareGrid, 

1143 DiscreteShareBool, 

1144 vFuncBool, 

1145 **unused_params, 

1146): 

1147 """ 

1148 Solves the income-contribution-share stag of the agent's problem 

1149 

1150 Parameters 

1151 ---------- 

1152 solution_next : RiskyContribCnsSolution 

1153 Solution to the agent's consumption stage problem that follows. 

1154 CRRA : float 

1155 Coefficient of relative risk aversion. 

1156 AdjustPrb : float 

1157 Probability that the agent will be able to rebalance his portfolio 

1158 next period. 

1159 mNrmGrid : numpy array 

1160 Exogenous grid for risk-free resources. 

1161 nNrmGrid : numpy array 

1162 Exogenous grid for risky resources. 

1163 ShareGrid : numpy array 

1164 Exogenous grid for the income contribution share. 

1165 DiscreteShareBool : bool 

1166 Boolean that determines whether only a discrete set of contribution 

1167 shares (ShareGrid) is allowed. 

1168 vFuncBool : bool 

1169 Determines whether the level of the value function is computed. 

1170 

1171 Yields 

1172 ------ 

1173 solution : RiskyContribShaSolution 

1174 Solution to the income-contribution-share stage of the agent's problem. 

1175 

1176 """ 

1177 # Unpack solution from the next sub-stage 

1178 vFunc_Cns_next = solution_next.vFunc 

1179 cFunc_next = solution_next.cFunc 

1180 dvdmFunc_Cns_next = solution_next.dvdmFunc 

1181 dvdnFunc_Cns_next = solution_next.dvdnFunc 

1182 dvdsFunc_Cns_next = solution_next.dvdsFunc 

1183 

1184 uPinv = lambda x: utilityP_inv(x, CRRA) 

1185 

1186 # Create tiled grids 

1187 

1188 # Add 0 to the m and n grids 

1189 nNrmGrid = np.concatenate([np.array([0.0]), nNrmGrid]) 

1190 nNrm_N = len(nNrmGrid) 

1191 mNrmGrid = np.concatenate([np.array([0.0]), mNrmGrid]) 

1192 mNrm_N = len(mNrmGrid) 

1193 

1194 if AdjustPrb == 1.0: 

1195 # If the readjustment probability is 1, set the share to 0: 

1196 # - If there is a withdrawal tax: better for the agent to observe 

1197 # income before rebalancing. 

1198 # - If there is no tax: all shares should yield the same value. 

1199 mNrm_tiled, nNrm_tiled = np.meshgrid(mNrmGrid, nNrmGrid, indexing="ij") 

1200 

1201 opt_idx = np.zeros_like(mNrm_tiled, dtype=int) 

1202 opt_Share = ShareGrid[opt_idx] 

1203 

1204 if vFuncBool: 

1205 vNvrsSha = vFunc_Cns_next.vFuncNvrs(mNrm_tiled, nNrm_tiled, opt_Share) 

1206 

1207 else: 

1208 # Figure out optimal share by evaluating all alternatives at all 

1209 # (m,n) combinations 

1210 m_idx_tiled, n_idx_tiled = np.meshgrid( 

1211 np.arange(mNrm_N), np.arange(nNrm_N), indexing="ij" 

1212 ) 

1213 

1214 mNrm_tiled, nNrm_tiled, Share_tiled = np.meshgrid( 

1215 mNrmGrid, nNrmGrid, ShareGrid, indexing="ij" 

1216 ) 

1217 

1218 if DiscreteShareBool: 

1219 # Evaluate value function to optimize over shares. 

1220 # Do it in inverse space 

1221 vNvrs = vFunc_Cns_next.vFuncNvrs(mNrm_tiled, nNrm_tiled, Share_tiled) 

1222 

1223 # Find the optimal share at each (m,n). 

1224 opt_idx = np.argmax(vNvrs, axis=2) 

1225 

1226 # Compute objects needed for the value function and its derivatives 

1227 vNvrsSha = vNvrs[m_idx_tiled, n_idx_tiled, opt_idx] 

1228 opt_Share = ShareGrid[opt_idx] 

1229 

1230 # Project grids 

1231 mNrm_tiled = mNrm_tiled[:, :, 0] 

1232 nNrm_tiled = nNrm_tiled[:, :, 0] 

1233 

1234 else: 

1235 # Evaluate the marginal value of the contribution share at 

1236 # every (m,n,s) gridpoint 

1237 dvds = dvdsFunc_Cns_next(mNrm_tiled, nNrm_tiled, Share_tiled) 

1238 

1239 # If the derivative is negative at the lowest share, then s[0] is optimal 

1240 constrained_bot = dvds[:, :, 0] <= 0.0 

1241 # If it is poitive at the highest share, then s[-1] is optimal 

1242 constrained_top = dvds[:, :, -1] >= 0.0 

1243 

1244 # Find indices at which the derivative crosses 0 for the 1st time 

1245 # will be 0 if it never does, but "constrained_top/bot" deals with that 

1246 crossings = np.logical_and(dvds[:, :, :-1] >= 0.0, dvds[:, :, 1:] <= 0.0) 

1247 idx = np.argmax(crossings, axis=2) 

1248 

1249 # Linearly interpolate the optimal share 

1250 idx1 = idx + 1 

1251 slopes = ( 

1252 dvds[m_idx_tiled, n_idx_tiled, idx1] 

1253 - dvds[m_idx_tiled, n_idx_tiled, idx] 

1254 ) / (ShareGrid[idx1] - ShareGrid[idx]) 

1255 opt_Share = ShareGrid[idx] - dvds[m_idx_tiled, n_idx_tiled, idx] / slopes 

1256 

1257 # Replace the ones we knew were constrained 

1258 opt_Share[constrained_bot] = ShareGrid[0] 

1259 opt_Share[constrained_top] = ShareGrid[-1] 

1260 

1261 # Project grids 

1262 mNrm_tiled = mNrm_tiled[:, :, 0] 

1263 nNrm_tiled = nNrm_tiled[:, :, 0] 

1264 

1265 # Evaluate the inverse value function at the optimal shares 

1266 if vFuncBool: 

1267 vNvrsSha = vFunc_Cns_next.func(mNrm_tiled, nNrm_tiled, opt_Share) 

1268 

1269 dvdmNvrsSha = cFunc_next(mNrm_tiled, nNrm_tiled, opt_Share) 

1270 dvdnSha = dvdnFunc_Cns_next(mNrm_tiled, nNrm_tiled, opt_Share) 

1271 dvdnNvrsSha = uPinv(dvdnSha) 

1272 

1273 # Interpolators 

1274 

1275 # Value function if needed 

1276 if vFuncBool: 

1277 vNvrsFunc_Sha = BilinearInterp(vNvrsSha, mNrmGrid, nNrmGrid) 

1278 vFunc_Sha = ValueFuncCRRA(vNvrsFunc_Sha, CRRA) 

1279 else: 

1280 vFunc_Sha = NullFunc() 

1281 

1282 # Contribution share function 

1283 if DiscreteShareBool: 

1284 ShareFunc = DiscreteInterp( 

1285 BilinearInterp(opt_idx, mNrmGrid, nNrmGrid), ShareGrid 

1286 ) 

1287 else: 

1288 ShareFunc = BilinearInterp(opt_Share, mNrmGrid, nNrmGrid) 

1289 

1290 # Derivatives 

1291 dvdmNvrsFunc_Sha = BilinearInterp(dvdmNvrsSha, mNrmGrid, nNrmGrid) 

1292 dvdmFunc_Sha = MargValueFuncCRRA(dvdmNvrsFunc_Sha, CRRA) 

1293 dvdnNvrsFunc_Sha = BilinearInterp(dvdnNvrsSha, mNrmGrid, nNrmGrid) 

1294 dvdnFunc_Sha = MargValueFuncCRRA(dvdnNvrsFunc_Sha, CRRA) 

1295 

1296 solution = RiskyContribShaSolution( 

1297 vFunc_Adj=vFunc_Sha, 

1298 ShareFunc_Adj=ShareFunc, 

1299 dvdmFunc_Adj=dvdmFunc_Sha, 

1300 dvdnFunc_Adj=dvdnFunc_Sha, 

1301 # The fixed agent does nothing at this stage, 

1302 # so his value functions are the next problem's 

1303 vFunc_Fxd=vFunc_Cns_next, 

1304 ShareFunc_Fxd=IdentityFunction(i_dim=2, n_dims=3), 

1305 dvdmFunc_Fxd=dvdmFunc_Cns_next, 

1306 dvdnFunc_Fxd=dvdnFunc_Cns_next, 

1307 dvdsFunc_Fxd=dvdsFunc_Cns_next, 

1308 ) 

1309 

1310 return solution 

1311 

1312 

1313# Solver for the asset rebalancing stage 

1314def solve_RiskyContrib_Reb( 

1315 solution_next, CRRA, tau, nNrmGrid, mNrmGrid, dfracGrid, vFuncBool, **unused_params 

1316): 

1317 """ 

1318 Solves the asset-rebalancing-stage of the agent's problem 

1319 

1320 Parameters 

1321 ---------- 

1322 solution_next : RiskyContribShaSolution 

1323 Solution to the income-contribution-share stage problem that follows. 

1324 CRRA : float 

1325 Coefficient of relative risk aversion. 

1326 tau : float 

1327 Tax rate on risky asset withdrawals. 

1328 nNrmGrid : numpy array 

1329 Exogenous grid for risky resources. 

1330 mNrmGrid : numpy array 

1331 Exogenous grid for risk-free resources. 

1332 dfracGrid : numpy array 

1333 Grid for rebalancing flows. The final grid will be equivalent to 

1334 [-nNrm*dfracGrid, dfracGrid*mNrm]. 

1335 vFuncBool : bool 

1336 Determines whether the level of th value function must be computed. 

1337 

1338 Returns 

1339 ------- 

1340 solution : RiskyContribShaSolution 

1341 Solution to the asset-rebalancing stage of the agent's problem. 

1342 

1343 """ 

1344 # Extract next stage's solution 

1345 vFunc_Adj_next = solution_next.vFunc_Adj 

1346 dvdmFunc_Adj_next = solution_next.dvdmFunc_Adj 

1347 dvdnFunc_Adj_next = solution_next.dvdnFunc_Adj 

1348 

1349 vFunc_Fxd_next = solution_next.vFunc_Fxd 

1350 dvdmFunc_Fxd_next = solution_next.dvdmFunc_Fxd 

1351 dvdnFunc_Fxd_next = solution_next.dvdnFunc_Fxd 

1352 dvdsFunc_Fxd_next = solution_next.dvdsFunc_Fxd 

1353 

1354 uPinv = lambda x: utilityP_inv(x, CRRA) 

1355 

1356 # Create tiled grids 

1357 

1358 # Add 0 to the m and n grids 

1359 nNrmGrid = np.concatenate([np.array([0.0]), nNrmGrid]) 

1360 nNrm_N = len(nNrmGrid) 

1361 mNrmGrid = np.concatenate([np.array([0.0]), mNrmGrid]) 

1362 mNrm_N = len(mNrmGrid) 

1363 d_N = len(dfracGrid) 

1364 

1365 # Duplicate d so that possible values are -dfracGrid,dfracGrid. Duplicate 0 is 

1366 # intentional since the tax causes a discontinuity. We need the value 

1367 # from the left and right. 

1368 dfracGrid = np.concatenate((-1 * np.flip(dfracGrid), dfracGrid)) 

1369 

1370 # It will be useful to pre-evaluate marginals at every (m,n,d) combination 

1371 

1372 # Create tiled arrays for every d,m,n option 

1373 d_N2 = len(dfracGrid) 

1374 d_tiled, mNrm_tiled, nNrm_tiled = np.meshgrid( 

1375 dfracGrid, mNrmGrid, nNrmGrid, indexing="ij" 

1376 ) 

1377 

1378 # Get post-rebalancing assets. 

1379 m_tilde, n_tilde = rebalance_assets(d_tiled, mNrm_tiled, nNrm_tiled, tau) 

1380 

1381 # Now the marginals, in inverse space 

1382 dvdmNvrs = dvdmFunc_Adj_next.cFunc(m_tilde, n_tilde) 

1383 dvdnNvrs = dvdnFunc_Adj_next.cFunc(m_tilde, n_tilde) 

1384 

1385 # Pre-evaluate the inverse of (1-tau) 

1386 taxNvrs = uPinv(1 - tau) 

1387 # Create a tiled array of the tax 

1388 taxNvrs_tiled = np.tile( 

1389 np.reshape( 

1390 np.concatenate([np.repeat(taxNvrs, d_N), np.ones(d_N, dtype=np.double)]), 

1391 (d_N2, 1, 1), 

1392 ), 

1393 (1, mNrm_N, nNrm_N), 

1394 ) 

1395 

1396 # The FOC is dvdn = tax*dvdm or dvdnNvrs = taxNvrs*dvdmNvrs 

1397 dvdDNvrs = dvdnNvrs - taxNvrs_tiled * dvdmNvrs 

1398 # The optimal d will be at the first point where dvdD < 0. The inverse 

1399 # transformation flips the sign. 

1400 

1401 # If the derivative is negative (inverse positive) at the lowest d, 

1402 # then d == -1.0 is optimal 

1403 constrained_bot = dvdDNvrs[0, :, :] >= 0.0 

1404 # If it is positive (inverse negative) at the highest d, then d[-1] = 1.0 

1405 # is optimal 

1406 constrained_top = ( 

1407 dvdDNvrs[ 

1408 -1, 

1409 :, 

1410 :, 

1411 ] 

1412 <= 0.0 

1413 ) 

1414 

1415 # Find indices at which the derivative crosses 0 for the 1st time 

1416 # will be 0 if it never does, but "constrained_top/bot" deals with that 

1417 crossings = np.logical_and(dvdDNvrs[:-1, :, :] <= 0.0, dvdDNvrs[1:, :, :] >= 0.0) 

1418 idx = np.argmax(crossings, axis=0) 

1419 

1420 m_idx_tiled, n_idx_tiled = np.meshgrid( 

1421 np.arange(mNrm_N), np.arange(nNrm_N), indexing="ij" 

1422 ) 

1423 

1424 # Linearly interpolate the optimal withdrawal percentage d 

1425 idx1 = idx + 1 

1426 slopes = ( 

1427 dvdDNvrs[idx1, m_idx_tiled, n_idx_tiled] 

1428 - dvdDNvrs[idx, m_idx_tiled, n_idx_tiled] 

1429 ) / (dfracGrid[idx1] - dfracGrid[idx]) 

1430 dfrac_opt = dfracGrid[idx] - dvdDNvrs[idx, m_idx_tiled, n_idx_tiled] / slopes 

1431 

1432 # Replace the ones we knew were constrained 

1433 dfrac_opt[constrained_bot] = dfracGrid[0] 

1434 dfrac_opt[constrained_top] = dfracGrid[-1] 

1435 

1436 # Find m_tilde and n_tilde 

1437 mtil_opt, ntil_opt = rebalance_assets(dfrac_opt, mNrm_tiled[0], nNrm_tiled[0], tau) 

1438 

1439 # Now the derivatives. These are not straight forward because of corner 

1440 # solutions with partial derivatives that change the limits. The idea then 

1441 # is to evaluate the possible uses of the marginal unit of resources and 

1442 # take the maximum. 

1443 

1444 # An additional unit of m 

1445 marg_m = dvdmFunc_Adj_next(mtil_opt, ntil_opt) 

1446 # An additional unit of n kept in n 

1447 marg_n = dvdnFunc_Adj_next(mtil_opt, ntil_opt) 

1448 # An additional unit of n withdrawn to m 

1449 marg_n_to_m = marg_m * (1 - tau) 

1450 

1451 # Marginal value is the maximum of the marginals in their possible uses 

1452 dvdm_Adj = np.maximum(marg_m, marg_n) 

1453 dvdmNvrs_Adj = uPinv(dvdm_Adj) 

1454 dvdn_Adj = np.maximum(marg_n, marg_n_to_m) 

1455 dvdnNvrs_Adj = uPinv(dvdn_Adj) 

1456 

1457 # Interpolators 

1458 

1459 # Value 

1460 if vFuncBool: 

1461 vNvrs_Adj = vFunc_Adj_next.vFuncNvrs(mtil_opt, ntil_opt) 

1462 vNvrsFunc_Adj = BilinearInterp(vNvrs_Adj, mNrmGrid, nNrmGrid) 

1463 vFunc_Adj = ValueFuncCRRA(vNvrsFunc_Adj, CRRA) 

1464 else: 

1465 vFunc_Adj = NullFunc() 

1466 

1467 # Marginals 

1468 dvdmFunc_Adj = MargValueFuncCRRA( 

1469 BilinearInterp(dvdmNvrs_Adj, mNrmGrid, nNrmGrid), CRRA 

1470 ) 

1471 dvdnFunc_Adj = MargValueFuncCRRA( 

1472 BilinearInterp(dvdnNvrs_Adj, mNrmGrid, nNrmGrid), CRRA 

1473 ) 

1474 

1475 # Decison 

1476 dfracFunc_Adj = BilinearInterp(dfrac_opt, mNrmGrid, nNrmGrid) 

1477 

1478 solution = RiskyContribRebSolution( 

1479 # Rebalancing stage adjusting 

1480 vFunc_Adj=vFunc_Adj, 

1481 dfracFunc_Adj=dfracFunc_Adj, 

1482 dvdmFunc_Adj=dvdmFunc_Adj, 

1483 dvdnFunc_Adj=dvdnFunc_Adj, 

1484 # Rebalancing stage fixed (nothing happens, so value functions are 

1485 # the ones from the next stage) 

1486 vFunc_Fxd=vFunc_Fxd_next, 

1487 dfracFunc_Fxd=ConstantFunction(0.0), 

1488 dvdmFunc_Fxd=dvdmFunc_Fxd_next, 

1489 dvdnFunc_Fxd=dvdnFunc_Fxd_next, 

1490 dvdsFunc_Fxd=dvdsFunc_Fxd_next, 

1491 ) 

1492 

1493 return solution 

1494 

1495 

1496def solveRiskyContrib( 

1497 solution_next, 

1498 ShockDstn, 

1499 IncShkDstn, 

1500 RiskyDstn, 

1501 IndepDstnBool, 

1502 LivPrb, 

1503 DiscFac, 

1504 CRRA, 

1505 Rfree, 

1506 PermGroFac, 

1507 tau, 

1508 BoroCnstArt, 

1509 aXtraGrid, 

1510 nNrmGrid, 

1511 mNrmGrid, 

1512 ShareGrid, 

1513 dfracGrid, 

1514 vFuncBool, 

1515 AdjustPrb, 

1516 DiscreteShareBool, 

1517 joint_dist_solver, 

1518): 

1519 """ 

1520 Solve a full period (with its three stages) of the agent's problem 

1521 

1522 Parameters 

1523 ---------- 

1524 solution_next : RiskyContribSolution 

1525 Solution to next period's problem. 

1526 ShockDstn : DiscreteDistribution 

1527 Joint distribution of next period's (0) permanent income shock, (1) 

1528 transitory income shock, and (2) risky asset return factor. 

1529 IncShkDstn : DiscreteDistribution 

1530 Joint distribution of next period's (0) permanent income shock and (1) 

1531 transitory income shock. 

1532 RiskyDstn : DiscreteDistribution 

1533 Distribution of next period's risky asset return factor. 

1534 IndepDstnBool : bool 

1535 Indicates whether the income and risky return distributions are 

1536 independent. 

1537 LivPrb : float 

1538 Probability of surviving until next period. 

1539 DiscFac : float 

1540 Time-preference discount factor. 

1541 CRRA : float 

1542 Coefficient of relative risk aversion. 

1543 Rfree : float 

1544 Risk-free return factor. 

1545 PermGroFac : float 

1546 Deterministic permanent income growth factor. 

1547 tau : float 

1548 Tax rate on risky asset withdrawals. 

1549 BoroCnstArt : float 

1550 Minimum allowed market resources (must be 0). 

1551 aXtraGrid : numpy array 

1552 Exogenous grid for end-of-period risk free resources. 

1553 nNrmGrid : numpy array 

1554 Exogenous grid for risky resources. 

1555 mNrmGrid : numpy array 

1556 Exogenous grid for risk-free resources. 

1557 ShareGrid : numpy array 

1558 Exogenous grid for the income contribution share. 

1559 dfracGrid : numpy array 

1560 Grid for rebalancing flows. The final grid will be equivalent to 

1561 [-nNrm*dfracGrid, dfracGrid*mNrm]. 

1562 vFuncBool : bool 

1563 Determines whether the level of th value function must be computed. 

1564 AdjustPrb : float 

1565 Probability that the agent will be able to rebalance his portfolio 

1566 next period. 

1567 DiscreteShareBool : bool 

1568 Boolean that determines whether only a discrete set of contribution 

1569 shares (ShareGrid) is allowed. 

1570 joint_dist_solver: bool 

1571 Should the general solver be used even if income and returns are 

1572 independent? 

1573 

1574 Returns 

1575 ------- 

1576 periodSol : RiskyContribSolution 

1577 Solution to the agent's current-period problem. 

1578 

1579 """ 

1580 # Pack parameters to be passed to stage-specific solvers 

1581 kws = { 

1582 "ShockDstn": ShockDstn, 

1583 "IncShkDstn": IncShkDstn, 

1584 "RiskyDstn": RiskyDstn, 

1585 "IndepDstnBool": IndepDstnBool, 

1586 "LivPrb": LivPrb, 

1587 "DiscFac": DiscFac, 

1588 "CRRA": CRRA, 

1589 "Rfree": Rfree, 

1590 "PermGroFac": PermGroFac, 

1591 "tau": tau, 

1592 "BoroCnstArt": BoroCnstArt, 

1593 "aXtraGrid": aXtraGrid, 

1594 "nNrmGrid": nNrmGrid, 

1595 "mNrmGrid": mNrmGrid, 

1596 "ShareGrid": ShareGrid, 

1597 "dfracGrid": dfracGrid, 

1598 "vFuncBool": vFuncBool, 

1599 "AdjustPrb": AdjustPrb, 

1600 "DiscreteShareBool": DiscreteShareBool, 

1601 "joint_dist_solver": joint_dist_solver, 

1602 } 

1603 

1604 # Stages of the problem in chronological order 

1605 Stages = ["Reb", "Sha", "Cns"] 

1606 n_stages = len(Stages) 

1607 # Solvers, indexed by stage names 

1608 Solvers = { 

1609 "Reb": solve_RiskyContrib_Reb, 

1610 "Sha": solve_RiskyContrib_Sha, 

1611 "Cns": solve_RiskyContrib_Cns, 

1612 } 

1613 

1614 # Initialize empty solution 

1615 stage_sols = {} 

1616 # Solve stages backwards 

1617 for i in reversed(range(n_stages)): 

1618 stage = Stages[i] 

1619 

1620 # In the last stage, the next solution is the first stage of the next 

1621 # period. Otherwise, its the next stage of his period. 

1622 if i == n_stages - 1: 

1623 sol_next_stage = solution_next.stage_sols[Stages[0]] 

1624 else: 

1625 sol_next_stage = stage_sols[Stages[i + 1]] 

1626 

1627 # Solve 

1628 stage_sols[stage] = Solvers[stage](sol_next_stage, **kws) 

1629 

1630 # Assemble stage solutions into period solution 

1631 periodSol = RiskyContribSolution(**stage_sols) 

1632 

1633 return periodSol 

1634 

1635 

1636# %% Base risky-contrib dictionaries 

1637 

1638risky_contrib_constructor_dict = { 

1639 "IncShkDstn": construct_lognormal_income_process_unemployment, 

1640 "PermShkDstn": get_PermShkDstn_from_IncShkDstn, 

1641 "TranShkDstn": get_TranShkDstn_from_IncShkDstn, 

1642 "aXtraGrid": make_assets_grid, 

1643 "RiskyDstn": make_lognormal_RiskyDstn, 

1644 "ShockDstn": combine_IncShkDstn_and_RiskyDstn, 

1645 "ShareLimit": calc_ShareLimit_for_CRRA, 

1646 "AdjustDstn": make_AdjustDstn, 

1647 "solution_terminal": make_solution_terminal_risky_contrib, 

1648 "ShareGrid": make_bounded_ShareGrid, 

1649 "dfracGrid": make_simple_dGrid, 

1650 "mNrmGrid": make_mNrm_grid, 

1651 "nNrmGrid": make_nNrm_grid, 

1652 "kNrmInitDstn": make_lognormal_kNrm_init_dstn, 

1653 "pLvlInitDstn": make_lognormal_pLvl_init_dstn, 

1654} 

1655 

1656risky_contrib_params = { 

1657 "constructors": risky_contrib_constructor_dict, 

1658 # Preferences. The points of the model are more evident for more risk 

1659 # averse and impatient agents 

1660 "CRRA": 5.0, 

1661 "DiscFac": 0.90, 

1662 # Artificial borrowing constraint must be on 

1663 "BoroCnstArt": 0.0, 

1664 # Grids go up high wealth/P ratios and are less clustered at the bottom. 

1665 "aXtraMax": 250, 

1666 "aXtraCount": 50, 

1667 "aXtraNestFac": 1, 

1668 # Same goes for the new grids of the model 

1669 "mNrmMin": 1e-6, 

1670 "mNrmMax": 250, 

1671 "mNrmCount": 50, 

1672 "mNrmNestFac": 1, 

1673 "nNrmMin": 1e-6, 

1674 "nNrmMax": 250, 

1675 "nNrmCount": 50, 

1676 "nNrmNestFac": 1, 

1677 # Income deduction/contribution share grid 

1678 "ShareCount": 10, 

1679 "ShareMax": 0.9, 

1680 "DiscreteShareBool": False, 

1681 # Grid for finding the optimal rebalancing flow 

1682 "dCount": 20, 

1683 "joint_dist_solver": False, 

1684} 

1685risky_asset_params = { 

1686 # Risky return factor moments. Based on SP500 real returns from Shiller's 

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

1688 "RiskyAvg": 1.080370891, 

1689 "RiskyStd": 0.177196585, 

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

1691 # Number of integration nodes to use in approximation of risky returns 

1692 "RiskyCount": 5, 

1693 # Probability that the agent can adjust their portfolio each period 

1694 "AdjustPrb": [1.0], 

1695 # When simulating the model, should all agents get the same risky return in 

1696 # a given period? 

1697 "sim_common_Rrisky": True, 

1698} 

1699 

1700# Infinite horizon version 

1701init_risky_contrib = init_risky_asset.copy() 

1702init_risky_contrib.update(risky_contrib_params) 

1703 

1704# Lifecycle version 

1705init_risky_contrib_lifecycle = init_lifecycle.copy() 

1706init_risky_contrib_lifecycle.update(risky_asset_params) 

1707init_risky_contrib_lifecycle.update(risky_contrib_params) 

1708 

1709############################################################################### 

1710 

1711 

1712class RiskyContribConsumerType(RiskyAssetConsumerType): 

1713 """ 

1714 A consumer type with idiosyncratic shocks to permanent and transitory income, 

1715 who can save in both a risk-free and a risky asset but faces frictions to 

1716 moving funds between them. The agent can only consume out of his risk-free 

1717 asset. 

1718 

1719 The frictions are: 

1720 

1721 - A proportional tax on funds moved from the risky to the risk-free 

1722 asset. 

1723 - A stochastic inability to move funds between his accounts. 

1724 

1725 To partially avoid the second friction, the agent can commit to have a 

1726 fraction of his labor income, which is usually deposited in his risk-free 

1727 account, diverted to his risky account. He can change this fraction 

1728 only in periods where he is able to move funds between accounts. 

1729 """ 

1730 

1731 # The model is solved and simulated spliting each of the agent's 

1732 # decisions into its own "stage". The stages in chronological order 

1733 # are 

1734 # - Reb: asset-rebalancing stage. 

1735 # - Sha: definition of the income contribution share. 

1736 # - Cns: consumption stage. 

1737 stages = ["Reb", "Sha", "Cns"] 

1738 # Each stage has its own states and controls, and its methods to find them. 

1739 

1740 time_inv_ = RiskyAssetConsumerType.time_inv_ + [ 

1741 "DiscreteShareBool", 

1742 "joint_dist_solver", 

1743 "ShareGrid", 

1744 "nNrmGrid", 

1745 "mNrmGrid", 

1746 "RiskyDstn", 

1747 "dfracGrid", 

1748 ] 

1749 time_vary_ = RiskyAssetConsumerType.time_vary_ + ["tau", "AdjustPrb"] 

1750 

1751 # The new state variables (over those in ConsIndShock) are: 

1752 # - nNrm: start-of-period risky resources. 

1753 # - mNrmTilde: post-rebalancing risk-free resources. 

1754 # - nNrmTilde: post-rebalancing risky resources. 

1755 # - Share: income-deduction share. 

1756 # For details, see 

1757 # https://github.com/Mv77/RiskyContrib/blob/main/RiskyContrib.pdf 

1758 state_vars = RiskyAssetConsumerType.state_vars + [ 

1759 "gNrm", 

1760 "nNrm", 

1761 "mNrmTilde", 

1762 "nNrmTilde", 

1763 "Share", 

1764 ] 

1765 shock_vars_ = RiskyAssetConsumerType.shock_vars_ 

1766 default_ = {"params": init_risky_contrib, "solver": solveRiskyContrib} 

1767 

1768 def __init__(self, **kwds): 

1769 super().__init__(**kwds) 

1770 # It looks like I can't assign this at the class level, unfortunately 

1771 self.get_states = { 

1772 "Reb": self.get_states_Reb, 

1773 "Sha": self.get_states_Sha, 

1774 "Cns": self.get_states_Cns, 

1775 } 

1776 self.get_controls = { 

1777 "Reb": self.get_controls_Reb, 

1778 "Sha": self.get_controls_Sha, 

1779 "Cns": self.get_controls_Cns, 

1780 } 

1781 

1782 def pre_solve(self): 

1783 self.construct("solution_terminal") 

1784 

1785 def initialize_sim(self): 

1786 """ 

1787 Initialize the state of simulation attributes. 

1788 

1789 Parameters 

1790 ---------- 

1791 None 

1792 

1793 Returns 

1794 ------- 

1795 None 

1796 """ 

1797 RiskyAssetConsumerType.initialize_sim(self) 

1798 self.state_now["Share"] = np.zeros(self.AgentCount) 

1799 

1800 def sim_birth(self, which_agents): 

1801 """ 

1802 Create new agents to replace ones who have recently died; takes draws of 

1803 initial aNrm and pLvl, as in ConsIndShockModel, then sets Share, Adjust 

1804 and post-rebalancing risky asset nNrmTilde to zero as initial values. 

1805 Parameters 

1806 ---------- 

1807 which_agents : np.array 

1808 Boolean array of size AgentCount indicating which agents should be "born". 

1809 

1810 Returns 

1811 ------- 

1812 None 

1813 """ 

1814 

1815 RiskyAssetConsumerType.sim_birth(self, which_agents) 

1816 self.state_now["Share"][which_agents] = 0.0 

1817 self.state_now["nNrmTilde"][which_agents] = 0.0 

1818 

1819 def sim_one_period(self): 

1820 """ 

1821 Simulates one period for this type. 

1822 

1823 Has to be re-defined instead of using AgentType.sim_one_period() because 

1824 of the "stages" structure. 

1825 

1826 Parameters 

1827 ---------- 

1828 None 

1829 Returns 

1830 ------- 

1831 None 

1832 """ 

1833 

1834 if not hasattr(self, "solution"): 

1835 raise Exception( 

1836 "Model instance does not have a solution stored. To simulate, it is necessary" 

1837 " to run the `solve()` method of the class first." 

1838 ) 

1839 

1840 # Mortality adjusts the agent population 

1841 self.get_mortality() # Replace some agents with "newborns" 

1842 

1843 # Make state_now into state_prev, clearing state_now 

1844 for var in self.state_now: 

1845 self.state_prev[var] = self.state_now[var] 

1846 

1847 if isinstance(self.state_now[var], np.ndarray): 

1848 self.state_now[var] = np.empty(self.AgentCount) 

1849 else: 

1850 # Probably an aggregate variable. It may be getting set by the Market. 

1851 pass 

1852 

1853 if self.read_shocks: # If shock histories have been pre-specified, use those 

1854 self.read_shocks_from_history() 

1855 else: # Otherwise, draw shocks as usual according to subclass-specific method 

1856 self.get_shocks() 

1857 

1858 # Sequentially get states and controls of every stage 

1859 for s in self.stages: 

1860 self.get_states[s]() 

1861 self.get_controls[s]() 

1862 

1863 self.get_post_states() 

1864 

1865 # Advance time for all agents 

1866 self.t_age = self.t_age + 1 # Age all consumers by one period 

1867 self.t_cycle = self.t_cycle + 1 # Age all consumers within their cycle 

1868 self.t_cycle[self.t_cycle == self.T_cycle] = ( 

1869 0 # Resetting to zero for those who have reached the end 

1870 ) 

1871 

1872 def get_states_Reb(self): 

1873 """ 

1874 Get states for the first "stage": rebalancing. 

1875 """ 

1876 

1877 pLvlPrev = self.state_prev["pLvl"] 

1878 aNrmPrev = self.state_prev["aNrm"] 

1879 SharePrev = self.state_prev["Share"] 

1880 nNrmTildePrev = self.state_prev["nNrmTilde"] 

1881 Rfree = self.get_Rfree() 

1882 Rrisk = self.shocks["Risky"] 

1883 

1884 # Calculate new states: 

1885 

1886 # Permanent income 

1887 self.state_now["pLvl"] = pLvlPrev * self.shocks["PermShk"] 

1888 self.state_now["PlvlAgg"] = self.state_prev["PlvlAgg"] * self.PermShkAggNow 

1889 

1890 # Assets: mNrm and nNrm 

1891 

1892 # Compute the effective growth factor of each asset 

1893 RfEff = Rfree / self.shocks["PermShk"] 

1894 RrEff = Rrisk / self.shocks["PermShk"] 

1895 

1896 self.state_now["bNrm"] = RfEff * aNrmPrev # Liquid balances before labor income 

1897 self.state_now["gNrm"] = ( 

1898 RrEff * nNrmTildePrev 

1899 ) # Iliquid balances before labor income 

1900 

1901 # Liquid balances after labor income 

1902 self.state_now["mNrm"] = self.state_now["bNrm"] + self.shocks["TranShk"] * ( 

1903 1 - SharePrev 

1904 ) 

1905 # Iliquid balances after labor income 

1906 self.state_now["nNrm"] = ( 

1907 self.state_now["gNrm"] + self.shocks["TranShk"] * SharePrev 

1908 ) 

1909 

1910 return None 

1911 

1912 def get_controls_Reb(self): 

1913 """ 

1914 Get controls for the first stage: rebalancing 

1915 """ 

1916 dfrac = np.zeros(self.AgentCount) + np.nan 

1917 

1918 # Loop over each period of the cycle, getting controls separately depending on "age" 

1919 for t in range(self.T_cycle): 

1920 # Find agents in this period-stage 

1921 these = t == self.t_cycle 

1922 

1923 # Get controls for agents who *can* adjust. 

1924 those = np.logical_and(these, self.shocks["Adjust"]) 

1925 dfrac[those] = ( 

1926 self.solution[t] 

1927 .stage_sols["Reb"] 

1928 .dfracFunc_Adj( 

1929 self.state_now["mNrm"][those], self.state_now["nNrm"][those] 

1930 ) 

1931 ) 

1932 

1933 # Get Controls for agents who *can't* adjust. 

1934 those = np.logical_and(these, np.logical_not(self.shocks["Adjust"])) 

1935 dfrac[those] = ( 

1936 self.solution[t] 

1937 .stage_sols["Reb"] 

1938 .dfracFunc_Fxd( 

1939 self.state_now["mNrm"][those], 

1940 self.state_now["nNrm"][those], 

1941 self.state_prev["Share"][those], 

1942 ) 

1943 ) 

1944 

1945 # Limit dfrac to [-1,1] to prevent negative balances. Values outside 

1946 # the range can come from extrapolation. 

1947 self.controls["dfrac"] = np.minimum(np.maximum(dfrac, -1), 1.0) 

1948 

1949 def get_states_Sha(self): 

1950 """ 

1951 Get states for the second "stage": choosing the contribution share. 

1952 """ 

1953 

1954 # Post-states are assets after rebalancing 

1955 

1956 if "tau" not in self.time_vary: 

1957 mNrmTilde, nNrmTilde = rebalance_assets( 

1958 self.controls["dfrac"], 

1959 self.state_now["mNrm"], 

1960 self.state_now["nNrm"], 

1961 self.tau, 

1962 ) 

1963 

1964 else: 

1965 # Initialize 

1966 mNrmTilde = np.zeros_like(self.state_now["mNrm"]) + np.nan 

1967 nNrmTilde = np.zeros_like(self.state_now["mNrm"]) + np.nan 

1968 

1969 # Loop over each period of the cycle, getting controls separately depending on "age" 

1970 for t in range(self.T_cycle): 

1971 # Find agents in this period-stage 

1972 these = t == self.t_cycle 

1973 

1974 if np.sum(these) > 0: 

1975 tau = self.tau[t] 

1976 

1977 mNrmTilde[these], nNrmTilde[these] = rebalance_assets( 

1978 self.controls["dfrac"][these], 

1979 self.state_now["mNrm"][these], 

1980 self.state_now["nNrm"][these], 

1981 tau, 

1982 ) 

1983 

1984 self.state_now["mNrmTilde"] = mNrmTilde 

1985 self.state_now["nNrmTilde"] = nNrmTilde 

1986 

1987 def get_controls_Sha(self): 

1988 """ 

1989 Get controls for the second "stage": choosing the contribution share. 

1990 """ 

1991 

1992 Share = np.zeros(self.AgentCount) + np.nan 

1993 

1994 # Loop over each period of the cycle, getting controls separately depending on "age" 

1995 for t in range(self.T_cycle): 

1996 # Find agents in this period-stage 

1997 these = t == self.t_cycle 

1998 

1999 # Get controls for agents who *can* adjust. 

2000 those = np.logical_and(these, self.shocks["Adjust"]) 

2001 Share[those] = ( 

2002 self.solution[t] 

2003 .stage_sols["Sha"] 

2004 .ShareFunc_Adj( 

2005 self.state_now["mNrmTilde"][those], 

2006 self.state_now["nNrmTilde"][those], 

2007 ) 

2008 ) 

2009 

2010 # Get Controls for agents who *can't* adjust. 

2011 those = np.logical_and(these, np.logical_not(self.shocks["Adjust"])) 

2012 Share[those] = ( 

2013 self.solution[t] 

2014 .stage_sols["Sha"] 

2015 .ShareFunc_Fxd( 

2016 self.state_now["mNrmTilde"][those], 

2017 self.state_now["nNrmTilde"][those], 

2018 self.state_prev["Share"][those], 

2019 ) 

2020 ) 

2021 

2022 # Store controls as attributes of self 

2023 self.controls["Share"] = Share 

2024 

2025 def get_states_Cns(self): 

2026 """ 

2027 Get states for the third "stage": consumption. 

2028 """ 

2029 

2030 # Contribution share becomes a state in the consumption problem 

2031 self.state_now["Share"] = self.controls["Share"] 

2032 

2033 def get_controls_Cns(self): 

2034 """ 

2035 Get controls for the third "stage": consumption. 

2036 """ 

2037 

2038 cNrm = np.zeros(self.AgentCount) + np.nan 

2039 

2040 # Loop over each period of the cycle, getting controls separately depending on "age" 

2041 for t in range(self.T_cycle): 

2042 # Find agents in this period-stage 

2043 these = t == self.t_cycle 

2044 

2045 # Get consumption 

2046 cNrm[these] = ( 

2047 self.solution[t] 

2048 .stage_sols["Cns"] 

2049 .cFunc( 

2050 self.state_now["mNrmTilde"][these], 

2051 self.state_now["nNrmTilde"][these], 

2052 self.state_now["Share"][these], 

2053 ) 

2054 ) 

2055 

2056 # Store controls as attributes of self 

2057 # Since agents might be willing to end the period with a = 0, make 

2058 # sure consumption does not go over m because of some numerical error. 

2059 self.controls["cNrm"] = np.minimum(cNrm, self.state_now["mNrmTilde"]) 

2060 

2061 def get_post_states(self): 

2062 """ 

2063 Set variables that are not a state to any problem but need to be 

2064 computed in order to interact with shocks and produce next period's 

2065 states. 

2066 """ 

2067 self.state_now["aNrm"] = self.state_now["mNrmTilde"] - self.controls["cNrm"]