Case Study 2: Amortization Schedule Generator

Background

Valley Mortgage Corporation (VMC) is a regional mortgage lender operating in six states, originating approximately 4,500 residential mortgage loans per year. When a borrower closes on a loan, VMC generates an amortization schedule -- a month-by-month breakdown showing the payment amount, the portion applied to interest, the portion applied to principal, and the remaining loan balance. This schedule is a regulatory requirement under the Truth in Lending Act (TILA) and is provided to the borrower at closing.

VMC's existing amortization program, written in 1998, generated schedules for fixed-rate loans only. In 2024, VMC expanded its product line to include adjustable-rate mortgages (ARMs) with annual rate adjustments, and the company needed a new amortization program that could handle both fixed-rate and adjustable-rate products, generate multi-loan comparison reports, and perform "what-if" analysis for loan officers.

Angela Foster, a COBOL developer with 16 years of experience in mortgage processing, was assigned to build the new amortization schedule generator. Her implementation demonstrates PERFORM VARYING for iterating through payment periods, PERFORM UNTIL for convergence calculations, nested PERFORM for multi-loan comparison, and the standard COBOL patterns for table-based report generation.

This case study follows Angela's design and implementation, showing how the PERFORM statement in its various forms drives the iterative calculations at the heart of mortgage processing.

The Requirements

The amortization program must:

  1. Accept loan parameters: principal amount, annual interest rate, term in years, and loan type (fixed or ARM)
  2. Calculate the monthly payment using the standard amortization formula
  3. Generate a month-by-month schedule showing payment number, payment date, payment amount, interest portion, principal portion, and remaining balance
  4. For ARM loans, adjust the interest rate annually and recalculate the monthly payment
  5. Support multi-loan comparison: generate side-by-side schedules for up to three loan scenarios
  6. Display period subtotals at each yearly break (annual interest paid, annual principal paid)
  7. Display loan summary totals (total payments, total interest, total principal)

