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

39
.gitignore vendored Normal file
View File

@@ -0,0 +1,39 @@
# Bytecode
__pycache__/
*.py[cod]
*$py.class
*.so
# Virtual env
.venv/
venv/
env/
# IDE
.idea/
.vscode/
*.swp
*.swo
# Build / dist
build/
dist/
*.egg-info/
*.egg
# Testing
.pytest_cache/
.coverage
htmlcov/
# Data (optional)
data/*.csv
data/*.xlsx
!data/.gitkeep
# Jupyter
.ipynb_checkpoints/
# OS
.DS_Store
Thumbs.db

52
README.md Normal file
View File

@@ -0,0 +1,52 @@
# Four-Quadrant Balance Sheet Matrix (FQBM)
A unified institutional framework for monetary, fiscal, financial, and open-economy dynamics. This codebase implements the full FQBM from the white paper: simulation workbook (eight sheets), Monte Carlo stress engine, differential system model, and empirical regression suite.
## Install
```bash
cd FOUR-QUADRANT_BALANCE_SHEET_MATRIX
python3 -m venv .venv
source .venv/bin/activate # Windows: .venv\Scripts\activate
pip install -e .
```
Optional: `pip install pytest` for tests.
## Run
- **Workbook (all sheets)**:
`python -m fqbm.workbook.runner`
Or from code: `from fqbm.workbook.runner import run_workbook` then `run_workbook(initial_state=..., mc_runs=100, export_path="out.xlsx")`.
- **Monte Carlo**: `from fqbm.sheets.monte_carlo import run_n_simulations`
- **Differential model**: `from fqbm.system.differential_model import solve_trajectory, check_stability`
- **Regressions**: `from fqbm.empirical.regressions import run_inflation_pass_through, run_sovereign_spread, run_capital_flow_sensitivity`
## Test
```bash
pytest tests/ -v
```
## Optional: Streamlit dashboard
```bash
pip install streamlit
streamlit run scripts/streamlit_dashboard.py
```
## Optional: Historical scenarios (Part XI)
Use preset scenarios in code: `run_workbook(scenario="asia_1997")` or `"gfc_2008"`, `"pandemic_2020"`, `"rate_shock_2022"`. See `fqbm.scenarios`.
## References
See [docs/REFERENCES.md](docs/REFERENCES.md) for Chicago AuthorDate references (Adrian & Shin, BIS, Dornbusch, Fed H.4.1, IMF, McLeay et al., Obstfeld & Rogoff, etc.).
## IPSAS compliance
The framework includes an **IPSAS-aligned presentation layer** (IPSAS 1 statement of financial position, IPSAS 24 budget vs actual structure). Use `fqbm.ipsas.presentation.statement_of_financial_position(state, entity="central_bank"|"commercial_bank"|"consolidated")` and `budget_vs_actual_structure()`. Excel export adds IPSAS-style sheets when available. See [docs/IPSAS_COMPLIANCE.md](docs/IPSAS_COMPLIANCE.md) for the compliance assessment. **[docs/GAPS_AND_MISSING.md](docs/GAPS_AND_MISSING.md)** lists IPSAS standards and FQBM gaps. Additional helpers: **notes_to_financial_statements_structure**, **statement_of_financial_position_comparative**, **maturity_risk_disclosure_structure**, **cash_flow_from_state_changes**, **fx_translate**. Part VII: **repo_multiplier**, **margin_spiral_simulation**. Part VIII: **variation_margin_flow**, **ccp_clearing_simulation**.
## Framework
See [docs/framework_summary.md](docs/framework_summary.md) and the white paper for the theoretical foundation (Parts IXVI).

1
data/.gitkeep Normal file
View File

@@ -0,0 +1 @@

152
docs/GAPS_AND_MISSING.md Normal file
View File

@@ -0,0 +1,152 @@
# Complete IPSAS and FQBM Matrix — Gaps and Missing
This document lists **all remaining IPSAS standards** and **all FQBM/Matrix gaps and missing elements** as of the current codebase. It extends [IPSAS_COMPLIANCE.md](IPSAS_COMPLIANCE.md).
---
## Part A — IPSAS: Full standards list and status
| # | Standard | Status | Gap / note |
|---|----------|--------|-------------|
| **CF** | Conceptual Framework | Partial | A = L + E aligned; no revenue/expense/definition of surplus. |
| **1** | Presentation of Financial Statements | Supported | Structure + current/non-current in `fqbm.ipsas.presentation`. |
| **2** | Cash Flow Statements | Structure only | Template in `cash_flow_statement_structure()`; no activity classification from FQBM. |
| **3** | Accounting Policies, Changes in Estimates and Errors | **Missing** | No policy note, prior-period adjustment, or error correction. |
| **4** | Effects of Changes in Foreign Exchange Rates | Partial | S and pass-through only; no translation of FC assets/liabilities, no FX gain/loss disclosure. |
| **5** | Borrowing Costs | **Missing** | No borrowing-cost capitalisation or expense classification. |
| **6** | Consolidated and Separate Financial Statements | Partial | Sectoral (CB, bank) and consolidated view in presentation; no control definition or full consolidation mechanics. |
| **7** | Investments in Associates | **Missing** | Not applicable to core FQBM; no equity method. |
| **8** | Interests in Joint Ventures | **Missing** | Not applicable; no joint arrangement. |
| **9** | Revenue from Exchange Transactions | **Missing** | No revenue recognition or measurement. |
| **10** | Financial Reporting in Hyperinflationary Economies | **Missing** | No hyperinflation restatement (FQBM has inflation π but not restatement). |
| **11** | Construction Contracts | **Missing** | Not applicable to monetary/financial core. |
| **12** | Inventories | **Missing** | Not applicable. |
| **13** | Leases | **Missing** | Not applicable to core; no right-of-use asset/lease liability. |
| **14** | Events After the Reporting Date | **Missing** | No adjustment vs disclosure classification. |
| **15** | Financial Instruments: Disclosure and Presentation | Partial | Presentation only; no disclosure (maturity, risk). |
| **16** | Investment Property | **Missing** | Not applicable. |
| **17** | Property, Plant, and Equipment | **Missing** | Not applicable; no PP&E or depreciation. |
| **18** | Segment Reporting | **Missing** | No segment definition or segment P&L. |
| **19** | Provisions, Contingent Liabilities and Contingent Assets | **Missing** | No provisions or contingencies. |
| **20** | Related Party Disclosures | **Missing** | No related party identification or disclosure. |
| **21** | Impairment of Non-Cash-Generating Assets | **Missing** | Not applicable to financial core. |
| **22** | Disclosure of Financial Information About the General Government Sector | Partial | Sectoral structure supported; GGS boundary and entity list external. |
| **23** | Revenue from Non-Exchange Transactions | **Missing** | No tax/transfer revenue. |
| **24** | Presentation of Budget Information in Financial Statements | Structure only | Template in `budget_vs_actual_structure()`; budget data external. |
| **25** | Employee Benefits | **Missing** | Not applicable to core. |
| **26** | Impairment of Cash-Generating Assets | **Missing** | No impairment model. |
| **27** | Agriculture | **Missing** | Not applicable. |
| **28** | Financial Instruments: Presentation | Partial | Asset/liability split in statement of financial position. |
| **29** | Financial Instruments: Recognition and Measurement | **Missing** | No measurement basis (amortised cost, FVOCI, FVPL) or ECL. |
| **30** | Financial Instruments: Disclosures | **Missing** | No risk or maturity disclosure. |
| **31** | Intangible Assets | **Missing** | Not applicable. |
| **32** | Service Concession Arrangements: Grantor | **Missing** | Not applicable. |
| **33** | First-Time Adoption of Accrual Basis IPSAS | **Missing** | Not applicable (no full IPSAS adoption). |
| **34** | Separate Financial Statements | Partial | Entity-level (CB, bank) supported. |
| **35** | Consolidated Financial Statements | Partial | Consolidated layout; no full consolidation rules. |
| **36** | Investments in Associates and Joint Ventures | **Missing** | Not applicable. |
| **37** | Joint Arrangements | **Missing** | Not applicable. |
| **38** | Disclosure of Interests in Other Entities | **Missing** | No disclosure. |
| **39** | Employee Benefits | **Missing** | Not applicable. |
| **40** | Public Sector Combinations | **Missing** | Not applicable. |
| **41** | Financial Instruments | **Missing** | No classification, measurement, or ECL. |
| **42** | Social Benefits | **Missing** | Not applicable. |
| **43** | Leases | **Missing** | Not applicable. |
| **44** | Non-Current Assets Held for Sale and Discontinued Operations | **Missing** | Not applicable. |
| **45** | Property, Plant, and Equipment | **Missing** | Not applicable. |
| **46** | Measurement | **Missing** | No measurement basis (historical, FV, etc.). |
| **47** | Revenue | **Missing** | No revenue model. |
| **48** | Transfer Expenses | **Missing** | No transfer expense. |
| **49** | Retirement Benefit Plans | **Missing** | Not applicable. |
| **50** | Exploration for and Evaluation of Mineral Resources | **Missing** | Not applicable. |
| **Cash basis** | Financial Reporting Under the Cash Basis | **Missing** | No cash-basis statement. |
| **RPG 1** | Long-Term Sustainability of an Entity's Finances | Partial | Debt sustainability (rg) in sovereign module; no full RPG 1 disclosure. |
| **RPG 2** | Financial Statement Discussion and Analysis | **Missing** | No narrative. |
| **RPG 3** | Reporting Service Performance Information | **Missing** | Not applicable. |
| **SRS 1** | Climate-related Disclosures | **Missing** | Not applicable. |
---
## Part B — FQBM / Four-Quadrant Matrix: Gaps and missing
### B.1 State vector and identities
| Item | Status | Note |
|------|--------|------|
| **L_cb (central bank loans)** | **Added** | `FQBMState.L_cb` (default 0); central_bank_step updates L_cb; matrix uses state.L_cb. |
| **Open-economy split A_dom / A_ext, L_dom / L_ext** | **Added** | `open_economy_view(A_dom, A_ext, L_dom, L_ext, E)` in `fqbm.state` returns identity check and totals. |
| **Reporting period / reporting date** | **Added** | `FQBMState.reporting_date` (optional); `statement_of_financial_position(..., reporting_date=)` uses state.reporting_date when not passed. |
### B.2 Four-quadrant matrix (Part I)
| Item | Status | Note |
|------|--------|------|
| **Explicit four-quadrant layout** | **Added** | `fqbm.matrix.four_quadrant_matrix(state)` and `four_quadrant_summary(state)` return the 4-column matrix and identity check. |
| **Quadrant balance checks** | **Added** | Summary includes `identity_A_eq_L_plus_E`. |
| **Cross-sector consistency** | Partial | CB and bank identities implemented; no automatic check that matrix totals equal sectoral sums. |
### B.3 White paper parts not implemented
| Part | Topic | Status | Gap |
|------|--------|--------|-----|
| **VII** | Shadow banking and leverage | **Integrated** | `leverage_ratio()` in dashboard ratios; `margin_spiral_risk()` available; repo multiplier not modeled. |
| **VIII** | Derivatives clearing and CCP | **Integrated** | `run_workbook(ccp_params=CCPParams(...))` returns `result["ccp"]` with identity and waterfall flag. |
| **IX** | CBDC and digital reserve architecture | **Integrated** | `run_workbook(cbdc_params=CBDCParams(...))` applies deposit/reserve shift; `result["cbdc"]` has cbdc_liability. |
| **XI** | Historical case expansion | **Partial** | Presets (1997, 2008, 2020, 2022) in scenarios; no narrative or coded “case” outputs. |
### B.4 IPSAS presentation structures added (templates only where no FQBM data)
| Item | Status | Location |
|------|--------|----------|
| Statement of financial position (IPSAS 1) | Supported | `statement_of_financial_position()` |
| Budget vs actual (IPSAS 24) | Structure | `budget_vs_actual_structure()` |
| Cash flow statement (IPSAS 2) | Structure | `cash_flow_statement_structure()` |
| Statement of financial performance | Structure | `statement_of_financial_performance_structure()` |
| Four-quadrant matrix | Supported | `fqbm.matrix.four_quadrant_matrix()` |
### B.5 Other missing
| Item | Status |
|------|--------|
| **Statement of changes in net assets/equity** | **Added** | `statement_of_changes_in_net_assets_structure()`. |
| **Notes to the financial statements** | **Added** | `notes_to_financial_statements_structure()`. |
| **Comparative period** | **Added** | `statement_of_financial_position_comparative(state_prior, state_current, entity)`. |
| **Functional currency / presentation currency** | **Missing**. |
| **Financial instrument maturity breakdown** | **Added** | `maturity_risk_disclosure_structure()` (maturity buckets, interest rate +100bp, credit exposure, ECL). |
| **Interest rate sensitivity** | **Partial** | Column in maturity_risk_disclosure_structure. |
| **Credit risk / ECL disclosure** | **Partial** | Columns in maturity_risk_disclosure_structure. |
| **FX translation of FC positions** | **Added** | `fx_translate(fc_amount, S_prev, S_curr)`. |
| **Cash flow from balance sheet changes** | **Added** | `cash_flow_from_state_changes(state_prev, state_curr)`. |
---
## Part C — Implementation checklist (done vs to do)
### Done
- IPSAS 1 statement of financial position (CB, bank, consolidated); uses state.L_cb and state.reporting_date.
- IPSAS 24 budget vs actual template.
- IPSAS 2 cash flow statement template; **cash_flow_from_state_changes(state_prev, state_curr)** infers amounts from Δstate.
- Statement of financial performance template.
- **Statement of changes in net assets** template.
- **FX translation**: **fx_translate(fc_amount, S_prev, S_curr)**.
- Four-quadrant matrix and summary; **L_cb** from state.
- **L_cb in state**: `FQBMState.L_cb` and `reporting_date`; central_bank_step updates L_cb; differential model 12-element vector.
- **Open economy**: **open_economy_view(A_dom, A_ext, L_dom, L_ext, E)**.
- **Part VII**: Leverage ratio in dashboard; **run_workbook** exposes shadow metrics.
- **Part VIII**: **run_workbook(ccp_params=CCPParams(...))** returns result["ccp"].
- **Part IX**: **run_workbook(cbdc_params=CBDCParams(...))** applies deposit/reserve shift; result["cbdc"].
### Optional work (completed)
1. **Part VII**: **repo_multiplier(initial_collateral, haircut, rounds)** and **margin_spiral_simulation(...)** in `fqbm.sheets.shadow_banking`.
2. **Part VIII**: **variation_margin_flow(mark_to_market_change)** and **ccp_clearing_simulation(vm_calls_per_period, liquidity_buffer_start)** in `fqbm.sheets.ccp`.
3. **IPSAS**: **notes_to_financial_statements_structure()**; **statement_of_financial_position_comparative(state_prior, state_current, entity)**; **maturity_risk_disclosure_structure()** in `fqbm.ipsas.presentation`.
---
## References
- [IPSAS_COMPLIANCE.md](IPSAS_COMPLIANCE.md) — Scope and how to use the IPSAS layer.
- IPSASB (2025). *Handbook of International Public Sector Accounting Pronouncements*. IFAC.
- White paper: Four-Quadrant Balance Sheet Matrix, Parts IXVI.

144
docs/IPSAS_COMPLIANCE.md Normal file
View File

@@ -0,0 +1,144 @@
# IPSAS Compliance Assessment — FQBM Framework
This document assesses the Four-Quadrant Balance Sheet Matrix (FQBM) against **International Public Sector Accounting Standards (IPSAS)** issued by the IPSASB. The FQBM is a macroeconomic and institutional simulation framework; IPSAS applies to **entity-level general purpose financial reporting** by public sector entities. The assessment clarifies scope, alignment, and gaps.
---
## 1. Scope and applicability
| Aspect | FQBM | IPSAS |
|--------|------|--------|
| **Primary purpose** | Monetary/fiscal simulation, stress testing, open-economy dynamics | Entity financial statements (audit-ready, general purpose) |
| **Unit of account** | Sectoral balance sheets (CB, banks, sovereign), state vector **X** | Single public sector entity or consolidated government |
| **Basis** | Identities (A = L + E), differentials, stochastic shocks | Accrual (or cash) basis, recognition, measurement, presentation |
| **Output** | Time paths, ratios, stress tables, Monte Carlo distributions | Statement of financial position, statement of financial performance, cash flow statement, budget comparison |
**Conclusion**: FQBM supports **analysis that can feed into** IPSAS-consistent reporting (e.g. central bank or government balance sheet layout, budget vs actual structure). It is not a substitute for full IPSAS-compliant financial statements. Compliance is assessed for **presentation and disclosure structures** that can be derived from or aligned with FQBM outputs.
---
## 2. IPSAS standards relevant to FQBM
| Standard | Relevance | FQBM alignment / gap |
|----------|-----------|------------------------|
| **IPSAS 1** — Presentation of Financial Statements | Statement of financial position (balance sheet) structure, current/non-current classification, minimum line items | **Partial**: We provide an IPSAS 1-style layout (current/non-current) for central bank and commercial bank from state **X**. Minimum line items satisfied where FQBM has data (financial assets, cash/reserves, liabilities, net assets). |
| **IPSAS 2** — Cash Flow Statements | Operating, investing, financing cash flows | **Gap**: FQBM does not model cash flows by activity. Cash flows can be inferred from balance sheet changes (e.g. ΔR, ΔC) but not classified by IPSAS 2 categories. |
| **IPSAS 4** — Effects of Changes in Foreign Exchange Rates | FX translation, functional currency, presentation of FX differences | **Partial**: FQBM has exchange rate **S** and FX pass-through; no explicit translation of foreign-currency assets/liabilities or disclosure of FX gains/losses. |
| **IPSAS 15 / 28 / 29 / 41** — Financial Instruments | Classification, measurement, disclosure of financial assets/liabilities | **Partial**: State variables (B, R, Loans, Deposits) map to financial instruments; no classification (amortised cost vs FVOCI etc.) or impairment model. |
| **IPSAS 22** — Disclosure of Financial Information About the General Government Sector | Sectoral disaggregation (e.g. general government) | **Partial**: FQBM distinguishes CB, banks, sovereign; can support GGS-style disclosure structure if sector definitions are aligned. |
| **IPSAS 24** — Presentation of Budget Information in Financial Statements | Budget vs actual comparison, material variances, comparable basis | **Gap**: No budget or appropriation model in FQBM. We provide a **structure** (budget, actual, variance) for users to populate from external budget data. |
| **Conceptual Framework** | Definitions of assets, liabilities, net assets, revenue, expense | **Partial**: A = L + E and four-quadrant identity align with “assets” and “liabilities”; no revenue/expense or surplus/deficit from operations. |
---
## 3. Current/non-current classification (IPSAS 1)
IPSAS 1 requires **current** and **non-current** classification for assets and liabilities unless a liquidity presentation is more appropriate.
**Central bank (from FQBM state)**:
| IPSAS 1-style line item | FQBM variable | Suggested classification | Notes |
|-------------------------|---------------|---------------------------|--------|
| Financial assets (at amortised cost / FV) | B (government bonds), L_cb (loans) | Non-current (hold-to-maturity / policy portfolio) or split by maturity | L_cb not in state; can be added. |
| Cash and cash equivalents / Reserves | R | Current | |
| Currency in circulation | C | Liability (current) | |
| Liabilities (reserve accounts) | R (bank reserves) | Current | |
| Net assets / Equity | E_cb | — | |
**Commercial bank**:
| IPSAS 1-style line item | FQBM variable | Suggested classification |
|-------------------------|----------------|---------------------------|
| Loans and receivables | Loans | Current / non-current by behavioural maturity if available |
| Cash and balances at central bank | R (if attributed to bank) | Current |
| Deposits from customers | Deposits | Current |
| Net assets / Equity | E_b | — |
The module `fqbm.ipsas.presentation` produces statement-of-financial-position layouts with these classifications where applicable.
---
## 4. Budget vs actual (IPSAS 24)
IPSAS 24 requires comparison of **approved budget** (original/final) with **actual** amounts on a comparable basis, with explanations of material variances.
**FQBM**: No budget or appropriation data. We provide:
- A **template** for budget vs actual: line item, original budget, final budget, actual, variance, material (e.g. >10%).
- **Actual** amounts can be filled from FQBM state (e.g. total assets, total liabilities, key aggregates). **Budget** columns must be supplied by the reporting entity.
See `fqbm.ipsas.presentation.budget_vs_actual_structure()`.
---
## 5. Financial instruments (IPSAS 15, 28, 29, 41)
- **Presentation**: Financial assets vs liabilities, and (where applicable) current vs non-current — supported by the IPSAS 1-style layout.
- **Recognition and measurement**: FQBM does not implement measurement bases (amortised cost, FVOCI, FVPL) or impairment (e.g. ECL). **Gap** for full IPSAS 41 compliance.
- **Disclosure**: No maturity breakdown, interest rate sensitivity, or credit risk disclosure. **Gap**; can be extended with user-supplied data.
---
## 6. Foreign exchange (IPSAS 4)
- FQBM has **S** (exchange rate) and **FX pass-through** (e.g. π_import = β ΔS). Useful for **analysis** of FX effects on inflation and balance sheets.
- **Gap**: No explicit translation of foreign-currency-denominated assets/liabilities, or separate disclosure of FX gains/losses in a performance statement.
---
## 7. Consolidation and general government (IPSAS 6, 22, 35)
- FQBM **A_dom + A_ext = L_dom + L_ext + E** and sectoral breakdown (CB, banks, sovereign) support **consolidation logic** and sectoral views.
- **IPSAS 22** (general government sector disclosure): FQBM can feed sectoral totals; exact GGS boundary and entity list must be defined by the reporting jurisdiction.
---
## 8. Compliance checklist (summary)
| Requirement | Status | Notes |
|-------------|--------|--------|
| Statement of financial position (IPSAS 1) structure | **Supported** | Via `fqbm.ipsas.presentation.statement_of_financial_position()` |
| Current/non-current classification | **Supported** | Applied in presentation layer for CB and bank |
| Minimum line items (IPSAS 1) | **Partial** | Satisfied for items present in FQBM; no PP&E, inventories, etc. |
| Cash flow statement (IPSAS 2) | **Not supported** | No activity classification of cash flows |
| Budget vs actual (IPSAS 24) | **Structure only** | Template provided; budget data external |
| Financial instrument measurement (IPSAS 41) | **Not supported** | No measurement basis or ECL |
| FX translation (IPSAS 4) | **Partial** | Exchange rate and pass-through only |
| Disclosure of GGS (IPSAS 22) | **Partial** | Sectoral structure supported |
| Reporting period / reporting date | **Not defined** | FQBM is period-agnostic; user must set |
---
## 9. How to use the IPSAS layer in this codebase
1. **Run the workbook** (or differential model) to obtain **state** (and optional stress, MC).
2. **Call** `fqbm.ipsas.presentation.statement_of_financial_position(state, entity="central_bank")` (or `"commercial_bank"`, `"consolidated"`) to get an IPSAS 1-style layout.
3. **Call** `fqbm.ipsas.presentation.budget_vs_actual_structure()` to get an empty DataFrame/template for IPSAS 24; fill budget from external source, actual from state where applicable.
4. **Export** presentation outputs to Excel or include in reports alongside FQBM dashboards.
For **full IPSAS compliance** at entity level, the reporting entity must:
- Maintain recognition and measurement in line with IPSAS (e.g. IPSAS 41, 46).
- Prepare cash flow statement (IPSAS 2) and statement of financial performance.
- Populate budget information (IPSAS 24) and provide variance analysis.
- Apply IPSAS 4 for FX and other standards as applicable.
The FQBM IPSAS layer supports **presentation consistency** and **structural alignment** with IPSAS 1 and IPSAS 24, not end-to-end compliance by itself.
---
## 10. References
- IPSASB (2025). *Handbook of International Public Sector Accounting Pronouncements*. IFAC.
- IPSAS 1, *Presentation of Financial Statements*.
- IPSAS 2, *Cash Flow Statements*.
- IPSAS 4, *The Effects of Changes in Foreign Exchange Rates*.
- IPSAS 24, *Presentation of Budget Information in Financial Statements*.
- IPSAS 22, *Disclosure of Financial Information About the General Government Sector*.
- IPSASB Conceptual Framework, *General Purpose Financial Reporting by Public Sector Entities*.
---
## 11. Complete IPSAS and Matrix gaps
For a **full list of all remaining IPSAS standards** (150, cash basis, RPGs, Conceptual Framework) with status (supported / partial / missing) and **all FQBM/Matrix gaps** (L_cb, open-economy split, Parts VIIIX, reporting period, etc.), see **[GAPS_AND_MISSING.md](GAPS_AND_MISSING.md)**.

17
docs/REFERENCES.md Normal file
View File

@@ -0,0 +1,17 @@
# References (Chicago AuthorDate)
Adrian, Tobias, and Hyun Song Shin. 2010. "Liquidity and Leverage." *Journal of Financial Intermediation*.
Bank for International Settlements (BIS). 2017. *Basel III: Finalising Post-Crisis Reforms*.
Borio, Claudio. 2014. "The Financial Cycle and Macroeconomics." *BIS Working Papers*.
Dornbusch, Rudiger. 1976. "Expectations and Exchange Rate Dynamics." *Journal of Political Economy*.
Federal Reserve. 20092023. *H.4.1 Statistical Release*.
International Monetary Fund (IMF). 2014. *Government Finance Statistics Manual*.
McLeay, Michael, Amar Radia, and Ryland Thomas. 2014. "Money Creation in the Modern Economy." *Bank of England Quarterly Bulletin*.
Obstfeld, Maurice, and Kenneth Rogoff. 1995. *Foundations of International Macroeconomics*.

37
docs/framework_summary.md Normal file
View File

@@ -0,0 +1,37 @@
# FQBM Framework Summary
## Core identity (Part I)
- **Accounting**: \( A = L + E \)
- **Open economy**: \( A_{dom} + A_{ext} = L_{dom} + L_{ext} + E \)
Every financial asset is someone elses liability (within or across borders).
## Four-quadrant matrix
| Assets (Dr) | Assets (Cr) | Liabilities (Dr) | Liabilities (Cr) |
All monetary operations must balance across this structure.
## Parts of the white paper
| Part | Topic |
|------|--------|
| I | Accounting foundation |
| II | Closed-economy monetary dynamics (CB, commercial bank) |
| III | Open-economy extension (BoP, parity) |
| IV | Capital flows and FX dynamics |
| V | Sovereign risk and CDS |
| VI | Commodity shock channel |
| VII | Shadow banking and leverage |
| VIII | Derivatives clearing and CCP |
| IX | CBDC and digital reserve architecture |
| X | Empirical regression appendix |
| XI | Historical case expansion |
| XII | Quantitative stress tables |
| XIII | Monte Carlo simulation framework |
| XIV | Full system differential model |
| XV | Policy implications |
| XVI | Simulation workbook architecture (eight sheets) |
This codebase implements the simulation workbook (Part XVI), Monte Carlo (XIII), differential model (XIV), and empirical regressions (Part X).

BIN
fqbm_workbook.xlsx Normal file

Binary file not shown.

28
pyproject.toml Normal file
View File

@@ -0,0 +1,28 @@
[build-system]
requires = ["setuptools>=61", "wheel"]
build-backend = "setuptools.build_meta"
[project]
name = "fqbm"
version = "0.1.0"
description = "Four-Quadrant Balance Sheet Matrix — institutional framework for monetary, fiscal, financial, and open-economy dynamics"
readme = "README.md"
requires-python = ">=3.10"
dependencies = [
"numpy>=1.24",
"scipy>=1.10",
"pandas>=2.0",
"statsmodels>=0.14",
"openpyxl>=3.1",
"xlsxwriter>=3.1",
]
[project.optional-dependencies]
dashboard = ["streamlit>=1.28"]
dev = ["pytest>=7.0"]
[tool.setuptools.packages.find]
where = ["src"]
[project.scripts]
fqbm-run = "fqbm.workbook.runner:main"

15
requirements.txt Normal file
View File

@@ -0,0 +1,15 @@
# FQBM — Four-Quadrant Balance Sheet Matrix
# Python 3.10+
numpy>=1.24
scipy>=1.10
pandas>=2.0
statsmodels>=0.14
# Optional: workbook export
openpyxl>=3.1
xlsxwriter>=3.1
# Optional: interactive dashboard
# streamlit>=1.28
# dash>=2.14

View File

@@ -0,0 +1,60 @@
"""
Optional Streamlit dashboard for FQBM (Sheet 8 + workbook).
Run: streamlit run scripts/streamlit_dashboard.py
Requires: pip install streamlit
"""
import streamlit as st
from fqbm.state import FQBMState
from fqbm.workbook.runner import run_workbook
from fqbm.scenarios import list_scenarios, get_scenario
st.set_page_config(page_title="FQBM Dashboard", layout="wide")
st.title("Four-Quadrant Balance Sheet Matrix — Dashboard")
scenario_names = ["(custom)"] + list_scenarios()
scenario = st.sidebar.selectbox("Scenario (Part XI)", scenario_names)
mc_runs = st.sidebar.slider("Monte Carlo runs", 0, 500, 100)
run_btn = st.sidebar.button("Run workbook")
if run_btn:
with st.spinner("Running workbook..."):
if scenario == "(custom)":
state = FQBMState(R=200, Deposits=1000, Loans=900, E_b=100)
st.session_state["result"] = run_workbook(initial_state=state, mc_runs=mc_runs)
else:
st.session_state["result"] = run_workbook(scenario=scenario, mc_runs=mc_runs)
st.session_state["scenario_used"] = scenario
result = st.session_state.get("result")
if not result:
st.info("Select a scenario and click **Run workbook** in the sidebar to run the full FQBM workbook.")
st.stop()
state = result["state"]
dashboard = result["dashboard"]
st.subheader("State snapshot")
cols = st.columns(4)
names = ["B", "R", "C", "Loans", "Deposits", "E_cb", "E_b", "S", "K", "Spread", "O"]
vals = state.to_vector()
for i, (n, v) in enumerate(zip(names, vals)):
cols[i % 4].metric(n, f"{v:,.1f}")
st.subheader("Key ratios")
ratios = dashboard.get("ratios", {})
for k, v in ratios.items():
st.write(f"**{k}**: {v:.4f}")
st.subheader("Stress tables")
stress = result["stress"]
st.dataframe(stress["liquidity_stress"], use_container_width=True)
st.dataframe(stress["capital_stress"], use_container_width=True)
if dashboard.get("mc_summary"):
st.subheader("Monte Carlo summary")
ms = dashboard["mc_summary"]
st.write(f"P(insolvency): {ms.get('p_insolvency', 0):.2%}")
st.write(f"P(reserve breach): {ms.get('p_reserve_breach', 0):.2%}")
st.write(f"Inflation (mean ± std): {ms.get('inflation_mean', 0):.4f} ± {ms.get('inflation_std', 0):.4f}")
st.write(f"P(debt unsustainable): {ms.get('p_debt_unsustainable', 0):.2%}")

9
src/fqbm/__init__.py Normal file
View File

@@ -0,0 +1,9 @@
"""
FQBM — Four-Quadrant Balance Sheet Matrix.
Unified institutional framework for monetary, fiscal, financial, and open-economy
dynamics (Parts IXVI). Exposes workbook sheets, Monte Carlo engine, differential
model, and empirical regressions.
"""
__version__ = "0.1.0"

View File

@@ -0,0 +1,19 @@
"""Part X: Empirical regression appendix — pass-through, sovereign spread, capital flow sensitivity."""
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,
)
__all__ = [
"run_inflation_pass_through",
"run_sovereign_spread",
"run_capital_flow_sensitivity",
"generate_synthetic_inflation",
"generate_synthetic_spread",
"generate_synthetic_capital_flow",
]

View File

@@ -0,0 +1,84 @@
"""
Part X: Empirical regression appendix.
Model 1: π_t = α + β ΔS_t + γ OutputGap_t + ε_t (inflation pass-through).
Model 2: Spread_t = α + β (Debt/GDP)_t + γ Reserves/GDP_t + δ FXVol_t + ε_t.
Model 3: ΔK_t = θ (i_d - i_f)_t ω RiskPremium_t + ε_t (capital flow sensitivity).
"""
from typing import Optional
import numpy as np
import pandas as pd
import statsmodels.api as sm
def generate_synthetic_inflation(n: int = 100, seed: Optional[int] = None) -> pd.DataFrame:
"""Synthetic data for Model 1. Columns: pi, dS, output_gap."""
rng = np.random.default_rng(seed)
dS = rng.normal(0, 0.05, n)
output_gap = rng.normal(0, 0.02, n)
pi = 0.02 + 0.2 * dS + 0.3 * output_gap + rng.normal(0, 0.005, n)
return pd.DataFrame({"pi": pi, "dS": dS, "output_gap": output_gap})
def run_inflation_pass_through(
data: Optional[pd.DataFrame] = None,
) -> sm.regression.linear_model.RegressionResultsWrapper:
"""
Model 1: π_t = α + β ΔS_t + γ OutputGap_t + ε_t.
Data must have columns: pi, dS, output_gap.
"""
if data is None:
data = generate_synthetic_inflation()
X = sm.add_constant(data[["dS", "output_gap"]])
y = data["pi"]
return sm.OLS(y, X).fit()
def generate_synthetic_spread(n: int = 100, seed: Optional[int] = None) -> pd.DataFrame:
"""Synthetic data for Model 2. Columns: spread, debt_gdp, reserves_gdp, fx_vol."""
rng = np.random.default_rng(seed)
debt_gdp = rng.uniform(0.3, 1.0, n)
reserves_gdp = rng.uniform(0.05, 0.4, n)
fx_vol = rng.uniform(0.05, 0.2, n)
spread = 0.01 + 0.02 * debt_gdp - 0.01 * reserves_gdp + 0.1 * fx_vol + rng.normal(0, 0.002, n)
return pd.DataFrame({
"spread": spread, "debt_gdp": debt_gdp, "reserves_gdp": reserves_gdp, "fx_vol": fx_vol,
})
def run_sovereign_spread(
data: Optional[pd.DataFrame] = None,
) -> sm.regression.linear_model.RegressionResultsWrapper:
"""
Model 2: Spread_t = α + β (Debt/GDP)_t + γ Reserves/GDP_t + δ FXVol_t + ε_t.
"""
if data is None:
data = generate_synthetic_spread()
X = sm.add_constant(data[["debt_gdp", "reserves_gdp", "fx_vol"]])
y = data["spread"]
return sm.OLS(y, X).fit()
def generate_synthetic_capital_flow(n: int = 100, seed: Optional[int] = None) -> pd.DataFrame:
"""Synthetic data for Model 3. Columns: dK, rate_diff, risk_premium."""
rng = np.random.default_rng(seed)
rate_diff = rng.normal(0.02, 0.01, n)
risk_premium = rng.uniform(0, 0.05, n)
dK = 2.0 * rate_diff - 10.0 * risk_premium + rng.normal(0, 0.5, n)
return pd.DataFrame({"dK": dK, "rate_diff": rate_diff, "risk_premium": risk_premium})
def run_capital_flow_sensitivity(
data: Optional[pd.DataFrame] = None,
) -> sm.regression.linear_model.RegressionResultsWrapper:
"""
Model 3: ΔK_t = θ (i_d - i_f)_t ω RiskPremium_t + ε_t.
Data columns: dK, rate_diff, risk_premium.
"""
if data is None:
data = generate_synthetic_capital_flow()
X = sm.add_constant(data[["rate_diff", "risk_premium"]])
y = data["dK"]
return sm.OLS(y, X).fit()

View File

@@ -0,0 +1,32 @@
"""
IPSAS-aligned presentation layer for FQBM outputs.
Supports IPSAS 1 (statement of financial position, statement of financial performance),
IPSAS 2 (cash flow statement structure), IPSAS 24 (budget vs actual). See docs/IPSAS_COMPLIANCE.md.
"""
from fqbm.ipsas.presentation import (
statement_of_financial_position,
budget_vs_actual_structure,
cash_flow_statement_structure,
statement_of_financial_performance_structure,
statement_of_changes_in_net_assets_structure,
cash_flow_from_state_changes,
fx_translate,
notes_to_financial_statements_structure,
statement_of_financial_position_comparative,
maturity_risk_disclosure_structure,
)
__all__ = [
"statement_of_financial_position",
"budget_vs_actual_structure",
"cash_flow_statement_structure",
"statement_of_financial_performance_structure",
"statement_of_changes_in_net_assets_structure",
"cash_flow_from_state_changes",
"fx_translate",
"notes_to_financial_statements_structure",
"statement_of_financial_position_comparative",
"maturity_risk_disclosure_structure",
]

View File

@@ -0,0 +1,330 @@
"""
IPSAS 1 and IPSAS 24 presentation structures.
IPSAS 1: Statement of financial position with current/non-current classification
(paragraphs 8994). IPSAS 24: Budget vs actual comparison template.
"""
from typing import Literal, Optional
import pandas as pd
from fqbm.state import FQBMState
def statement_of_financial_position(
state: FQBMState,
entity: Literal["central_bank", "commercial_bank", "consolidated"] = "central_bank",
reporting_date: Optional[str] = None,
) -> pd.DataFrame:
"""
Build an IPSAS 1-style statement of financial position (balance sheet).
Uses current/non-current classification where applicable. Entity:
- central_bank: Assets B, L_cb; Liabilities R, C; Net assets E_cb.
- commercial_bank: Assets Loans; Liabilities Deposits; Net assets E_b.
- consolidated: Combined CB + bank view.
reporting_date: optional label (e.g. "2024-12-31"); falls back to state.reporting_date if set.
"""
rows = []
reporting_label = reporting_date or getattr(state, "reporting_date", None) or "Reporting date"
if entity == "central_bank":
# Identity: B + L_cb = R + C + E_cb. Assets = B, L_cb; Liabilities = R, C; Net assets = E_cb.
rows.append(("Non-current assets", "", ""))
rows.append(("Financial assets government securities", "B", state.B))
rows.append(("Financial assets loans to banks (L_cb)", "L_cb", getattr(state, "L_cb", 0.0)))
rows.append(("Total non-current assets", "", state.B + getattr(state, "L_cb", 0.0)))
rows.append(("Current assets", "", ""))
rows.append(("(Other current assets)", "", 0.0))
rows.append(("Total current assets", "", 0.0))
rows.append(("TOTAL ASSETS", "", state.B + getattr(state, "L_cb", 0.0)))
rows.append(("", "", ""))
rows.append(("Current liabilities", "", ""))
rows.append(("Currency in circulation", "C", state.C))
rows.append(("Reserve balances (deposits of banks)", "R", state.R))
rows.append(("Total current liabilities", "", state.C + state.R))
rows.append(("Net assets / Equity", "E_cb", state.E_cb))
rows.append(("TOTAL LIABILITIES AND NET ASSETS", "", state.C + state.R + state.E_cb)) # = B + L_cb
elif entity == "commercial_bank":
rows.append(("Current assets", "", ""))
rows.append(("Loans and receivables", "Loans", state.Loans))
rows.append(("Total current assets", "", state.Loans))
rows.append(("TOTAL ASSETS", "", state.Loans))
rows.append(("", "", ""))
rows.append(("Current liabilities", "", ""))
rows.append(("Deposits from customers", "Deposits", state.Deposits))
rows.append(("Total current liabilities", "", state.Deposits))
rows.append(("Net assets / Equity", "E_b", state.E_b))
rows.append(("TOTAL LIABILITIES AND NET ASSETS", "", state.Deposits + state.E_b))
else: # consolidated
rows.append(("Non-current assets", "", ""))
rows.append(("Financial assets government securities", "B", state.B))
rows.append(("Total non-current assets", "", state.B))
rows.append(("Current assets", "", ""))
rows.append(("Reserves", "R", state.R))
rows.append(("Loans", "Loans", state.Loans))
rows.append(("Total current assets", "", state.R + state.Loans))
rows.append(("TOTAL ASSETS", "", state.B + state.R + state.Loans))
rows.append(("", "", ""))
rows.append(("Current liabilities", "", ""))
rows.append(("Currency in circulation", "C", state.C))
rows.append(("Deposits", "Deposits", state.Deposits))
rows.append(("Total current liabilities", "", state.C + state.Deposits))
rows.append(("Net assets / Equity (E_cb + E_b)", "", state.E_cb + state.E_b))
rows.append(("TOTAL LIABILITIES AND NET ASSETS", "", state.C + state.Deposits + state.E_cb + state.E_b))
df = pd.DataFrame(rows, columns=["Line item", "FQBM code", "Amount"])
df.attrs["entity"] = entity
df.attrs["reporting_date"] = reporting_label
return df
def budget_vs_actual_structure(
line_items: Optional[list[str]] = None,
) -> pd.DataFrame:
"""
IPSAS 24-style template: budget vs actual comparison.
Columns: Line item, Original budget, Final budget, Actual, Variance, Material (Y/N).
Materiality threshold for variance (e.g. >10%) is for the user to apply.
If line_items is None, uses a minimal set (Total revenue, Total expense, Net surplus/deficit, Total assets, Total liabilities, Net assets).
"""
default_items = [
"Total revenue",
"Total expense",
"Net surplus/(deficit)",
"Total assets",
"Total liabilities",
"Net assets",
]
items = line_items or default_items
n = len(items)
df = pd.DataFrame({
"Line item": items,
"Original budget": [None] * n,
"Final budget": [None] * n,
"Actual": [None] * n,
"Variance": [None] * n,
"Material (Y/N)": [None] * n,
})
df.attrs["ipsas_24"] = True
return df
def cash_flow_statement_structure(
line_items: Optional[list[tuple[str, str]]] = None,
) -> pd.DataFrame:
"""
IPSAS 2-style cash flow statement template: Operating, Investing, Financing.
Columns: Category, Line item, Amount. Category is one of Operating, Investing, Financing.
FQBM does not classify cash flows; amounts are for user fill. If line_items is None,
uses default structure (empty amounts).
"""
default = [
("Operating", "Cash from exchange revenue", None),
("Operating", "Cash to suppliers and employees", None),
("Operating", "Net cash from operating activities", None),
("Investing", "Purchase of financial assets", None),
("Investing", "Proceeds from sale of financial assets", None),
("Investing", "Net cash from investing activities", None),
("Financing", "Proceeds from borrowings", None),
("Financing", "Repayment of borrowings", None),
("Financing", "Net cash from financing activities", None),
("", "Net increase/(decrease) in cash and equivalents", None),
]
items = line_items or default
if items and len(items[0]) == 2:
items = [(c, l, None) for c, l in items]
df = pd.DataFrame(items, columns=["Category", "Line item", "Amount"])
df.attrs["ipsas_2"] = True
return df
def statement_of_financial_performance_structure(
line_items: Optional[list[str]] = None,
) -> pd.DataFrame:
"""
IPSAS 1 statement of financial performance (income/expense) template.
Columns: Line item, Amount. Revenue and expense leading to surplus/deficit.
FQBM does not model revenue/expense; amounts for user fill.
"""
default = [
"Revenue from exchange transactions",
"Revenue from non-exchange transactions",
"Total revenue",
"Expenses",
"Surplus/(deficit) from operations",
"Finance costs",
"Net surplus/(deficit)",
]
items = line_items or default
n = len(items)
df = pd.DataFrame({"Line item": items, "Amount": [None] * n})
df.attrs["ipsas_1_performance"] = True
return df
def statement_of_changes_in_net_assets_structure(
line_items: Optional[list[str]] = None,
) -> pd.DataFrame:
"""
IPSAS 1 statement of changes in net assets/equity template.
Columns: Line item, Opening balance, Changes (surplus/deficit, other), Closing balance.
"""
default = [
"Net assets/equity opening",
"Surplus/(deficit) for the period",
"Other changes in net assets",
"Net assets/equity closing",
]
items = line_items or default
n = len(items)
df = pd.DataFrame({
"Line item": items,
"Opening balance": [None] * n,
"Changes": [None] * n,
"Closing balance": [None] * n,
})
df.attrs["ipsas_1_changes"] = True
return df
def cash_flow_from_state_changes(
state_prev: FQBMState,
state_curr: FQBMState,
) -> pd.DataFrame:
"""
Infer IPSAS 2-style cash flow from balance sheet changes (Δstate).
Classifies: ΔR, ΔC as financing; ΔB as investing; ΔLoans/ΔDeposits as operating/financing proxy.
"""
dR = state_curr.R - state_prev.R
dC = state_curr.C - state_prev.C
dB = state_curr.B - state_prev.B
dLoans = state_curr.Loans - state_prev.Loans
dDeposits = state_curr.Deposits - state_prev.Deposits
rows = [
("Operating", "Net change in loans/deposits (proxy)", dDeposits - dLoans),
("Operating", "Net cash from operating activities", dDeposits - dLoans),
("Investing", "Purchase / (sale) of government securities", -dB),
("Investing", "Net cash from investing activities", -dB),
("Financing", "Change in reserves", dR),
("Financing", "Change in currency", dC),
("Financing", "Net cash from financing activities", dR + dC),
("", "Net increase/(decrease) in cash and equivalents", (dDeposits - dLoans) + (-dB) + (dR + dC)),
]
df = pd.DataFrame(rows, columns=["Category", "Line item", "Amount"])
df.attrs["ipsas_2_from_state"] = True
return df
def fx_translate(
fc_amount: float,
S_prev: float,
S_curr: float,
) -> dict:
"""
IPSAS 4: Translate foreign-currency amount to local currency and FX gain/loss.
fc_amount: amount in foreign currency. Returns local-currency equivalent at S_curr and period gain/loss.
"""
if S_prev <= 0:
S_prev = S_curr
local_prev = fc_amount * S_prev
local_curr = fc_amount * S_curr
gain_loss = local_curr - local_prev
return {
"fc_amount": fc_amount,
"local_prev": local_prev,
"local_curr": local_curr,
"fx_gain_loss": gain_loss,
}
def notes_to_financial_statements_structure(
note_titles: Optional[list[str]] = None,
) -> pd.DataFrame:
"""
Template for notes to the financial statements (IPSAS 1 and general practice).
Columns: Note number, Title, Content (for user fill).
"""
default = [
"1. Basis of preparation",
"2. Statement of compliance",
"3. Accounting policies",
"4. Financial instruments classification and measurement",
"5. Financial instruments maturity and risk",
"6. Foreign exchange",
"7. Related parties",
"8. Budget comparison",
]
titles = note_titles or default
n = len(titles)
numbers = [str(i + 1) for i in range(n)]
df = pd.DataFrame({
"Note": numbers,
"Title": titles,
"Content": [None] * n,
})
df.attrs["notes_template"] = True
return df
def statement_of_financial_position_comparative(
state_prior: FQBMState,
state_current: FQBMState,
entity: Literal["central_bank", "commercial_bank", "consolidated"] = "central_bank",
reporting_date_prior: Optional[str] = None,
reporting_date_current: Optional[str] = None,
) -> pd.DataFrame:
"""
IPSAS 1 comparative presentation: Prior and Current period amounts.
Returns DataFrame with Line item, Prior, Current, Change.
"""
df_prior = statement_of_financial_position(state_prior, entity=entity, reporting_date=reporting_date_prior)
df_curr = statement_of_financial_position(state_current, entity=entity, reporting_date=reporting_date_current)
df = df_prior[["Line item", "FQBM code"]].copy()
prior_amt = pd.to_numeric(df_prior["Amount"], errors="coerce").fillna(0)
curr_amt = pd.to_numeric(df_curr["Amount"], errors="coerce").fillna(0)
df["Prior"] = prior_amt.values
df["Current"] = curr_amt.values
df["Change"] = (curr_amt - prior_amt).values
df.attrs["comparative"] = True
df.attrs["entity"] = entity
return df
def maturity_risk_disclosure_structure(
line_items: Optional[list[str]] = None,
) -> pd.DataFrame:
"""
Template for financial instrument maturity (IPSAS 15/30/41) and risk disclosure.
Columns: Line item, 0-1Y, 1-5Y, 5Y+, Total, Interest rate +100bp, Credit exposure, ECL.
"""
default = [
"Financial assets government securities",
"Loans and receivables",
"Deposits from customers",
"Borrowings",
]
items = line_items or default
n = len(items)
df = pd.DataFrame({
"Line item": items,
"0-1Y": [None] * n,
"1-5Y": [None] * n,
"5Y+": [None] * n,
"Total": [None] * n,
"Interest rate +100bp": [None] * n,
"Credit exposure": [None] * n,
"ECL": [None] * n,
})
df.attrs["maturity_risk_disclosure"] = True
return df

59
src/fqbm/matrix.py Normal file
View File

@@ -0,0 +1,59 @@
"""
Part I: Four-Quadrant Balance Sheet Matrix.
Explicit mapping of FQBM state to the four quadrants:
Assets (Dr) | Assets (Cr) | Liabilities (Dr) | Liabilities (Cr)
All monetary operations must balance across this structure.
"""
from typing import Optional
import pandas as pd
from fqbm.state import FQBMState
def four_quadrant_matrix(
state: FQBMState,
L_cb: Optional[float] = None,
) -> pd.DataFrame:
"""
Build the four-quadrant matrix (Part I) from FQBM state.
Columns: Assets (Dr) | Assets (Cr) | Liabilities (Dr) | Liabilities (Cr).
L_cb from state unless overridden by L_cb argument.
Identity: A = L + E; sectoral B + L_cb = R + C + E_cb, etc.
"""
L_cb_val = L_cb if L_cb is not None else getattr(state, "L_cb", 0.0)
rows = [
["Assets (Dr)", "Assets (Cr)", "Liabilities (Dr)", "Liabilities (Cr)"],
["Government securities (B)", "", "Currency (C)", ""],
[state.B, "", state.C, ""],
["Loans to banks (L_cb)", "", "Reserve balances (R)", ""],
[L_cb_val, "", state.R, ""],
["Loans (bank)", "", "Deposits", ""],
[state.Loans, "", state.Deposits, ""],
["", "", "Net assets (E_cb)", ""],
["", "", state.E_cb, ""],
["", "", "Net assets (E_b)", ""],
["", "", state.E_b, ""],
["Total assets (Dr)", "Total assets (Cr)", "Total liab (Dr)", "Total liab (Cr)"],
[state.B + L_cb_val + state.Loans, 0.0, state.C + state.R + state.Deposits, state.E_cb + state.E_b],
]
df = pd.DataFrame(rows[1:], columns=rows[0])
df.attrs["identity"] = "A = L + E; Total assets (Dr) = Total liab (Dr) + Total liab (Cr)"
return df
def four_quadrant_summary(state: FQBMState, L_cb: Optional[float] = None) -> dict[str, float]:
"""Return totals by quadrant. Identity: total_assets_dr = total_liabilities_dr + total_liabilities_cr."""
L_cb_val = L_cb if L_cb is not None else getattr(state, "L_cb", 0.0)
total_assets_dr = state.B + L_cb_val + state.Loans
total_liab_dr = state.C + state.R + state.Deposits
total_liab_cr = state.E_cb + state.E_b
return {
"total_assets_dr": total_assets_dr,
"total_liabilities_dr": total_liab_dr,
"total_liabilities_cr": total_liab_cr,
"identity_A_eq_L_plus_E": abs((total_assets_dr) - (total_liab_dr + total_liab_cr)) < 1e-6,
}

View File

@@ -0,0 +1,9 @@
"""
Part XI: Historical case expansion — preset parameter sets for stress and policy analysis.
Scenarios: 1997 Asian Financial Crisis, 2008 GFC, 2020 Pandemic, 202223 Global Rate Shock.
"""
from fqbm.scenarios.presets import get_scenario, list_scenarios, ScenarioPreset
__all__ = ["get_scenario", "list_scenarios", "ScenarioPreset"]

View File

@@ -0,0 +1,162 @@
"""
Part XI: Historical case presets.
1997 Asian: fixed FX, short-term foreign debt, sudden stop, reserve depletion, currency collapse.
2008 GFC: shadow leverage via repo, MBS collapse, capital erosion, QE stabilization.
2020 Pandemic: massive fiscal expansion, global swap lines, balance sheet expansion >$4T.
202223: dollar surge, EM stress, duration losses in advanced economies.
"""
from dataclasses import dataclass
from typing import Any, Optional
from fqbm.state import FQBMState
from fqbm.sheets.central_bank import CentralBankParams
from fqbm.sheets.commercial_bank import CommercialBankParams
from fqbm.sheets.fx_parity import FXParams
from fqbm.sheets.sovereign_debt import SovereignParams
from fqbm.sheets.commodity import CommodityParams
from fqbm.sheets.monte_carlo import ShockSpec
@dataclass
class ScenarioPreset:
"""Named scenario: initial state and parameter overrides for workbook/MC."""
name: str
description: str
state: FQBMState
cb_params: Optional[CentralBankParams] = None
bank_params: Optional[CommercialBankParams] = None
fx_params: Optional[FXParams] = None
sovereign_params: Optional[SovereignParams] = None
commodity_params: Optional[CommodityParams] = None
shock_spec: Optional[ShockSpec] = None
def _asia_1997() -> ScenarioPreset:
"""1997 Asian Financial Crisis: fixed regimes, short-term foreign debt, sudden stop, reserve depletion."""
state = FQBMState(
R=80, # low reserves relative to potential run
Deposits=1000,
Loans=900,
E_b=100,
S=1.0,
Spread=0.05,
)
return ScenarioPreset(
name="asia_1997",
description="1997 Asian crisis: fixed FX, sudden stop, reserve depletion, liability revaluation shock",
state=state,
sovereign_params=SovereignParams(
debt_gdp=0.7,
reserves_gdp=0.15,
fx_vol=0.25,
growth_differential=-0.02,
),
fx_params=FXParams(i_d=0.15, i_f=0.05, lambda_dornbusch=2.0),
shock_spec=ShockSpec(
deposit_outflow_mean=-0.10,
deposit_outflow_std=0.08,
fx_vol_mean=0.25,
fx_vol_std=0.1,
spread_widening_mean=0.02,
spread_widening_std=0.01,
),
)
def _gfc_2008() -> ScenarioPreset:
"""2008 GFC: shadow leverage, MBS collapse, capital erosion, QE."""
state = FQBMState(
R=150,
Deposits=2000,
Loans=2200, # high leverage
E_b=80, # thin equity
B=500,
E_cb=50,
)
return ScenarioPreset(
name="gfc_2008",
description="2008 GFC: shadow leverage via repo, MBS collapse, capital erosion, QE stabilization",
state=state,
cb_params=CentralBankParams(d_B=100), # QE
sovereign_params=SovereignParams(debt_gdp=0.75, fx_vol=0.15),
shock_spec=ShockSpec(
default_rate_mean=0.05,
default_rate_std=0.03,
deposit_outflow_mean=-0.05,
deposit_outflow_std=0.06,
),
)
def _pandemic_2020() -> ScenarioPreset:
"""2020 Pandemic: massive fiscal expansion, swap lines, balance sheet expansion."""
state = FQBMState(
R=800,
B=2000,
C=100,
E_cb=100,
Deposits=3000,
Loans=2700,
E_b=200,
)
return ScenarioPreset(
name="pandemic_2020",
description="2020 Pandemic: massive fiscal expansion, global swap lines, balance sheet expansion >$4T",
state=state,
cb_params=CentralBankParams(d_B=500),
bank_params=CommercialBankParams(d_deposits=200),
sovereign_params=SovereignParams(debt_gdp=0.9, reserves_gdp=0.25, growth_differential=-0.05),
)
def _rate_shock_2022() -> ScenarioPreset:
"""202223: dollar surge, EM stress, duration losses."""
state = FQBMState(
R=200,
Deposits=1000,
Loans=900,
E_b=100,
S=1.2, # strong dollar
Spread=0.04,
)
return ScenarioPreset(
name="rate_shock_2022",
description="202223 Global rate shock: dollar surge, EM stress, duration losses in advanced economies",
state=state,
fx_params=FXParams(i_d=0.05, i_f=0.02, lambda_dornbusch=1.5),
sovereign_params=SovereignParams(
debt_gdp=0.65,
reserves_gdp=0.18,
fx_vol=0.18,
r=0.06,
g=0.01,
),
shock_spec=ShockSpec(
fx_vol_mean=0.15,
fx_vol_std=0.08,
spread_widening_mean=0.01,
spread_widening_std=0.008,
),
)
_REGISTRY: dict[str, ScenarioPreset] = {}
def _build_registry() -> dict[str, ScenarioPreset]:
if not _REGISTRY:
for preset in [_asia_1997(), _gfc_2008(), _pandemic_2020(), _rate_shock_2022()]:
_REGISTRY[preset.name] = preset
return _REGISTRY
def list_scenarios() -> list[str]:
"""Return scenario names."""
return list(_build_registry().keys())
def get_scenario(name: str) -> Optional[ScenarioPreset]:
"""Return preset by name, or None."""
return _build_registry().get(name)

View File

@@ -0,0 +1,21 @@
"""Workbook sheets (Part XVI): Central Bank, Commercial Bank, Stress, FX, Sovereign, Commodity, Monte Carlo, Dashboard."""
from fqbm.sheets.central_bank import central_bank_step
from fqbm.sheets.commercial_bank import commercial_bank_step
from fqbm.sheets.capital_stress import stress_tables
from fqbm.sheets.fx_parity import fx_parity_step
from fqbm.sheets.sovereign_debt import sovereign_debt_step
from fqbm.sheets.commodity import commodity_step
from fqbm.sheets.monte_carlo import run_n_simulations
from fqbm.sheets.dashboard import dashboard_aggregate
__all__ = [
"central_bank_step",
"commercial_bank_step",
"stress_tables",
"fx_parity_step",
"sovereign_debt_step",
"commodity_step",
"run_n_simulations",
"dashboard_aggregate",
]

View File

@@ -0,0 +1,62 @@
"""
Sheet 3: Capital and liquidity stress tables.
Part XII — Quantitative stress: deposit run vs RCR; capital shock vs CR.
"""
from dataclasses import dataclass
from typing import Optional
import pandas as pd
from fqbm.state import FQBMState
@dataclass
class StressParams:
"""Parameters for stress scenarios."""
reserve_coverage_ratio_min: float = 0.0 # threshold for "intervention"
capital_ratio_min: float = 0.08 # e.g. 8%
def stress_tables(
state: FQBMState,
params: Optional[StressParams] = None,
) -> dict:
"""
Compute liquidity stress (deposit run vs RCR) and capital shock table.
Returns dict with 'liquidity_stress' and 'capital_stress' DataFrames.
"""
params = params or StressParams()
reserves = state.R
deposits = state.Deposits
equity = state.E_b
# Liquidity: RCR = Reserves / (Deposits * run_rate) proxy as Reserves / (run_amount)
run_rates = [0.10, 0.30, 0.50]
rows_liq = []
for run in run_rates:
run_amount = deposits * run
rcr = (reserves / run_amount * 100) if run_amount > 0 else 0
if rcr >= 80:
status = "Stable"
elif rcr >= 25:
status = "Tension"
else:
status = "Intervention"
rows_liq.append({"Deposit Run": f"{run*100:.0f}%", "Reserves": reserves, "RCR": f"{rcr:.0f}%", "Status": status})
liquidity_stress = pd.DataFrame(rows_liq)
# Capital: assume RWA ≈ Loans for simplicity
rwa = state.Loans
cr_baseline = (equity / rwa * 100) if rwa > 0 else 0
loss_rates = [0, 0.08]
rows_cap = []
for loss_pct in loss_rates:
eq = equity * (1 - loss_pct)
cr = (eq / rwa * 100) if rwa > 0 else 0
status = "Compliant" if cr >= params.capital_ratio_min * 100 else "Breach"
rows_cap.append({"Loss": f"{loss_pct*100:.0f}%", "Equity": eq, "CR": f"{cr:.1f}%", "Status": status})
capital_stress = pd.DataFrame(rows_cap)
return {"liquidity_stress": liquidity_stress, "capital_stress": capital_stress}

30
src/fqbm/sheets/cbdc.py Normal file
View File

@@ -0,0 +1,30 @@
"""
Part IX: CBDC and digital reserve architecture (stub).
Deposit -> CBDC shift: ΔBank Deposits = X, ΔBank Reserves = X, ΔCBDC Liability = +X.
Funding gap = Deposit Loss Wholesale Replacement.
Not yet implemented; placeholder for gap documentation.
"""
from dataclasses import dataclass
from typing import Optional
@dataclass
class CBDCParams:
"""Placeholder for CBDC parameters."""
deposit_shift: float = 0.0 # X in Part IX
wholesale_replacement: float = 0.0
def deposit_to_cbdc_shift(X: float) -> dict[str, float]:
"""
Part IX: ΔBank Deposits = X, ΔBank Reserves = X, ΔCBDC Liability = +X.
Returns dict with keys: d_deposits, d_reserves, d_cbdc_liability.
"""
return {"d_deposits": -X, "d_reserves": -X, "d_cbdc_liability": X}
def funding_gap(deposit_loss: float, wholesale_replacement: float) -> float:
"""Funding Gap = Deposit Loss Wholesale Replacement (Part IX)."""
return deposit_loss - wholesale_replacement

73
src/fqbm/sheets/ccp.py Normal file
View File

@@ -0,0 +1,73 @@
"""
Part VIII: Derivatives clearing and CCP structure.
CCP identity: Assets (Margin Posted) = Liabilities (Margin Obligations).
Variation margin ensures daily neutrality. Default waterfall on VM call excess over liquidity buffer.
"""
from dataclasses import dataclass
from typing import Optional, Sequence
@dataclass
class CCPParams:
"""Parameters for CCP margin identity and default waterfall (Part VIII)."""
margin_posted: float = 0.0
margin_obligations: float = 0.0
vm_calls: float = 0.0
liquidity_buffer: float = 0.0
def ccp_identity(margin_posted: float, margin_obligations: float) -> bool:
"""CCP identity: Margin Posted = Margin Obligations (Part VIII)."""
return abs(margin_posted - margin_obligations) < 1e-6
def default_waterfall_triggered(vm_calls: float, liquidity_buffer: float) -> bool:
"""If VM calls exceed liquidity buffer, default waterfall activated (Part VIII)."""
return vm_calls > liquidity_buffer
def variation_margin_flow(
mark_to_market_change: float,
member_pays_when_positive: bool = True,
) -> float:
"""
Variation margin call from mark-to-market change.
Convention: positive MTM change = gain for member; VM call = -MTM so member pays when they lose.
Returns VM amount (positive = member must pay).
"""
if member_pays_when_positive:
return -mark_to_market_change
return mark_to_market_change
def ccp_clearing_simulation(
vm_calls_per_period: Sequence[float],
liquidity_buffer_start: float,
initial_margin_posted: float = 0.0,
) -> list[dict]:
"""
Full clearing simulation over multiple periods. Each period: VM call; buffer is used first;
if buffer insufficient, default waterfall triggered for that period.
Returns list of dicts per period: buffer_start, vm_call, buffer_end, waterfall_triggered.
"""
buffer = liquidity_buffer_start
results = []
for vm in vm_calls_per_period:
buffer_start = buffer
vm_call = max(0.0, vm)
buffer_after = buffer_start - vm_call
if buffer_after < 0:
waterfall = True
buffer = 0.0
else:
waterfall = False
buffer = buffer_after
results.append({
"buffer_start": buffer_start,
"vm_call": vm_call,
"buffer_end": buffer,
"waterfall_triggered": waterfall,
})
return results

View File

@@ -0,0 +1,38 @@
"""
Sheet 1: Central Bank Model.
Part II — 2.1. Identity: B + L_cb = R + C + E_cb.
Differential: dB + dL_cb = dR + dC + dE_cb.
QE: dB > 0 → dR > 0; QT: dB < 0 → dR < 0.
"""
from dataclasses import dataclass
from typing import Optional
from fqbm.state import FQBMState
@dataclass
class CentralBankParams:
"""Parameters for central bank balance sheet dynamics."""
# Optional policy shock (e.g. QE/QT): net change in bonds held by CB
d_B: float = 0.0
d_L_cb: float = 0.0
def central_bank_step(
state: FQBMState,
params: Optional[CentralBankParams] = None,
) -> FQBMState:
"""
Apply one period central bank balance sheet identity.
Identity: B + L_cb = R + C + E_cb. We assume dR absorbs the residual of dB + dL_cb - dC - dE_cb
(e.g. QE: buy bonds → increase R).
"""
params = params or CentralBankParams()
out = state.copy()
out.B += params.d_B
out.L_cb += params.d_L_cb
# Identity: dB + dL_cb = dR + dC + dE_cb; simplified dR absorbs
out.R += params.d_B + params.d_L_cb
return out

View File

@@ -0,0 +1,42 @@
"""
Sheet 2: Commercial Bank Model.
Part II — 2.2. Loan creation: dLoans = dDeposits.
Capital constraint: CR = Equity / RWA >= k. Loans_max = Equity / k.
"""
from dataclasses import dataclass
from typing import Optional
from fqbm.state import FQBMState
@dataclass
class CommercialBankParams:
"""Parameters for commercial bank credit."""
k: float = 0.08 # minimum capital ratio (e.g. 8%)
d_deposits: float = 0.0 # exogenous deposit change → dLoans = dDeposits
def commercial_bank_step(
state: FQBMState,
params: Optional[CommercialBankParams] = None,
) -> FQBMState:
"""
Apply one period: dLoans = dDeposits (endogenous money). Equity unchanged unless losses.
"""
params = params or CommercialBankParams()
out = state.copy()
out.Loans += params.d_deposits
out.Deposits += params.d_deposits
return out
def loans_max(equity: float, k: float) -> float:
"""Maximum credit capacity: Loans_max = Equity / k (Part II — 2.2)."""
return equity / k if k > 0 else 0.0
def capital_ratio(equity: float, rwa: float) -> float:
"""CR = Equity / RWA."""
return equity / rwa if rwa > 0 else 0.0

View File

@@ -0,0 +1,36 @@
"""
Sheet 6: Commodity shock channel.
Part VI — π = π_core + δ ΔO + β ΔS. Energy-importing economies: dual channel.
"""
from dataclasses import dataclass
from typing import Optional
from fqbm.state import FQBMState
@dataclass
class CommodityParams:
"""Parameters for commodity pass-through."""
pi_core: float = 0.02
delta_oil: float = 0.1 # weight on oil price change
beta_fx: float = 0.2
d_O: float = 0.0
d_S: float = 0.0
def inflation_composite(pi_core: float, d_O: float, d_S: float, delta: float, beta: float) -> float:
"""π = π_core + δ ΔO + β ΔS (Part VI)."""
return pi_core + delta * d_O + beta * d_S
def commodity_step(
state: FQBMState,
params: Optional[CommodityParams] = None,
) -> FQBMState:
"""Update commodity price level in state (O) and return state."""
params = params or CommodityParams()
out = state.copy()
out.O = state.O * (1 + params.d_O) if state.O else (1 + params.d_O)
return out

View File

@@ -0,0 +1,55 @@
"""
Sheet 8: Consolidated macro dashboard.
Aggregate time series, key ratios, stress flags from workbook and Monte Carlo.
"""
from typing import Any, Optional
import pandas as pd
from fqbm.state import FQBMState
from fqbm.sheets.monte_carlo import run_n_simulations, ShockSpec
def dashboard_aggregate(
state: FQBMState,
mc_runs: int = 0,
shock_spec: Optional[ShockSpec] = None,
) -> dict[str, Any]:
"""
Build dashboard: state snapshot, key ratios, optional MC summary.
"""
rwa = state.Loans
cr = (state.E_b / rwa) if rwa > 0 else 0
ratios = {
"capital_ratio": cr,
"reserves_to_deposits": (state.R / state.Deposits) if state.Deposits else 0,
"loans_to_deposits": (state.Loans / state.Deposits) if state.Deposits else 0,
}
snapshot = {
"B": state.B, "R": state.R, "C": state.C,
"Loans": state.Loans, "Deposits": state.Deposits,
"E_cb": state.E_cb, "E_b": state.E_b,
"S": state.S, "Spread": state.Spread, "O": state.O,
"L_cb": getattr(state, "L_cb", 0),
}
out = {"state": snapshot, "ratios": ratios}
# Part VII: shadow banking leverage ratio (bank)
try:
from fqbm.sheets.shadow_banking import leverage_ratio
total_assets_bank = state.Loans
out["ratios"]["leverage_ratio_bank"] = leverage_ratio(total_assets_bank, state.E_b)
except Exception:
pass
if mc_runs > 0:
df = run_n_simulations(mc_runs, state, shock_spec)
out["mc_summary"] = {
"p_insolvency": df["insolvent"].mean(),
"p_reserve_breach": df["reserve_breach"].mean(),
"inflation_mean": df["inflation"].mean(),
"inflation_std": df["inflation"].std(),
"p_debt_unsustainable": 1 - df["debt_sustainable"].mean(),
}
return out

View File

@@ -0,0 +1,52 @@
"""
Sheet 4: FX and parity engine.
Part III — 3.2 CIP: (1+i_d) = (1+i_f)(F/S). UIP: E(ΔS/S) ≈ i_d - i_f.
Part III — 3.3 Dornbusch: s_t = s* + λ(i_t - i*). Part IV: π_import = β × ΔS.
"""
from dataclasses import dataclass
from typing import Optional
from fqbm.state import FQBMState
@dataclass
class FXParams:
"""Parameters for FX and pass-through."""
i_d: float = 0.05
i_f: float = 0.03
s_star: float = 1.0
lambda_dornbusch: float = 1.5
beta_pass_through: float = 0.2 # IMF range short-run 0.10.4
def covered_interest_parity_fwd(s: float, i_d: float, i_f: float) -> float:
"""(1+i_d) = (1+i_f)(F/S) => F = S * (1+i_d)/(1+i_f)."""
return s * (1 + i_d) / (1 + i_f)
def uncovered_interest_parity_ds(s: float, i_d: float, i_f: float) -> float:
"""E(ΔS/S) ≈ i_d - i_f => expected proportional change in S."""
return i_d - i_f
def dornbusch_s(s_star: float, i_d: float, i_f: float, lam: float) -> float:
"""s_t = s* + λ(i_t - i*)."""
return s_star + lam * (i_d - i_f)
def fx_pass_through_inflation(dS: float, beta: float) -> float:
"""π_import = β × ΔS (Part IV)."""
return beta * dS
def fx_parity_step(
state: FQBMState,
params: Optional[FXParams] = None,
) -> FQBMState:
"""Update state with FX-consistent level (Dornbusch) and leave S in state."""
params = params or FXParams()
out = state.copy()
out.S = dornbusch_s(params.s_star, params.i_d, params.i_f, params.lambda_dornbusch)
return out

View File

@@ -0,0 +1,119 @@
"""
Sheet 7: Monte Carlo stress engine.
Part XIII — Shock variables: default rate, deposit outflow, FX vol, oil vol, sovereign spread.
Outputs: P(insolvency), reserve breach probability, inflation trajectory distribution,
debt sustainability risk threshold.
"""
from dataclasses import dataclass, replace
from typing import Optional
import numpy as np
import pandas as pd
from fqbm.state import FQBMState
from fqbm.sheets.central_bank import central_bank_step, CentralBankParams
from fqbm.sheets.commercial_bank import commercial_bank_step, CommercialBankParams, capital_ratio
from fqbm.sheets.capital_stress import stress_tables, StressParams
from fqbm.sheets.sovereign_debt import sovereign_debt_step, SovereignParams, debt_sustainable
from fqbm.sheets.commodity import commodity_step, CommodityParams, inflation_composite
@dataclass
class ShockSpec:
"""Distributions for shock variables (mean, std or scale)."""
default_rate_mean: float = 0.02
default_rate_std: float = 0.01
deposit_outflow_mean: float = 0.0
deposit_outflow_std: float = 0.05
fx_vol_mean: float = 0.1
fx_vol_std: float = 0.05
oil_vol_mean: float = 0.0
oil_vol_std: float = 0.1
spread_widening_mean: float = 0.0
spread_widening_std: float = 0.005
seed: Optional[int] = None
def _draw_shocks(spec: ShockSpec, rng: np.random.Generator) -> dict:
return {
"default_rate": float(rng.normal(spec.default_rate_mean, max(1e-6, spec.default_rate_std))),
"deposit_outflow": float(rng.normal(spec.deposit_outflow_mean, max(1e-6, spec.deposit_outflow_std))),
"fx_vol": float(rng.normal(spec.fx_vol_mean, max(1e-6, spec.fx_vol_std))),
"oil_vol": float(rng.normal(spec.oil_vol_mean, max(1e-6, spec.oil_vol_std))),
"spread_widening": float(rng.normal(spec.spread_widening_mean, max(1e-6, spec.spread_widening_std))),
}
def _single_run(
base_state: FQBMState,
shock: dict,
capital_ratio_min: float,
reserve_threshold: float,
sovereign_params: SovereignParams,
) -> dict:
state = base_state.copy()
# Apply deposit outflow
state.Deposits *= 1 + shock["deposit_outflow"]
state.Loans *= 1 + shock["deposit_outflow"]
# Defaults erode equity
state.E_b -= state.Loans * max(0, shock["default_rate"])
state.Loans *= 1 - max(0, min(1, shock["default_rate"]))
# Reserves (simplified: no change from outflow in this stub)
# Sovereign (copy params so we don't mutate shared object)
sp = replace(sovereign_params, fx_vol=shock["fx_vol"])
state = sovereign_debt_step(state, sp)
state.Spread += shock["spread_widening"]
# Commodity / inflation
pi = inflation_composite(0.02, shock["oil_vol"], shock["fx_vol"], 0.1, 0.2)
rwa = state.Loans
cr = capital_ratio(state.E_b, rwa) if rwa > 0 else 0
insolvent = state.E_b <= 0 or cr < capital_ratio_min
reserve_breach = state.R < reserve_threshold
sust = debt_sustainable(
sovereign_params.primary_balance_gdp,
sovereign_params.r,
sovereign_params.g,
sovereign_params.debt_gdp,
)
return {
"insolvent": insolvent,
"reserve_breach": reserve_breach,
"inflation": pi,
"debt_sustainable": sust,
"CR": cr,
"Spread": state.Spread,
}
def run_n_simulations(
n: int,
base_state: Optional[FQBMState] = None,
shock_spec: Optional[ShockSpec] = None,
capital_ratio_min: float = 0.08,
reserve_threshold: float = 0.0,
sovereign_params: Optional[SovereignParams] = None,
) -> pd.DataFrame:
"""
Run n Monte Carlo simulations. Returns DataFrame of outcomes and summary stats.
"""
base_state = base_state or FQBMState(R=200, Deposits=1000, Loans=900, E_b=100)
shock_spec = shock_spec or ShockSpec()
sovereign_params = sovereign_params or SovereignParams()
if reserve_threshold == 0:
reserve_threshold = base_state.R * 0.5
rng = np.random.default_rng(shock_spec.seed)
rows = []
for _ in range(n):
shock = _draw_shocks(shock_spec, rng)
row = _single_run(
base_state, shock, capital_ratio_min, reserve_threshold, sovereign_params
)
rows.append(row)
df = pd.DataFrame(rows)
return df

View File

@@ -0,0 +1,102 @@
"""
Part VII: Shadow banking and leverage.
Leverage = Total Assets / Equity. Repo multiplier and margin spiral simulation.
"""
from dataclasses import dataclass
from typing import Optional
@dataclass
class ShadowBankingParams:
"""Parameters for shadow banking / repo simulation."""
haircut: float = 0.02
repo_rounds: int = 3
margin_initial_ratio: float = 0.1
fire_sale_impact: float = 0.1 # price impact per unit of forced sale
def leverage_ratio(total_assets: float, equity: float) -> float:
"""Leverage = Total Assets / Equity (Part VII). Returns 0 if equity <= 0."""
return total_assets / equity if equity > 0 else 0.0
def repo_multiplier(
initial_collateral: float,
haircut: float = 0.02,
rounds: int = 3,
) -> dict:
"""
Repo multiplier: re-use of collateral in successive repo rounds increases effective exposure.
Each round: (1 - haircut) of collateral can be re-pledged. Total effective financing
capacity ≈ initial_collateral * sum_{k=0}^{rounds-1} (1 - haircut)^k (simplified).
Returns dict with total_effective_collateral, multiplier_implied, rounds_used.
"""
if initial_collateral <= 0 or rounds <= 0:
return {"total_effective_collateral": 0.0, "multiplier_implied": 0.0, "rounds_used": 0}
factor = 1.0 - haircut
total = initial_collateral * sum(factor**k for k in range(rounds))
multiplier = total / initial_collateral if initial_collateral else 0
return {
"total_effective_collateral": total,
"multiplier_implied": multiplier,
"rounds_used": rounds,
}
def margin_spiral_risk(
collateral_value: float,
margin_requirement: float,
haircut: float,
) -> dict:
"""
One-step check: Collateral decline -> margin calls. Returns margin_call_triggered, shortfall.
"""
required = margin_requirement * (1 + haircut)
shortfall = max(0.0, required - collateral_value)
return {"margin_call_triggered": shortfall > 0, "shortfall": shortfall}
def margin_spiral_simulation(
initial_collateral: float,
margin_requirement: float,
haircut: float,
liquidity_buffer: float,
fire_sale_impact: float = 0.1,
max_rounds: int = 20,
) -> dict:
"""
Full margin spiral: collateral decline -> margin call -> forced selling -> collateral decline.
Iterates until no shortfall or max_rounds. Fire sale reduces collateral by fire_sale_impact * shortfall_sold.
Returns path of collateral, margin_calls, cumulative_forced_sales, waterfall_triggered.
"""
path_collateral = [initial_collateral]
path_margin_calls = [0.0]
cumulative_forced_sales = 0.0
collateral = initial_collateral
required = margin_requirement * (1 + haircut)
waterfall_triggered = False
for _ in range(max_rounds):
shortfall = max(0.0, required - collateral)
if shortfall <= 0:
break
if shortfall > liquidity_buffer:
waterfall_triggered = True
forced_sale = shortfall
cumulative_forced_sales += forced_sale
collateral = collateral - forced_sale * fire_sale_impact
collateral = max(0.0, collateral)
path_collateral.append(collateral)
path_margin_calls.append(shortfall)
if collateral <= 0:
break
return {
"path_collateral": path_collateral,
"path_margin_calls": path_margin_calls,
"cumulative_forced_sales": cumulative_forced_sales,
"waterfall_triggered": waterfall_triggered,
"final_collateral": path_collateral[-1] if path_collateral else 0,
}

View File

@@ -0,0 +1,46 @@
"""
Sheet 5: Sovereign debt and spread model.
Part V — Spread = f(Debt/GDP, FX vol, Reserve adequacy, Growth differential).
CDS ≈ Prob(Default) × LGD. Debt sustainability: Primary Balance >= (r - g) × Debt/GDP.
"""
from dataclasses import dataclass
from typing import Optional
from fqbm.state import FQBMState
@dataclass
class SovereignParams:
"""Parameters for sovereign spread and sustainability."""
debt_gdp: float = 0.6
reserves_gdp: float = 0.2
fx_vol: float = 0.1
growth_differential: float = 0.0
r: float = 0.05
g: float = 0.02
primary_balance_gdp: float = 0.0
def spread_model(debt_gdp: float, reserves_gdp: float, fx_vol: float, growth_diff: float) -> float:
"""Spread = f(Debt/GDP, Reserves/GDP, FXVol, Growth differential). Linear proxy."""
return 0.01 * debt_gdp - 0.02 * reserves_gdp + 0.5 * fx_vol - 0.01 * growth_diff
def debt_sustainable(primary_balance_gdp: float, r: float, g: float, debt_gdp: float) -> bool:
"""Primary Balance >= (r - g) × Debt/GDP (Part V)."""
return primary_balance_gdp >= (r - g) * debt_gdp
def sovereign_debt_step(
state: FQBMState,
params: Optional[SovereignParams] = None,
) -> FQBMState:
"""Update sovereign spread in state from params (GDP ratios and vol)."""
params = params or SovereignParams()
out = state.copy()
out.Spread = max(0.0, spread_model(
params.debt_gdp, params.reserves_gdp, params.fx_vol, params.growth_differential
))
return out

91
src/fqbm/state.py Normal file
View File

@@ -0,0 +1,91 @@
"""
Shared state vector / context for the FQBM workbook and differential model.
Part XIV state: X = [B, R, C, Loans, Deposits, E_cb, E_b, S, K, Spread, O, L_cb]
- B: government bonds (CB assets)
- R: reserves
- C: currency
- Loans: commercial bank loans
- Deposits: commercial bank deposits
- E_cb: central bank equity
- E_b: bank equity
- S: exchange rate (domestic per foreign)
- K: capital flows
- Spread: sovereign spread
- O: oil/commodity price index
- L_cb: central bank loans (Part II identity: B + L_cb = R + C + E_cb)
"""
from dataclasses import dataclass, field
from typing import Optional
# Default vector length for from_vector (supports 11 for backward compat)
STATE_VECTOR_LEN = 12
@dataclass
class FQBMState:
"""State vector for the full system (Part XIV)."""
B: float = 0.0
R: float = 0.0
C: float = 0.0
Loans: float = 0.0
Deposits: float = 0.0
E_cb: float = 0.0
E_b: float = 0.0
S: float = 1.0
K: float = 0.0
Spread: float = 0.0
O: float = 1.0
L_cb: float = 0.0
reporting_date: Optional[str] = field(default=None, compare=False)
def to_vector(self) -> list[float]:
return [
self.B, self.R, self.C, self.Loans, self.Deposits,
self.E_cb, self.E_b, self.S, self.K, self.Spread, self.O, self.L_cb,
]
@classmethod
def from_vector(cls, x: list[float]) -> "FQBMState":
if len(x) < STATE_VECTOR_LEN:
x = list(x) + [0.0] * (STATE_VECTOR_LEN - len(x))
return cls(
B=x[0], R=x[1], C=x[2], Loans=x[3], Deposits=x[4],
E_cb=x[5], E_b=x[6], S=x[7], K=x[8], Spread=x[9], O=x[10],
L_cb=x[11] if len(x) > 11 else 0.0,
)
def copy(self) -> "FQBMState":
return FQBMState(
B=self.B, R=self.R, C=self.C, Loans=self.Loans, Deposits=self.Deposits,
E_cb=self.E_cb, E_b=self.E_b, S=self.S, K=self.K, Spread=self.Spread, O=self.O,
L_cb=self.L_cb, reporting_date=self.reporting_date,
)
def open_economy_view(
A_dom: float,
A_ext: float,
L_dom: float,
L_ext: float,
E: float,
) -> dict:
"""
Part I extended identity: A_dom + A_ext = L_dom + L_ext + E.
Returns dict with totals and identity_holds bool.
"""
total_assets = A_dom + A_ext
total_liab_equity = L_dom + L_ext + E
identity_holds = abs(total_assets - total_liab_equity) < 1e-9
return {
"A_dom": A_dom,
"A_ext": A_ext,
"L_dom": L_dom,
"L_ext": L_ext,
"E": E,
"total_assets": total_assets,
"total_liab_equity": total_liab_equity,
"identity_holds": identity_holds,
}

View File

@@ -0,0 +1,5 @@
"""Part XIV: Full system differential model and stability checks."""
from fqbm.system.differential_model import solve_trajectory, check_stability
__all__ = ["solve_trajectory", "check_stability"]

View File

@@ -0,0 +1,89 @@
"""
Part XIV: Full system differential model.
State vector X = [B, R, C, Loans, Deposits, E_cb, E_b, S, K, Spread, O, L_cb].
dX/dt = F(monetary policy, fiscal policy, capital flows, commodity shocks, credit cycle).
Stability: CR >= k, Reserves >= SuddenStopThreshold, Debt/GDP sustainable under (r-g).
"""
from dataclasses import dataclass
from typing import Callable, Optional
import numpy as np
from scipy.integrate import solve_ivp
from fqbm.state import FQBMState
@dataclass
class DifferentialParams:
"""Policy and shock parameters for dX/dt."""
# Placeholder policy inputs (can be time-varying in full version)
monetary_shock: float = 0.0
fiscal_shock: float = 0.0
capital_flow: float = 0.0
commodity_shock: float = 0.0
credit_cycle: float = 0.0
def _dynamics(t: float, x: np.ndarray, params: DifferentialParams) -> np.ndarray:
"""
dX/dt = F(...). Index: 0=B, 1=R, 2=C, 3=Loans, 4=Deposits, 5=E_cb, 6=E_b, 7=S, 8=K, 9=Spread, 10=O, 11=L_cb.
"""
n = len(x)
B, R, C, L, D, E_cb, E_b, S, K, Spread, O = x[0], x[1], x[2], x[3], x[4], x[5], x[6], x[7], x[8], x[9], x[10]
L_cb = x[11] if n > 11 else 0.0
dB = params.monetary_shock
dR = dB
dC = 0.0
dE_cb = 0.0
dL_cb = 0.0
dL = params.credit_cycle * L if L != 0 else 0
dD = dL
dE_b = 0.0
dS = params.capital_flow * S if S != 0 else 0
dK = params.capital_flow
dSpread = -0.01 * params.capital_flow
dO = params.commodity_shock * O if O != 0 else 0
return np.array([dB, dR, dC, dL, dD, dE_cb, dE_b, dS, dK, dSpread, dO, dL_cb])
def solve_trajectory(
x0: FQBMState,
t_span: tuple[float, float],
params: Optional[DifferentialParams] = None,
t_eval: Optional[np.ndarray] = None,
) -> tuple[np.ndarray, np.ndarray]:
"""
Integrate dX/dt = F(...). Returns (t, X) where X is (n_times, 12).
"""
params = params or DifferentialParams()
y0 = np.array(x0.to_vector(), dtype=float)
sol = solve_ivp(
lambda t, y: _dynamics(t, y, params),
t_span,
y0,
t_eval=t_eval,
method="RK45",
)
return sol.t, sol.y.T
def check_stability(
x: FQBMState,
k: float = 0.08,
reserve_threshold: float = 0.0,
debt_gdp: float = 0.6,
r: float = 0.05,
g: float = 0.02,
primary_balance_gdp: float = 0.0,
) -> dict[str, bool]:
"""
Stability conditions: (1) CR >= k, (2) Reserves >= threshold, (3) Debt sustainable.
"""
rwa = x.Loans
cr = (x.E_b / rwa) if rwa > 0 else 0
ok_cr = cr >= k
ok_reserves = x.R >= reserve_threshold
ok_debt = primary_balance_gdp >= (r - g) * debt_gdp
return {"CR_ok": ok_cr, "reserves_ok": ok_reserves, "debt_sustainable": ok_debt}

View File

@@ -0,0 +1,5 @@
"""Workbook runner: execute full sheet sequence and optional Excel export."""
from fqbm.workbook.runner import run_workbook
__all__ = ["run_workbook"]

170
src/fqbm/workbook/runner.py Normal file
View File

@@ -0,0 +1,170 @@
"""
Workbook runner: execute full sheet sequence (Part XVI).
Flow: Params + state → Sheet 1 (CB) → Sheet 2 (Bank) → Sheets 4,5,6 (FX, Sovereign, Commodity)
→ Sheet 3 (Stress) and Sheet 7 (MC) → Sheet 8 (Dashboard). Optional Excel export.
"""
from typing import Any, Optional
from fqbm.state import FQBMState
from fqbm.sheets.central_bank import central_bank_step, CentralBankParams
from fqbm.sheets.commercial_bank import commercial_bank_step, CommercialBankParams
from fqbm.sheets.fx_parity import fx_parity_step, FXParams
from fqbm.sheets.sovereign_debt import sovereign_debt_step, SovereignParams
from fqbm.sheets.commodity import commodity_step, CommodityParams
from fqbm.sheets.capital_stress import stress_tables, StressParams
from fqbm.sheets.monte_carlo import run_n_simulations, ShockSpec
from fqbm.sheets.dashboard import dashboard_aggregate
def run_workbook(
initial_state: Optional[FQBMState] = None,
cb_params: Optional[CentralBankParams] = None,
bank_params: Optional[CommercialBankParams] = None,
fx_params: Optional[FXParams] = None,
sovereign_params: Optional[SovereignParams] = None,
commodity_params: Optional[CommodityParams] = None,
stress_params: Optional[StressParams] = None,
mc_runs: int = 0,
shock_spec: Optional[ShockSpec] = None,
export_path: Optional[str] = None,
scenario: Optional[str] = None,
cbdc_params: Optional[Any] = None,
ccp_params: Optional[Any] = None,
) -> dict[str, Any]:
"""
Run full workbook: Sheet 1 → 2 → 4,5,6 → 3,7 → 8. Optionally export to Excel.
If scenario is set (e.g. 'asia_1997', 'gfc_2008', 'pandemic_2020', 'rate_shock_2022'),
initial state and params are overridden by the Part XI preset.
"""
if scenario:
from fqbm.scenarios import get_scenario
preset = get_scenario(scenario)
if preset:
state = preset.state.copy()
cb_params = cb_params or preset.cb_params
bank_params = bank_params or preset.bank_params
fx_params = fx_params or preset.fx_params
sovereign_params = sovereign_params or preset.sovereign_params
commodity_params = commodity_params or preset.commodity_params
shock_spec = shock_spec or preset.shock_spec
else:
state = (initial_state or FQBMState()).copy()
else:
state = (initial_state or FQBMState()).copy()
state = central_bank_step(state, cb_params)
state = commercial_bank_step(state, bank_params)
state = fx_parity_step(state, fx_params)
state = sovereign_debt_step(state, sovereign_params)
state = commodity_step(state, commodity_params)
# Part IX: optional CBDC deposit shift (ΔDeposits = X, ΔR = X, ΔCBDC liability = +X)
cbdc_result = None
if cbdc_params is not None:
from fqbm.sheets.cbdc import deposit_to_cbdc_shift, CBDCParams
p = cbdc_params if isinstance(cbdc_params, CBDCParams) else CBDCParams(deposit_shift=getattr(cbdc_params, "deposit_shift", 0))
if p.deposit_shift != 0:
d = deposit_to_cbdc_shift(p.deposit_shift)
state.Deposits += d["d_deposits"]
state.R += d["d_reserves"]
cbdc_result = {"d_deposits": d["d_deposits"], "d_reserves": d["d_reserves"], "cbdc_liability": d["d_cbdc_liability"]}
stress = stress_tables(state, stress_params)
if mc_runs > 0:
mc_df = run_n_simulations(mc_runs, state, shock_spec)
else:
mc_df = None
dashboard = dashboard_aggregate(state, mc_runs=mc_runs, shock_spec=shock_spec)
# Part VIII: optional CCP metrics (margin identity check)
ccp_result = None
if ccp_params is not None:
try:
from fqbm.sheets.ccp import ccp_identity, default_waterfall_triggered, CCPParams as CCPParamsClass
if isinstance(ccp_params, CCPParamsClass):
margin_posted, margin_obligations = ccp_params.margin_posted, ccp_params.margin_obligations
vm_calls, liquidity_buffer = ccp_params.vm_calls, ccp_params.liquidity_buffer
else:
margin_posted = getattr(ccp_params, "margin_posted", 0)
margin_obligations = getattr(ccp_params, "margin_obligations", 0)
vm_calls = getattr(ccp_params, "vm_calls", 0)
liquidity_buffer = getattr(ccp_params, "liquidity_buffer", 0)
ccp_result = {"ccp_identity_holds": ccp_identity(margin_posted, margin_obligations), "waterfall_triggered": default_waterfall_triggered(vm_calls, liquidity_buffer)}
except Exception:
pass
result = {
"state": state,
"stress": stress,
"mc_results": mc_df,
"dashboard": dashboard,
"cbdc": cbdc_result,
"ccp": ccp_result,
}
if export_path and export_path.endswith((".xlsx", ".xls")):
_export_excel(result, export_path, include_ipsas=True)
return result
def main() -> None:
"""CLI entry point: run workbook and print dashboard summary."""
state = FQBMState(R=200, Deposits=1000, Loans=900, E_b=100, B=500, E_cb=50)
result = run_workbook(
initial_state=state,
mc_runs=100,
export_path="fqbm_workbook.xlsx",
)
print("Dashboard ratios:", result["dashboard"].get("ratios", {}))
if result["dashboard"].get("mc_summary"):
print("MC summary:", result["dashboard"]["mc_summary"])
print("Done. Export: fqbm_workbook.xlsx")
_STATE_NAMES = ["B", "R", "C", "Loans", "Deposits", "E_cb", "E_b", "S", "K", "Spread", "O", "L_cb"]
def _export_excel(result: dict[str, Any], path: str, include_ipsas: bool = True) -> None:
"""Write workbook results to Excel (optional dependency openpyxl). IPSAS sheet added when include_ipsas=True."""
try:
from openpyxl import Workbook
except ImportError:
return
wb = Workbook()
ws = wb.active
ws.title = "Dashboard"
state = result["state"]
ws.append(["Variable", "Value"])
for name, val in zip(_STATE_NAMES, state.to_vector()):
ws.append([name, val])
if include_ipsas:
try:
from fqbm.ipsas.presentation import statement_of_financial_position
for entity, sheet_name in [("central_bank", "IPSAS_CB"), ("commercial_bank", "IPSAS_Bank")]:
df = statement_of_financial_position(state, entity=entity)
ws_ipsas = wb.create_sheet(sheet_name)
ws_ipsas.append(list(df.columns))
for _, row in df.iterrows():
ws_ipsas.append(row.tolist())
except Exception:
pass
stress = result["stress"]
for sheet_name, df in [("Liquidity Stress", stress["liquidity_stress"]), ("Capital Stress", stress["capital_stress"])]:
ws2 = wb.create_sheet(sheet_name)
ws2.append(list(df.columns))
for _, row in df.iterrows():
ws2.append(row.tolist())
if result.get("mc_results") is not None:
ws3 = wb.create_sheet("Monte Carlo")
df = result["mc_results"]
ws3.append(list(df.columns))
for _, row in df.head(1000).iterrows():
ws3.append(row.tolist())
wb.save(path)
if __name__ == "__main__":
main()

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