Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
43 commits
Select commit Hold shift + click to select a range
4e299b7
Design spec: nominal price and wage rigidities
adambutlin Aug 5, 2026
acf4154
Spec: benchmark nominal rigidities against Bi-Foerster-Traum (2026)
adambutlin Aug 5, 2026
dc6d2ff
Spec: drop wage rigidity, price Phillips curves only
adambutlin Aug 5, 2026
d92c39f
Spec: add nominal deposits; route the markup rent
adambutlin Aug 5, 2026
387d5ae
Implementation plan: nominal rigidities
adambutlin Aug 5, 2026
f3711bd
Plan fix: doc gate needs all three docs, and long runs need background
adambutlin Aug 5, 2026
9608041
Plan: replace stale thresholds with the measured baseline
adambutlin Aug 5, 2026
2015edd
refactor: single build_block_list() shared by full_model, tpi, regimes
adambutlin Aug 5, 2026
56c7c2b
feat: firm_profit_D/F routes the markup rent proportional to e
adambutlin Aug 5, 2026
7e20ea9
feat: price_nkpc_D/F Rotemberg Phillips curves in PPI inflation
adambutlin Aug 5, 2026
a59ea52
feat: markup wedge mu_p*mc in labor_demand_D/F
adambutlin Aug 5, 2026
c956f4c
feat: terms_of_trade and union_inflation close the nominal side
adambutlin Aug 5, 2026
78f376d
test: assert the 93/7 inflation split on exact arithmetic
adambutlin Aug 5, 2026
46d898f
feat: markup rent enters household income proportional to e
adambutlin Aug 5, 2026
bc15c9a
feat: mu_p, kappa_p, omega_pi_D calibration; mc retargeted to 1/mu_p
adambutlin Aug 5, 2026
6f776b9
feat: seed mc, pi, profit and the global residuals into the SS solve
adambutlin Aug 5, 2026
a8b6c78
feat: 27x27 sticky-price system; passes the kappa_p -> inf equivalenc…
adambutlin Aug 5, 2026
f691864
Plan: record the SSJ H_Z defect, add Task 9b, correct the gate kappa
adambutlin Aug 5, 2026
e927a7d
fix: route every Jacobian call site through solve_jacobian_padded
adambutlin Aug 6, 2026
57b9bd4
Task 10: price stickiness alone flips C_D[0]; record numbers and caveat
adambutlin Aug 6, 2026
9518a02
feat: nominal deposit contracts; ex-ante and ex-post real rates
adambutlin Aug 6, 2026
2e4c372
feat: bank_return and capital_fund pay the ex-post real deposit rate
adambutlin Aug 6, 2026
9057b37
feat: nominal deposits wired; Fisher channel deepens the net-worth loss
adambutlin Aug 6, 2026
151ef3d
calib: re-tune psi_lambda_B to the 150bp spread moment under sticky p…
adambutlin Aug 6, 2026
120dcf6
regen: rebuild regime cache and regenerate E1-E4 on the sticky-price …
adambutlin Aug 6, 2026
6201216
docs: sticky prices and nominal deposits become the baseline
adambutlin Aug 6, 2026
231327c
fix: derive paper figure captions from results, regenerate artefacts
adambutlin Aug 6, 2026
000c632
feat: investment-flow adjustment cost S(I/I(-1)), inactive at omega_I=0
adambutlin Aug 6, 2026
ec4f862
calib: discipline rho_def by the MS regime estimate; re-tune psi_lamb…
adambutlin Aug 6, 2026
933344d
regen: rebuild all results and figures on the MS-disciplined shock
adambutlin Aug 6, 2026
77dc1c6
feat: country-size asymmetry — F is 11.7x D (size_F)
adambutlin Aug 7, 2026
5fe36ce
audit: fiscal rule and fiscal limit; regenerate artefacts; fix compre…
adambutlin Aug 7, 2026
91ac778
Merge: sticky prices, nominal deposits, country-size asymmetry, fisca…
adambutlin Aug 7, 2026
ea23e94
GK refactor stage 1: bounded, exported pledgeability map
adambutlin Aug 17, 2026
70567ba
GK structural refactor: delete the sovereign spread wedge; canonical …
adambutlin Aug 19, 2026
8fc0786
docs: hostile second-referee report on the paper's motivation
adambutlin Aug 24, 2026
be83ec9
Empirics: motivation figures for the introduction
adambutlin Aug 24, 2026
feb8dc4
docs: psi_lambda_B is slated for deletion, not a diagnostic arm
adambutlin Aug 24, 2026
d0e0f79
docs: third referee report on the introduction, and its disposition
adambutlin Aug 27, 2026
149f7ca
docs: results paragraph dropped, not deferred
adambutlin Aug 27, 2026
8735c32
docs: fourth referee report, on Section 2
adambutlin Aug 27, 2026
ba218d4
docs: fifth referee report, on the introduction
adambutlin Aug 28, 2026
51c1914
docs: correct the MTM attribution in CLAUDE.md
adambutlin Aug 28, 2026
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
14 changes: 14 additions & 0 deletions .claude/hooks/gloss-variables-reminder.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
#!/usr/bin/env bash
# UserPromptSubmit hook — standing writing-style rule for this repo.
#
# Injects a reminder that model symbols must be glossed with a short definition
# the first time they appear in prose. The model's variable names (n_inter_F,
# rdep_F, def_rate_ss, goods_mkt_D, ...) are opaque without one, and prose that
# leans on them is unreadable to anyone but the author.
#
# Emits JSON on stdout; `additionalContext` is injected into the model's context
# for the turn. Contains no double quotes so the quoted heredoc below is valid
# JSON verbatim — keep it that way when editing the text.
cat <<'JSON'
{"hookSpecificOutput":{"hookEventName":"UserPromptSubmit","additionalContext":"Writing-style rule for this repo (prose only): the first time a model variable, parameter, coefficient or residual appears in a sentence, gloss it with a short definition in parentheses immediately after it — e.g. `n_inter_F` (F-bank net worth), `rdep_F` (F ex-ante real deposit rate), `def_rate_ss` (steady-state default probability), `goods_mkt_D` (D goods-market residual), `psi_lambda_B` (collateral-friction amplification dial), `size_F` (F size relative to D). Applies to prose and table cells; skip inside code blocks, file paths, diffs and pasted program output. Do not re-gloss the same symbol twice in one reply."}}
JSON
12 changes: 12 additions & 0 deletions .claude/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,18 @@
}
]
}
],
"UserPromptSubmit": [
{
"hooks": [
{
"type": "command",
"command": "bash \"${CLAUDE_PROJECT_DIR:-.}/.claude/hooks/gloss-variables-reminder.sh\" || true",
"timeout": 5,
"suppressOutput": true
}
]
}
]
}
}
155 changes: 137 additions & 18 deletions CLAUDE.md

