Portfolio Example

z/OS COBOL Mortgage Calculator

A batch amortization engine running on IBM z/OS, invoked via zOSMF REST API from a Python CGI layer.

Overview

Language
COBOL / JCL / Python
Platform
IBM z/OS
Interface
zOSMF REST
Output
HTML Amort. Table
Extra Principal
ROI Analysis
Output Modes
TEXT / HTML

The COBOL program reads loan records from a sequential dataset, validates each record, computes a standard amortization schedule with optional extra principal payments, and emits HTML output via SYSOUT. A Python CGI script submits the job to z/OS via the zOSMF REST API, polls for completion, intercepts the spool output, injects a stylesheet, and serves the result directly to the browser.

Run the Calculator

Submits MORTWEB batch job > polls every 2s > streams amortization table

Run ->

Request Pipeline

Browser
HTTP GET
>
CGI
mortgage.py
>
zOSMF REST
Job Submit
>
z/OS JCL
MORTWEB
>
COBOL PGM
MORTGAGE
>
JES Spool > HTML
CSS injected

The Python layer handles all orchestration: submitting JCL inline (no dataset indirection), polling status == "OUTPUT", locating the LOANOUT DD among spool files, stripping fixed-length padding from the 256-byte COBOL output records, and splicing a stylesheet into the COBOL-generated <head>.

COBOL Program Structure

ParagraphResponsibilityNotes
MAINPARM parsing, file open/close, read loopPARM(1:4) selects TEXT or HTML mode
VALIDATE-INPUTNumeric checks, range validationSets REC-OK / REC-BAD condition names
PROCESS-LOANPayment formula, amortization loopAPR=0 handled as interest-free loan
WRITE-HEADERHTML/TEXT page headerDual-format branching throughout
WRITE-PAYMENT-LINEPer-payment row output9 edited fields; tracks cumulative interest
WRITE-ROI-SUMMARYExtra principal ROI calculationInterest saved / extra paid * 100
WRITE-RUN-METADATAUser ID, date, time footerPopulated from PARM and CURRENT-DATE

Design Notes

The COBOL uses CBL ARITH(EXTEND) for 31-digit intermediate precision, which matters when computing (1 + r) ** -N in the payment formula. All monetary fields carry implied decimal via V99 and are edited into ZZZ,ZZZ,ZZ9.99 pictures for output, no floating point anywhere in the pipeline.

Output mode is selected at runtime via JCL PARM: PARM='HTML/WEBUSER' sets positions 1-4 to the format token and positions 6+ to the user ID logged in the run metadata footer. The Python CGI injects CSS by string-replacing </head> in the raw spool text, no parsing required.

Extra principal payments short-circuit the amortization loop: the loop terminates when WS-BALANCE <= 0.01 rather than at the scheduled term end, and the ROI summary is only emitted when extra payments were actually applied.

Annotated Source

MORTGAGE.cbl compiler option
CBL ARITH(EXTEND) bumps intermediate arithmetic precision from 18 to 31 digits. Without it, the exponentiation in the payment formula: (1 + r) ** -N loses enough precision on long-term loans to produce noticeably wrong figures. It costs nothing at runtime; it only affects how the compiler emits the arithmetic instructions.
       CBL ARITH(EXTEND)
       IDENTIFICATION DIVISION.
        PROGRAM-ID. MORTGAGE.
MORTGAGE.cbl input record layout
The input record is exactly 80 characters: one punched-card-width line. Field layout is positional: the compiler maps each PIC clause to a fixed byte offset with no delimiters. 9(7)V99 means 7 integer digits plus 2 implied decimal places stored as pure digits (no decimal point in the file). FILLER absorbs the 6 trailing bytes that are not used, keeping the record exactly 80 bytes wide.
        FD  LOAN-FILE
                RECORD CONTAINS 80 CHARACTERS
                RECORDING MODE IS F.
        01  LOAN-REC.
            05 LOAN-TITLE           PIC X(40).
            05 LOAN-AMOUNT          PIC 9(7)V99.
            05 APR                  PIC 9(2)V99.
            05 LOAN-TERM            PIC 9(3).
            05 ADDITIONAL-PRINCIPAL PIC 9(7)V99.
            05 FILLER               PIC X(6).
MORTGAGE.cbl output record
The output record is 256 bytes, chosen to comfortably hold a single HTML <tr> row with all nine edited numeric fields and their tags. Each WRITE emits exactly one 256-byte fixed-length record to the spool; the Python CGI later strips the trailing spaces that pad short rows to fill the width.
        FD  OUT-FILE
                RECORD CONTAINS 256 CHARACTERS
                RECORDING MODE IS F.
        01  OUT-REC                 PIC X(256).