The Complete Program

       IDENTIFICATION DIVISION.
       PROGRAM-ID. AMORTIZE.
      *================================================================*
      * Program:     AMORTIZE
      * Description: Amortization Schedule Generator
      * Author:      Angela Foster
      * Date:        2024-05-20
      * Purpose:     Demonstrates PERFORM VARYING, PERFORM UNTIL,
      *              and nested PERFORM for iterative mortgage
      *              calculations at Valley Mortgage Corporation.
      *================================================================*

       DATA DIVISION.
       WORKING-STORAGE SECTION.

      *================================================================*
      * PROGRAM CONSTANTS                                              *
      *================================================================*
       01  WS-C-MONTHS-PER-YEAR      PIC 9(02) VALUE 12.
       01  WS-C-MAX-TERM-YEARS       PIC 9(02) VALUE 30.
       01  WS-C-MAX-PERIODS           PIC 9(03) VALUE 360.
       01  WS-C-MAX-LOANS             PIC 9(01) VALUE 3.
       01  WS-C-ARM-ADJ-INTERVAL     PIC 9(02) VALUE 12.
       01  WS-C-PENNY-THRESHOLD      PIC 9(01)V99 VALUE 0.01.
       01  WS-C-CONVERGENCE-LIMIT    PIC 9(01)V9(06)
           VALUE 0.000001.
       01  WS-C-MAX-ITERATIONS       PIC 9(03)  VALUE 100.

       01  WS-C-REPORT-SEP           PIC X(72) VALUE ALL '='.
       01  WS-C-DETAIL-SEP           PIC X(72) VALUE ALL '-'.
       01  WS-C-YEAR-SEP             PIC X(72) VALUE ALL '*'.

      *================================================================*
      * LOAN INPUT PARAMETERS                                          *
      *================================================================*
       01  WS-LOAN-PARAMS.
           05  WS-LOAN-PRINCIPAL      PIC 9(09)V99.
           05  WS-LOAN-ANNUAL-RATE    PIC 9(02)V9(04).
           05  WS-LOAN-TERM-YEARS     PIC 9(02).
           05  WS-LOAN-TERM-MONTHS    PIC 9(03).
           05  WS-LOAN-TYPE           PIC X(01).
               88  WS-LOAN-FIXED                 VALUE 'F'.
               88  WS-LOAN-ARM                   VALUE 'A'.
           05  WS-LOAN-ARM-MARGIN     PIC 9(02)V9(04).
           05  WS-LOAN-ARM-INDEX      PIC 9(02)V9(04).
           05  WS-LOAN-ARM-CAP-PER    PIC 9(02)V9(04).
           05  WS-LOAN-ARM-CAP-LIFE   PIC 9(02)V9(04).
           05  WS-LOAN-DESCRIPTION    PIC X(30).

      *================================================================*
      * CALCULATION WORK FIELDS                                        *
      *================================================================*
       01  WS-WK-CALC-FIELDS.
           05  WS-WK-MONTHLY-RATE    PIC 9(01)V9(08) VALUE ZERO.
           05  WS-WK-NUM-PAYMENTS    PIC 9(03)        VALUE ZERO.
           05  WS-WK-PAYMENT-AMT     PIC 9(07)V99     VALUE ZERO.
           05  WS-WK-INTEREST-AMT    PIC 9(07)V99     VALUE ZERO.
           05  WS-WK-PRINCIPAL-AMT   PIC 9(07)V99     VALUE ZERO.
           05  WS-WK-REMAINING-BAL   PIC S9(09)V99    VALUE ZERO.
           05  WS-WK-POWER-FACTOR    PIC 9(05)V9(10)  VALUE ZERO.
           05  WS-WK-NUMERATOR       PIC 9(09)V9(08)  VALUE ZERO.
           05  WS-WK-DENOMINATOR     PIC 9(09)V9(08)  VALUE ZERO.
           05  WS-WK-CURRENT-RATE    PIC 9(02)V9(04)  VALUE ZERO.
           05  WS-WK-NEW-RATE        PIC 9(02)V9(04)  VALUE ZERO.
           05  WS-WK-ADJUSTMENT      PIC S9(07)V99    VALUE ZERO.

      *--- Annual subtotal accumulators ---
       01  WS-WK-ANNUAL-ACCUM.
           05  WS-WK-ANN-INTEREST    PIC 9(09)V99 VALUE ZERO.
           05  WS-WK-ANN-PRINCIPAL   PIC 9(09)V99 VALUE ZERO.
           05  WS-WK-ANN-PAYMENTS    PIC 9(09)V99 VALUE ZERO.

      *--- Loan total accumulators ---
       01  WS-WK-LOAN-TOTALS.
           05  WS-WK-TOTAL-PAYMENTS  PIC 9(11)V99 VALUE ZERO.
           05  WS-WK-TOTAL-INTEREST  PIC 9(11)V99 VALUE ZERO.
           05  WS-WK-TOTAL-PRINCIPAL PIC 9(11)V99 VALUE ZERO.

      *--- Convergence calculation fields ---
       01  WS-WK-CONVERGE-FIELDS.
           05  WS-WK-PREV-ESTIMATE   PIC 9(07)V9(06) VALUE ZERO.
           05  WS-WK-CURR-ESTIMATE   PIC 9(07)V9(06) VALUE ZERO.
           05  WS-WK-DIFFERENCE      PIC 9(07)V9(06) VALUE ZERO.
           05  WS-WK-ITERATION-CTR   PIC 9(03)        VALUE ZERO.

      *================================================================*
      * MULTI-LOAN COMPARISON TABLE                                    *
      *================================================================*
       01  WS-COMPARISON-TABLE.
           05  WS-COMP-ENTRY OCCURS 3 TIMES.
               10  WS-COMP-DESC      PIC X(30).
               10  WS-COMP-PRINCIPAL  PIC 9(09)V99.
               10  WS-COMP-RATE       PIC 9(02)V9(04).
               10  WS-COMP-TERM       PIC 9(02).
               10  WS-COMP-PAYMENT    PIC 9(07)V99.
               10  WS-COMP-TOT-INT    PIC 9(11)V99.
               10  WS-COMP-TOT-PAID   PIC 9(11)V99.
               10  WS-COMP-TYPE       PIC X(01).

      *================================================================*
      * LOOP CONTROL VARIABLES                                         *
      *================================================================*
       01  WS-PERIOD-INDEX           PIC 9(03)  VALUE ZERO.
       01  WS-YEAR-NUMBER            PIC 9(02)  VALUE ZERO.
       01  WS-MONTH-IN-YEAR          PIC 9(02)  VALUE ZERO.
       01  WS-LOAN-INDEX             PIC 9(01)  VALUE ZERO.
       01  WS-NUM-LOANS              PIC 9(01)  VALUE ZERO.

      *================================================================*
      * FLAGS                                                          *
      *================================================================*
       01  WS-F-OVERFLOW-FLAG        PIC X(01)  VALUE 'N'.
           88  WS-OVERFLOW-OCCURRED              VALUE 'Y'.
           88  WS-NO-OVERFLOW                    VALUE 'N'.

       01  WS-F-CONVERGED-FLAG       PIC X(01)  VALUE 'N'.
           88  WS-HAS-CONVERGED                  VALUE 'Y'.
           88  WS-NOT-CONVERGED                  VALUE 'N'.

       01  WS-F-YEAR-BREAK-FLAG      PIC X(01)  VALUE 'N'.
           88  WS-IS-YEAR-BREAK                  VALUE 'Y'.
           88  WS-NOT-YEAR-BREAK                 VALUE 'N'.

       01  WS-F-SHOW-DETAIL          PIC X(01)  VALUE 'Y'.
           88  WS-SHOW-MONTHLY-DETAIL            VALUE 'Y'.
           88  WS-SUMMARY-ONLY                   VALUE 'N'.

      *================================================================*
      * DISPLAY FIELDS                                                 *
      *================================================================*
       01  WS-DSP-AMOUNT             PIC $$$,$$$,MATH1MATH2$,$$9.99.
       01  WS-DSP-RATE               PIC Z9.9999.
       01  WS-DSP-PERIOD             PIC ZZ9.
       01  WS-DSP-YEAR               PIC Z9.
       01  WS-DSP-LABEL              PIC X(30).

       PROCEDURE DIVISION.
       0000-MAIN-CONTROL.
           PERFORM 1000-INITIALIZATION
           PERFORM 2000-GENERATE-SCHEDULES
           PERFORM 3000-MULTI-LOAN-COMPARISON
           STOP RUN
           .

      *================================================================*
      * 1000-INITIALIZATION                                            *
      *================================================================*
       1000-INITIALIZATION.
           DISPLAY WS-C-REPORT-SEP
           DISPLAY '  VALLEY MORTGAGE CORPORATION'
           DISPLAY '  AMORTIZATION SCHEDULE GENERATOR'
           DISPLAY WS-C-REPORT-SEP
           DISPLAY SPACES
           .

      *================================================================*
      * 2000-GENERATE-SCHEDULES: Process each loan scenario            *
      *================================================================*
       2000-GENERATE-SCHEDULES.
      *    Scenario 1: 30-year fixed rate
           PERFORM 2010-CLEAR-WORK-FIELDS
           PERFORM 2100-LOAD-LOAN-1
           PERFORM 2200-CALCULATE-PAYMENT
           PERFORM 2300-GENERATE-SCHEDULE
           PERFORM 2400-DISPLAY-LOAN-SUMMARY
           PERFORM 2500-STORE-COMPARISON-DATA

      *    Scenario 2: 15-year fixed rate (same principal)
           PERFORM 2010-CLEAR-WORK-FIELDS
           PERFORM 2100-LOAD-LOAN-2
           PERFORM 2200-CALCULATE-PAYMENT
           PERFORM 2300-GENERATE-SCHEDULE
           PERFORM 2400-DISPLAY-LOAN-SUMMARY
           PERFORM 2500-STORE-COMPARISON-DATA

      *    Scenario 3: 30-year ARM
           PERFORM 2010-CLEAR-WORK-FIELDS
           PERFORM 2100-LOAD-LOAN-3
           PERFORM 2200-CALCULATE-PAYMENT
           PERFORM 2300-GENERATE-SCHEDULE
           PERFORM 2400-DISPLAY-LOAN-SUMMARY
           PERFORM 2500-STORE-COMPARISON-DATA
           .

      *================================================================*
      * 2010-CLEAR-WORK-FIELDS: Reset per-loan fields                  *
      *================================================================*
       2010-CLEAR-WORK-FIELDS.
           INITIALIZE WS-WK-CALC-FIELDS
           INITIALIZE WS-WK-ANNUAL-ACCUM
           INITIALIZE WS-WK-LOAN-TOTALS
           INITIALIZE WS-WK-CONVERGE-FIELDS
           MOVE ZERO TO WS-PERIOD-INDEX
           MOVE ZERO TO WS-YEAR-NUMBER
           SET WS-NO-OVERFLOW TO TRUE
           SET WS-NOT-CONVERGED TO TRUE
           ADD 1 TO WS-NUM-LOANS
           .

      *================================================================*
      * 2100-LOAD-LOAN-1: 30-year fixed, 6.75%                        *
      *================================================================*
       2100-LOAD-LOAN-1.
           MOVE 350000.00        TO WS-LOAN-PRINCIPAL
           MOVE 06.7500          TO WS-LOAN-ANNUAL-RATE
           MOVE 30               TO WS-LOAN-TERM-YEARS
           COMPUTE WS-LOAN-TERM-MONTHS =
               WS-LOAN-TERM-YEARS * WS-C-MONTHS-PER-YEAR
           SET  WS-LOAN-FIXED    TO TRUE
           MOVE ZERO             TO WS-LOAN-ARM-MARGIN
           MOVE ZERO             TO WS-LOAN-ARM-INDEX
           MOVE ZERO             TO WS-LOAN-ARM-CAP-PER
           MOVE ZERO             TO WS-LOAN-ARM-CAP-LIFE
           MOVE '30-YR FIXED @ 6.75%'
               TO WS-LOAN-DESCRIPTION
           SET  WS-SUMMARY-ONLY  TO TRUE
           MOVE 1                TO WS-LOAN-INDEX
           .

      *================================================================*
      * Load Loan 2: 15-year fixed, 6.00%                             *
      *================================================================*
       2100-LOAD-LOAN-2.
           MOVE 350000.00        TO WS-LOAN-PRINCIPAL
           MOVE 06.0000          TO WS-LOAN-ANNUAL-RATE
           MOVE 15               TO WS-LOAN-TERM-YEARS
           COMPUTE WS-LOAN-TERM-MONTHS =
               WS-LOAN-TERM-YEARS * WS-C-MONTHS-PER-YEAR
           SET  WS-LOAN-FIXED    TO TRUE
           MOVE ZERO             TO WS-LOAN-ARM-MARGIN
           MOVE ZERO             TO WS-LOAN-ARM-INDEX
           MOVE ZERO             TO WS-LOAN-ARM-CAP-PER
           MOVE ZERO             TO WS-LOAN-ARM-CAP-LIFE
           MOVE '15-YR FIXED @ 6.00%'
               TO WS-LOAN-DESCRIPTION
           SET  WS-SUMMARY-ONLY  TO TRUE
           MOVE 2                TO WS-LOAN-INDEX
           .

      *================================================================*
      * Load Loan 3: 30-year ARM, 5.25% initial, 2% cap/adj           *
      *================================================================*
       2100-LOAD-LOAN-3.
           MOVE 350000.00        TO WS-LOAN-PRINCIPAL
           MOVE 05.2500          TO WS-LOAN-ANNUAL-RATE
           MOVE 30               TO WS-LOAN-TERM-YEARS
           COMPUTE WS-LOAN-TERM-MONTHS =
               WS-LOAN-TERM-YEARS * WS-C-MONTHS-PER-YEAR
           SET  WS-LOAN-ARM      TO TRUE
           MOVE 02.7500          TO WS-LOAN-ARM-MARGIN
           MOVE 03.5000          TO WS-LOAN-ARM-INDEX
           MOVE 02.0000          TO WS-LOAN-ARM-CAP-PER
           MOVE 05.0000          TO WS-LOAN-ARM-CAP-LIFE
           MOVE '30-YR ARM @ 5.25% INIT'
               TO WS-LOAN-DESCRIPTION
           SET  WS-SHOW-MONTHLY-DETAIL TO TRUE
           MOVE 3                TO WS-LOAN-INDEX
           .

      *================================================================*
      * 2200-CALCULATE-PAYMENT                                         *
      * Monthly payment formula: M = P * [r(1+r)^n] / [(1+r)^n - 1]  *
      * Uses COMPUTE with ROUNDED for precision.                       *
      *================================================================*
       2200-CALCULATE-PAYMENT.
      *    Convert annual rate to monthly rate
           IF WS-LOAN-ANNUAL-RATE > ZERO
               COMPUTE WS-WK-MONTHLY-RATE =
                   WS-LOAN-ANNUAL-RATE / 100 / 12
           ELSE
               MOVE ZERO TO WS-WK-MONTHLY-RATE
           END-IF

           MOVE WS-LOAN-TERM-MONTHS TO WS-WK-NUM-PAYMENTS
           MOVE WS-LOAN-ANNUAL-RATE TO WS-WK-CURRENT-RATE

           IF WS-WK-MONTHLY-RATE > ZERO
      *        Calculate (1 + r)^n using COMPUTE exponentiation
               COMPUTE WS-WK-POWER-FACTOR =
                   (1 + WS-WK-MONTHLY-RATE)
                   ** WS-WK-NUM-PAYMENTS
                   ON SIZE ERROR
                       SET WS-OVERFLOW-OCCURRED TO TRUE
                       DISPLAY 'OVERFLOW: Power factor'
               END-COMPUTE

      *        Numerator: r * (1+r)^n
               COMPUTE WS-WK-NUMERATOR =
                   WS-WK-MONTHLY-RATE * WS-WK-POWER-FACTOR

      *        Denominator: (1+r)^n - 1
               COMPUTE WS-WK-DENOMINATOR =
                   WS-WK-POWER-FACTOR - 1

      *        Monthly payment
               IF WS-WK-DENOMINATOR > ZERO
                   COMPUTE WS-WK-PAYMENT-AMT ROUNDED =
                       WS-LOAN-PRINCIPAL
                       * WS-WK-NUMERATOR
                       / WS-WK-DENOMINATOR
                       ON SIZE ERROR
                           SET WS-OVERFLOW-OCCURRED TO TRUE
                           DISPLAY 'OVERFLOW: Payment calc'
                   END-COMPUTE
               ELSE
                   MOVE ZERO TO WS-WK-PAYMENT-AMT
               END-IF
           ELSE
      *        Zero interest: simple division
               IF WS-WK-NUM-PAYMENTS > ZERO
                   COMPUTE WS-WK-PAYMENT-AMT ROUNDED =
                       WS-LOAN-PRINCIPAL / WS-WK-NUM-PAYMENTS
               END-IF
           END-IF

           DISPLAY WS-C-DETAIL-SEP
           DISPLAY '  Loan: ' WS-LOAN-DESCRIPTION
           MOVE WS-LOAN-PRINCIPAL TO WS-DSP-AMOUNT
           DISPLAY '  Principal:      ' WS-DSP-AMOUNT
           MOVE WS-LOAN-ANNUAL-RATE TO WS-DSP-RATE
           DISPLAY '  Annual Rate:    ' WS-DSP-RATE '%'
           DISPLAY '  Term:           '
               WS-LOAN-TERM-YEARS ' years ('
               WS-LOAN-TERM-MONTHS ' months)'
           MOVE WS-WK-PAYMENT-AMT TO WS-DSP-AMOUNT
           DISPLAY '  Monthly Payment:' WS-DSP-AMOUNT
           IF WS-LOAN-ARM
               DISPLAY '  Type:           Adjustable Rate'
               MOVE WS-LOAN-ARM-CAP-PER TO WS-DSP-RATE
               DISPLAY '  Per-Adj Cap:    ' WS-DSP-RATE '%'
               MOVE WS-LOAN-ARM-CAP-LIFE TO WS-DSP-RATE
               DISPLAY '  Lifetime Cap:   ' WS-DSP-RATE '%'
           ELSE
               DISPLAY '  Type:           Fixed Rate'
           END-IF
           DISPLAY SPACES
           .

      *================================================================*
      * 2300-GENERATE-SCHEDULE                                         *
      * Uses PERFORM VARYING to iterate through all payment periods.   *
      * This is the core amortization loop.                            *
      *================================================================*
       2300-GENERATE-SCHEDULE.
           MOVE WS-LOAN-PRINCIPAL TO WS-WK-REMAINING-BAL
           INITIALIZE WS-WK-ANNUAL-ACCUM
           INITIALIZE WS-WK-LOAN-TOTALS
           MOVE ZERO TO WS-YEAR-NUMBER

           IF WS-SHOW-MONTHLY-DETAIL
               DISPLAY '  Pmt#  Payment     Interest'
                   '    Principal   Balance'
               DISPLAY WS-C-DETAIL-SEP
           END-IF

      *    Main amortization loop: PERFORM VARYING through all periods
           PERFORM VARYING WS-PERIOD-INDEX
               FROM 1 BY 1
               UNTIL WS-PERIOD-INDEX > WS-LOAN-TERM-MONTHS
                   OR WS-WK-REMAINING-BAL <= ZERO

      *        Check for ARM rate adjustment at each year boundary
               IF WS-LOAN-ARM
                   PERFORM 2310-CHECK-ARM-ADJUSTMENT
               END-IF

      *        Calculate interest for this period
               COMPUTE WS-WK-INTEREST-AMT ROUNDED =
                   WS-WK-REMAINING-BAL * WS-WK-MONTHLY-RATE
                   ON SIZE ERROR
                       SET WS-OVERFLOW-OCCURRED TO TRUE
               END-COMPUTE

      *        Calculate principal for this period
      *        Last payment adjustment: if remaining balance
      *        is less than the normal payment, adjust
               IF WS-PERIOD-INDEX = WS-LOAN-TERM-MONTHS
                   OR (WS-WK-REMAINING-BAL + WS-WK-INTEREST-AMT)
                       <= WS-WK-PAYMENT-AMT
                   COMPUTE WS-WK-PRINCIPAL-AMT ROUNDED =
                       WS-WK-REMAINING-BAL
                   COMPUTE WS-WK-PAYMENT-AMT ROUNDED =
                       WS-WK-PRINCIPAL-AMT + WS-WK-INTEREST-AMT
               ELSE
                   COMPUTE WS-WK-PRINCIPAL-AMT ROUNDED =
                       WS-WK-PAYMENT-AMT - WS-WK-INTEREST-AMT
               END-IF

      *        Update remaining balance
               SUBTRACT WS-WK-PRINCIPAL-AMT
                   FROM WS-WK-REMAINING-BAL

      *        Fix any tiny negative balance from rounding
               IF WS-WK-REMAINING-BAL < ZERO
                   AND WS-WK-REMAINING-BAL > -0.01
                   MOVE ZERO TO WS-WK-REMAINING-BAL
               END-IF

      *        Accumulate annual subtotals
               ADD WS-WK-INTEREST-AMT
                   TO WS-WK-ANN-INTEREST
               ADD WS-WK-PRINCIPAL-AMT
                   TO WS-WK-ANN-PRINCIPAL
               ADD WS-WK-PAYMENT-AMT
                   TO WS-WK-ANN-PAYMENTS

      *        Accumulate loan totals
               ADD WS-WK-PAYMENT-AMT
                   TO WS-WK-TOTAL-PAYMENTS
               ADD WS-WK-INTEREST-AMT
                   TO WS-WK-TOTAL-INTEREST
               ADD WS-WK-PRINCIPAL-AMT
                   TO WS-WK-TOTAL-PRINCIPAL

      *        Display monthly detail line (if enabled)
               IF WS-SHOW-MONTHLY-DETAIL
                   PERFORM 2320-DISPLAY-DETAIL-LINE
               END-IF

      *        Check for year-end break
               PERFORM 2330-CHECK-YEAR-BREAK

           END-PERFORM
           .

      *================================================================*
      * 2310-CHECK-ARM-ADJUSTMENT                                      *
      * For ARM loans, recalculate payment at each annual boundary.    *
      * Uses PERFORM UNTIL pattern (not a VARYING loop).               *
      *================================================================*
       2310-CHECK-ARM-ADJUSTMENT.
      *    Check if this period is an annual adjustment point
      *    (every 12 months after the first year)
           IF WS-PERIOD-INDEX > WS-C-ARM-ADJ-INTERVAL
               COMPUTE WS-MONTH-IN-YEAR =
                   FUNCTION MOD(WS-PERIOD-INDEX - 1, 12)
               IF WS-MONTH-IN-YEAR = 0
      *            Calculate new rate: index + margin
                   COMPUTE WS-WK-NEW-RATE =
                       WS-LOAN-ARM-INDEX + WS-LOAN-ARM-MARGIN

      *            Apply per-adjustment cap
                   COMPUTE WS-WK-ADJUSTMENT =
                       WS-WK-NEW-RATE - WS-WK-CURRENT-RATE
                   IF WS-WK-ADJUSTMENT > WS-LOAN-ARM-CAP-PER
                       COMPUTE WS-WK-NEW-RATE =
                           WS-WK-CURRENT-RATE
                           + WS-LOAN-ARM-CAP-PER
                   END-IF
                   IF WS-WK-ADJUSTMENT < ZERO
                       AND (ZERO - WS-WK-ADJUSTMENT)
                           > WS-LOAN-ARM-CAP-PER
                       COMPUTE WS-WK-NEW-RATE =
                           WS-WK-CURRENT-RATE
                           - WS-LOAN-ARM-CAP-PER
                   END-IF

      *            Apply lifetime cap
                   COMPUTE WS-WK-ADJUSTMENT =
                       WS-WK-NEW-RATE - WS-LOAN-ANNUAL-RATE
                   IF WS-WK-ADJUSTMENT > WS-LOAN-ARM-CAP-LIFE
                       COMPUTE WS-WK-NEW-RATE =
                           WS-LOAN-ANNUAL-RATE
                           + WS-LOAN-ARM-CAP-LIFE
                   END-IF

      *            Update rate and recalculate payment
                   MOVE WS-WK-NEW-RATE TO WS-WK-CURRENT-RATE
                   COMPUTE WS-WK-MONTHLY-RATE =
                       WS-WK-CURRENT-RATE / 100 / 12

      *            Recalculate payment for remaining term
                   COMPUTE WS-WK-NUM-PAYMENTS =
                       WS-LOAN-TERM-MONTHS
                       - WS-PERIOD-INDEX + 1

                   IF WS-WK-MONTHLY-RATE > ZERO
                       AND WS-WK-NUM-PAYMENTS > ZERO
                       COMPUTE WS-WK-POWER-FACTOR =
                           (1 + WS-WK-MONTHLY-RATE)
                           ** WS-WK-NUM-PAYMENTS
                       COMPUTE WS-WK-NUMERATOR =
                           WS-WK-MONTHLY-RATE
                           * WS-WK-POWER-FACTOR
                       COMPUTE WS-WK-DENOMINATOR =
                           WS-WK-POWER-FACTOR - 1
                       IF WS-WK-DENOMINATOR > ZERO
                           COMPUTE WS-WK-PAYMENT-AMT ROUNDED =
                               WS-WK-REMAINING-BAL
                               * WS-WK-NUMERATOR
                               / WS-WK-DENOMINATOR
                           END-IF
                   END-IF

                   IF WS-SHOW-MONTHLY-DETAIL
                       DISPLAY '  >>> RATE ADJUSTMENT: '
                           WS-WK-CURRENT-RATE '%'
                       MOVE WS-WK-PAYMENT-AMT
                           TO WS-DSP-AMOUNT
                       DISPLAY '  >>> NEW PAYMENT:     '
                           WS-DSP-AMOUNT
                   END-IF
               END-IF
           END-IF
           .

      *================================================================*
      * 2320-DISPLAY-DETAIL-LINE                                       *
      *================================================================*
       2320-DISPLAY-DETAIL-LINE.
           MOVE WS-PERIOD-INDEX    TO WS-DSP-PERIOD
           MOVE WS-WK-PAYMENT-AMT TO WS-DSP-AMOUNT
           DISPLAY '  ' WS-DSP-PERIOD '   '
               WS-DSP-AMOUNT
               WITH NO ADVANCING
           MOVE WS-WK-INTEREST-AMT TO WS-DSP-AMOUNT
           DISPLAY '  ' WS-DSP-AMOUNT
               WITH NO ADVANCING
           MOVE WS-WK-PRINCIPAL-AMT TO WS-DSP-AMOUNT
           DISPLAY '  ' WS-DSP-AMOUNT
               WITH NO ADVANCING
           MOVE WS-WK-REMAINING-BAL TO WS-DSP-AMOUNT
           DISPLAY '  ' WS-DSP-AMOUNT
           .

      *================================================================*
      * 2330-CHECK-YEAR-BREAK                                          *
      * Displays annual subtotals when 12 months have passed.          *
      *================================================================*
       2330-CHECK-YEAR-BREAK.
           COMPUTE WS-MONTH-IN-YEAR =
               FUNCTION MOD(WS-PERIOD-INDEX, 12)

           IF WS-MONTH-IN-YEAR = 0
               OR WS-PERIOD-INDEX = WS-LOAN-TERM-MONTHS
               OR WS-WK-REMAINING-BAL <= ZERO

               ADD 1 TO WS-YEAR-NUMBER
               MOVE WS-YEAR-NUMBER TO WS-DSP-YEAR

               IF WS-SHOW-MONTHLY-DETAIL
                   DISPLAY '  Year ' WS-DSP-YEAR ' Totals:'
                       WITH NO ADVANCING
               ELSE
                   DISPLAY '  Year ' WS-DSP-YEAR ':'
                       WITH NO ADVANCING
               END-IF

               MOVE WS-WK-ANN-PAYMENTS TO WS-DSP-AMOUNT
               DISPLAY '  Pmts=' WS-DSP-AMOUNT
                   WITH NO ADVANCING
               MOVE WS-WK-ANN-INTEREST TO WS-DSP-AMOUNT
               DISPLAY '  Int=' WS-DSP-AMOUNT
                   WITH NO ADVANCING
               MOVE WS-WK-ANN-PRINCIPAL TO WS-DSP-AMOUNT
               DISPLAY '  Prin=' WS-DSP-AMOUNT
                   WITH NO ADVANCING
               MOVE WS-WK-REMAINING-BAL TO WS-DSP-AMOUNT
               DISPLAY '  Bal=' WS-DSP-AMOUNT

      *        Reset annual accumulators
               INITIALIZE WS-WK-ANNUAL-ACCUM
           END-IF
           .

      *================================================================*
      * 2400-DISPLAY-LOAN-SUMMARY                                      *
      *================================================================*
       2400-DISPLAY-LOAN-SUMMARY.
           DISPLAY SPACES
           DISPLAY '  LOAN SUMMARY: ' WS-LOAN-DESCRIPTION
           DISPLAY WS-C-DETAIL-SEP

           MOVE WS-WK-TOTAL-PAYMENTS TO WS-DSP-BIG-AMOUNT
           DISPLAY '  Total of all payments:  '
               WS-DSP-BIG-AMOUNT

           MOVE WS-WK-TOTAL-INTEREST TO WS-DSP-BIG-AMOUNT
           DISPLAY '  Total interest paid:    '
               WS-DSP-BIG-AMOUNT

           MOVE WS-WK-TOTAL-PRINCIPAL TO WS-DSP-BIG-AMOUNT
           DISPLAY '  Total principal paid:   '
               WS-DSP-BIG-AMOUNT

           MOVE WS-LOAN-PRINCIPAL TO WS-DSP-AMOUNT
           DISPLAY '  Original principal:     '
               WS-DSP-AMOUNT

           MOVE WS-WK-REMAINING-BAL TO WS-DSP-AMOUNT
           DISPLAY '  Remaining balance:      '
               WS-DSP-AMOUNT

           IF WS-LOAN-PRINCIPAL > ZERO
               COMPUTE WS-WK-NUMERATOR =
                   (WS-WK-TOTAL-INTEREST /
                    WS-LOAN-PRINCIPAL) * 100
               MOVE WS-WK-NUMERATOR TO WS-DSP-RATE
               DISPLAY '  Interest as % of loan:  '
                   WS-DSP-RATE '%'
           END-IF

           DISPLAY SPACES
           .

      *================================================================*
      * 2500-STORE-COMPARISON-DATA: Save for multi-loan comparison     *
      *================================================================*
       2500-STORE-COMPARISON-DATA.
           MOVE WS-LOAN-DESCRIPTION
               TO WS-COMP-DESC(WS-LOAN-INDEX)
           MOVE WS-LOAN-PRINCIPAL
               TO WS-COMP-PRINCIPAL(WS-LOAN-INDEX)
           MOVE WS-LOAN-ANNUAL-RATE
               TO WS-COMP-RATE(WS-LOAN-INDEX)
           MOVE WS-LOAN-TERM-YEARS
               TO WS-COMP-TERM(WS-LOAN-INDEX)
           MOVE WS-WK-PAYMENT-AMT
               TO WS-COMP-PAYMENT(WS-LOAN-INDEX)
           MOVE WS-WK-TOTAL-INTEREST
               TO WS-COMP-TOT-INT(WS-LOAN-INDEX)
           MOVE WS-WK-TOTAL-PAYMENTS
               TO WS-COMP-TOT-PAID(WS-LOAN-INDEX)
           MOVE WS-LOAN-TYPE
               TO WS-COMP-TYPE(WS-LOAN-INDEX)
           .

      *================================================================*
      * 3000-MULTI-LOAN-COMPARISON                                     *
      * Nested PERFORM: outer loop iterates comparison fields,         *
      * inner loop iterates across loans.                              *
      *================================================================*
       3000-MULTI-LOAN-COMPARISON.
           DISPLAY WS-C-REPORT-SEP
           DISPLAY '  LOAN COMPARISON SUMMARY'
           DISPLAY WS-C-REPORT-SEP
           DISPLAY SPACES

      *    Header row: iterate across loans using PERFORM VARYING
           DISPLAY '                     '
               WITH NO ADVANCING
           PERFORM VARYING WS-LOAN-INDEX FROM 1 BY 1
               UNTIL WS-LOAN-INDEX > WS-NUM-LOANS
               DISPLAY '  Loan ' WS-LOAN-INDEX '          '
                   WITH NO ADVANCING
           END-PERFORM
           DISPLAY SPACES

      *    Description row
           DISPLAY '  Description:       '
               WITH NO ADVANCING
           PERFORM VARYING WS-LOAN-INDEX FROM 1 BY 1
               UNTIL WS-LOAN-INDEX > WS-NUM-LOANS
               DISPLAY '  '
                   WS-COMP-DESC(WS-LOAN-INDEX)(1:15)
                   WITH NO ADVANCING
           END-PERFORM
           DISPLAY SPACES

      *    Rate row
           DISPLAY '  Rate:              '
               WITH NO ADVANCING
           PERFORM VARYING WS-LOAN-INDEX FROM 1 BY 1
               UNTIL WS-LOAN-INDEX > WS-NUM-LOANS
               MOVE WS-COMP-RATE(WS-LOAN-INDEX)
                   TO WS-DSP-RATE
               DISPLAY '  ' WS-DSP-RATE '%     '
                   WITH NO ADVANCING
           END-PERFORM
           DISPLAY SPACES

      *    Term row
           DISPLAY '  Term (years):      '
               WITH NO ADVANCING
           PERFORM VARYING WS-LOAN-INDEX FROM 1 BY 1
               UNTIL WS-LOAN-INDEX > WS-NUM-LOANS
               MOVE WS-COMP-TERM(WS-LOAN-INDEX)
                   TO WS-DSP-YEAR
               DISPLAY '  ' WS-DSP-YEAR ' years       '
                   WITH NO ADVANCING
           END-PERFORM
           DISPLAY SPACES

      *    Monthly payment row
           DISPLAY '  Monthly Payment:   '
               WITH NO ADVANCING
           PERFORM VARYING WS-LOAN-INDEX FROM 1 BY 1
               UNTIL WS-LOAN-INDEX > WS-NUM-LOANS
               MOVE WS-COMP-PAYMENT(WS-LOAN-INDEX)
                   TO WS-DSP-AMOUNT
               DISPLAY '  ' WS-DSP-AMOUNT
                   WITH NO ADVANCING
           END-PERFORM
           DISPLAY SPACES

      *    Total interest row
           DISPLAY '  Total Interest:    '
               WITH NO ADVANCING
           PERFORM VARYING WS-LOAN-INDEX FROM 1 BY 1
               UNTIL WS-LOAN-INDEX > WS-NUM-LOANS
               MOVE WS-COMP-TOT-INT(WS-LOAN-INDEX)
                   TO WS-DSP-AMOUNT
               DISPLAY '  ' WS-DSP-AMOUNT
                   WITH NO ADVANCING
           END-PERFORM
           DISPLAY SPACES

      *    Total payments row
           DISPLAY '  Total Paid:        '
               WITH NO ADVANCING
           PERFORM VARYING WS-LOAN-INDEX FROM 1 BY 1
               UNTIL WS-LOAN-INDEX > WS-NUM-LOANS
               MOVE WS-COMP-TOT-PAID(WS-LOAN-INDEX)
                   TO WS-DSP-AMOUNT
               DISPLAY '  ' WS-DSP-AMOUNT
                   WITH NO ADVANCING
           END-PERFORM
           DISPLAY SPACES

      *    Interest savings comparison (vs. loan 1)
           DISPLAY SPACES
           DISPLAY '  INTEREST SAVINGS vs. Loan 1:'
           PERFORM VARYING WS-LOAN-INDEX FROM 2 BY 1
               UNTIL WS-LOAN-INDEX > WS-NUM-LOANS
               IF WS-COMP-TOT-INT(1)
                   > WS-COMP-TOT-INT(WS-LOAN-INDEX)
                   COMPUTE WS-WK-ADJUSTMENT =
                       WS-COMP-TOT-INT(1)
                       - WS-COMP-TOT-INT(WS-LOAN-INDEX)
                   MOVE WS-WK-ADJUSTMENT TO WS-DSP-AMOUNT
                   DISPLAY '    Loan ' WS-LOAN-INDEX
                       ' saves ' WS-DSP-AMOUNT
                       ' in interest'
               ELSE
                   COMPUTE WS-WK-ADJUSTMENT =
                       WS-COMP-TOT-INT(WS-LOAN-INDEX)
                       - WS-COMP-TOT-INT(1)
                   MOVE WS-WK-ADJUSTMENT TO WS-DSP-AMOUNT
                   DISPLAY '    Loan ' WS-LOAN-INDEX
                       ' costs ' WS-DSP-AMOUNT
                       ' more in interest'
               END-IF
           END-PERFORM

      *    Monthly payment difference (vs. loan 1)
           DISPLAY SPACES
           DISPLAY '  MONTHLY PAYMENT DIFFERENCE vs. Loan 1:'
           PERFORM VARYING WS-LOAN-INDEX FROM 2 BY 1
               UNTIL WS-LOAN-INDEX > WS-NUM-LOANS
               IF WS-COMP-PAYMENT(WS-LOAN-INDEX)
                   > WS-COMP-PAYMENT(1)
                   COMPUTE WS-WK-ADJUSTMENT =
                       WS-COMP-PAYMENT(WS-LOAN-INDEX)
                       - WS-COMP-PAYMENT(1)
                   MOVE WS-WK-ADJUSTMENT TO WS-DSP-AMOUNT
                   DISPLAY '    Loan ' WS-LOAN-INDEX
                       ' costs ' WS-DSP-AMOUNT
                       ' more per month'
               ELSE
                   COMPUTE WS-WK-ADJUSTMENT =
                       WS-COMP-PAYMENT(1)
                       - WS-COMP-PAYMENT(WS-LOAN-INDEX)
                   MOVE WS-WK-ADJUSTMENT TO WS-DSP-AMOUNT
                   DISPLAY '    Loan ' WS-LOAN-INDEX
                       ' saves ' WS-DSP-AMOUNT
                       ' per month'
               END-IF
           END-PERFORM

           DISPLAY SPACES
           DISPLAY WS-C-REPORT-SEP
           .

