Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,16 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [Unreleased]

### Added

- Fixes Issue [#1202](https://github.com/PSLmodels/OG-Core/issues/1202):
`get_r_gov` clipped the interest rate on government debt at zero, so a
sovereign that genuinely pays a negative real rate could not be modelled.
The bound is now a parameter, `r_gov_floor`, with a default of 0.0 that
reproduces the previous behaviour exactly.

## [0.19.1] - 2026-08-10 12:00:00

### Added
Expand Down
21 changes: 21 additions & 0 deletions ogcore/default_parameters.json
Original file line number Diff line number Diff line change
Expand Up @@ -1145,6 +1145,27 @@
}
}
},
"r_gov_floor": {
"title": "Floor on the interest rate on government debt",
"section_1": "Fiscal Policy Parameters",
"description": "Lower bound applied to the interest rate on government debt after the wedge is computed. The default of 0.0 reproduces the long-standing behavior. Lower it for a sovereign that pays a negative real rate on its debt.",
"short_description": "Floor on the government interest rate",
"param_notation": "$\\underline{r}_{gov}$",
"notes": "",
"type": "float",
"number_dims": 0,
"value": [
{
"value": 0.0
}
],
"validators": {
"range": {
"min": -0.3,
"max": 0.3
}
}
},
"r_gov_DY": {
"title": "Linear effect of debt to GDP ratio on government interest rate",
"section_1": "Fiscal Policy Parameters",
Expand Down
6 changes: 3 additions & 3 deletions ogcore/fiscal.py
Original file line number Diff line number Diff line change
Expand Up @@ -394,7 +394,7 @@ def get_r_gov(r, DY_ratio, p, method, t=0):
.. math::
r_{gov,t} = \max\{(1-\tau_{d,t}r_{t} - \mu_d
+ \beta_1 \frac{D_t}{Y_t}
+ \beta_2 \left(\frac{D_t}{Y_t}\right)^2, 0.0\}
+ \beta_2 \left(\frac{D_t}{Y_t}\right)^2, \underline{r}_{gov}\}

Args:
r (array_like): interest rate on private capital debt over the
Expand All @@ -416,15 +416,15 @@ def get_r_gov(r, DY_ratio, p, method, t=0):
- p.r_gov_shift[t]
+ p.r_gov_DY * DY_ratio
+ p.r_gov_DY2 * DY_ratio**2,
0.00,
p.r_gov_floor,
)
else:
r_gov = np.maximum(
p.r_gov_scale[: p.T] * r[: p.T]
- p.r_gov_shift[: p.T]
+ p.r_gov_DY * DY_ratio[: p.T]
+ p.r_gov_DY2 * DY_ratio[: p.T] ** 2,
0.00,
p.r_gov_floor,
)

return r_gov
Expand Down
26 changes: 26 additions & 0 deletions tests/test_fiscal.py
Original file line number Diff line number Diff line change
Expand Up @@ -292,6 +292,19 @@ def test_get_TR(
r_gov1 = 0.02
r_gov2 = 0.01
r_gov3 = 0.0
# p5/p6: the wedge of p3 (which the default floor clips to zero), with the
# floor lowered so the negative rate comes through, and with the floor set
# above the wedge so it still binds.
p5 = Specifications()
p5.update_specifications(
{"r_gov_scale": [0.5], "r_gov_shift": [0.03], "r_gov_floor": -0.05}
)
r_gov5 = 0.5 * 0.04 - 0.03 # -0.01, no longer clipped
p6 = Specifications()
p6.update_specifications(
{"r_gov_scale": [0.5], "r_gov_shift": [0.03], "r_gov_floor": -0.005}
)
r_gov6 = -0.005 # floor binds
p_tpi = Specifications()
p_tpi.update_specifications(
{
Expand Down Expand Up @@ -323,6 +336,13 @@ def test_get_TR(
)


p_tpi_floor = Specifications()
p_tpi_floor.update_specifications(
{"r_gov_scale": [1.5], "r_gov_shift": [0.09], "r_gov_floor": -0.05}
)
r_gov_tpi_floor = 1.5 * r_tpi[: p_tpi_floor.T] - 0.09


@pytest.mark.parametrize(
"r,p,DY_ratio,method,t,r_gov_expected",
[
Expand All @@ -332,6 +352,9 @@ def test_get_TR(
(r_tpi, p_tpi, DY_tpi, "TPI", None, r_gov_tpi),
(r, p3, 0, "scalar", 0, r_gov3),
(r, p4, 0.5, "scalar", 0, r_gov4),
(r, p5, 0, "scalar", -1, r_gov5),
(r, p6, 0, "scalar", -1, r_gov6),
(r_tpi, p_tpi_floor, DY_tpi, "TPI", None, r_gov_tpi_floor),
],
ids=[
"Scale only",
Expand All @@ -340,6 +363,9 @@ def test_get_TR(
"TPI",
"scalar",
"DY params",
"negative floor lets r_gov < 0",
"floor still binds",
"negative floor, TPI",
],
)
def test_get_r_gov(r, p, DY_ratio, method, t, r_gov_expected):
Expand Down