MORTGAGE.cbl core working storage fields
All monetary working-storage fields use V99 implied decimal, the decimal point exists logically but is never stored. WS-MONTHLY-RATE uses 9V9(6) (one integer digit, six decimal places) because a monthly rate like 0.004167 needs that precision to avoid compounding rounding errors over 360 payments. WS-BEGIN-BALANCE and WS-ACTUAL-EXTRA are separate from the running WS-BALANCE so each output row can show both the opening balance and the exact extra amount applied that month.
        01  WS-PRINCIPAL-ORIG    PIC 9(9)V99 VALUE 0.
        01  WS-BALANCE           PIC 9(9)V99 VALUE 0.
        01  WS-MONTHLY-RATE      PIC 9V9(6)  VALUE 0.
        01  WS-PAYMENT           PIC 9(9)V99 VALUE 0.
        01  WS-INTEREST          PIC 9(9)V99 VALUE 0.
        01  WS-PRIN-PAY          PIC 9(9)V99 VALUE 0.
        01  WS-EXTRA-PRINCIPAL   PIC 9(9)V99 VALUE 0.

        01  WS-BASE-PRIN         PIC 9(9)V99 VALUE 0.
        01  WS-ACTUAL-EXTRA      PIC 9(9)V99 VALUE 0.
        01  WS-BEGIN-BALANCE     PIC 9(9)V99 VALUE 0.
MORTGAGE.cbl edited output pictures
Edited pictures like ZZZ,ZZZ,ZZ9.99 exist solely for output formatting. The Z suppresses leading zeros (replacing them with spaces), the commas and period are inserted literally, and the trailing 9.99 guarantees at least one digit before the decimal and always two after. You MOVE a raw numeric into an edited field and COBOL formats it automatically: no sprintf equivalent needed.
        01  WS-PAY-NO-EDIT        PIC Z(4).
        01  WS-BEGIN-BAL-EDIT     PIC ZZZ,ZZZ,ZZ9.99.
        01  WS-SCHED-PAY-EDIT     PIC ZZZ,ZZZ,ZZ9.99.
        01  WS-EXTRA-PAY-EDIT     PIC ZZZ,ZZZ,ZZ9.99.
        01  WS-TOTAL-PAY-EDIT     PIC ZZZ,ZZZ,ZZ9.99.
        01  WS-PRIN-PAY-EDIT      PIC ZZZ,ZZZ,ZZ9.99.
        01  WS-INT-EDIT           PIC ZZZ,ZZZ,ZZ9.99.
        01  WS-END-BAL-EDIT       PIC ZZZ,ZZZ,ZZ9.99.
        01  WS-CUM-INT-EDIT       PIC ZZZ,ZZZ,ZZ9.99.
MORTGAGE.cbl condition names (88-levels)
88 level items are condition names: essentially named boolean tests against the parent field. SET FMT-HTML TO TRUE simply moves 'HTML' into WS-OUTPUT-FORMAT, and IF FMT-HTML tests whether it equals 'HTML'. This is COBOL's idiomatic way of avoiding magic string comparisons scattered throughout the code. The format is set once from the JCL PARM and then tested in every output paragraph.
        01  WS-OUTPUT-FORMAT     PIC X(4) VALUE 'TEXT'.
            88 FMT-TEXT          VALUE 'TEXT'.
            88 FMT-HTML          VALUE 'HTML'.
MORTGAGE.cbl MAIN: PARM parsing and read loop
The JCL PARM arrives in the Linkage Section as a length-prefixed string. PARM-LEN is a S9(4) COMP (signed 16-bit binary) holding the byte count of what follows. Positions 1-4 carry the format token (TEXT or HTML) and positions 6 onward carry the user ID logged in the run metadata footer , the slash in PARM='HTML/WEBUSER' sits at position 5 and is simply skipped over. The main loop is a PERFORM UNTIL EOF that reads one loan record at a time and dispatches to PROCESS-LOAN; the AT END clause flips the EOF condition name to exit cleanly.
        MAIN.
            IF PARM-LEN > 0
               MOVE PARM-TEXT(1:4) TO WS-OUTPUT-FORMAT
               IF PARM-LEN > 5
                  MOVE PARM-TEXT(6:PARM-LEN - 5)
                        TO WS-USER-ID
               ELSE
                  MOVE "UNKNOWN" TO WS-USER-ID
               END-IF
            ELSE
               MOVE "TEXT"    TO WS-OUTPUT-FORMAT
               MOVE "UNKNOWN" TO WS-USER-ID
            END-IF

            MOVE FUNCTION CURRENT-DATE TO WS-CD

            OPEN INPUT  LOAN-FILE
                 OUTPUT OUT-FILE

            PERFORM UNTIL EOF
                READ LOAN-FILE
                    AT END
                        MOVE 'Y' TO EOF-LOAN-FILE
                    NOT AT END
                        PERFORM PROCESS-LOAN
                END-READ
            END-PERFORM

            CLOSE LOAN-FILE OUT-FILE
            STOP RUN.
