Initial commit: Four-Quadrant Balance Sheet Matrix (FQBM) framework

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
defiQUG
2026-02-22 23:39:47 -08:00
commit abed763a4f
51 changed files with 2923 additions and 0 deletions

1
tests/__init__.py Normal file
View File

@@ -0,0 +1 @@
# FQBM tests

View File

@@ -0,0 +1,24 @@
"""Tests for Sheet 3: Capital and liquidity stress (Part XII)."""
from fqbm.state import FQBMState
from fqbm.sheets.capital_stress import stress_tables, StressParams
def test_stress_tables_structure():
state = FQBMState(R=200, Deposits=1000, Loans=900, E_b=100)
out = stress_tables(state)
assert "liquidity_stress" in out
assert "capital_stress" in out
assert len(out["liquidity_stress"]) == 3
assert len(out["capital_stress"]) == 2
assert "RCR" in out["liquidity_stress"].columns
assert "Status" in out["liquidity_stress"].columns
assert "CR" in out["capital_stress"].columns
def test_stress_tables_with_params():
state = FQBMState(R=50, Deposits=500, Loans=400, E_b=50)
params = StressParams(capital_ratio_min=0.10)
out = stress_tables(state, params)
assert out["capital_stress"] is not None
assert "Breach" in out["capital_stress"]["Status"].values or "Compliant" in out["capital_stress"]["Status"].values

View File

@@ -0,0 +1,23 @@
"""Unit tests for Sheet 1: Central Bank (Part II — 2.1)."""
import pytest
from fqbm.state import FQBMState
from fqbm.sheets.central_bank import central_bank_step, CentralBankParams
def test_central_bank_identity_qe():
"""QE: dB > 0 → dR > 0."""
state = FQBMState(B=100, R=50, C=20, E_cb=30)
params = CentralBankParams(d_B=10, d_L_cb=0)
out = central_bank_step(state, params)
assert out.B == 110
assert out.R == 60
def test_central_bank_identity_qt():
"""QT: dB < 0 → dR < 0."""
state = FQBMState(B=100, R=50, C=20, E_cb=30)
params = CentralBankParams(d_B=-5, d_L_cb=0)
out = central_bank_step(state, params)
assert out.B == 95
assert out.R == 45

View File

@@ -0,0 +1,31 @@
"""Unit tests for Sheet 2: Commercial Bank (Part II — 2.2)."""
import pytest
from fqbm.state import FQBMState
from fqbm.sheets.commercial_bank import (
commercial_bank_step,
CommercialBankParams,
loans_max,
capital_ratio,
)
def test_loan_deposit_identity():
"""dLoans = dDeposits."""
state = FQBMState(Loans=900, Deposits=1000, E_b=100)
params = CommercialBankParams(d_deposits=50)
out = commercial_bank_step(state, params)
assert out.Loans == 950
assert out.Deposits == 1050
def test_loans_max():
"""Loans_max = Equity / k."""
assert loans_max(100, 0.08) == pytest.approx(1250)
assert loans_max(100, 0.10) == 1000
def test_capital_ratio():
"""CR = Equity / RWA."""
assert capital_ratio(100, 1000) == 0.1
assert capital_ratio(80, 1000) == 0.08

32
tests/test_commodity.py Normal file
View File

@@ -0,0 +1,32 @@
"""Tests for Sheet 6: Commodity shock channel (Part VI)."""
import pytest
from fqbm.state import FQBMState
from fqbm.sheets.commodity import (
inflation_composite,
commodity_step,
CommodityParams,
)
def test_inflation_composite():
pi = inflation_composite(0.02, 0.1, 0.05, 0.1, 0.2)
assert pi == pytest.approx(0.02 + 0.1 * 0.1 + 0.2 * 0.05)
assert pi > 0.02
def test_commodity_step():
state = FQBMState(O=1.0)
params = CommodityParams(d_O=0.1)
out = commodity_step(state, params)
assert out.O == pytest.approx(1.1)
params2 = CommodityParams(d_O=-0.05)
out2 = commodity_step(out, params2)
assert out2.O == pytest.approx(1.1 * 0.95)
def test_commodity_step_zero_o():
state = FQBMState(O=0.0)
params = CommodityParams(d_O=0.1)
out = commodity_step(state, params)
assert out.O == pytest.approx(1.1)

