Skip to content

MOD-086 — Funds Transfer Pricing Engine

Status: Built (pending CI) Repo: bank-risk-platform System: SD06 Phase: 2 (depends on MOD-085 — Built+Deployed) ADRs: ADR-002, ADR-039, ADR-046, ADR-054

The bank's FTP framework in Snowflake. After EOD curve publication by MOD-085, computes a daily nine-tenor TP rate grid (ON, 1M, 3M, 6M, 1Y, 2Y, 3Y, 5Y, 10Y) per jurisdiction by blending market.swap_curve + market.ois_curve and overlaying a Treasury-configured liquidity premium. Joins the grid to every active loan and deposit balance from the CDC replica to produce daily NIM attribution by product code × business line × jurisdiction. A write-back Lambda upserts the TP grid to SD01 Postgres treasury.tp_rates for SD05 (credit decisioning) and SD01 (product rate config).

Functional requirements

ID Requirement How satisfied
FR-385 Daily TP grid for 9 tenor buckets/jurisdiction from MOD-085 curves + Treasury premium transfer_prices DT, FULL refresh, 30-min target_lag
FR-386 TP grid written to ftp.transfer_prices + SD01 treasury.tp_rates within 30 min of EOD DT lag (30 min) + cron schedule firing 35 min after EOD; write-back Lambda upserts both surfaces
FR-387 Daily NIM attribution by product/business_line, allocating TP cost/benefit per balance nim_attribution DT, FULL refresh, 4-hour target_lag
FR-388 Full version history of TP grids — effective_date, curve_source_version, premium_bps; ≥7-year retention ftp.writeback_runs audit log; transfer_prices DT retains daily history; NFR-024 immutability via absent UPDATE/DELETE/TRUNCATE on consumer roles
NFR-014 Snowflake write-back ≤ 60s Lambda timeout 120s; pg connection pool 1; 18-row UPSERT typically < 5s
NFR-019 RTO ≤ 4h / RPO ≤ 1h Tier-1 Daily run on cron; recovery = next scheduled fire (or manual invoke)
NFR-024 Audit log mutability = 0 ftp.writeback_runs SHOW GRANTS test confirms BANK_DBT_ROLE has no UPDATE/DELETE/TRUNCATE

Policies satisfied

Policy Mode Test
REP-001 CALC tests/policy/REP-001-calc.test.ts (DT shape, refresh mode, target_lag, cluster keys per SD06 wiki) + tests/integration/snowflake-objects.test.ts (live config)
CLQ-003 CALC tests/policy/CLQ-003-calc.test.ts — int_tp_rates SQL unconditionally joins LIQUIDITY_PREMIUM_CONFIG and computes tp_rate_bps = base_curve_bps + premium_bps (no bypass path)

Snowflake objects

Object Type Owner Notes
BANK_{ENV}_RISK.FTP schema DCM v2 (single-project, MOD-086 only) New schema; no co-ownership concerns
FTP.LIQUIDITY_PREMIUM_CONFIG table DCM v2 + pre-dbt seed 18 rows (9 tenors × NZ/AU); Treasury overrides via UPSERT
FTP.WRITEBACK_RUNS table DCM v2 FR-388 audit log; INSERT-only on consumer roles
FTP.TRANSFER_PRICES Dynamic Table dbt FULL refresh, 30-min target_lag, cluster (rate_date, jurisdiction), target.warehouse, post_hook RESUME. Schema matches SD06 wiki §ftp.transfer_prices exactly
FTP.NIM_ATTRIBUTION Dynamic Table dbt FULL refresh, 4-hour target_lag, cluster (attribution_date, business_line). Schema matches SD06 wiki §ftp.nim_attribution exactly
FTP.V_TRANSFER_PRICES_CURRENT view dbt ADR-046 §3 published contract
FTP.V_NIM_ATTRIBUTION_DAILY view dbt ADR-046 §3 published contract

The wiki does not yet have entries for ftp.liquidity_premium_config or ftp.writeback_runs. See docs/handoffs/MOD-086-sd06-wiki-update.handoff.md for the requested data-model addition.

SSM contract

Reads

