SPB Git forge

spb/wp19_uqo

Public
0commits 0branches 0releases
0 Bsize
maindefault branch
—last push
5.5 KB · 112 lines python
Raw Blame History
1"""Seller value function: existence/uniqueness, Proposition 1 and 2 comparative statics, boundary cases."""2import numpy as np3import pytest45from src.model import Pareto, Exponential6from src.solver import seller_value, solve_sellers789def test_residual_and_uniqueness(params):10    sb = solve_sellers(8.0, params.k, params)11    assert np.abs(sb.resid).max() < 1e-1012    # Phi is strictly decreasing: perturbing V changes the sign of the residual13    for i, c in enumerate(params.c):14        V = sb.V[i]15        up = -c + params.delta * params.w0 + 8.0 * params.beta * float(params.G.H(V + 1e-3 + params.k)) - (params.r + params.delta) * (V + 1e-3)16        dn = -c + params.delta * params.w0 + 8.0 * params.beta * float(params.G.H(V - 1e-3 + params.k)) - (params.r + params.delta) * (V - 1e-3)17        assert up < 0 < dn181920def test_prop1_monotonicity(params):21    sb = solve_sellers(8.0, params.k, params)22    assert np.all(np.diff(sb.V) < 0)          # reservation price falls with c23    assert np.all(np.diff(sb.eta) > 0)        # hazard rises with c24    assert np.all(np.diff(sb.pbar) < 0)       # expected price falls with c25    # analytic slope  V'(c) = -1/[(r+delta) + Lam beta (1-G)]  vs finite differences26    slope_an = -1.0 / ((params.r + params.delta) + 8.0 * params.beta * sb.acc)27    slope_fd = np.gradient(sb.V, params.c)28    assert np.allclose(slope_an[1:-1], slope_fd[1:-1], rtol=0.05)29    # |pbar'| <= |V'| under IFR30    assert np.all(np.abs(np.diff(sb.pbar)) <= np.abs(np.diff(sb.V)) + 1e-12)31    assert np.all(-1.0 / (params.r + params.delta) < slope_an) and np.all(slope_an < 0)323334def test_prop1_participation_threshold(params):35    Lam = 8.036    cbar = Lam * params.beta * float(params.G.H(params.w0 + params.k)) - params.r * params.w037    p = params.copy(c=np.array([cbar - 1e-4, cbar + 1e-4]), f=np.array([0.5, 0.5]))38    sb = solve_sellers(Lam, params.k, p)39    assert sb.V[0] >= params.w0 - 1e-9 and sb.V[1] < params.w0404142def test_prop2_arrival_rate(params):43    lams = np.array([2.0, 4.0, 8.0, 16.0])44    V = np.array([solve_sellers(L, params.k, params).V for L in lams])45    eta = np.array([solve_sellers(L, params.k, params).eta for L in lams])46    pbar = np.array([solve_sellers(L, params.k, params).pbar for L in lams])47    assert np.all(np.diff(V, axis=0) > 0)48    assert np.all(np.diff(pbar, axis=0) > 0)49    assert np.all(np.diff(eta, axis=0) > 0)                 # IFR => hazard increasing in Lam_s50    gap = V[:, 0] - V[:, -1]51    assert np.all(np.diff(gap) < 0)                         # reservation gap narrows with Lam_s52    # analytic dV/dLam vs finite difference53    sb = solve_sellers(8.0, params.k, params)54    an = params.beta * sb.H / ((params.r + params.delta) + 8.0 * params.beta * sb.acc)55    fd = (solve_sellers(8.0 + 1e-4, params.k, params).V - solve_sellers(8.0 - 1e-4, params.k, params).V) / 2e-456    assert np.allclose(an, fd, rtol=1e-4)575859def test_prop2_pareto_counterexample(params):60    """With Pareto G (DFR), d eta / d Lam_s < 0 iff c > delta*w0 + (r+delta)*U, independently of Lam_s (Proposition 2(ii))."""61    lams = (2.0, 5.0, 10.0, 20.0, 40.0)62    for w0, c in ((0.0, 0.05), (0.8, 0.2)):            # condition holds -> hazard decreasing in Lam_s63        p = params.copy(G=Pareto(a=1.5, lo=0.7), c=np.array([c]), f=np.array([1.0]), delta=0.1, w0=w0)64        assert c > p.delta * w0 + (p.r + p.delta) * p.k65        eta = np.array([solve_sellers(L, p.k, p).eta[0] for L in lams])66        assert np.all(np.diff(eta) < 0)67    for w0, c in ((0.0, 0.002), (0.8, 0.05)):          # condition fails -> hazard increasing68        p = params.copy(G=Pareto(a=1.5, lo=0.7), c=np.array([c]), f=np.array([1.0]), delta=0.1, w0=w0)69        assert c < p.delta * w0 + (p.r + p.delta) * p.k70        eta = np.array([solve_sellers(L, p.k, p).eta[0] for L in lams])71        assert np.all(np.diff(eta) > 0)727374def test_prop6_exponential_elasticity(params):75    """epsilon(c) = (r+delta)/(r+delta+beta*eta) for exponential G."""76    p = params.copy(G=Exponential(rate=8.0, lo=0.7))77    L = 8.078    sb = solve_sellers(L, p.k, p)79    fd = (np.log(solve_sellers(L * 1.001, p.k, p).eta) - np.log(solve_sellers(L / 1.001, p.k, p).eta)) / (2 * np.log(1.001))80    an = (p.r + p.delta) / ((p.r + p.delta) + p.beta * sb.eta)81    assert np.allclose(fd, an, rtol=2e-3)82    assert np.all(np.diff(an) < 0)838485def test_boundary_cases(params):86    # zero urgency, no withdrawal: V solves r V = Lam beta H(V+k)87    p = params.copy(c=np.array([0.0]), f=np.array([1.0]), delta=0.0)88    sb = solve_sellers(8.0, p.k, p)89    assert abs(p.r * sb.V[0] - 8.0 * p.beta * float(p.G.H(sb.V[0] + p.k))) < 1e-1090    # frozen market: Lam -> 0 gives V -> (-c + delta w0)/(r+delta)91    sbf = solve_sellers(1e-9, params.k, params)92    assert np.allclose(sbf.V, (-params.c + params.delta * params.w0) / (params.r + params.delta), atol=1e-7)93    # very liquid market: v* -> w_H, prices -> w_H - k94    sbl = solve_sellers(1e7, params.k, params)95    assert np.all(sbl.vstar > params.G.hi - 1e-2)96    assert np.all(np.abs(sbl.pbar - (params.G.hi - params.k)) < 2e-2)97    # survival and hazards98    sb = solve_sellers(8.0, params.k, params)99    assert np.all(sb.eta >= 0) and np.all(sb.acc >= 0) and np.all(sb.acc <= 1)100    tau = np.linspace(0, 3, 50)101    surv = np.exp(-np.outer(tau, sb.eta + params.delta))102    assert np.all(surv <= 1) and np.all(np.diff(surv, axis=0) <= 0)103104105def test_lemma3_spurious_duration_dependence(params):106    sb = solve_sellers(8.0, params.k, params)107    mu = sb.eta + params.delta108    tau = np.linspace(0, 2, 400)109    S = np.exp(-np.outer(tau, mu)) @ params.f110    hazard = -np.gradient(np.log(S), tau)111    assert np.all(np.diff(hazard)[5:-5] < 0)112