View File

@@ -0,0 +1,30 @@
"""Tests for Part XIV: differential model and stability checks."""
import numpy as np
from fqbm.state import FQBMState
from fqbm.system.differential_model import (
solve_trajectory,
check_stability,
DifferentialParams,
)
def test_solve_trajectory():
x0 = FQBMState(B=100, R=50, Loans=500, Deposits=600, E_b=50, S=1.0)
params = DifferentialParams(monetary_shock=1.0, credit_cycle=0.01)
t, X = solve_trajectory(x0, (0, 1), params, t_eval=np.linspace(0, 1, 11))
assert t.shape[0] == 11
assert X.shape == (11, 12)
assert X[-1, 0] > X[0, 0] # B increased
assert X[-1, 1] > X[0, 1] # R increased
assert X[-1, 3] >= X[0, 3] # Loans
def test_check_stability():
x = FQBMState(R=100, Loans=1000, E_b=100)
checks = check_stability(x, k=0.08, reserve_threshold=50, debt_gdp=0.5, r=0.05, g=0.02, primary_balance_gdp=0.02)
assert "CR_ok" in checks
assert "reserves_ok" in checks
assert "debt_sustainable" in checks
assert checks["CR_ok"] is True
assert checks["reserves_ok"] is True

42
tests/test_fx_parity.py Normal file
View File

@@ -0,0 +1,42 @@
"""Tests for Sheet 4: FX parity (Part IIIIV)."""
import pytest
from fqbm.state import FQBMState
from fqbm.sheets.fx_parity import (
covered_interest_parity_fwd,
uncovered_interest_parity_ds,
dornbusch_s,
fx_pass_through_inflation,
fx_parity_step,
FXParams,
)
def test_cip():
F = covered_interest_parity_fwd(1.0, 0.05, 0.03)
assert F > 1.0
assert abs(F - 1.0 * (1.05 / 1.03)) < 1e-6
def test_uip():
ds = uncovered_interest_parity_ds(1.0, 0.05, 0.03)
assert abs(ds - 0.02) < 1e-6
def test_dornbusch():
s = dornbusch_s(1.0, 0.05, 0.03, 1.5)
assert s > 1.0
assert abs(s - 1.0 - 1.5 * 0.02) < 1e-6
def test_fx_pass_through():
pi = fx_pass_through_inflation(0.1, 0.2)
assert pi == pytest.approx(0.02)
def test_fx_parity_step():
state = FQBMState(S=1.0)
params = FXParams(s_star=1.0, i_d=0.05, i_f=0.03, lambda_dornbusch=2.0)
out = fx_parity_step(state, params)
assert out.S != 1.0
assert out.S == pytest.approx(1.0 + 2.0 * 0.02)

111
tests/test_ipsas.py Normal file
View File