MORTGAGE.cbl VALIDATE-INPUT: chained field checks
Validation is chained with IF REC-OK guards so a field is only checked if all previous fields passed, this prevents misleading cascading errors from a single bad record. IS NUMERIC is a built-in COBOL class test that verifies every byte in a field is a valid digit. APR is allowed to be zero (interest-free loan handled downstream). Extra principal is allowed to be zero, which simply means no extra payments are made and no ROI summary is emitted.
        VALIDATE-INPUT.
            SET REC-OK TO TRUE
            MOVE SPACES TO WS-ERROR-REASON

            IF NOT LOAN-AMOUNT IS NUMERIC
               SET REC-BAD TO TRUE
               MOVE "NON-NUMERIC LOAN AMOUNT" TO WS-ERROR-REASON
            ELSE
               IF LOAN-AMOUNT <= 0
                  SET REC-BAD TO TRUE
                  MOVE "LOAN AMOUNT <= 0" TO WS-ERROR-REASON
               END-IF
            END-IF

            IF REC-OK
               IF NOT LOAN-TERM IS NUMERIC ...
            END-IF

            IF REC-OK
               IF NOT APR IS NUMERIC ...
               ELSE IF APR < 0 ...
            END-IF

            IF REC-OK
               IF NOT ADDITIONAL-PRINCIPAL IS NUMERIC ...
               ELSE IF ADDITIONAL-PRINCIPAL < 0 ...
            END-IF.
MORTGAGE.cbl PROCESS-LOAN: payment formula
The standard fixed-rate monthly payment formula is P * r / (1 - (1 + r)^-N) where P is principal, r is the monthly rate (APR / 1200), and N is total payments. The zero-APR branch avoids division-by-zero: if there is no interest, the payment is simply principal divided by the number of months. WS-TOTAL-INT-NO-EXTRA is computed up front from the scheduled payment and term, this is the baseline the ROI calculation later subtracts from to find how much interest was saved.
               COMPUTE WS-MONTHLY-RATE ROUNDED = APR / 1200

               IF WS-MONTHLY-RATE = 0
                  COMPUTE WS-PAYMENT ROUNDED =
                          WS-PRINCIPAL-ORIG / WS-N-PAY
               ELSE
                  COMPUTE WS-PAYMENT ROUNDED =
                     WS-PRINCIPAL-ORIG * WS-MONTHLY-RATE /
                     (1 - (1 + WS-MONTHLY-RATE) ** (-WS-N-PAY))
               END-IF

               COMPUTE WS-TOTAL-INT-NO-EXTRA ROUNDED =
                 (WS-PAYMENT * WS-N-PAY) - WS-PRINCIPAL-ORIG
MORTGAGE.cbl PROCESS-LOAN: amortization loop and short-circuit
The loop has two exit conditions joined by OR: the balance falling below a penny (<= 0.01) or the payment counter exceeding the scheduled term. Without extra payments only the counter condition ever fires. With extra principal the balance shrinks faster than scheduled, so the balance condition fires first, the loan pays off early and the loop exits before reaching WS-N-PAY. WS-BEGIN-BALANCE is snapshotted at the top of each iteration so the output row shows the opening balance, while WS-BALANCE is updated by SUBTRACT at the bottom and shows the ending balance.
               PERFORM VARYING WS-PAY-NO FROM 1 BY 1
                       UNTIL WS-BALANCE <= 0.01
                          OR WS-PAY-NO > WS-N-PAY

                   MOVE WS-BALANCE TO WS-BEGIN-BALANCE

                   COMPUTE WS-INTEREST ROUNDED =
                           WS-BALANCE * WS-MONTHLY-RATE

                   COMPUTE WS-BASE-PRIN ROUNDED =
                           WS-PAYMENT - WS-INTEREST

                   IF WS-BASE-PRIN > WS-BALANCE
                       MOVE WS-BALANCE TO WS-BASE-PRIN
                   END-IF

                   ADD WS-INTEREST TO WS-TOTAL-INT-WITH-EXTRA
                   ADD WS-INTEREST TO WS-CUM-INTEREST
                   SUBTRACT WS-PRIN-PAY FROM WS-BALANCE
                   PERFORM WRITE-PAYMENT-LINE

               END-PERFORM