Solution Walkthrough

PERFORM VARYING: The Core Amortization Loop

The heart of the amortization schedule is the PERFORM VARYING loop in paragraph 2300-GENERATE-SCHEDULE:

       PERFORM VARYING WS-PERIOD-INDEX
           FROM 1 BY 1
           UNTIL WS-PERIOD-INDEX > WS-LOAN-TERM-MONTHS
               OR WS-WK-REMAINING-BAL <= ZERO

This loop iterates through every payment period (up to 360 months for a 30-year loan), executing the amortization calculation once per period. The compound UNTIL condition provides two exit criteria: the loop ends when all scheduled payments have been processed OR when the remaining balance reaches zero. The second condition handles the case where rounding causes the loan to be paid off slightly before the scheduled final payment.

Inside the loop, each iteration performs the same sequence: 1. Check for ARM rate adjustment (if applicable) 2. Calculate interest on the current remaining balance 3. Calculate the principal portion (payment minus interest) 4. Update the remaining balance 5. Accumulate annual and loan-lifetime totals 6. Display the detail line (if enabled) 7. Check for year-end break

This is the standard COBOL iteration pattern: the PERFORM VARYING manages the loop counter, and the loop body performs a fixed sequence of operations per iteration. The pattern is identical in structure to processing records from a sequential file -- the only difference is that the data comes from calculation rather than from a file.