@@ -0,0 +1,111 @@
"""Tests for IPSAS presentation layer."""
import pytest
from fqbm.state import FQBMState
from fqbm.ipsas.presentation import (
statement_of_financial_position,
budget_vs_actual_structure,
)
def test_statement_of_financial_position_central_bank():
state = FQBMState(B=100, R=50, C=30, E_cb=20)
df = statement_of_financial_position(state, entity="central_bank")
assert "Line item" in df.columns
assert "Amount" in df.columns
total_assets = df[df["Line item"] == "TOTAL ASSETS"]["Amount"].iloc[0]
assert total_assets == 100
total_liab = df[df["Line item"] == "TOTAL LIABILITIES AND NET ASSETS"]["Amount"].iloc[0]
assert total_liab == 30 + 50 + 20
def test_statement_of_financial_position_commercial_bank():
state = FQBMState(Loans=900, Deposits=800, E_b=100)
df = statement_of_financial_position(state, entity="commercial_bank")
total_assets = df[df["Line item"] == "TOTAL ASSETS"]["Amount"].iloc[0]
assert total_assets == 900
total_liab = df[df["Line item"] == "TOTAL LIABILITIES AND NET ASSETS"]["Amount"].iloc[0]
assert total_liab == 800 + 100
def test_statement_of_financial_position_consolidated():
state = FQBMState(B=50, R=100, C=20, Loans=500, Deposits=480, E_cb=50, E_b=100)
df = statement_of_financial_position(state, entity="consolidated")
total_assets = df[df["Line item"] == "TOTAL ASSETS"]["Amount"].iloc[0]
assert total_assets == 50 + 100 + 500
total_liab = df[df["Line item"] == "TOTAL LIABILITIES AND NET ASSETS"]["Amount"].iloc[0]
assert total_liab == 20 + 480 + 50 + 100
def test_budget_vs_actual_structure():
df = budget_vs_actual_structure()
assert "Original budget" in df.columns
assert "Final budget" in df.columns
assert "Actual" in df.columns
assert "Variance" in df.columns
assert "Material (Y/N)" in df.columns
assert len(df) >= 6
assert df.attrs.get("ipsas_24") is True
def test_budget_vs_actual_custom_lines():
df = budget_vs_actual_structure(line_items=["Revenue", "Expense"])
assert list(df["Line item"]) == ["Revenue", "Expense"]
assert df["Actual"].isna().all()
def test_cash_flow_statement_structure():
from fqbm.ipsas.presentation import cash_flow_statement_structure
df = cash_flow_statement_structure()
assert "Category" in df.columns
assert "Line item" in df.columns
assert "Amount" in df.columns
assert df.attrs.get("ipsas_2") is True
cats = set(df["Category"].dropna().unique())
assert "Operating" in cats
assert "Investing" in cats
assert "Financing" in cats
def test_statement_of_financial_performance_structure():
from fqbm.ipsas.presentation import statement_of_financial_performance_structure
df = statement_of_financial_performance_structure()
assert "Line item" in df.columns
assert "Amount" in df.columns
assert df.attrs.get("ipsas_1_performance") is True
assert "Revenue" in " ".join(df["Line item"].astype(str))
def test_notes_to_financial_statements_structure():
from fqbm.ipsas.presentation import notes_to_financial_statements_structure
df = notes_to_financial_statements_structure()
assert "Note" in df.columns
assert "Title" in df.columns
assert "Content" in df.columns
assert df.attrs.get("notes_template") is True
assert len(df) >= 5
def test_statement_of_financial_position_comparative():
from fqbm.ipsas.presentation import statement_of_financial_position_comparative
prior = FQBMState(B=80, R=40, Loans=400, Deposits=380, E_b=20)
current = FQBMState(B=90, R=45, Loans=420, Deposits=398, E_b=22)
df = statement_of_financial_position_comparative(prior, current, entity="commercial_bank")
assert "Prior" in df.columns
assert "Current" in df.columns
assert "Change" in df.columns
assert df.attrs.get("comparative") is True
# Change should equal Current - Prior for numeric rows
num_mask = df["Prior"].apply(lambda x: isinstance(x, (int, float)))
if num_mask.any():
assert (df.loc[num_mask, "Current"] - df.loc[num_mask, "Prior"] - df.loc[num_mask, "Change"]).abs().max() < 1e-6
def test_maturity_risk_disclosure_structure():
from fqbm.ipsas.presentation import maturity_risk_disclosure_structure
df = maturity_risk_disclosure_structure()
assert "0-1Y" in df.columns
assert "1-5Y" in df.columns
assert "Interest rate +100bp" in df.columns
assert "ECL" in df.columns
assert df.attrs.get("maturity_risk_disclosure") is True

32
tests/test_matrix.py Normal file
View File

