Skip to content
Merged
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
2 changes: 1 addition & 1 deletion economy/topics/public-finances/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -124,7 +124,7 @@ <h2>The model view, and the same breath its limits.</h2>
</ul>
<p class="callout">
<strong>Predictive validation: weak.</strong>
Free-running GDP and consumption MAPE are 5.75% and 9.56%; the anchored fit is by construction.
Free-running GDP and consumption MAPE are 4.48% and 7.49%; the anchored fit is by construction.
<strong>Policy counterfactuals: weak.</strong>
One income-tax costing is independently compared with HMRC; trade, labour, prices and parts of household income remain constrained.
<a href="/obr/validation">Read the validation page →</a>
Expand Down
2 changes: 1 addition & 1 deletion index.html
Original file line number Diff line number Diff line change
Expand Up @@ -210,7 +210,7 @@ <h2>What the evidence shows—and what it doesn't.</h2>
</p>
<div class="verification-grid" aria-label="The verification gradient, model by model">
<article class="verification-card"><div><strong class="mono">pe-microsim</strong><span class="verification-tag verification-tag-statute">Statutory checks</span></div><p class="verification-what">PolicyEngine&rsquo;s own tax&ndash;benefit microsimulation for the UK and US &mdash; the engine behind policyengine.org.</p><p>Rules are tested against statute; population totals inherit <strong>survey uncertainty</strong>. It cannot answer GDP, inflation, interest rates or macro feedback &mdash; that is what the models below add.</p></article>
<article class="verification-card"><div><strong class="mono">obr-macro</strong><span class="verification-tag">Anchored replication</span></div><p class="verification-what">Emulator of the OBR&rsquo;s UK macroeconometric forecast model.</p><p>Tracks the March 2026 EFO to <strong>0.15% GDP MAPE</strong> with published anchors; free-running error is <strong>5.75%</strong>.</p></article>
<article class="verification-card"><div><strong class="mono">obr-macro</strong><span class="verification-tag">Anchored replication</span></div><p class="verification-what">Emulator of the OBR&rsquo;s UK macroeconometric forecast model.</p><p>Tracks the March 2026 EFO to <strong>0.15% GDP MAPE</strong> with published anchors; free-running error is <strong>4.48%</strong>.</p></article>
<article class="verification-card"><div><strong class="mono">boe-svar</strong><span class="verification-tag">Forecast evaluation</span></div><p class="verification-what">Bank of England structural VAR separating global and domestic shocks.</p><p>Reproduces the published decomposition for GDP (<strong>37.4% vs ~40%</strong>) and falls about <strong>8pp short on CPI</strong> (42.3% vs ~50%) once measured the way the paper defines it. Against a random walk <em>with drift</em>, and after adjusting for the 64 tests run, <strong>no variable at any horizon shows skill</strong>. <a href="/svar/validation">See the evaluation →</a></p></article>
<article class="verification-card"><div><strong class="mono">frb-us</strong><span class="verification-tag">Software replication</span></div><p class="verification-what">The Federal Reserve&rsquo;s large-scale model of the US economy.</p><p>Matches the Fed&rsquo;s pyfrbus to within the Fed&rsquo;s own two releases&rsquo; disagreement (<strong>~1&times;10⁻⁸</strong>); no predictive claim.</p></article>
<article class="verification-card"><div><strong class="mono">us-hank</strong><span class="verification-tag">Published replication</span></div><p class="verification-what">Heterogeneous-agent New Keynesian model of US households.</p><p>Hits every Auclert et al. (2021) calibration target; responses are <strong>first-order and stylized</strong>.</p></article>
Expand Down
2 changes: 1 addition & 1 deletion integration/modal_app.py
Original file line number Diff line number Diff line change
Expand Up @@ -129,7 +129,7 @@
BOE_URL = "https://github.com/PolicyEngine/boe-var-model"
FRB_URL = "https://github.com/PolicyEngine/us-frb-model"
HANK_URL = "https://github.com/PolicyEngine/us-hank-model"
OBR_REVISION = "85ada9be070a34a4a4a0a65d92f8f148a476ccb1"
OBR_REVISION = "9c4b595f3b60b305459ba8c91eac5bc69fd0ffc8"
BOE_REVISION = "42cbcbb728b55b3f416764568fa8f3aef68a6730"
FRB_REVISION = "2508a2d3ee7d078fd82f38bc17567e4433489913"
HANK_REVISION = "19a8e5e5cd8e7120fbbd2e3de3c5b63509824f3c"
Expand Down
2 changes: 1 addition & 1 deletion integration/pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ Issues = "https://github.com/PolicyEngine/macro/issues"
# particular, frbus includes model.xml and LONGBASE.TXT inside its wheel, so a
# clean non-editable installation is a supported access path.
models = [
"obr-macro-model @ git+https://github.com/PolicyEngine/obr-macroeconomic-model@85ada9be070a34a4a4a0a65d92f8f148a476ccb1",
"obr-macro-model @ git+https://github.com/PolicyEngine/obr-macroeconomic-model@9c4b595f3b60b305459ba8c91eac5bc69fd0ffc8",
"boe_var @ git+https://github.com/PolicyEngine/boe-var-model@42cbcbb728b55b3f416764568fa8f3aef68a6730",
"frbus @ git+https://github.com/PolicyEngine/us-frb-model@2508a2d3ee7d078fd82f38bc17567e4433489913",
"us-hank-model @ git+https://github.com/PolicyEngine/us-hank-model@19a8e5e5cd8e7120fbbd2e3de3c5b63509824f3c",
Expand Down
2 changes: 1 addition & 1 deletion integration/src/policyengine_macro/capabilities.py
Original file line number Diff line number Diff line change
Expand Up @@ -204,7 +204,7 @@ def _quality(level: str, evidence: str, next_gate: str) -> dict:
),
"predictive_validation": _quality(
"weak",
"Free-running GDP and consumption MAPE are 5.75% and 9.56%; the "
"Free-running GDP and consumption MAPE are 4.48% and 7.49%; the "
"anchored fit is by construction.",
"Pass rolling-origin historical-vintage tests against simple "
"benchmarks and first-release outturns.",
Expand Down
35 changes: 34 additions & 1 deletion integration/src/policyengine_macro/core.py
Original file line number Diff line number Diff line change
Expand Up @@ -136,12 +136,33 @@ def _import_boe_var():
"description": "Corporation tax (main) rate",
"units": "rate change in decimal (e.g. -0.05 = 5pp cut from 25% to 20%)",
"investment_closure": True,
# The investment closure's stabiliser anchors the LEVEL, not the
# deviation, and the deviation does not converge at any horizon: for a
# sustained +5pp rise |delta_IF| runs £97m at q3, £2,876m at q12 and
# £43,387m at q25, growing 1.21-1.27x every quarter for 25 quarters.
# Truncating the shock from 12 quarters to 8 barely moves the q12
# response, so the number is carried by accumulated drift rather than
# by the tax rate. The 12-quarter default is not where this settles;
# it is where the magnitude still looks plausible.
"caveat": (
"the response does not converge: it compounds at roughly "
"25%/quarter with no steady state, so read the sign and the "
"first few quarters, not the level or the cumulative total"
),
},
{
"var": "CGIPS",
"description": "Nominal central government investment (feeds real GGI via the GGIPS/GGIDEF chain)",
"units": "£m nominal per quarter (e.g. 3000 ≈ £2.5bn real per quarter)",
"investment_closure": False,
# Measured: delta_IF is exactly 0.0 in all twelve quarters and the
# residual GDP effect goes negative (-0.047 by q12) against the OBR's
# published 1.0. The channel is not weak, it is absent.
"caveat": (
"this channel is dead under the demand closure: business "
"investment does not respond at all and the residual GDP effect "
"is wrong-signed. Do not use it to score capital spending"
),
},
]