Large diffs are not rendered by default.

126 changes: 126 additions & 0 deletions Empirics/fiscal_limit.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
"""Estimate the Greek fiscal limit and back out def_scale_D / def_curvature_D.

Mirrors Bi-Foerster-Traum (FRBSF WP 2025-10) eq (3.1): they fit
log(P/(1-P)) = eta0 + eta_s * s_{t-1} + u
on PRE-CRISIS Italian CDS, then let the fiscal-limit shifter carry the crisis.
Our shock_def_D is the shifter analogue, so the same split applies.

Data
debt Eurostat gov_10q_ggdebt, geo=EL, Maastricht GG consolidated gross debt,
% of GDP, quarterly (the same concept BFT use).
spread repo's Empirics/outputs/spreads_fred.csv, GRC 10y minus DEU 10y, monthly,
averaged to quarterly.

Spread -> default probability uses the credit triangle at the MODEL's own
recovery_rate_D = 0.30, so the calibration is internally consistent:
lambda_annual = spread / (1 - recovery); P_quarterly = 1 - exp(-lambda/4)
These are RISK-NEUTRAL probabilities, as are BFT's CDS-implied ones — the
risk-premium wedge is inherited from their design, not introduced here.
"""
import json, urllib.request
import numpy as np
import pandas as pd

RECOVERY = 0.30 # model recovery_rate_D
DEF_OFFSET = 0.05 # model def_offset_D, held fixed (2 moments, 2 params)
DR_SS = 1.152 # model b_gov_ss_D, quarterly-GDP units (bank-held)
EBA_DATE = "2010-Q4" # EBA moment base date, 2010-12-31

# ── data ─────────────────────────────────────────────────────────────────────
url = ("https://ec.europa.eu/eurostat/api/dissemination/statistics/1.0/data/gov_10q_ggdebt"
"?format=JSON&lang=EN&geo=EL&unit=PC_GDP&sector=S13&na_item=GD")
with urllib.request.urlopen(url, timeout=60) as r:
d = json.load(r)
idx = d["dimension"]["time"]["category"]["index"]
inv = {v: k for k, v in idx.items()}
debt = pd.Series({inv[int(i)]: v for i, v in d["value"].items()}).sort_index()
debt.index = pd.PeriodIndex(debt.index.str.replace("-Q", "Q"), freq="Q")
debt = debt / 100.0 # -> share of annual GDP

sp = pd.read_csv("/Users/Adam/Documents/uni/phd/research/QUANTITATIVE_MODEL/"
"Empirics/outputs/spreads_fred.csv", parse_dates=["date"])
sp = sp.set_index("date")["GRC_spread"].dropna() / 100.0 # pp -> decimal
spq = sp.resample("QE").mean()
spq.index = pd.PeriodIndex(spq.index, freq="Q")