@@ -0,0 +1,32 @@
"""Tests for Part I: Four-quadrant matrix."""
from fqbm.state import FQBMState
from fqbm.matrix import four_quadrant_matrix, four_quadrant_summary
def test_four_quadrant_matrix():
state = FQBMState(B=100, R=50, C=30, Loans=200, Deposits=180, E_cb=20, E_b=20)
df = four_quadrant_matrix(state)
assert "Assets (Dr)" in df.columns
assert "Liabilities (Dr)" in df.columns
assert "Liabilities (Cr)" in df.columns
last = df.iloc[-1]
assert float(last["Assets (Dr)"]) == 300
assert float(last["Liabilities (Cr)"]) == 40
def test_four_quadrant_matrix_with_L_cb():
state = FQBMState(B=80, R=40, C=20, Loans=100, Deposits=90, E_cb=20, E_b=10)
df = four_quadrant_matrix(state, L_cb=10)
last = df.iloc[-1]
assert float(last["Assets (Dr)"]) == 190
def test_four_quadrant_summary_identity():
state = FQBMState(B=50, R=30, C=10, Loans=100, Deposits=95, E_cb=10, E_b=5)
s = four_quadrant_summary(state)
assert "total_assets_dr" in s
assert "identity_A_eq_L_plus_E" in s
assert s["total_assets_dr"] == 150
assert s["total_liabilities_dr"] + s["total_liabilities_cr"] == 150
assert s["identity_A_eq_L_plus_E"] is True

33
tests/test_regressions.py Normal file
View File

@@ -0,0 +1,33 @@
"""Tests for Part X empirical regressions."""
import pytest
from fqbm.empirical.regressions import (
run_inflation_pass_through,
run_sovereign_spread,
run_capital_flow_sensitivity,
generate_synthetic_inflation,
generate_synthetic_spread,
generate_synthetic_capital_flow,
)
def test_inflation_pass_through():
data = generate_synthetic_inflation(50, seed=42)
res = run_inflation_pass_through(data)
assert hasattr(res, "params")
assert "dS" in res.params.index
assert res.rsquared >= 0
def test_sovereign_spread():
data = generate_synthetic_spread(50, seed=42)
res = run_sovereign_spread(data)
assert "debt_gdp" in res.params.index
assert "reserves_gdp" in res.params.index
def test_capital_flow_sensitivity():
data = generate_synthetic_capital_flow(50, seed=42)
res = run_capital_flow_sensitivity(data)
assert "rate_diff" in res.params.index
assert "risk_premium" in res.params.index

29
tests/test_scenarios.py Normal file
View File

@@ -0,0 +1,29 @@
"""Tests for Part XI: Historical scenario presets."""
from fqbm.scenarios import get_scenario, list_scenarios
from fqbm.workbook.runner import run_workbook
def test_list_scenarios():
names = list_scenarios()
assert "asia_1997" in names
assert "gfc_2008" in names
assert "pandemic_2020" in names
assert "rate_shock_2022" in names
def test_get_scenario():
p = get_scenario("asia_1997")
assert p is not None
assert p.name == "asia_1997"
assert p.state.R == 80
assert get_scenario("nonexistent") is None
def test_run_workbook_with_scenario():
result = run_workbook(scenario="asia_1997", mc_runs=3)
assert result["state"].R == 80
assert "dashboard" in result
result2 = run_workbook(scenario="gfc_2008", mc_runs=2)
assert result2["state"].Loans == 2200
assert result2["state"].E_b == 80

View File

@@ -0,0 +1,31 @@
"""Tests for Sheet 5: Sovereign debt and spread (Part V)."""
from fqbm.state import FQBMState
from fqbm.sheets.sovereign_debt import (
spread_model,
debt_sustainable,
sovereign_debt_step,
SovereignParams,
)
def test_spread_model():
s = spread_model(0.8, 0.1, 0.2, -0.01)
assert s > 0
# higher debt_gdp -> higher spread; higher reserves_gdp -> lower spread
assert spread_model(0.9, 0.1, 0.1, 0) > spread_model(0.5, 0.1, 0.1, 0)
assert spread_model(0.5, 0.3, 0.1, 0) < spread_model(0.5, 0.1, 0.1, 0)
def test_debt_sustainable():
assert debt_sustainable(0.03, 0.05, 0.02, 0.6) is True # 0.03 >= 0.018
assert debt_sustainable(0.01, 0.05, 0.02, 0.6) is False
assert debt_sustainable(0.0, 0.05, 0.02, 0.5) is False
def test_sovereign_debt_step():
state = FQBMState(Spread=0.0)
params = SovereignParams(debt_gdp=0.7, reserves_gdp=0.2, fx_vol=0.1)
out = sovereign_debt_step(state, params)
assert out.Spread >= 0
assert out.Spread != state.Spread or (params.debt_gdp == 0.6 and params.reserves_gdp == 0.2 and params.fx_vol == 0.1)