MORTGAGE.cbl PROCESS-LOAN: extra principal clamping
FUNCTION MIN clamps the extra payment to whatever principal remains after the scheduled portion. Without this, on the final payment the extra amount could overshoot, paying more than the remaining balance. The subsequent IF WS-ACTUAL-EXTRA < 0 guard is belt-and-suspenders: if the balance were somehow smaller than the scheduled principal portion, MIN would return negative, which we zero out rather than subtract.
                   IF WS-EXTRA-PRINCIPAL > 0
                       COMPUTE WS-ACTUAL-EXTRA ROUNDED =
                               FUNCTION MIN(WS-EXTRA-PRINCIPAL,
                                            WS-BALANCE - WS-BASE-PRIN)
                       IF WS-ACTUAL-EXTRA < 0
                           MOVE 0 TO WS-ACTUAL-EXTRA
                       END-IF
                       ADD WS-ACTUAL-EXTRA TO WS-PRIN-PAY
                       ADD WS-ACTUAL-EXTRA TO WS-TOTAL-EXTRA-PAID
                   END-IF
MORTGAGE.cbl PROCESS-LOAN: ROI summary guard
Two conditions must both be true before the ROI block emits: the input field must be nonzero and extra payments must have actually been accumulated. The second condition matters because on very short loans the clamping logic might zero out every extra payment; the input asked for extra principal but none was ever applicable. WS-ROI-PCT expresses the interest saving as a percentage of the total extra cash put in: a genuine return-on-investment figure.
               IF WS-EXTRA-PRINCIPAL > 0
                  AND WS-TOTAL-EXTRA-PAID > 0
                   COMPUTE WS-INTEREST-SAVED ROUNDED =
                        WS-TOTAL-INT-NO-EXTRA - WS-TOTAL-INT-WITH-EXTRA
                   COMPUTE WS-ROI-PCT ROUNDED =
                        (WS-INTEREST-SAVED / WS-TOTAL-EXTRA-PAID) * 100
                   PERFORM WRITE-ROI-SUMMARY
               END-IF
MORTGAGE.cbl WRITE-PAYMENT-LINE: edited field moves
Each payment row moves nine working-storage numeric values into their corresponding edited pictures before writing. The MOVE to an edited field triggers COBOL's built-in formatting: zero suppression, comma insertion, and decimal alignment all happen implicitly. WS-SCHED-PAY-EDIT receives WS-BASE-PRIN (scheduled principal only, not including extra), while WS-TOTAL-PAY-EDIT receives the full recomputed payment. This is what produces the separate Scheduled and Total payment columns in the output table.
        WRITE-PAYMENT-LINE.
            MOVE WS-PAY-NO        TO WS-PAY-NO-EDIT
            MOVE WS-BEGIN-BALANCE TO WS-BEGIN-BAL-EDIT
            MOVE WS-BASE-PRIN     TO WS-SCHED-PAY-EDIT
            MOVE WS-ACTUAL-EXTRA  TO WS-EXTRA-PAY-EDIT
            MOVE WS-PAYMENT       TO WS-TOTAL-PAY-EDIT
            MOVE WS-PRIN-PAY      TO WS-PRIN-PAY-EDIT
            MOVE WS-INTEREST      TO WS-INT-EDIT
            MOVE WS-BALANCE       TO WS-END-BAL-EDIT
            MOVE WS-CUM-INTEREST  TO WS-CUM-INT-EDIT
MORTGAGE.cbl WRITE-RUN-METADATA: audit trail
Every report closes with a metadata footer showing who ran it and when. WS-USER-ID comes from the JCL PARM (positions 6 onward), so whoever submits the job is recorded in the output. WS-CD is populated at startup via MOVE FUNCTION CURRENT-DATE TO WS-CD the CURRENT-DATE intrinsic function returns a 21-character string broken into date, time, hundredths, and UTC offset subfields by the group item layout. This gives each report a timestamp tied to the batch job execution rather than a user-supplied value.
        WRITE-RUN-METADATA.
            STRING
               "Run by: "  DELIMITED BY SIZE
               WS-USER-ID  DELIMITED BY SIZE
               "  Date: "  DELIMITED BY SIZE
               WS-CD-DATE  DELIMITED BY SIZE
               "  Time: "  DELIMITED BY SIZE
               WS-CD-TIME  DELIMITED BY SIZE
               INTO WS-LINE-OUT
            END-STRING
            MOVE WS-LINE-OUT TO OUT-REC
            WRITE OUT-REC.