Calculating APR using Reg Z Appendix J

回眸只為那壹抹淺笑 提交于 2019-12-04 11:15:01

Alright, you weren't kidding about the document being a bit hard to read. The solution is actually not that bad though, depending on implementation. I failed repeatedly trying to use their various simplified forumulae and eventually got it using the general formula up top(8). Technically this is a simplification. The actual general formula would take arrays of length period for the other arguments and use their indexes in the loop. You use this method to get A' and A'' for the iteration step. Odd days are handled by (1.0 + fractions*rate), which appears as 1 + f i in the document. Rate is the rate per period, not overall apr.

public double generalEquation(int period, double payment, double initialPeriods, double fractions, double rate)
{
    double retval = 0;
    for (int x = 0; x < period; x++)
        retval += payment / ((1.0 + fractions*rate)*Math.pow(1+rate,initialPeriods + x));
    return retval;
}

Iteration behaves just as the document says in its example(9).

/**
 * 
 * @param amount The initial amount A
 * @param payment The periodic payment P
 * @param payments The total number of payments n
 * @param ppy The number of payment periods per year 
 * @param APRGuess The guess to start estimating from, 10% is 0.1, not 0.001
 * @param partial Odd days, as a fraction of a pay period.  10 days of a month is 0.33333...
 * @param full Full pay periods before the first payment.  Usually 1.
 * @return The calculated APR
 */
public double findAPRGEQ(double amount, double payment, int payments, double ppy, double APRGuess, double partial, double full)    
{
    double result = APRGuess;
    double tempguess = APRGuess;       

    do
    {
        result = tempguess;
        //Step 1
        double i = tempguess/(100*ppy);
        double A1 = generalEquation(payments, payment, full, partial, i);
        //Step 2
        double i2 = (tempguess + 0.1)/(100*ppy);
        double A2 = generalEquation(payments, payment, full, partial, i2);
        //Step 3
        tempguess = tempguess + 0.1*(amount - A1)/(A2 - A1);
        System.out.println(tempguess);
    } while (Math.abs(result*10000 - tempguess*10000) > 1);
    return result; 
}

Note that as a general rule it is BAD to use double for monetary calculations as I have done here, but I'm writing a SO example, not production code. Also, it's java instead of .net, but it should help you with the algorithm.

Tho this is an old thread, I'd like to help others avoid wasting time on this - translating the code to PHP (or even javascript), gives wildly inaccurate results, causing me to wonder if it really worked in Java -

<?php
function generalEquation($period, $payment, $initialPeriods, $fractions, $rate){
    $retval = 0;
    for ($x = 0; $x < $period; $x++)
        $retval += $payment / ((1.0 + $fractions*$rate)*pow(1+$rate,$initialPeriods + $x));
    return $retval;
}

/**
 * 
 * @param amount The initial amount A
 * @param payment The periodic payment P
 * @param payments The total number of payments n
 * @param ppy The number of payment periods per year 
 * @param APRGuess The guess to start estimating from, 10% is 0.1, not 0.001
 * @param partial Odd days, as a fraction of a pay period.  10 days of a month is 0.33333...
 * @param full Full pay periods before the first payment.  Usually 1.
 * @return The calculated APR
 */
function findAPR($amount, $payment, $payments, $ppy, $APRGuess, $partial, $full)    
{
    $result = $APRGuess;
    $tempguess = $APRGuess;       

    do
    {
        $result = $tempguess;
        //Step 1
        $i = $tempguess/(100*$ppy);
        $A1 = generalEquation($payments, $payment, $full, $partial, $i);
        //Step 2
        $i2 = ($tempguess + 0.1)/(100*$ppy);
        $A2 = generalEquation($payments, $payment, $full, $partial, $i2);
        //Step 3
        $tempguess = $tempguess + 0.1*($amount - $A1)/($A2 - $A1);
    } while (abs($result*10000 - $tempguess*10000) > 1);
    return $result; 
}
// these figures should calculate to 12.5 apr (see below)..
$apr = findAPR(10000,389.84,(30*389.84),12,.11,0,1);
echo "APR: $apr" . "%";
?>

APR: 12.5000% Total Financial Charges: $1,695.32 Amount Financed: $10,000.00 Total Payments: $11,695.32 Total Loan: $10,000.00 Monthly Payment: $389.84 Total Interest: $1,695.32

I got me a Python (3.4) translation here. And since my application takes dates as inputs, not full and partial payment periods, I threw in a way to calculate those. I referenced a document by one of the guys that wrote the OCC's APRWIN, and I'd advise others to read it if you need to re-translate this.