View File

@@ -0,0 +1,61 @@
"""Tests for state (L_cb, open economy) and new IPSAS helpers."""
import pytest
from fqbm.state import FQBMState, open_economy_view
from fqbm.ipsas.presentation import (
statement_of_changes_in_net_assets_structure,
cash_flow_from_state_changes,
fx_translate,
)
def test_state_has_L_cb():
state = FQBMState(B=100, L_cb=20, R=80, C=30, E_cb=10)
assert state.L_cb == 20
assert len(state.to_vector()) == 12
assert state.to_vector()[-1] == 20
def test_from_vector_backward_compat():
# 11 elements still supported
state = FQBMState.from_vector([1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11])
assert state.L_cb == 0
state12 = FQBMState.from_vector([1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12])
assert state12.L_cb == 12
def test_open_economy_view():
v = open_economy_view(100, 50, 80, 40, 30)
assert v["total_assets"] == 150
assert v["total_liab_equity"] == 150
assert v["identity_holds"] is True
v2 = open_economy_view(100, 50, 90, 40, 30)
assert v2["identity_holds"] is False
def test_statement_of_changes_in_net_assets_structure():
df = statement_of_changes_in_net_assets_structure()
assert "Opening balance" in df.columns
assert "Closing balance" in df.columns
assert df.attrs.get("ipsas_1_changes") is True
def test_cash_flow_from_state_changes():
prev = FQBMState(R=100, C=20, B=50, Loans=200, Deposits=180)
curr = FQBMState(R=110, C=22, B=55, Loans=210, Deposits=195)
df = cash_flow_from_state_changes(prev, curr)
assert "Category" in df.columns
assert "Amount" in df.columns
assert df.attrs.get("ipsas_2_from_state") is True
# Net change should be consistent
row = df[df["Line item"].str.contains("Net increase", na=False)]
assert len(row) == 1
def test_fx_translate():
r = fx_translate(100.0, 1.0, 1.1)
assert r["local_prev"] == pytest.approx(100)
assert r["local_curr"] == pytest.approx(110)
assert r["fx_gain_loss"] == pytest.approx(10)
r2 = fx_translate(100.0, 1.2, 1.0)
assert r2["fx_gain_loss"] == pytest.approx(-20)

93
tests/test_stubs.py Normal file
View File

