# Banker's rounding (round-half-to-even) for interest calculations

- **Pattern:** `ab-001439` (`finserv-form-validation.currency-amount.bankers-rounding`)
- **Severity:** high
- **Lifecycle:** active
- **Last modified:** 2026-04-18
- **Canonical URL:** https://auditbuffet.com/patterns/ab-001439
- **License:** CC-BY-4.0 — attribute to AuditBuffet Pattern Catalog (https://auditbuffet.com/patterns/ab-001439)

## Why it matters

Standard `Math.round()` always rounds 0.5 upward, introducing a systematic positive bias in interest calculations. Over millions of compounding periods this bias is non-trivial and legally relevant — financial regulators and ISO 25010 functional-correctness standards expect rounding to be demonstrably unbiased. Banker's rounding (round-half-to-even) is the industry standard because it distributes rounding error evenly across the dataset. CWE-682 covers this class of incorrect calculation leading to incorrect financial results.

## Severity rationale

High because systematic rounding bias in interest accrual produces incorrect balances at scale, creating regulatory exposure and customer disputes.

## Remediation

Replace bare `Math.round()` calls in financial calculation logic with a banker's rounding implementation or a library that supports `ROUND_HALF_EVEN`. Centralize this in `src/lib/finance.ts`.

```ts
// src/lib/finance.ts
import Decimal from 'decimal.js';

export function roundToNearestCent(value: number): number {
  return new Decimal(value)
    .toDecimalPlaces(2, Decimal.ROUND_HALF_EVEN)
    .toNumber();
}
```

Search for all `Math.round`, `.toFixed`, and `Math.ceil` calls in `src/` and replace with `roundToNearestCent()` for any interest, APY, or fee calculation.

## Detection

- **ID:** `bankers-rounding`
- **Severity:** `high`
- **What to look for:** Count all rounding call sites in the codebase (search for `Math.round`, `Math.floor`, `Math.ceil`, `.toFixed`, `toDecimalPlaces`, `ROUND_HALF_EVEN`). For each, classify whether it uses banker's rounding or standard rounding. Quote the actual rounding expression found (e.g., `Math.round(interest * 100)` or `Decimal.ROUND_HALF_EVEN`). Report the ratio: "X of Y rounding call sites use banker's rounding."
- **Pass criteria:** At least 100% of interest, APY, and fee rounding call sites use banker's rounding (round-half-to-even) or a financial library (decimal.js, big.js, dinero.js) with `ROUND_HALF_EVEN`. No more than 0 financial rounding sites use `Math.round()`. Report even on pass: "X rounding call sites found, all using banker's rounding."
- **Fail criteria:** Any financial rounding site uses standard `Math.round()` (round-half-up), or no rounding strategy is documented for calculations that produce fractional cents.
- **Must not pass when:** A financial library is imported but rounding mode is not explicitly set to `ROUND_HALF_EVEN` — default modes vary by library.
- **Skip (N/A) when:** The project has no interest, APY, or fee calculations (0 rounding call sites found in financial logic after searching `src/`, `lib/`, `utils/`).
- **Detail on fail:** Describe the rounding issue. Example: `"Interest calculation uses Math.round() (round-half-up). At scale, this biases results upward. Should use banker's rounding."` or `"APY calculation has no rounding strategy documented"`
- **Cross-reference:** The Calculation Accuracy category's interest-rounding-consistency check verifies rounding is applied consistently across all locations.
- **Remediation:** Banker's rounding eliminates systematic bias. In `src/lib/finance.ts` or equivalent calculation utility:

  **Using decimal.js in `src/lib/finance.ts`:**
  ```ts
  import Decimal from 'decimal.js';
  const interestRate = new Decimal('0.045'); // 4.5% APY
  const principal = new Decimal('1000.00');
  const interest = principal.times(interestRate).toDecimalPlaces(2, Decimal.ROUND_HALF_EVEN);
  ```

  **Manual banker's rounding in `src/lib/finance.ts`:**
  ```ts
  function bankersRound(value) {
    const shifted = value * 100;
    const floor = Math.floor(shifted);
    const frac = shifted - floor;
    if (frac < 0.5) return floor;
    if (frac > 0.5) return floor + 1;
    // Exactly 0.5: round to nearest even
    return floor % 2 === 0 ? floor : floor + 1;
  }
  const roundedCents = bankersRound(15.505); // Returns 1551 (15.51)
  ```

## External references

- cwe CWE-682
- iso-25010 functional-correctness

Taxons: data-integrity

HTML version: https://auditbuffet.com/patterns/ab-001439
