MOD-032 — LCR / NSFR Calculator
Status: Built (pending CI)
Repo: bank-risk-platform
System: SD06
Phase: 2 (depends on MOD-085 — Built+Deployed 2026-05-04)
ADRs: ADR-002 (Snowflake analytics), ADR-039 (per-domain schemas), ADR-046 (data-product architecture), ADR-054 (DCM v2)
A Snowflake-native LCR / NSFR calculator. Continuous (not T+1) computation
of the Basel III liquidity coverage and net stable funding ratios from
CDC-replicated balance-sheet data, with FX conversion and discount-curve
weighting from MOD-085 market views. Snowflake Alert detects breaches and
publishes via SNS → Lambda → EventBridge liquidity_mat_breach to MOD-076
for treasury escalation. Per-run audit trail in append-only
RISK_CAPITAL.MODEL_RUNS for the 7-year regulatory retention.
Functional requirements
| ID |
Requirement |
How satisfied |
| FR-201 |
Daily LCR + NSFR per applicable reporting entity, sourced from core ledger HQLA + funding data |
liquidity_positions Dynamic Table, INCREMENTAL refresh, target_lag = 1 hour |
| FR-202 |
Results available in prudential dashboard ≤ 5 min after EOD ledger snapshot |
DT target_lag well below NFR-008's 5-min ceiling; dbt incremental refresh propagates within seconds |
| FR-203 |
Alert treasury + finance ≤ 5 min from breach detection |
Snowflake Alert (5-min schedule) → SNS → alert-publish Lambda → EventBridge liquidity_mat_breach → MOD-076 |
| FR-204 |
Record input snapshot, methodology version, and results for every run; 7-year retention |
RISK_CAPITAL.MODEL_RUNS append-only audit table; immutability enforced by absent UPDATE/DELETE/TRUNCATE grants |
| NFR-006 |
Interest accrual posted by 23:59 daily |
Transitive — interest accrual runs in MOD-006/MOD-007 (out-of-scope); MOD-032 reads the post-accrual snapshot |
| NFR-008 |
Capital/liquidity metrics available ≤ 5 min after material event |
DT target_lag = 1 hour is the worst-case bound; incremental refresh on actual material change is seconds |
| NFR-024 |
Audit log mutability = 0 modifications allowed |
MODEL_RUNS has no UPDATE/DELETE/TRUNCATE grants on any role; verified by tests/integration/fr-204-immutability.test.ts |
Policies satisfied
| Policy |
Mode |
Test |
| CLQ-002 |
CALC |
tests/policy/CLQ-002-calc.test.ts (structural) — DT materialization, INCREMENTAL refresh, 1-hour target_lag, RESUME post-hook. Numerical correctness exercised by tests/unit/severity.test.ts and the integration LIQUIDITY_POSITIONS row inspection. |
| REP-002 |
CALC |
tests/policy/REP-002-calc.test.ts — single DT feeds both internal monitoring (V_LCR_CURRENT / V_NSFR_CURRENT) and the regulatory return path (MOD-036 reads LIQUIDITY_POSITIONS via the SSM-published table reference). |
| GOV-002 |
ALERT |
tests/policy/GOV-002-alert.test.ts (structural) + tests/integration/aws-resources.test.ts (live SNS topic + subscription + Lambda) — automatic escalation path declared end-to-end; manual monitoring never enters the loop. |
Snowflake objects
| Object |
Type |
Owner |
Notes |
BANK_{ENV}_RISK.RISK_CAPITAL |
schema |
DCM v2 (this module + MOD-033 share) |
define schema is idempotent — safe co-ownership |
RISK_CAPITAL.MODEL_RUNS |
table |
DCM v2 |
FR-204 audit log; INSERT-only grant; immutability test |
RISK_CAPITAL.CONFIG |
table |
DCM v2 |
CLQ-002 / GOV-002 thresholds (regulatory_minimum, mat, raf); effective-from/to versioned |
RISK_CAPITAL.LIQUIDITY_POSITIONS |
Dynamic Table |
dbt |
INCREMENTAL, target_lag 1h, cluster (position_date, ratio_type), post_hook RESUME |
RISK_CAPITAL.V_LCR_CURRENT |
view |
dbt |
ADR-046 §3 published contract — current LCR per jurisdiction |
RISK_CAPITAL.V_NSFR_CURRENT |
view |
dbt |
ADR-046 §3 published contract — current NSFR per jurisdiction |
RISK_CAPITAL.MAT_BREACH_ALERT |
Alert |
DCM v2 (post-dbt) |
5-min schedule; reads V_LCR_CURRENT / V_NSFR_CURRENT against CONFIG; publishes via BANK_SNS_INTEGRATION |
SSM contract
Reads (consumed)
| SSM path |
From |
/bank/{env}/eventbridge/bank-risk-platform/arn |
MOD-104 |
/bank/{env}/eventbridge/bank-risk-platform/dlq-arn |
MOD-104 |
/bank/{env}/iam/lambda/bank-risk-platform/arn |
MOD-104 |
/bank/{env}/observability/adot-layer-arn |
MOD-076 |
/bank/{env}/sns/alerts/arn |
MOD-104 |
/bank/{env}/snowflake/account-locator |
MOD-102 |
/bank/{env}/snowflake/databases/risk |
MOD-102 |
/bank/{env}/snowflake/warehouses/etl |
MOD-102 |
/bank/{env}/snowflake/roles/domain-{nonprod,prod} |
MOD-102 (template) |
/bank/{env}/risk-platform/market/swap-curve-table |
MOD-085 |
/bank/{env}/risk-platform/market/ois-curve-table |
MOD-085 |
Writes (published)
| SSM path |
Value |
Consumed by |
/bank/{env}/risk-platform/liquidity/positions-table |
RISK_CAPITAL.LIQUIDITY_POSITIONS |
MOD-034, MOD-036 |
/bank/{env}/risk-platform/liquidity/lcr-view |
RISK_CAPITAL.V_LCR_CURRENT |
MOD-034, MOD-036 |
/bank/{env}/risk-platform/liquidity/nsfr-view |
RISK_CAPITAL.V_NSFR_CURRENT |
MOD-034, MOD-036 |
/bank/{env}/risk-platform/liquidity/event-source-name |
bank.risk-platform |
SD04 EB rules; MOD-076 |
/bank/{env}/risk-platform/liquidity/alert-publish-arn |
Lambda ARN |
Ops |
/bank/{env}/risk-platform/liquidity/alert-topic-arn |
SNS topic ARN |
MOD-102 SNS integration setup |
EventBridge contract
| Direction |
Bus |
Source |
Detail-type |
Schema |
| Publish |
bank-risk-platform-{env} |
bank.risk-platform |
liquidity_mat_breach |
{ trace_id, run_id, ratio_type: "LCR"\|"NSFR", jurisdiction: "NZ"\|"AU", ratio_value, regulatory_minimum, mat_threshold, raf_threshold, breach_severity: "regulatory_minimum"\|"mat"\|"raf", position_date, calculated_at } |
No subscriptions. Market data (MOD-085) is consumed via dbt source() per
ADR-046 §2 — not the market_rates_updated event.
Dependencies
| Module |
Why |
What's read |
| MOD-042 |
CDC pipeline lands RAW_CDC_CORE.BALANCES, RAW_CDC_PAYMENTS.NOSTRO_POSITIONS, RAW_CDC_CREDIT.FUNDING_BOOK |
dbt adapter.get_relation lazy-resolved (bootstrap-resilient — empty stub branch when CDC absent) |
| MOD-085 |
Market curves for NSFR term-structure RSF + LCR discount factor |
dbt source() against MARKET.V_SWAP_CURVE, MARKET.V_OIS_CURVE |
| MOD-102 |
Snowflake account, RISK database, domain roles, BANK_SNS_INTEGRATION |
SSM reads |
| MOD-104 |
AWS bootstrap (EB bus, Lambda IAM role, ADOT layer, SNS, KMS) |
SSM reads |
V1 limitations + future work
- HQLA inflow projection not implemented. V1 uses the Basel III
conservative form
net_cash_outflows ≥ outflows × 0.25 (75% inflow cap).
A future revision will add the inflow projection from RAW_CDC_PAYMENTS.PAYMENT_FLOWS
once that CDC table is materialised in dev.
- NSFR term-structure adjustment is static. V1 uses the per-record
rsf_factor set upstream by treasury. A future revision will join
stg_market_curves to inform a dynamic factor.
- Stress scenarios.
liquidity_positions has a stress_scenario
column (matches SD06 wiki schema) but V1 emits NULL — base case only.
MOD-034 will write a separate liquidity_positions_stress projection.
- CDC sources are bootstrap-resilient. When MOD-042 lands the
RAW_CDC_* schemas in dev, replace the adapter.get_relation
branches in stg_*.sql with {{ source(...) }} — the published
contract stays identical so downstream models don't change.
V1 numerical reference
For the integration test tests/integration/fr-201-lcr-calc.test.ts
(planned — out of scope this initial commit):
Inputs (NZ jurisdiction, single test fixture):
Cash + central bank reserves = 100m NZD (L1, 100% factor)
Sovereign AAA bonds = 50m NZD (L1, 100% factor)
Corporate AA bonds = 60m NZD (L2A, 85% factor)
Retail deposits, stable = 80m NZD × 5% runoff_rate_30d
Wholesale deposits, op'l = 20m NZD × 25% runoff_rate_30d
Computed:
HQLA L1 = 100m + 50m = 150m
HQLA L2A = 60m × 0.85 = 51m
L2A cap = min(51m, 150m × 40/60) = min(51m, 100m) = 51m
HQLA total = 150m + 51m = 201m
Outflows = 80m × 0.05 + 20m × 0.25 = 4m + 5m = 9m
Net 30d = 9m × 0.25 = 2.25m (75% inflow cap)
LCR ratio = 201m / 2.25m = 89.33 (well above regulatory 1.0)