My tests come straight from the Reg Z examples. I haven't done further testing with APRWIN yet. An edge case I don't have to deal with (so haven't coded for) is when you only have 2 installments and the first is an irregular period. Check the document above if that's a potential use case for your app. I also haven't fully tested most of the payment schedules because my app only needs monthly and quarterly. The rest are just there to use Reg Z's examples.

# loan_amt: initial amount of A
# payment_amt: periodic payment P
# num_of_pay: total number of payment P
# ppy: number of payment periods per year
# apr_guess: guess to start estimating from. Default = .05, or 5%
# odd_days: odd days, meaning the fraction of a pay period for the first
    # installment. If the pay period is monthly & the first installment is
    # due after 45 days, the odd_days are 15/30.
# full: full pay periods before the first payment. Usually 1
# advance: date the finance contract is supposed to be funded
# first_payment_due: first due date on the finance contract

import datetime
from dateutil.relativedelta import relativedelta

def generalEquation(period, payment_amt, full, odd_days, rate):
    retval = 0
    for x in range(period):
        retval += payment_amt / ((1.0 + odd_days * rate) * ((1 + rate) ** (
            x + full)))
    return retval

def _dt_to_int(dt):
    """A convenience function to change datetime objects into a day count,
        represented by an integer"""
    date_to_int = datetime.timedelta(days=1)
    _int = int(dt / date_to_int)
    return _int

def dayVarConversions(advance, first_payment_due, ppy):
    """Takes two datetime.date objects plus the ppy and returns the remainder
    of a pay period for the first installment of an irregular first payment 
    period (odd_days) and the number of full pay periods before the first 
    installment (full)."""

    if isinstance(advance, datetime.date) and isinstance(first_payment_due, 
        datetime.date):
        advance_to_first = -relativedelta(advance, first_payment_due)
            # returns a relativedelta object. 

            ## Appendix J requires calculating odd_days by counting BACKWARDS
            ## from the later date, first subtracting full unit-periods, then
            ## taking the remainder as odd_days. relativedelta lets you
            ## calculate this easily.

            # advance_date = datetime.date(2015, 2, 27)
            # first_pay_date = datetime.date(2015, 4, 1)
            # incorrect = relativedelta(first_pay_date, advance_date)
            # correct = -relativedelta(advance_date, first_pay_date)
            # print("See the difference between ", correct, " and ", incorrect, "?")

        if ppy == 12:
            # If the payment schedule is monthly
            full = advance_to_first.months + (advance_to_first.years * 12)
            odd_days = advance_to_first.days / 30
            if odd_days == 1:
                odd_days = 0
                full += 1
                # Appendix J (b)(5)(ii) requires the use of 30 in the 
                # denominator even if a month has 31 days, so Jan 1 to Jan 31
                # counts as a full month without any odd days.
            return full, odd_days

        elif ppy == 4:
            # If the payment schedule is quarterly
            full = (advance_to_first.months // 3) + (advance_to_first.years * 4)
            odd_days = ((advance_to_first.months % 3) * 30 + advance_to_first. \
                days) / 90
            if odd_days == 1:
                odd_days = 0
                full += 1
                # Same as above. Sometimes odd_days would be 90/91, but not under
                # Reg Z.
            return full, odd_days

        elif ppy == 2:
            # Semiannual payments
            full = (advance_to_first.months // 6) + (advance_to_first.years * 2)
            odd_days = ((advance_to_first.months % 6) * 30 + advance_to_first. \
                days) / 180
            if odd_days == 1:
                odd_days = 0
                full += 1
            return full, odd_days

        elif ppy == 24:
            # Semimonthly payments
            full = (advance_to_first.months * 2) + (advance_to_first.years * \
                24) + (advance_to_first.days // 15)
            odd_days = ((advance_to_first.days % 15) / 15)
            if odd_days == 1:
                odd_days = 0
                full += 1
            return full, odd_days

        elif ppy == 52:
            # If the payment schedule is weekly, then things get real
            convert_to_days = first_payment_due - advance
                # Making a timedelta object
            days_per_week = datetime.timedelta(days=7)
                # A timedelta object equal to 1 week
            if advance_to_first.years == 0:
                full, odd_days = divmod(convert_to_days, days_per_week)
                    # Divide, save the remainder
                odd_days = _dt_to_int(odd_days) / 7
                    # Convert odd_days from a timedelta object to an int
                return full, odd_days
            elif advance_to_first.years != 0 and advance_to_first.months == 0 \
                and advance_to_first.days == 0:
                # An exact year is an edge case. By convention, we consider 
                # this 52 weeks, not 52 weeks & 1 day (2 if a leap year)
                full = 52 * advance_to_first.years
                odd_days = 0
                return full, odd_days                
            else:
                # For >1 year, there need to be exactly 52 weeks per year, 
                # meaning 364 day years. The 365th day is a freebie.
                year_remainder = convert_to_days - datetime.timedelta(days=(
                    365 * advance_to_first.years))
                full, odd_days = divmod(year_remainder, days_per_week)
                full += 52 * advance_to_first.years
                    # Sum weeks from this year, weeks from past years
                odd_days = _dt_to_int(odd_days) / 7
                    # Convert odd_days from a timedelta object to an int
                return full, odd_days

        else:
            print("What ppy was that?") 
                ### Raise an error appropriate to your application

    else:
        print("'advance' and 'first_payment_due' should both be datetime.date objects")

def regulationZ_APR(loan_amt, payment_amt, num_of_pay, ppy, advance,
    first_payment_due, apr_guess=.05):
    """Returns the calculated APR using Regulation Z/Truth In Lending Appendix
    J's calculation method"""
    result = apr_guess
    tempguess = apr_guess + .1
    full, odd_days = dayVarConversions(advance, first_payment_due, ppy)

    while abs(result - tempguess) > .00001:
        result = tempguess
            # Step 1
        rate = tempguess/(100 * ppy)
        A1 = generalEquation(num_of_pay, payment_amt, full, odd_days, rate)
            # Step 2
        rate2 = (tempguess + 0.1)/(100 * ppy)
        A2 = generalEquation(num_of_pay, payment_amt, full, odd_days, rate2)
            # Step 3
        tempguess = tempguess + 0.1 * (loan_amt - A1)/(A2 - A1)

    return result


import unittest
class RegZTest(unittest.TestCase):
    def test_regular_first_period(self):
        testVar = round(regulationZ_APR(5000, 230, 24, 12, 
            datetime.date(1978, 1, 10), datetime.date(1978, 2, 10)), 2)
        self.assertEqual(testVar, 9.69)

    def test_long_first_payment(self):
        testVar = round(regulationZ_APR(6000, 200, 36, 12, 
            datetime.date(1978, 2, 10), datetime.date(1978, 4, 1)), 2)
        self.assertEqual(testVar, 11.82)

    def test_semimonthly_payment_short_first_period(self):
        testVar = round(regulationZ_APR(5000, 219.17, 24, 24, 
            datetime.date(1978, 2, 23), datetime.date(1978, 3, 1)), 2)
        self.assertEqual(testVar, 10.34)

    def test_semimonthly_payment_short_first_period2(self):
        testVar = round(regulationZ_APR(5000, 219.17, 24, 24, 
            datetime.date(1978, 2, 23), datetime.date(1978, 3, 1), apr_guess=
            10.34), 2)
        self.assertEqual(testVar, 10.34)

    def test_quarterly_payment_long_first_period(self):
        testVar = round(regulationZ_APR(10000, 385, 40, 4, 
            datetime.date(1978, 5, 23), datetime.date(1978, 10, 1), apr_guess=
            .35), 2)
        self.assertEqual(testVar, 8.97)

    def test_weekly_payment_long_first_period(self):
        testVar = round(regulationZ_APR(500, 17.6, 30, 52, 
            datetime.date(1978, 3, 20), datetime.date(1978, 4, 21), apr_guess=
            .1), 2)
        self.assertEqual(testVar, 14.96)

class dayVarConversionsTest(unittest.TestCase):     
    def test_regular_month(self):
        full, odd_days = dayVarConversions(datetime.date(1978, 1, 10), datetime.date(
            1978, 2, 10), 12)
        self.assertEqual(full, 1)
        self.assertEqual(odd_days, 0)

    def test_long_month(self):
        full, odd_days = dayVarConversions(datetime.date(1978, 2, 10), datetime.date(
            1978, 4, 1), 12)
        self.assertEqual(full, 1)
        self.assertEqual(odd_days, 19/30)

    def test_semimonthly_short(self):
        full, odd_days = dayVarConversions(datetime.date(1978, 2, 23), datetime.date(
            1978, 3, 1), 24)
        self.assertEqual(full, 0)
        self.assertEqual(odd_days, 6/15)

    def test_quarterly_long(self):
        full, odd_days = dayVarConversions(datetime.date(1978, 5, 23), datetime.date(
            1978, 10, 1), 4)
        self.assertEqual(full, 1)
        self.assertEqual(odd_days, 39/90)

    def test_weekly_long(self):
        full, odd_days = dayVarConversions(datetime.date(1978, 3, 20), datetime.date(
            1978, 4, 21), 52)
        self.assertEqual(full, 4)
        self.assertEqual(odd_days, 4/7)
易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!