Last Payment Adjustment: Handling Rounding Residuals

A critical detail in amortization calculations is the final payment adjustment. Due to rounding of the monthly payment to the nearest cent, the last payment will almost never equal the regular payment amount. The remaining balance at that point might be slightly more or less than what one regular payment would pay off.

The program handles this with a conditional check:

           IF WS-PERIOD-INDEX = WS-LOAN-TERM-MONTHS
               OR (WS-WK-REMAINING-BAL + WS-WK-INTEREST-AMT)
                   <= WS-WK-PAYMENT-AMT
               COMPUTE WS-WK-PRINCIPAL-AMT ROUNDED =
                   WS-WK-REMAINING-BAL
               COMPUTE WS-WK-PAYMENT-AMT ROUNDED =
                   WS-WK-PRINCIPAL-AMT + WS-WK-INTEREST-AMT

When the remaining balance plus one month of interest is less than or equal to the regular payment, the program adjusts the final payment to exactly pay off the remaining balance plus that month's interest. This ensures the loan ends with a zero balance, avoiding the common bug where amortization schedules show a tiny residual balance (positive or negative) after the last payment.

The additional check for a small negative balance after the subtraction provides defense in depth:

           IF WS-WK-REMAINING-BAL < ZERO
               AND WS-WK-REMAINING-BAL > -0.01
               MOVE ZERO TO WS-WK-REMAINING-BAL
           END-IF