SSM path From
/bank/{env}/eventbridge/bank-risk-platform/arn, /dlq-arn MOD-104
/bank/{env}/iam/lambda/bank-risk-platform/arn MOD-104
/bank/{env}/observability/adot-layer-arn MOD-076
/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
/bank/{env}/neon/direct-host MOD-103
/bank/{env}/snowflake/databases/core MOD-102 (used to derive Neon DB name)

Plus the Secrets Manager secret bank-risk-platform/{env}/sd01-tp-writeback for the SD01 Postgres write-back username/password (provisioned by MOD-104 / bank-platform secret-bootstrap).

Writes

SSM path Value Consumed by
/bank/{env}/risk-platform/ftp/transfer-prices-view FTP.V_TRANSFER_PRICES_CURRENT Management accounts, MOD-080, write-back
/bank/{env}/risk-platform/ftp/nim-attribution-view FTP.V_NIM_ATTRIBUTION_DAILY Management accounts, MOD-080
/bank/{env}/risk-platform/ftp/transfer-prices-table FTP.TRANSFER_PRICES History queries
/bank/{env}/risk-platform/ftp/nim-attribution-table FTP.NIM_ATTRIBUTION History queries
/bank/{env}/risk-platform/ftp/writeback-lambda-arn Lambda ARN Ops

Schedule

Time (UTC) What Why
22:00 NZT (10:00 UTC) EOD reference Curve publication target (MOD-085)
10:30 UTC transfer_prices DT refresh 30-min target_lag from EOD
10:35 UTC Write-back Lambda (cron) 5-minute buffer past DT refresh
14:00 UTC (4h post-EOD) nim_attribution DT refresh 4-hour target_lag for the larger aggregate

EventBridge contract

No custom event publish — positive-flow data sync, not breach detection. The Lambda runs on mod-086-writeback-schedule-{env} cron rule.

Dependencies

Module Why Status
MOD-042 CDC of RAW_CDC_CORE.BALANCES + RAW_CDC_CORE.ACCOUNT_PRODUCTS for NIM attribution Built; lazy-resolved by stg_balances.sql
MOD-085 MARKET.V_SWAP_CURVE, V_OIS_CURVE via dbt source() Built+Deployed
MOD-102 Snowflake account, RISK database, domain roles Built
MOD-103 Neon SD01 (bank_core) connection details for write-back Built
MOD-104 Lambda IAM, EB bus, ADOT, KMS, SNS Built
bank-core (SD01) treasury.tp_rates table (Flyway migration) Pending — see handoff

V1 limitations + future work

  • treasury.tp_rates migration pending in bank-core. The write-back Lambda will fail on every invocation with TABLE_NOT_FOUND until bank-core ships the Flyway migration (handoff filed: docs/handoffs/MOD-086-bank-core-treasury-tp-rates-migration.handoff.md). The Lambda fails gracefully with a structured error and records status='FAILED' + error_detail in ftp.writeback_runs. Once the table exists, subsequent invocations succeed without redeploy.
  • CDC RAW_CDC_CORE.BALANCES / ACCOUNT_PRODUCTS are bootstrap-resilient. When CDC isn't materialised yet, stg_balances returns 0 rows and nim_attribution is empty. No redeploy needed when CDC arrives — the lazy adapter.get_relation branch swaps automatically.
  • Treasury liquidity premium is a flat 50 bp seed in dev. Treasury policy in real envs reviews quarterly; overrides via UPSERT on FTP.LIQUIDITY_PREMIUM_CONFIG with new effective_from + effective_to set on the prior row.

V1 numerical reference

For the integration test fixture (planned out-of-scope this commit):

Inputs:
  NZ 1Y swap rate              = 4.75%   (475 bps base_curve_bps)
  NZ 1Y liquidity premium      = 50 bps   (LIQUIDITY_PREMIUM_CONFIG)
  TP rate                      = 525 bps

  Mortgage book balance        = $100m
  Mortgage customer rate       = 6.55%   (655 bps customer_rate_bps)
  margin_bps                   = 655 - 525 = 130 bps
  Daily NIM                    = $100m × 130 / 10000 / 365 = $3,562

After upsert to SD01 treasury.tp_rates:
  18 rows expected (9 tenors × NZ + AU)
  ON CONFLICT DO UPDATE on (rate_date, tenor_bucket, jurisdiction) —
  duplicate run on the same day converges to the same 18 rows.