-
-
Notifications
You must be signed in to change notification settings - Fork 204
Approximate share in Portfolio Model #1122
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
6648b7e
671b395
79f5a91
df8efb6
9c6f220
5aabd03
91fb9ae
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -166,7 +166,7 @@ class PortfolioConsumerType(RiskyAssetConsumerType): | |||||||||||||||||||
| """ | ||||||||||||||||||||
|
|
||||||||||||||||||||
| time_inv_ = deepcopy(RiskyAssetConsumerType.time_inv_) | ||||||||||||||||||||
| time_inv_ = time_inv_ + ["AdjustPrb", "DiscreteShareBool"] | ||||||||||||||||||||
| time_inv_ = time_inv_ + ["AdjustPrb", "DiscreteShareBool", "ApproxShareBool"] | ||||||||||||||||||||
|
|
||||||||||||||||||||
| def __init__(self, verbose=False, quiet=False, **kwds): | ||||||||||||||||||||
| params = init_portfolio.copy() | ||||||||||||||||||||
|
|
@@ -213,7 +213,7 @@ def update_solution_terminal(self): | |||||||||||||||||||
| None | ||||||||||||||||||||
| """ | ||||||||||||||||||||
| # Consume all market resources: c_T = m_T | ||||||||||||||||||||
| cFuncAdj_terminal = IdentityFunction() | ||||||||||||||||||||
| cFuncAdj_terminal = LinearInterp([0.0, 1.0], [0.0, 1.0]) | ||||||||||||||||||||
| cFuncFxd_terminal = IdentityFunction(i_dim=0, n_dims=2) | ||||||||||||||||||||
|
|
||||||||||||||||||||
| # Risky share is irrelevant-- no end-of-period assets; set to zero | ||||||||||||||||||||
|
|
@@ -244,6 +244,8 @@ def update_solution_terminal(self): | |||||||||||||||||||
| dvdsFuncFxd=dvdsFuncFxd_terminal, | ||||||||||||||||||||
| ) | ||||||||||||||||||||
|
|
||||||||||||||||||||
| self.solution_terminal.ShareEndOfPrdFunc = ShareFuncAdj_terminal | ||||||||||||||||||||
|
|
||||||||||||||||||||
| def update_ShareGrid(self): | ||||||||||||||||||||
| """ | ||||||||||||||||||||
| Creates the attribute ShareGrid as an evenly spaced grid on [0.,1.], using | ||||||||||||||||||||
|
|
@@ -492,6 +494,7 @@ def __init__( | |||||||||||||||||||
| DiscreteShareBool, | ||||||||||||||||||||
| ShareLimit, | ||||||||||||||||||||
| IndepDstnBool, | ||||||||||||||||||||
| ApproxShareBool, | ||||||||||||||||||||
| ): | ||||||||||||||||||||
| """ | ||||||||||||||||||||
| Constructor for portfolio choice problem solver. | ||||||||||||||||||||
|
|
@@ -514,6 +517,7 @@ def __init__( | |||||||||||||||||||
| self.DiscreteShareBool = DiscreteShareBool | ||||||||||||||||||||
| self.ShareLimit = ShareLimit | ||||||||||||||||||||
| self.IndepDstnBool = IndepDstnBool | ||||||||||||||||||||
| self.ApproxShareBool = ApproxShareBool | ||||||||||||||||||||
|
|
||||||||||||||||||||
| # Make sure the individual is liquidity constrained. Allowing a consumer to | ||||||||||||||||||||
| # borrow *and* invest in an asset with unbounded (negative) returns is a bad mix. | ||||||||||||||||||||
|
|
@@ -822,6 +826,8 @@ def make_ShareFuncAdj(self): | |||||||||||||||||||
| Construct the risky share function when the agent can adjust | ||||||||||||||||||||
| """ | ||||||||||||||||||||
|
|
||||||||||||||||||||
| # Share function for mGrid | ||||||||||||||||||||
|
|
||||||||||||||||||||
| if self.zero_bound: | ||||||||||||||||||||
| Share_lower_bound = self.ShareLimit | ||||||||||||||||||||
| else: | ||||||||||||||||||||
|
|
@@ -834,6 +840,96 @@ def make_ShareFuncAdj(self): | |||||||||||||||||||
| slope_limit=0.0, | ||||||||||||||||||||
| ) | ||||||||||||||||||||
|
|
||||||||||||||||||||
| # Share function on aGrid | ||||||||||||||||||||
|
|
||||||||||||||||||||
| if self.zero_bound: | ||||||||||||||||||||
| aNrm_temp = np.append(0.0, self.aNrmGrid) | ||||||||||||||||||||
| share_temp = np.append(self.ShareLimit, self.Share_now) | ||||||||||||||||||||
| else: | ||||||||||||||||||||
| aNrm_temp = self.aNrmGrid | ||||||||||||||||||||
| share_temp = self.Share_now | ||||||||||||||||||||
|
|
||||||||||||||||||||
| self.ShareEndOfPrdFunc = LinearInterp( | ||||||||||||||||||||
| aNrm_temp, share_temp, intercept_limit=self.ShareLimit, slope_limit=0.0 | ||||||||||||||||||||
| ) | ||||||||||||||||||||
|
|
||||||||||||||||||||
| def make_share_func_approx(self): | ||||||||||||||||||||
| """ | ||||||||||||||||||||
| Alternative share functions from linear approximation. | ||||||||||||||||||||
| """ | ||||||||||||||||||||
|
|
||||||||||||||||||||
| # get next period's consumption and share function | ||||||||||||||||||||
| cFunc_next = self.solution_next.cFuncAdj | ||||||||||||||||||||
| sFunc_next = self.solution_next.ShareEndOfPrdFunc | ||||||||||||||||||||
|
|
||||||||||||||||||||
| def premium(shock): | ||||||||||||||||||||
| """ | ||||||||||||||||||||
| Used to evaluate mean and variance of equity premium. | ||||||||||||||||||||
| """ | ||||||||||||||||||||
| r_diff = shock - self.Rfree | ||||||||||||||||||||
|
|
||||||||||||||||||||
| return r_diff, r_diff ** 2, r_diff ** 3 | ||||||||||||||||||||
|
|
||||||||||||||||||||
| prem_mean, prem_sqrd, prem_cube = calc_expectation(self.RiskyDstn, premium) | ||||||||||||||||||||
|
|
||||||||||||||||||||
| def c_nrm_and_deriv(shocks, a_nrm): | ||||||||||||||||||||
| """ | ||||||||||||||||||||
| Used to calculate expected consumption and MPC given today's savings, | ||||||||||||||||||||
| assuming that today's risky share is the same as it would be tomorrow | ||||||||||||||||||||
| with that same level of savings. | ||||||||||||||||||||
| """ | ||||||||||||||||||||
| p_shk = shocks[0] * self.PermGroFac | ||||||||||||||||||||
| t_shk = shocks[1] | ||||||||||||||||||||
| share = sFunc_next(a_nrm) | ||||||||||||||||||||
| r_diff = shocks[2] - self.Rfree | ||||||||||||||||||||
| r_port = self.Rfree + r_diff * share | ||||||||||||||||||||
| m_nrm_next = a_nrm * r_port / p_shk + t_shk | ||||||||||||||||||||
|
|
||||||||||||||||||||
| c_next, cP_next = cFunc_next.eval_with_derivative(m_nrm_next) | ||||||||||||||||||||
|
|
||||||||||||||||||||
| return c_next, cP_next | ||||||||||||||||||||
|
|
||||||||||||||||||||
| exp_c_values = calc_expectation(self.ShockDstn, c_nrm_and_deriv, self.aNrmGrid) | ||||||||||||||||||||
|
|
||||||||||||||||||||
| exp_c_values = exp_c_values[:, :, 0] | ||||||||||||||||||||
| exp_c_next = exp_c_values[0] | ||||||||||||||||||||
| exp_cP_next = exp_c_values[1] | ||||||||||||||||||||
|
|
||||||||||||||||||||
| MPC = exp_cP_next * self.aNrmGrid / exp_c_next | ||||||||||||||||||||
|
|
||||||||||||||||||||
| # first order approximation | ||||||||||||||||||||
| approx_share = prem_mean / (self.CRRA * MPC * prem_sqrd) | ||||||||||||||||||||
|
|
||||||||||||||||||||
| # clip at 0 and 1, although we know the Share limit we | ||||||||||||||||||||
| # want to see what the approximation would give us | ||||||||||||||||||||
| approx_share = np.clip(approx_share, 0, 1) | ||||||||||||||||||||
|
|
||||||||||||||||||||
| self.ApproxFirstOrderShareFunc = LinearInterp( | ||||||||||||||||||||
| self.aNrmGrid, | ||||||||||||||||||||
| approx_share, | ||||||||||||||||||||
| intercept_limit=self.ShareLimit, | ||||||||||||||||||||
| slope_limit=0.0, | ||||||||||||||||||||
| ) | ||||||||||||||||||||
|
|
||||||||||||||||||||
| # second order approximation | ||||||||||||||||||||
|
|
||||||||||||||||||||
| a = -self.CRRA * prem_cube * MPC ** 2 * (-self.CRRA - 1) / 2 | ||||||||||||||||||||
| b = -self.CRRA * MPC * prem_sqrd | ||||||||||||||||||||
| c = prem_mean | ||||||||||||||||||||
|
|
||||||||||||||||||||
| temp = np.sqrt(b ** 2 - 4 * a * c) | ||||||||||||||||||||
|
|
||||||||||||||||||||
| roots = np.array([(-b + temp) / (2 * a), (-b - temp) / (2 * a)]) | ||||||||||||||||||||
| roots[:, 0] = 1.0 | ||||||||||||||||||||
| roots = np.where( | ||||||||||||||||||||
| np.logical_and(roots[0] >= 0, roots[0] <= 1), roots[0], roots[1] | ||||||||||||||||||||
| ) | ||||||||||||||||||||
| roots = np.clip(roots, 0, 1) | ||||||||||||||||||||
|
|
||||||||||||||||||||
| self.ApproxSecondOrderShareFunc = LinearInterp( | ||||||||||||||||||||
| self.aNrmGrid, roots, intercept_limit=self.ShareLimit, slope_limit=0.0, | ||||||||||||||||||||
| ) | ||||||||||||||||||||
|
|
||||||||||||||||||||
| def add_save_points(self): | ||||||||||||||||||||
| # This is a point at which (a,c,share) have consistent length. Take the | ||||||||||||||||||||
| # snapshot for storing the grid and values in the solution. | ||||||||||||||||||||
|
|
@@ -978,6 +1074,11 @@ def make_porfolio_solution(self): | |||||||||||||||||||
| AdjPrb=self.AdjustPrb, | ||||||||||||||||||||
| ) | ||||||||||||||||||||
|
|
||||||||||||||||||||
| if self.ApproxShareBool: | ||||||||||||||||||||
|
||||||||||||||||||||
| if self.ApproxShareBool: | |
| if self.ApproxShareBool and all( | |
| hasattr(self, attr_name) | |
| for attr_name in ( | |
| "ShareEndOfPrdFunc", | |
| "ApproxFirstOrderShareFunc", | |
| "ApproxSecondOrderShareFunc", | |
| ) | |
| ): |
Copilot
AI
Jan 28, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The new approximation path controlled by ApproxShareBool (i.e., make_share_func_approx and the extra solution fields it populates) is not covered by the existing tests in tests/test_ConsPortfolioModel.py, which only exercise the default (ApproxShareBool=False) behavior. Given that this branch changes both the solver’s expectations (uses solution_next.ShareEndOfPrdFunc and eval_with_derivative) and the contents of the per-period PortfolioSolution, it would be valuable to add at least a smoke test that solves the model with ApproxShareBool=True (for the continuous-share, independent-shocks case) and verifies that the approximate share functions are well-defined over the asset grid and free of nan values.
Copilot
AI
Jan 28, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Comment typo: "Flat for wether to approximate risky share" should read "Flag for whether to approximate risky share" to match the surrounding parameter comments.
| # Flat for wether to approximate risky share | |
| # Flag for whether to approximate risky share |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The quadratic used for the second-order share approximation (
a = -self.CRRA * prem_cube * MPC ** 2 * (-self.CRRA - 1) / 2,b = -self.CRRA * MPC * prem_sqrd,c = prem_mean) does not guarantee a non-negative discriminantb ** 2 - 4 * a * c. For admissible parameter combinations where4 * a * c > b ** 2,temp = np.sqrt(b ** 2 - 4 * a * c)will benan, and the resultingroots(and thusApproxSecondOrderShareFunc) will containnanvalues. It would be safer to guard against a negative discriminant (e.g., by falling back to the first-order approximation or clipping the discriminant at zero) so that enabling the second-order approximation cannot silently producenanpolicies.