This corrects for the case where accumulated rounding errors cause the balance to go slightly negative (by less than one cent) rather than landing exactly on zero.

ARM Rate Adjustment: PERFORM Within PERFORM VARYING

The ARM rate adjustment logic (2310-CHECK-ARM-ADJUSTMENT) is called from within the main PERFORM VARYING loop, demonstrating the nested PERFORM pattern. This paragraph uses FUNCTION MOD to determine whether the current period falls on an annual boundary:

           COMPUTE WS-MONTH-IN-YEAR =
               FUNCTION MOD(WS-PERIOD-INDEX - 1, 12)
           IF WS-MONTH-IN-YEAR = 0

When an adjustment is triggered, the program: 1. Calculates the new rate from the ARM index plus margin 2. Applies the per-adjustment cap (limits how much the rate can change in one adjustment) 3. Applies the lifetime cap (limits total rate change from the initial rate) 4. Recalculates the monthly payment based on the new rate and the remaining balance for the remaining term

The payment recalculation reuses the same amortization formula as the initial calculation, but with the current remaining balance as the principal and the remaining months as the term. This is the standard approach for ARM payment recalculation.

Year-End Break: The Accumulator Pattern

The year-end break logic (2330-CHECK-YEAR-BREAK) uses the FUNCTION MOD check to detect when 12 months have elapsed. At each year boundary, it displays the annual subtotals (total payments, total interest, total principal for that year) and resets the annual accumulators.