@@ -0,0 +1,93 @@
"""Tests for Part VII, VIII, IX (shadow banking, CCP, CBDC)."""
import pytest
from fqbm.sheets.shadow_banking import (
leverage_ratio,
margin_spiral_risk,
repo_multiplier,
margin_spiral_simulation,
)
from fqbm.sheets.ccp import (
ccp_identity,
default_waterfall_triggered,
variation_margin_flow,
ccp_clearing_simulation,
)
from fqbm.sheets.cbdc import deposit_to_cbdc_shift, funding_gap
def test_leverage_ratio():
assert leverage_ratio(1000, 100) == 10.0
assert leverage_ratio(500, 0) == 0.0
def test_margin_spiral_risk():
r = margin_spiral_risk(100, 80, 0.1)
assert "margin_call_triggered" in r
assert "shortfall" in r
assert r["shortfall"] >= 0
def test_ccp_identity():
assert ccp_identity(100, 100) is True
assert ccp_identity(100, 99) is False
def test_default_waterfall_triggered():
assert default_waterfall_triggered(150, 100) is True
assert default_waterfall_triggered(50, 100) is False
def test_deposit_to_cbdc_shift():
d = deposit_to_cbdc_shift(50)
assert d["d_deposits"] == -50
assert d["d_reserves"] == -50
assert d["d_cbdc_liability"] == 50
def test_funding_gap():
assert funding_gap(100, 60) == 40
assert funding_gap(50, 80) == -30
def test_repo_multiplier():
r = repo_multiplier(100.0, haircut=0.02, rounds=3)
assert r["total_effective_collateral"] > 100
assert r["multiplier_implied"] > 1
assert r["rounds_used"] == 3
r0 = repo_multiplier(0, rounds=2)
assert r0["total_effective_collateral"] == 0
def test_margin_spiral_simulation():
r = margin_spiral_simulation(
initial_collateral=100,
margin_requirement=90,
haircut=0.1,
liquidity_buffer=5,
fire_sale_impact=0.2,
max_rounds=10,
)
assert "path_collateral" in r
assert "path_margin_calls" in r
assert "cumulative_forced_sales" in r
assert "waterfall_triggered" in r
assert len(r["path_collateral"]) >= 1
def test_variation_margin_flow():
vm = variation_margin_flow(-10.0, member_pays_when_positive=True)
assert vm == pytest.approx(10.0)
vm2 = variation_margin_flow(5.0, member_pays_when_positive=True)
assert vm2 == pytest.approx(-5.0)
def test_ccp_clearing_simulation():
results = ccp_clearing_simulation(
vm_calls_per_period=[5, 10, 15],
liquidity_buffer_start=20,
)
assert len(results) == 3
assert results[0]["buffer_end"] == 15
assert results[1]["buffer_end"] == 5
assert results[2]["waterfall_triggered"] is True

65
tests/test_workbook.py Normal file
View File

@@ -0,0 +1,65 @@
"""Tests for workbook runner and dashboard."""
import os
import tempfile
from fqbm.state import FQBMState
from fqbm.workbook.runner import run_workbook
from fqbm.sheets.dashboard import dashboard_aggregate
from fqbm.sheets.monte_carlo import run_n_simulations, ShockSpec
def test_run_workbook():
state = FQBMState(R=200, Deposits=1000, Loans=900, E_b=100)
result = run_workbook(initial_state=state, mc_runs=5)
assert "state" in result
assert "stress" in result
assert "dashboard" in result
assert "liquidity_stress" in result["stress"]
assert "capital_stress" in result["stress"]
assert "ratios" in result["dashboard"]
def test_dashboard_aggregate():
state = FQBMState(R=100, Deposits=500, Loans=400, E_b=80)
dash = dashboard_aggregate(state, mc_runs=3, shock_spec=ShockSpec(seed=42))
assert "state" in dash
assert "ratios" in dash
assert "mc_summary" in dash
assert "p_insolvency" in dash["mc_summary"]
def test_run_n_simulations():
df = run_n_simulations(20, shock_spec=ShockSpec(seed=1))
assert len(df) == 20
assert "insolvent" in df.columns
assert "reserve_breach" in df.columns
assert "inflation" in df.columns
assert "debt_sustainable" in df.columns
def test_run_workbook_with_cbdc():
from fqbm.sheets.cbdc import CBDCParams
state = FQBMState(R=100, Deposits=500, Loans=400, E_b=80)
result = run_workbook(initial_state=state, cbdc_params=CBDCParams(deposit_shift=10))
assert result["state"].Deposits == 490
assert result["state"].R == 90
assert result["cbdc"] is not None
assert result["cbdc"]["cbdc_liability"] == 10
def test_run_workbook_with_ccp():
from fqbm.sheets.ccp import CCPParams
result = run_workbook(ccp_params=CCPParams(margin_posted=100, margin_obligations=100, vm_calls=5, liquidity_buffer=10))
assert result["ccp"] is not None
assert result["ccp"]["ccp_identity_holds"] is True
assert result["ccp"]["waterfall_triggered"] is False
def test_workbook_excel_export():
with tempfile.TemporaryDirectory() as tmp:
path = os.path.join(tmp, "fqbm_out.xlsx")
state = FQBMState(R=100, Deposits=500, Loans=400, E_b=80)
run_workbook(initial_state=state, mc_runs=3, export_path=path)
assert os.path.isfile(path)
assert os.path.getsize(path) > 0