Break-cost calculator¶
| ID | MOD-163 |
| System | SD05 |
| Repo | bank-credit |
| Build status | Deployed |
| Deployed | Yes |
| Last commit | 19b0610 |
Purpose¶
Calculates the break cost or benefit when a fixed-rate loan component is prepaid, rolled over before maturity, or otherwise terminated before its contracted end date. Serves two call paths: on-demand indicative quotes (available to the customer at any time without triggering account action) and binding calculations (required by MOD-050 before any component change proceeds). Every calculation is immutably logged.
Context¶
The break-cost calculation is the single most important conduct and risk management feature of the Flexible Loan Facility. It is the mechanism by which the bank recovers (or passes back) the mark-to-market movement on the interest-rate position it hedged when the customer locked a rate. Getting this wrong — whether through formula error, stale market rates, or inconsistency between indicative and binding figures — creates both regulatory exposure and the specific conduct risk evidenced by the UK Tailored Business Loans episode.
This module implements the formula once, in a single service that is called by all paths — indicative, binding, and any future batch re-valuation. There is no separate or simplified formula for any call path.
Formula¶
In the simplified annuity form used for standard components:
break_cost = (contracted_rate − current_market_rate) × outstanding_principal × annuity_factor(discount_rate, remaining_months)
Where:
- contracted_rate — the fixed rate locked at component establishment, from credit.loan_facility_components
- current_market_rate — the mid-market swap rate for the residual tenor in months, sourced from MOD-085 at the moment of calculation
- outstanding_principal — the current outstanding principal of the component
- discount_rate — the current market rate for the component tenor (same as current_market_rate for a par-swap valuation)
- remaining_months — months between calculation date and the component maturity date (floor: 0)
A positive result means the customer pays the bank (rates have fallen since fixing). A negative result is a break benefit; the bank pays the customer in full per CRE-009.
Formula version is stored on every calculation row as formula_version (e.g. v1.0.0). A formula change requires a version increment.
Data model¶
credit.break_cost_calculations¶
Cat 1 immutable via trg_break_cost_calculations_immutable (ADR-048, reuses credit.fn_immutable_row()). One row per calculation call.
| Column | Type | Notes |
|---|---|---|
| id | uuid | PK |
| component_id | uuid | NOT NULL FK → credit.loan_facility_components |
| facility_id | uuid | NOT NULL FK → credit.loan_facilities |
| customer_id | uuid | NOT NULL |
| calculation_type | text | NOT NULL CHECK ('INDICATIVE','BINDING') |
| contracted_rate | numeric(8,6) | NOT NULL |
| current_market_rate | numeric(8,6) | NOT NULL — rate sourced from MOD-085 |
| market_rate_tenor_months | int | NOT NULL |
| market_rate_source | text | NOT NULL — identifies the MOD-085 feed and instrument |
| market_rate_timestamp | timestamptz | NOT NULL — timestamp of the rate from MOD-085 |
| outstanding_principal | numeric(18,2) | NOT NULL |
| remaining_months | int | NOT NULL |
| discount_rate | numeric(8,6) | NOT NULL |
| break_cost_amount | numeric(18,2) | NOT NULL — positive = customer pays; negative = bank pays benefit |
| currency | char(3) | NOT NULL |
| formula_version | text | NOT NULL DEFAULT 'v1.0.0' |
| acknowledgement_id | text | NULL — set for BINDING type once MOD-050 acknowledgement is confirmed |
| acknowledged_at | timestamptz | NULL |
| calculated_by | text | NOT NULL CHECK ('CUSTOMER','SYSTEM','ADMIN') |
| trace_id | text | NULL |
| calculated_at | timestamptz | NOT NULL DEFAULT now() |
Index: (component_id, calculation_type, calculated_at DESC) for current binding calculation lookup.
Handlers¶
calculate-indicative — Synchronous API callable by MOD-164 (customer-facing) and any internal caller. Reads component data from MOD-162, fetches current market rate from MOD-085 for the residual tenor, runs the formula, inserts a calculation_type='INDICATIVE' row, and returns the result. No account action is triggered. Response p99 ≤ 500 ms (NFR-007 applies).
calculate-binding — Invoked when a customer confirms intent to terminate or roll a fixed-rate component early. Runs the same formula but inserts calculation_type='BINDING'. Returns the calculation_id. The caller (MOD-164 or MOD-132) passes calculation_id to MOD-050 to trigger the break-cost acknowledgement disclosure. The component cannot be changed until MOD-050 returns a confirmed acknowledgement ID.
confirm-acknowledgement — Called by MOD-050 when the customer acknowledges the binding disclosure. Updates the binding row with acknowledgement_id and acknowledged_at. Emits bank.credit.break_cost_acknowledged for MOD-162 to action the component change.
Market rate staleness¶
The MOD-085 rate feed has a configured maximum age. If the cached rate is older than the staleness threshold (default: 15 minutes), calculate-indicative returns a RATE_STALE warning alongside the result; calculate-binding returns a RATE_STALE error and does not produce a binding calculation until a fresh rate is available. Binding calculations on stale rates are prohibited — the customer could be shown an incorrect break cost and acknowledge a figure that does not reflect the actual unwind cost.
Break benefit¶
When break_cost_amount is negative, the result is a break benefit. CRE-009 requires the bank to pass this benefit to the customer in full. The binding calculation handler, when producing a break benefit, sets a benefit_payable flag in the bank.credit.break_cost_acknowledged event. MOD-001 is responsible for posting the benefit credit to the customer's account via the standard payments path. The break-cost calculator does not post to the general ledger directly.
Consistency with MOD-116¶
MOD-116 (Mortgage servicing engine) uses the same break-cost formula for simple fixed-rate mortgage components. Both modules source market rates from MOD-085 and use the same PV formula. In v1, each module maintains its own implementation. A shared calculation library (@bank/break-cost) should be extracted as a v2 improvement to eliminate the duplicate and ensure formula changes are applied atomically across both modules.
Implementation notes¶
The formula must be deterministic for any given set of inputs. Given the same contracted rate, market rate, principal, remaining term, and discount rate, the formula must return the same result regardless of when or by whom it is called. This is a testability requirement (pure function in the core calculation service) and a regulatory requirement (CRE-009 formula consistency).
The market_rate_tenor_months must match the component's actual remaining term to the nearest available tenor in the MOD-085 rate curve, not the original component term. If the remaining term falls between two available tenors on the curve, interpolate linearly. Document the interpolation method in the design doc.
Module dependencies¶
Depends on¶
| Module | Title | Required? | Contract | Reason |
|---|---|---|---|---|
| MOD-162 | Loan facility & component manager | Required | — | Component principal, contracted rate, term, start date, and maturity date are read from the loan facility component record to compute remaining term and outstanding principal. |
| MOD-085 | Market rates ingestion & normalisation | Required | contract/events/ |
MOD-085 writes mid-market swap curve data into the credit.swap_rates_mirror Postgres table on each Snowflake refresh and emits bank.risk-platform.swap_curve_updated; MOD-163 reads the mirror table at calculation time — no inline Lambda call (ADR-046 compliance ruling 2026-05-18). |
| MOD-050 | Disclosure enforcement module | Required | — | Binding break-cost disclosures are delivered and acknowledged via the disclosure enforcement module; the acknowledgement record ID must be verified before any component termination proceeds. |
| MOD-103 | Neon database platform bootstrap | Required | — | Neon database and schema provisioned by MOD-103 must exist before this module can create credit.break_cost_calculations. |
| MOD-104 | AWS shared infrastructure bootstrap | Required | — | AWS shared infrastructure provisioned by MOD-104 (SSM parameters, KMS, Lambda execution role) is required before this module can be deployed. |
Required by¶
| Module | Title | As | Contract |
|---|---|---|---|
| MOD-132 | Loan restructure and variation workflow | Hard dependency | — |
| MOD-164 | Facility component self-service | Hard dependency | — |
Policies satisfied¶
| Policy | Title | Mode | How |
|---|---|---|---|
| CRE-009 | Fixed-Rate Component Break-Cost Methodology Policy | CALC |
Break cost is computed using the documented present-value formula against live market rates sourced from MOD-085; every calculation — indicative and binding — is immutably logged with contracted rate, current market rate, source timestamp, remaining principal, remaining term, and the resulting amount; the formula is version-controlled and produces identical results to any customer-facing quotation. |
| CON-005 | Fee & Pricing Transparency Policy | GATE |
The binding break-cost calculation is delivered to the customer via MOD-050 for acknowledgement before any fixed-rate component early termination or pre-maturity rollover is processed; the component status handler verifies the acknowledgement record ID before proceeding; no code path bypasses this gate. |
Capabilities satisfied¶
(No capabilities mapped)
Part of SD05 — Credit Decisioning & Loan Platform
Compiled 2026-05-22 from source/entities/modules/MOD-163.yaml