This pattern -- accumulate at the detail level, report and reset at the break level -- is the classic COBOL control break approach applied to a calculated schedule rather than a sorted file. The annual accumulators (WS-WK-ANNUAL-ACCUM) are reset at each year break, while the loan-lifetime accumulators (WS-WK-LOAN-TOTALS) persist for the entire schedule.

Multi-Loan Comparison: Nested PERFORM VARYING

The comparison report (3000-MULTI-LOAN-COMPARISON) demonstrates nested PERFORM VARYING for a different purpose: formatting a tabular report with multiple columns. The outer structure iterates through the rows of the comparison table (description, rate, term, payment, total interest, total paid), and for each row, an inner PERFORM VARYING iterates across the loans to print the corresponding value:

       DISPLAY '  Monthly Payment:   '
           WITH NO ADVANCING
       PERFORM VARYING WS-LOAN-INDEX FROM 1 BY 1
           UNTIL WS-LOAN-INDEX > WS-NUM-LOANS
           MOVE WS-COMP-PAYMENT(WS-LOAN-INDEX)
               TO WS-DSP-AMOUNT
           DISPLAY '  ' WS-DSP-AMOUNT
               WITH NO ADVANCING
       END-PERFORM
       DISPLAY SPACES

The WITH NO ADVANCING phrase keeps the cursor on the same line, allowing the PERFORM VARYING to build each row of the comparison table by displaying values side-by-side. The final DISPLAY SPACES moves to the next line after all loan values for that row have been printed.