Expand Down Expand Up @@ -203,7 +224,16 @@ def obr_shock(
rows = _obr_result_rows(df)
shocked = rows[: int(periods)]
peak = max(rows, key=lambda r: abs(r["pct_gdp"]))
return {
# A lever whose channel is dead or non-convergent must say so in the
# payload, not only in a docstring nobody reads over MCP. Of the four
# fiscal instruments here exactly one contains behaviour: CGG is an
# accounting identity (multiplier exactly 1.0000, flat, against the OBR's
# published 0.6), CGIPS is dead, TCPRO never converges, and the
# household-income lever is the only one with a response worth reading.
caveat = next(
(v.get("caveat") for v in OBR_VARIABLES if v["var"] == var), None
)
result = {
"name": name,
"provenance": _provenance(
model_id="obr-emulator",
Expand All @@ -222,6 +252,9 @@ def obr_shock(
"peak_pct_gdp": peak["pct_gdp"],
"peak_period": peak["period"],
}
if caveat:
result["caveat"] = caveat
return result


# ---------------------------------------------------------------------------
Expand Down
19 changes: 19 additions & 0 deletions integration/tests/test_wiring.py
Original file line number Diff line number Diff line change
Expand Up @@ -404,3 +404,22 @@ def test_cli_score_still_rejects_a_genuinely_unknown_model(runner):
assert res.exit_code != 0
assert "Traceback" not in res.output
assert "model must be one of" in res.output


def test_obr_shock_carries_the_lever_caveat_in_the_payload():
"""Two of the four OBR levers do not work, and the payload must say so.

Measured in the model repo: CGIPS moves business investment by exactly
zero in all twelve quarters and leaves a wrong-signed residual, and TCPRO
compounds at roughly 25%/quarter with no steady state. A docstring does
not reach an MCP caller reading a JSON result, so the caveat travels with
the numbers.
"""
from policyengine_macro import core

levers = {v["var"]: v for v in core.obr_list_variables()}
assert levers["TCPRO"]["caveat"], "the non-convergent lever declares no caveat"
assert levers["CGIPS"]["caveat"], "the dead lever declares no caveat"
# CGG has its own documented "~1 by construction" reading on the site and
# is not caveated here; asserting that keeps this test honest about scope.
assert "caveat" not in levers["CGG"]
8 changes: 4 additions & 4 deletions obr/code/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -170,8 +170,8 @@ <h2>Shock a variable, read the deviation.</h2>
<div class="dial-card">
<div class="dial-name mono">levers — set these (exogenous)</div>
<div class="dial-choice"><code>CGG</code><span class="dial-tradeoff"><strong>Government consumption</strong> — spending shocks, £m per quarter</span></div>
<div class="dial-choice"><code>CGIPS</code><span class="dial-tradeoff"><strong>Central government investment</strong> — public investment, £m per quarter (nominal)</span></div>
<div class="dial-choice"><code>TCPRO</code><span class="dial-tradeoff"><strong>Corporation tax rate</strong> — rate, e.g. &minus;0.05 for a 5pp cut</span></div>
<div class="dial-choice"><code>CGIPS</code><span class="dial-tradeoff"><strong>Central government investment</strong> — public investment, £m per quarter (nominal). <strong>Dead channel:</strong> business investment does not respond at all and the residual GDP effect is wrong-signed, against the OBR&rsquo;s published 1.0. Do not score capital spending with it</span></div>
<div class="dial-choice"><code>TCPRO</code><span class="dial-tradeoff"><strong>Corporation tax rate</strong> — rate, e.g. &minus;0.05 for a 5pp cut. <strong>Does not converge:</strong> the response compounds at roughly 25% a quarter with no steady state, so read the sign and the first few quarters, never the level or the cumulative total</span></div>
<div class="dial-choice"><code>HHDI_ADDFACTOR</code><span class="dial-tradeoff"><strong>Externally costed household reform</strong> — virtual instrument, quarterly £m; positive revenue lowers HHDI via a held add-factor</span></div>
<div class="dial-choice"><code>R / GILT / RX</code><span class="dial-tradeoff"><strong>Bank Rate &middot; gilt yields &middot; exchange rate</strong> — monetary &amp; market assumptions</span></div>
</div>
Expand Down Expand Up @@ -218,8 +218,8 @@ <h2>Shock a variable, read the deviation.</h2>
<div class="dial-card">
<div class="dial-name mono">levers — set these (exogenous)</div>
<div class="dial-choice"><code>CGG</code><span class="dial-tradeoff"><strong>Government consumption</strong> — spending shocks, £m per quarter</span></div>
<div class="dial-choice"><code>CGIPS</code><span class="dial-tradeoff"><strong>Central government investment</strong> — public investment, £m per quarter (nominal)</span></div>
<div class="dial-choice"><code>TCPRO</code><span class="dial-tradeoff"><strong>Corporation tax rate</strong> — rate, e.g. &minus;0.05 for a 5pp cut</span></div>
<div class="dial-choice"><code>CGIPS</code><span class="dial-tradeoff"><strong>Central government investment</strong> — public investment, £m per quarter (nominal). <strong>Dead channel:</strong> business investment does not respond at all and the residual GDP effect is wrong-signed, against the OBR&rsquo;s published 1.0. Do not score capital spending with it</span></div>
<div class="dial-choice"><code>TCPRO</code><span class="dial-tradeoff"><strong>Corporation tax rate</strong> — rate, e.g. &minus;0.05 for a 5pp cut. <strong>Does not converge:</strong> the response compounds at roughly 25% a quarter with no steady state, so read the sign and the first few quarters, never the level or the cumulative total</span></div>
<div class="dial-choice"><code>HHDI_ADDFACTOR</code><span class="dial-tradeoff"><strong>Externally costed household reform</strong> — virtual instrument, quarterly £m; positive revenue lowers HHDI via a held add-factor</span></div>
<div class="dial-choice"><code>R / GILT / RX</code><span class="dial-tradeoff"><strong>Bank Rate &middot; gilt yields &middot; exchange rate</strong> — monetary &amp; market assumptions</span></div>
</div>
Expand Down
2 changes: 1 addition & 1 deletion obr/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,7 @@ <h2>Microsimulation supplies the direct cost; this model returns the feedback.</
<div class="qa-facts">
<div class="qa-fact"><span class="icon">&rarr;</span><span><strong>Connection.</strong> Static-costing bridge, both hosted: <a href="/pe">PolicyEngine</a> computes the direct Exchequer effect of a statutory reform; <code>score_reform</code> injects it through the declared <code>HHDI_ADDFACTOR</code> point and this model returns the economy-wide second round.</span></div>
<div class="qa-fact"><span class="icon">&rarr;</span><span><strong>Worked example.</strong> 1p on the basic rate from April 2026: direct yield &pound;6.46bn in 2026; GDP &minus;0.020% on impact, deepening to &minus;0.058% by 2027Q4 (full walkthrough &rarr; <a href="/models#score">score a reform</a>).</span></div>
<div class="qa-fact"><span class="icon">&rarr;</span><span><strong>Validation.</strong> <a href="/obr/validation">Anchored to the March 2026 EFO</a> (0.15% GDP MAPE) &mdash; but anchored accuracy is a <strong>by-construction invariant</strong>, not a forecast claim: free-running, the same equations miss GDP by <strong>5.75%</strong>. Checked against HMRC&rsquo;s ready reckoner (&pound;6.46bn vs &pound;6.9bn).</span></div>
<div class="qa-fact"><span class="icon">&rarr;</span><span><strong>Validation.</strong> <a href="/obr/validation">Anchored to the March 2026 EFO</a> (0.15% GDP MAPE) &mdash; but anchored accuracy is a <strong>by-construction invariant</strong>, not a forecast claim: free-running, the same equations miss GDP by <strong>4.48%</strong>. Checked against HMRC&rsquo;s ready reckoner (&pound;6.46bn vs &pound;6.9bn).</span></div>
<div class="qa-fact"><span class="icon">&rarr;</span><span><strong>Results.</strong> Run it from <a href="/connect">any connected client</a>; every score carries its assumptions and data vintage.</span></div>
</div>
</div>
Expand Down
Loading