df = pd.DataFrame({"spread": spq, "debt": debt}).dropna()
df["debt_lag"] = df["debt"].shift(1)
df = df.dropna()

lam = df["spread"] / (1.0 - RECOVERY) # annual hazard
df["P_q"] = 1.0 - np.exp(-lam / 4.0) # quarterly default probability
df = df[(df["P_q"] > 1e-9) & (df["P_q"] < 1 - 1e-9)]
df["logodds"] = np.log(df["P_q"] / (1 - df["P_q"]))

print(f"merged sample: {df.index.min()}..{df.index.max()} n={len(df)}")
print(f"debt range {df['debt_lag'].min():.3f}..{df['debt_lag'].max():.3f} of annual GDP")
print(f"P_q range {df['P_q'].min():.6f}..{df['P_q'].max():.4f}\n")


def ols(y, x):
X = np.column_stack([np.ones_like(x), x])
b, *_ = np.linalg.lstsq(X, y, rcond=None)
res = y - X @ b
n, k = len(y), 2
s2 = res @ res / (n - k)
se = np.sqrt(np.diag(s2 * np.linalg.inv(X.T @ X)))
r2 = 1 - (res @ res) / ((y - y.mean()) @ (y - y.mean()))
return b, se, r2, n


SAMPLES = {
"pre-crisis 2000Q1-2009Q3 (BFT design)": (None, "2009Q3"),
"pre-OMT 2000Q1-2012Q3 (preferred)": (None, "2012Q3"),
"full 2000Q1-2026Q1": (None, None),
"crisis 2009Q4-2013Q4": ("2009Q4", "2013Q4"),
}
fits = {}
for name, (lo, hi) in SAMPLES.items():
s = df.copy()
if lo: s = s[s.index >= pd.Period(lo, "Q")]
if hi: s = s[s.index <= pd.Period(hi, "Q")]
if len(s) < 6:
print(f"{name}: n={len(s)}, skipped"); continue
b, se, r2, n = ols(s["logodds"].values, s["debt_lag"].values)
fits[name] = b
print(f"{name}\n eta0 = {b[0]:8.3f} ({se[0]:.3f}) eta_s = {b[1]:7.3f} ({se[1]:.3f})"
f" R2 = {r2:.3f} n = {n}")
print(f" [BFT Italian pre-crisis: eta0 = -10.70, eta_s = 5.25]\n")


# ── map the fitted logistic onto the model's power function ──────────────────
# Model: def_rate_D = def_scale * [ (dr + off)^curv - (dr_ss + off)^curv ],
# dr = b_gov_D(-1)/Y_ss_D in quarterly-GDP units of BANK-HELD debt.
# Scope: bank-held = sigma * total, so dr = 4 * sigma * s. sigma is pinned by
# requiring the model SS debt to be the EBA base-date debt ratio.
s_eba = float(debt.loc[pd.Period(EBA_DATE.replace("-Q", "Q"), "Q")])
sigma = (DR_SS / 4.0) / s_eba
print(f"scope factor: model SS bank-held debt = {DR_SS/4:.4f} of annual GDP; "
f"total debt at {EBA_DATE} = {s_eba:.4f} -> sigma = {sigma:.4f}")
print(f" (model dr = 4*sigma*s, so dr_ss = {4*sigma*s_eba:.4f})\n")


def logistic_slope(b, s):
z = b[0] + b[1] * s
P = 1.0 / (1.0 + np.exp(-z))
return b[1] * P * (1 - P)


for name, b in fits.items():
# Two moments: the data slope dP/ds at two debt levels -> def_scale, curv.
s1, s2 = 1.05, s_eba # pre-crisis norm and EBA base date
m1, m2 = logistic_slope(b, s1), logistic_slope(b, s2)
if m1 <= 0 or m2 <= 0:
print(f"{name}: degenerate slopes, skipped"); continue
# data slope in MODEL units: dP/d(dr) = (dP/ds) / (4*sigma)
g1, g2 = m1 / (4 * sigma), m2 / (4 * sigma)
dr1, dr2 = 4 * sigma * s1, 4 * sigma * s2
# model slope: def_scale*curv*(dr+off)^(curv-1); ratio kills def_scale
curv = 1.0 + np.log(g2 / g1) / np.log((dr2 + DEF_OFFSET) / (dr1 + DEF_OFFSET))
scale = g2 / (curv * (dr2 + DEF_OFFSET) ** (curv - 1.0))
print(f"{name}\n implied def_scale_D = {scale:.4f} def_curvature_D = {curv:.4f}"
f" [current 0.25 / 0.50]")
# what the model's CURRENT calibration implies for the same slope
cur = 0.25 * 0.5 * (dr2 + DEF_OFFSET) ** (0.5 - 1.0)
print(f" slope at EBA debt: data {g2:.5f} vs current calibration {cur:.5f} "
f"({g2/cur:.2f}x)\n")
Loading