The interest savings comparison uses a PERFORM VARYING starting FROM 2 (not 1) to compare each subsequent loan against the first loan:

       PERFORM VARYING WS-LOAN-INDEX FROM 2 BY 1
           UNTIL WS-LOAN-INDEX > WS-NUM-LOANS

This demonstrates that the FROM value in PERFORM VARYING does not have to be 1 -- it can be any value, which is useful when the first element serves as a reference point rather than a comparison target.

Lessons Learned

1. PERFORM VARYING Maps Naturally to Financial Period Calculations

Mortgage amortization is inherently iterative: the same calculation (interest, principal, balance update) is repeated for every payment period. PERFORM VARYING provides exactly the right abstraction -- a counted loop with automatic incrementing. The period index serves as both the loop counter and the payment number, making the code self-documenting.

2. Compound UNTIL Conditions Prevent Edge Case Bugs

The compound UNTIL condition (WS-PERIOD-INDEX > WS-LOAN-TERM-MONTHS OR WS-WK-REMAINING-BAL <= ZERO) handles two exit conditions cleanly. Without the balance check, a loan with favorable rounding might have its balance reach zero before the final scheduled period, causing negative principal amounts in subsequent periods. Without the period check, a loan with unfavorable rounding might loop indefinitely if the balance never quite reaches zero.

