SPB Git forge

spb/wp19_uqo

Public
0commits 0branches 0releases
0 Bsize
maindefault branch
—last push
9.8 KB · 176 lines python
Raw Blame History
1"""Stationary equilibrium with free entry, posted prices, financing extension, planner, transition."""2import numpy as np3import pytest45from src.solver import (solve_equilibrium, partial_equilibrium, free_entry_residual, solve_equilibrium_posted,6                        solve_equilibrium_financing, transition, frictionless_price, welfare_flow)789def test_baseline_equilibrium_residuals(params):10    eq = solve_equilibrium(params)11    assert eq.converged and len(eq.theta_roots) == 112    assert abs(eq.free_entry_resid) < 1e-1013    assert np.abs(eq.sellers.resid).max() < 1e-1014    # stock equation15    assert np.allclose(params.s * params.f, (eq.sellers.eta + params.delta) * eq.n)16    assert abs(eq.omega.sum() - 1) < 1e-12 and abs(eq.N.sum() - 1) < 1e-1217    m = eq.moments(params)18    assert m["participation_ok"]19    assert 0 < m["price_mean"] < params.G.hi20    assert m["D_urgent"] > 0 and m["mean_tom_sold"] > 021    # free-entry residual is decreasing in theta around the root (uniqueness check, heterogeneous case)22    th = eq.theta * np.array([0.5, 0.8, 1.0, 1.25, 2.0])23    res = np.array([free_entry_residual(t, params) for t in th])24    assert np.all(np.diff(res) < 0)252627def test_prop3_homogeneous_uniqueness_and_comparative_statics(params):28    p = params.copy(c=np.array([0.08]), f=np.array([1.0]))29    grid = np.geomspace(0.05, 50, 40)30    res = np.array([free_entry_residual(t, p) for t in grid])31    assert np.all(np.diff(res) < 0)                      # strictly decreasing => unique root32    th_phi = [solve_equilibrium(p.copy(phi=ph)).theta for ph in (0.6, 0.75, 0.9)]33    th_A = [solve_equilibrium(p.copy(A=a)).theta for a in (3.0, 4.5, 6.0)]34    assert np.all(np.diff(th_phi) > 0) and np.all(np.diff(th_A) > 0)35    # observational equivalence of A and phi in the baseline36    e1 = solve_equilibrium(p.copy(phi=0.5, A=9.0)); e2 = solve_equilibrium(p.copy(phi=1.0, A=4.5))37    assert abs(e1.theta - e2.theta) < 1e-8 and abs(e1.Lam_s - e2.Lam_s) < 1e-838    # amplification: d ln Lam_s / d ln phi > 139    lo, hi = solve_equilibrium(p.copy(phi=0.84)), solve_equilibrium(p.copy(phi=0.86))40    el = (np.log(hi.Lam_s) - np.log(lo.Lam_s)) / (np.log(0.86) - np.log(0.84))41    assert el > 1.0424344def test_prop4_liquidity_shock(params):45    base = solve_equilibrium(params)46    shock = solve_equilibrium(params.copy(A=params.A * 0.7))47    assert shock.theta < base.theta48    assert np.all(shock.sellers.V < base.sellers.V) and np.all(shock.sellers.pbar < base.sellers.pbar)49    assert np.all(shock.sellers.eta < base.sellers.eta)50    assert shock.S > base.S and shock.Q < base.Q                     # delta > 0: steady-state volume falls51    # impact effect: stock predetermined52    Q_impact = float((shock.sellers.eta * base.n).sum())53    assert Q_impact < base.Q54    # delta = 0: steady-state volume equals inflow and composition equals f (Proposition 6(i))55    p0 = params.copy(delta=0.0)56    e0 = solve_equilibrium(p0); e0s = solve_equilibrium(p0.copy(A=p0.A * 0.7))57    assert abs(e0.Q - p0.s) < 1e-10 and abs(e0s.Q - p0.s) < 1e-1058    assert np.allclose(e0.omega, p0.f) and np.allclose(e0s.omega, p0.f)596061def test_prop5_dispersion_bounds(params):62    eq = solve_equilibrium(params)63    m = eq.moments(params)64    assert m["price_var_between"] > 065    rng = eq.sellers.pbar[0] - eq.sellers.pbar[-1]66    assert rng <= (params.c[-1] - params.c[0]) / (params.r + params.delta)67    # beta -> 0: price = V(c); between variance <= Var(c)/(r+delta)^268    p = params.copy(beta=1e-6)69    e = solve_equilibrium(p)70    var_c = float((e.omega * (params.c - (e.omega * params.c).sum()) ** 2).sum())71    assert e.moments(p)["price_var_between"] <= var_c / (p.r + p.delta) ** 2 + 1e-12727374def test_prop6_impact_composition(params):75    """Impact decomposition of Proposition 6(ii): the composition term has the sign of Cov_omega(pbar, eps) d ln Lam_s.76    For the truncated-normal baseline h_G*M is increasing, so eps(c) increases in c and selection DAMPENS the index77    response; for exponential G (eps decreasing in c) selection AMPLIFIES it."""78    from src.model import Exponential79    for G, expect_amplify in ((params.G, False), (Exponential(rate=8.0, lo=0.7), True)):80        p = params.copy(G=G)81        base = solve_equilibrium(p)82        pe = partial_equilibrium(base.theta, p.copy(A=p.A * 0.9))   # same theta, lower Lam_s, stock predetermined83        eta_new = pe.sellers.eta84        omega_imp = eta_new * base.n / (eta_new * base.n).sum()85        P_obs = float((omega_imp * pe.sellers.pbar).sum()); P_fix = float((base.omega * pe.sellers.pbar).sum())86        P0 = base.moments(p)["price_mean"]87        eps = (np.log(eta_new) - np.log(base.sellers.eta)) / np.log(0.9)88        cov = float((base.omega * (base.sellers.pbar - P_fix) * (eps - (base.omega * eps).sum())).sum())89        assert P_fix < P0                                            # within-type prices fall90        assert np.sign(P_obs - P_fix) == np.sign(-cov)               # composition term = Cov * d ln Lam (<0)91        if expect_amplify:92            assert np.all(np.diff(eps) < 0) and P_obs < P_fix93        else:94            assert np.all(np.diff(eps) > 0) and P_obs > P_fix95        # analytic elasticity formula eps = 1 - beta eta h_G M /(r+delta+beta eta) at the base point (small-shock check)96        sb = base.sellers97        an = 1 - p.beta * sb.eta * np.asarray(G.hazard(sb.vstar)) * sb.M / ((p.r + p.delta) + p.beta * sb.eta)98        assert np.allclose(an, eps, atol=0.02)99100101def test_posted_prices(params):102    eq = solve_equilibrium_posted(params)103    assert eq.converged and np.abs(eq.sellers.resid).max() < 1e-9 and abs(eq.free_entry_resid) < 1e-9104    assert np.all(np.diff(eq.sellers.pbar) < 0) and np.all(np.diff(eq.sellers.eta) > 0)105    # posted threshold exceeds Nash threshold at equal (V,U)106    assert np.all(eq.sellers.vstar > eq.sellers.V + params.k)107    # FOC: y - U - V = (1-G)/g108    foc = eq.sellers.vstar - params.k - eq.sellers.V - params.G.surv(eq.sellers.vstar) / params.G.pdf(eq.sellers.vstar)109    assert np.abs(foc).max() < 1e-7110111112@pytest.mark.slow113def test_financing_extension(params):114    fe = solve_equilibrium_financing(params)115    assert fe.converged and abs(fe.resid["free_entry"]) < 1e-8 and fe.resid["max_abs"] < 1e-8116    # Proposition 7(ii): the selection term has the sign of v*_m - v*_c (positive iff the pending wedge exceeds U_c - U_m)117    assert np.all(np.sign(fe.sel_term) == np.sign(fe.vstar_m - fe.vstar_c))118    wedge = (params.c + params.kappa_b + params.r * (fe.V + fe.U_m)) / (params.nu * params.phi_m)119    assert np.allclose(fe.vstar_m - fe.vstar_c, wedge - (fe.U_c - fe.U_m))120    assert np.all(np.diff(fe.Delta_b) >= -1e-12)                         # Delta_b nondecreasing in c121    assert fe.U_c > fe.U_m                                               # cash buyers have the better outside option122    assert np.all(np.diff(fe.pbar_m - fe.pbar_c) > 0)                    # gap increasing in urgency (numerical, baseline)123    assert abs(fe.Q_c.sum() / fe.Q - params.pi) < 1e-8                   # steady-state sales composition = entry composition124    assert 0 < fe.pi_tilde < 1125    # Delta_b formula check against the closed form of Proposition 7126    an = ((1 - params.beta) * (params.c + params.r * fe.V) - params.beta * (params.kappa_b + params.r * fe.U_m)) / (params.nu * params.phi_m)127    assert np.allclose(an, fe.Delta_b)128    # uniqueness: different starting points reach the same equilibrium129    f2 = solve_equilibrium_financing(params, x0=np.array([np.log(1.5), 0.0, 0.5]))130    assert abs(f2.theta - fe.theta) < 1e-6131    # nesting: nu -> infinity, pi -> 1 reproduces the baseline with phi = 1132    p2 = params.copy(nu=1e5, pi=0.999)133    fe2 = solve_equilibrium_financing(p2); eq = solve_equilibrium(params.copy(phi=1.0))134    assert abs(fe2.theta - eq.theta) / eq.theta < 1e-2 and np.abs(fe2.V - eq.sellers.V).max() < 1e-3135136137@pytest.mark.slow138def test_transition_converges_and_matches_steady_states(params):139    p_new = params.copy(A=params.A * 0.8)140    tr = transition(params, lambda t: p_new, T=6.0, dt=0.05)141    assert tr.converged142    e0 = solve_equilibrium(params); e1 = solve_equilibrium(p_new)143    assert abs(tr.P_obs[0] - 0) >= 0144    assert np.allclose(tr.n[0], e0.n)145    assert abs(tr.theta[-1] - e1.theta) / e1.theta < 2e-2146    assert np.abs(tr.n[-1] - e1.n).max() < 2e-2 * e1.n.max()147    # composition term: sign on impact follows Proposition 6 (dampening for the truncated normal: P_obs > P_fix after a148    # negative shock) and is an order of magnitude smaller than the within-type change; volume and inventory adjust slowly149    assert tr.P_obs[0] > tr.P_fix[0]                                   # impact: composition relative to the pre-shock weights150    assert abs(np.log(tr.P_obs[0] / tr.P_fix[0])) < 0.1 * abs(np.log(tr.P_fix[0] / e0.moments(params)["price_mean"]))151    assert tr.Q[0] < tr.Q[-1] and tr.S[0] < tr.S[-1]152    assert np.all(tr.Q >= 0) and np.all(tr.S > 0)153    # exponential G: amplification (P_obs < P_fix on impact)154    from src.model import Exponential155    pe = params.copy(G=Exponential(rate=8.0, lo=0.7)); tre = transition(pe, lambda t: pe.copy(A=pe.A * 0.8), T=3.0, dt=0.05)156    assert tre.converged and tre.P_obs[0] < tre.P_fix[0]157158159def test_frictionless_price_and_welfare(params):160    ps = frictionless_price(params)161    assert abs(float(params.G.H(ps)) - params.r * params.k) < 1e-10162    eq = solve_equilibrium(params)163    w = welfare_flow(eq, params)164    assert w["total"] > 0 and w["holding_costs"] > 0165166167def test_grid_and_tolerance_convergence(params):168    """Equilibrium objects stable across the seller-type grid and root tolerances."""169    m9 = solve_equilibrium(params).moments(params)170    p25 = params.with_types(0.02, 0.3, 25, 2.0, 4.0)171    m25 = solve_equilibrium(p25).moments(p25)172    for key in ("theta", "price_mean", "mean_tom_sold", "Q"):173        assert abs(m9[key] - m25[key]) / abs(m25[key]) < 1e-2, key174    mt = solve_equilibrium(params.copy(root_xtol=1e-8)).moments(params)175    assert abs(mt["theta"] - m9["theta"]) < 1e-6176