Coverage for HARK/Calibration/Assets/AssetProcesses.py: 82%

45 statements  

« prev     ^ index     » next       coverage.py v7.11.0, created at 2025-11-02 05:14 +0000

1""" 

2This file contains tools for creating risky asset return distributions, for use 

3as inputs to several consumption-saving model solvers. 

4""" 

5 

6import numpy as np 

7from scipy.optimize import minimize_scalar 

8from HARK.distributions import ( 

9 combine_indep_dstns, 

10 DiscreteDistributionLabeled, 

11 IndexDistribution, 

12 Lognormal, 

13) 

14 

15 

16def make_lognormal_RiskyDstn(T_cycle, RiskyAvg, RiskyStd, RiskyCount, RNG): 

17 r""" 

18 Creates a discrete approximation of lognormal risky asset returns, either 

19 as a single distribution or as a lifecycle sequence. 

20 

21 .. math:: 

22 \begin{align} 

23 \phi_t &\sim \exp(\mathcal{N}(\textbf{RiskyStd}_{t}^2)) \\ 

24 \mathbb{E}_{t} \left[ \phi_t \right] &= \textbf{RiskyAvg}_{t}\\ 

25 \end{align} 

26 

27 Parameters 

28 ---------- 

29 T_cycle : int 

30 Number of non-terminal periods in this agent's cycle. 

31 RiskyAvg : float or [float] 

32 Mean return factor of risky asset. If a single number, it is used for all 

33 periods. If it is a list, then it represents lifecycle returns (or 

34 perceptions thereof). 

35 RiskyStd : float or [float] 

36 Standard deviation of log returns of the risky asset. Allows the same 

37 options as RiskyAvg. 

38 RiskyCount : int 

39 Number of equiprobable discrete nodes in the risky return distribution. 

40 RNG : RandomState 

41 Internal random number generator for the AgentType instance, used to 

42 generate random seeds. 

43 

44 Returns 

45 ------- 

46 RiskyDstn : DiscreteDistribution or [DiscreteDistribution] 

47 Discretized approximation to lognormal asset returns. 

48 """ 

49 # Determine whether this instance has time-varying risk perceptions 

50 if ( 

51 (type(RiskyAvg) is list) 

52 and (type(RiskyStd) is list) 

53 and (len(RiskyAvg) == len(RiskyStd)) 

54 and (len(RiskyAvg) == T_cycle) 

55 ): 

56 time_varying_RiskyDstn = True 

57 elif (type(RiskyStd) is list) or (type(RiskyAvg) is list): 

58 raise AttributeError( 

59 "If RiskyAvg is time-varying, then RiskyStd must be as well, and they must both have length of T_cycle!" 

60 ) 

61 else: 

62 time_varying_RiskyDstn = False 

63 

64 # Generate a discrete approximation to the risky return distribution 

65 # if its parameters are time-varying 

66 if time_varying_RiskyDstn: 

67 RiskyDstn = IndexDistribution( 

68 Lognormal, 

69 {"mean": RiskyAvg, "sigma": RiskyStd}, 

70 seed=RNG.integers(0, 2**31 - 1), 

71 ).discretize(RiskyCount, method="equiprobable") 

72 

73 # Generate a discrete approximation to the risky return distribution if 

74 # its parameters are constant 

75 else: 

76 RiskyDstn = Lognormal(mean=RiskyAvg, sigma=RiskyStd).discretize( 

77 RiskyCount, method="equiprobable" 

78 ) 

79 

80 return RiskyDstn 

81 

82 

83def combine_IncShkDstn_and_RiskyDstn(T_cycle, RiskyDstn, IncShkDstn): 

84 """ 

85 Combine the income shock distribution (over PermShk and TranShk) with the 

86 risky return distribution (RiskyDstn) to make a new object called ShockDstn. 

87 

88 Parameters 

89 ---------- 

90 T_cycle : int 

91 Number of non-terminal periods in this agent's cycle. 

92 RiskyDstn : DiscreteDistribution or [DiscreteDistribution] 

93 Discretized approximation to lognormal asset returns. 

94 IncShkDstn : [Distribution] 

95 A discrete approximation to the income process between each period. 

96 

97 Returns 

98 ------- 

99 ShockDstn : IndexDistribution 

100 A combined trivariate discrete distribution of permanent shocks, transitory 

101 shocks, and risky returns. Has one element per period of the agent's cycle. 

102 """ 

