Initial commit: Four-Quadrant Balance Sheet Matrix (FQBM) framework
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
1
tests/__init__.py
Normal file
1
tests/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
# FQBM tests
|
||||
24
tests/test_capital_stress.py
Normal file
24
tests/test_capital_stress.py
Normal 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
|
||||
23
tests/test_central_bank.py
Normal file
23
tests/test_central_bank.py
Normal 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
|
||||
31
tests/test_commercial_bank.py
Normal file
31
tests/test_commercial_bank.py
Normal 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
32
tests/test_commodity.py
Normal 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)
|
||||
30
tests/test_differential_model.py
Normal file
30
tests/test_differential_model.py
Normal 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
42
tests/test_fx_parity.py
Normal file
@@ -0,0 +1,42 @@
|
||||
"""Tests for Sheet 4: FX parity (Part III–IV)."""
|
||||
|
||||
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
111
tests/test_ipsas.py
Normal 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
32
tests/test_matrix.py
Normal 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
33
tests/test_regressions.py
Normal 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
29
tests/test_scenarios.py
Normal 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
|
||||
31
tests/test_sovereign_debt.py
Normal file
31
tests/test_sovereign_debt.py
Normal 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)
|
||||
61
tests/test_state_and_ipsas_remnants.py
Normal file
61
tests/test_state_and_ipsas_remnants.py
Normal 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
93
tests/test_stubs.py
Normal 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
65
tests/test_workbook.py
Normal 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
|
||||
Reference in New Issue
Block a user