3. The Control Break Pattern Works for Calculated Data, Not Just Files

The year-end subtotal logic demonstrates that the control break pattern -- accumulate, detect the break condition, report, reset -- is not limited to file processing. Any iterative computation that produces hierarchical results (monthly details within annual groups within a loan lifetime) benefits from this pattern.

4. Multi-Loan Comparison Requires Saving Results Before Comparing

Angela's design stores each loan's summary data in a comparison table (WS-COMPARISON-TABLE) using OCCURS 3 TIMES. This allows the comparison report to be generated after all individual schedules are complete. An alternative approach -- computing all schedules simultaneously in a single pass -- would require parallel sets of all work fields, dramatically increasing complexity.

5. ARM Payment Recalculation Must Use Remaining Term, Not Original Term

A common error in ARM amortization programs is recalculating the payment using the original loan term. When the rate adjusts after year 5 of a 30-year loan, the new payment must be calculated for 25 remaining years (300 months), not 30 years (360 months). Using the original term produces a payment that is too low, causing the loan to have a balloon balance at the end of the term.

Discussion Questions

  1. The program uses FUNCTION MOD(WS-PERIOD-INDEX - 1, 12) to detect annual boundaries for ARM adjustments, and FUNCTION MOD(WS-PERIOD-INDEX, 12) for year-end display breaks. Why are these two MOD expressions different? What would happen if the same expression were used for both purposes?

  2. The amortization loop uses a compound UNTIL condition with OR. If you changed this to AND, what would the behavior be? Would the program still terminate correctly?

  3. The last payment adjustment handles the case where rounding causes the loan to be paid off slightly before or after the scheduled final period. How would you test this logic to ensure it works correctly? What specific combinations of principal, rate, and term would produce interesting edge cases?

  4. The multi-loan comparison uses PERFORM VARYING with WITH NO ADVANCING to build tabular output. If you needed to write this comparison to a fixed-width report file (PIC X(132) record), how would you restructure the logic? Would you still use PERFORM VARYING, or would a different approach be more appropriate?

  5. The ARM rate adjustment applies two caps: a per-adjustment cap and a lifetime cap. The per-adjustment cap is checked first. Does the order of these checks matter? Could applying them in the opposite order produce a different result? Construct an example that demonstrates your answer.

  6. Angela chose to process each loan sequentially (complete schedule for loan 1, then loan 2, then loan 3). An alternative design would process all three loans in parallel, one period at a time, using a nested PERFORM with AFTER. What would be the advantages and disadvantages of the parallel approach? In what business scenario would parallel processing be essential?