1# Author: Simon-Pierre Boucher — simon-pierre.boucher@uqo.ca2"""Illustrative simulation of the buyer–seller–lender model (welfare.tex).3Outputs: results/theory/welfare_sim.csv, figures/welfare_failure_vs_s.pdf, figures/welfare_systemic_vs_rho.pdf.4Only LTV = 80 % and the haircut rule are transparent calibrations; all other parameters are ILLUSTRATIVE."""5from __future__ import annotations67import csv8import sys9import time10from pathlib import Path1112import numpy as np1314ROOT = Path(__file__).resolve().parents[1]15sys.path.insert(0, str(ROOT / "src"))16from agentic_valuation.theory import welfare as wf # noqa: E4021718SEED = 2026102719OUT = ROOT / "results" / "theory"20FIG = ROOT / "figures"21OUT.mkdir(parents=True, exist_ok=True)22FIG.mkdir(parents=True, exist_ok=True)23C1, C2, C3, C4 = "#2a78d6", "#eb6834", "#1baf7a", "#eda100"24INK, MUTED, GRID = "#1a1a19", "#6b6a66", "#e6e5e1"252627def style(ax):28 for side in ("top", "right"):29 ax.spines[side].set_visible(False)30 for side in ("left", "bottom"):31 ax.spines[side].set_color(GRID)32 ax.tick_params(colors=MUTED, labelsize=8)33 ax.yaxis.grid(True, color=GRID, lw=0.6)34 ax.set_axisbelow(True)353637def main() -> None:38 t0 = time.time()39 rng = np.random.default_rng(SEED)40 rows: list[dict] = []41 n = 200_00042 shocks = wf.draw_market_shocks(n, rng)43 s_grid = np.linspace(0.0, 0.25, 26)44 base = wf.WelfareParams()45 variants = {"h1=0,b=0": wf.with_params(base, h1=0.0), "h1=0.5,b=0": wf.with_params(base, h1=0.5)}46 biases = {"b=-0.03": -0.03, "b=0": 0.0, "b=+0.03": 0.03}47 market = {}48 for vname, p in variants.items():49 for bname, b in biases.items():50 for s in s_grid:51 m = wf.simulate_market(b, float(s), p, n, rng, shocks=shocks)52 el = wf.expected_loss(b, float(s), p)53 rows.append({"experiment": "transaction", "variant": vname, "bias": b, "s": round(float(s), 4),54 "rho": "", **m, "expected_loss": el})55 market[(vname, bname, round(float(s), 4))] = (m, el)5657 # Systemic block: N lenders, common-model correlation rho58 N, n_sys = 50, 20_00059 sys_shocks = {"C": rng.standard_normal(n_sys), "E": rng.standard_normal((n_sys, N))}60 rho_grid = np.linspace(0.0, 1.0, 11)61 systemic = {}62 for s in (0.05, 0.10, 0.15):63 for rho in rho_grid:64 r = wf.systemic_loss(float(rho), 0.0, s, base, N, n_sys, rng, shocks=sys_shocks)65 rows.append({"experiment": "systemic", "variant": f"s={s}", "bias": 0.0, "s": s, "rho": round(float(rho), 3),66 "sys_mean": r["mean"], "sys_sd": r["sd"], "sys_es95": r["es95"], "cov_pair": r["cov_pair"]})67 systemic[(s, round(float(rho), 3))] = r68 # "Adopting the best common AVM": accuracy improves (s falls) but rho rises to 169 for (s, rho, label) in ((0.15, 0.2, "many_avms"), (0.10, 1.0, "single_best_avm")):70 r = wf.systemic_loss(rho, 0.0, s, base, N, n_sys, rng, shocks=sys_shocks)71 rows.append({"experiment": "concentration", "variant": label, "bias": 0.0, "s": s, "rho": rho,72 "sys_mean": r["mean"], "sys_sd": r["sd"], "sys_es95": r["es95"], "cov_pair": r["cov_pair"]})7374 fields = ["experiment", "variant", "bias", "s", "rho", "beneficial_rate", "failure_rate", "failure_rate_exact",75 "completion_rate", "mean_price", "seller_surplus", "buyer_surplus", "total_surplus", "lost_surplus",76 "expected_loss", "sys_mean", "sys_sd", "sys_es95", "cov_pair"]77 with open(OUT / "welfare_sim.csv", "w", newline="") as f:78 w = csv.DictWriter(f, fieldnames=fields)79 w.writeheader()80 for row in rows:81 w.writerow({k: row.get(k, "") for k in fields})8283 import matplotlib8485 matplotlib.use("Agg")86 import matplotlib.pyplot as plt8788 plt.rcParams.update({"font.family": "serif", "font.size": 9, "axes.labelcolor": INK, "text.color": INK})8990 # Figure 1: failure rate of beneficial transactions vs s (two haircut rules x three biases)91 fig, axes = plt.subplots(1, 2, figsize=(7.2, 3.2), sharey=True)92 bcols = {"b=-0.03": C2, "b=0": C1, "b=+0.03": C3}93 for ax, (vname, _) in zip(axes, variants.items()):94 for bname, b in biases.items():95 ys = [market[(vname, bname, round(float(s), 4))][0]["failure_rate_exact"] for s in s_grid]96 ax.plot(s_grid, ys, color=bcols[bname], lw=2, label=bname.replace("b=", "bias $b=$"))97 ax.set_title("no uncertainty haircut ($h_1=0$)" if vname.startswith("h1=0,") else98 "haircut rising with $s$ ($h_1=0.5$)", fontsize=9)99 ax.set_xlabel("valuation error standard deviation $s$")100 style(ax)101 axes[0].set_ylabel("failure rate, beneficial matches")102 axes[0].legend(frameon=False, fontsize=8, loc="upper left")103 fig.tight_layout()104 fig.savefig(FIG / "welfare_failure_vs_s.pdf")105 plt.close(fig)106107 # Figure 2: systemic loss vs rho108 fig, axes = plt.subplots(1, 2, figsize=(7.2, 3.2))109 scols = {0.05: "#9ec5f4", 0.10: "#256abf", 0.15: "#104281"}110 for s in (0.05, 0.10, 0.15):111 axes[0].plot(rho_grid, [systemic[(s, round(float(r), 3))]["es95"] for r in rho_grid], color=scols[s], lw=2,112 label=f"ES$_{{95}}$, $s={s:.2f}$")113 axes[0].plot(rho_grid, [systemic[(s, round(float(r), 3))]["mean"] for r in rho_grid], color=scols[s], lw=1,114 ls="--")115 axes[1].plot(rho_grid, [systemic[(s, round(float(r), 3))]["sd"] for r in rho_grid], color=scols[s], lw=2,116 label=f"$s={s:.2f}$")117 axes[0].set_title("system loss: mean (dashed) and 95 % expected shortfall", fontsize=9)118 axes[1].set_title("standard deviation of the system loss", fontsize=9)119 for ax in axes:120 ax.set_xlabel(r"common-model correlation $\rho$ across lenders' AVMs")121 ax.set_ylim(0, None)122 style(ax)123 axes[0].set_ylabel(f"loss per $N={N}$ loans (share of $V$)")124 axes[0].legend(frameon=False, fontsize=7, loc="upper left")125 axes[1].legend(frameon=False, fontsize=8, loc="upper left")126 fig.tight_layout()127 fig.savefig(FIG / "welfare_systemic_vs_rho.pdf")128 plt.close(fig)129 print(f"wrote {OUT / 'welfare_sim.csv'} ({len(rows)} rows) and 2 figures in {time.time() - t0:.1f} s")130131132if __name__ == "__main__":133 main()134