103 # Create placeholder distributions 

104 try: 

105 dstn_list = [ 

106 combine_indep_dstns(IncShkDstn[t], RiskyDstn[t]) for t in range(T_cycle) 

107 ] 

108 except: 

109 dstn_list = [ 

110 combine_indep_dstns(IncShkDstn[t], RiskyDstn) for t in range(T_cycle) 

111 ] 

112 

113 # Names of the variables (hedging for the unlikely case that in 

114 # some index of IncShkDstn variables are in a switched order) 

115 names_list = [ 

116 list(IncShkDstn[t].variables.keys()) + ["Risky"] for t in range(T_cycle) 

117 ] 

118 

119 conditional = { 

120 "pmv": [x.pmv for x in dstn_list], 

121 "atoms": [x.atoms for x in dstn_list], 

122 "var_names": names_list, 

123 } 

124 

125 # Now create the actual distribution using the index and labeled class 

126 ShockDstn = IndexDistribution( 

127 engine=DiscreteDistributionLabeled, 

128 conditional=conditional, 

129 ) 

130 return ShockDstn 

131 

132 

133def calc_ShareLimit_for_CRRA(T_cycle, RiskyDstn, CRRA, Rfree): 

134 """ 

135 Calculates the lower bound on the risky asset share as market resources go 

136 to infinity, given that utility is CRRA. 

137 

138 Parameters 

139 ---------- 

140 T_cycle : int 

141 Number of non-terminal periods in this agent's cycle. 

142 RiskyDstn : DiscreteDistribution or [DiscreteDistribution] 

143 Discretized approximation to lognormal asset returns. 

144 CRRA : float 

145 Coefficient of relative risk aversion. 

146 Rfree : float 

147 Return factor on the risk-free asset. 

148 

149 Returns 

150 ------- 

151 ShareLimit : float or [float] 

152 Lower bound on risky asset share. Can be a single number or a lifecycle sequence. 

153 """ 

154 RiskyDstn_is_time_varying = hasattr(RiskyDstn, "__getitem__") 

155 Rfree_is_time_varying = type(Rfree) is list 

156 

157 # If the risky share lower bound is time varying... 

158 if RiskyDstn_is_time_varying or Rfree_is_time_varying: 

159 ShareLimit = [] 

160 for t in range(T_cycle): 

161 if RiskyDstn_is_time_varying: 

162 RiskyDstn_t = RiskyDstn[t] 

163 else: 

164 RiskyDstn_t = RiskyDstn 

165 if Rfree_is_time_varying: 

166 Rfree_t = Rfree[t] 

167 else: 

168 Rfree_t = Rfree 

169 

170 def temp_f(s): 

171 return -((1.0 - CRRA) ** -1) * np.dot( 

172 (Rfree_t + s * (RiskyDstn_t.atoms[0] - Rfree_t)) ** (1.0 - CRRA), 

173 RiskyDstn_t.pmv, 

174 ) 

175 

176 SharePF = minimize_scalar(temp_f, bounds=(0.0, 1.0), method="bounded").x 

177 ShareLimit.append(SharePF) 

178 

179 # If the risky share lower bound is not time-varying... 

180 else: 

181 

182 def temp_f(s): 

183 return -((1.0 - CRRA) ** -1) * np.dot( 

184 (Rfree + s * (RiskyDstn.atoms[0] - Rfree)) ** (1.0 - CRRA), 

185 RiskyDstn.pmv, 

186 ) 

187 

188 SharePF = minimize_scalar( 

189 temp_f, bracket=(0.0, 1.0), method="golden", tol=1e-10 

190 ).x 

191 if type(SharePF) is np.array: 

192 SharePF = SharePF[0] 

193 ShareLimit = SharePF 

194 

195 return ShareLimit