Skip to content

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)