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:
- Accept loan parameters: principal amount, annual interest rate, term in years, and loan type (fixed or ARM)
- Calculate the monthly payment using the standard amortization formula
- Generate a month-by-month schedule showing payment number, payment date, payment amount, interest portion, principal portion, and remaining balance
- For ARM loans, adjust the interest rate annually and recalculate the monthly payment
- Support multi-loan comparison: generate side-by-side schedules for up to three loan scenarios
- Display period subtotals at each yearly break (annual interest paid, annual principal paid)
- 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 $$$,$$$, MATH1 MATH2 $,$$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
-
The program uses
FUNCTION MOD(WS-PERIOD-INDEX - 1, 12)to detect annual boundaries for ARM adjustments, andFUNCTION 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? -
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?
-
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?
-
The multi-loan comparison uses PERFORM VARYING with
WITH NO ADVANCINGto 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? -
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.
-
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?