Esempio n. 1
0
    def setUp(self):
        """ Sets up class attributes for convenience. """
        super().setUp()
        # We use caps because this is a type.
        # pylint: disable=invalid-name
        self.AccountType = Debt
        # pylint: enable=invalid-name

        # It's important to synchronize the initial years of related
        # objects, so store it here:
        self.initial_year = 2000
        # Every init requires an owner, so store that here:
        self.owner = Person(self.initial_year,
                            "test",
                            2000,
                            raise_rate={year: 1
                                        for year in range(2000, 2066)},
                            retirement_date=2065)

        # We'll also need a timing value for various tests.
        # Use two inflows, at the start and end, evenly weighted:
        self.timing = {Decimal(0): 1, Decimal(1): 1}

        # Basic Debt account:
        self.debt = Debt(self.owner,
                         balance=-1000,
                         minimum_payment=Money(10),
                         accelerated_payment=Money('Infinity'))
Esempio n. 2
0
    def setUp_decimal(self):
        initial_year = 2000
        person = Person(initial_year,
                        'Testy McTesterson',
                        1980,
                        retirement_date=2045)

        self.timing = Timing({Decimal(0.5): Decimal(1)})

        # These accounts have different rates:
        self.debt_big_high_interest = Debt(
            person,
            balance=Decimal(1000),
            rate=Decimal(1),
            minimum_payment=Decimal(100),
            accelerated_payment=Decimal('Infinity'),
            high_precision=Decimal)
        self.debt_small_low_interest = Debt(
            person,
            balance=Decimal(100),
            rate=Decimal(0),
            minimum_payment=Decimal(10),
            accelerated_payment=Decimal('Infinity'),
            high_precision=Decimal)
        self.debt_medium = Debt(person,
                                balance=Decimal(500),
                                rate=Decimal(0.5),
                                minimum_payment=Decimal(50),
                                accelerated_payment=Decimal('Infinity'),
                                high_precision=Decimal)

        self.debts = {
            self.debt_big_high_interest, self.debt_medium,
            self.debt_small_low_interest
        }

        self.max_payments = {
            debt: self.max_payment({debt})
            for debt in self.debts
        }
        self.min_payments = {
            debt: self.min_payment({debt})
            for debt in self.debts
        }

        self.strategy_avalanche = DebtPaymentStrategy(
            DebtPaymentStrategy.strategy_avalanche, high_precision=Decimal)
        self.strategy_snowball = DebtPaymentStrategy(
            DebtPaymentStrategy.strategy_snowball, high_precision=Decimal)

        self.excess = Decimal(10)
Esempio n. 3
0
    def setUp(self):
        initial_year = 2000
        person = Person(initial_year,
                        'Testy McTesterson',
                        1980,
                        retirement_date=2045)

        self.timing = Timing({0.5: 1})

        # These accounts have different rates:
        self.debt_big_high_interest = Debt(person,
                                           balance=1000,
                                           rate=1,
                                           minimum_payment=100,
                                           accelerated_payment=float('inf'))
        self.debt_small_low_interest = Debt(person,
                                            balance=100,
                                            rate=0,
                                            minimum_payment=10,
                                            accelerated_payment=float('inf'))
        self.debt_medium = Debt(person,
                                balance=500,
                                rate=0.5,
                                minimum_payment=50,
                                accelerated_payment=float('inf'))

        self.debts = {
            self.debt_big_high_interest, self.debt_medium,
            self.debt_small_low_interest
        }

        self.max_payments = {
            debt: self.max_payment({debt})
            for debt in self.debts
        }
        self.min_payments = {
            debt: self.min_payment({debt})
            for debt in self.debts
        }

        self.strategy_avalanche = DebtPaymentStrategy(
            DebtPaymentStrategy.strategy_avalanche)
        self.strategy_snowball = DebtPaymentStrategy(
            DebtPaymentStrategy.strategy_snowball)

        self.excess = 10
Esempio n. 4
0
 def setUp_decimal(self):
     """ Sets up variables based on Decimal inputs. """
     self.initial_year = 2000
     self.person = Person(self.initial_year,
                          "Test",
                          "1 January 1980",
                          retirement_date="1 January 2030",
                          high_precision=Decimal)
     # An RRSP with a $1000 balance and $100 in contribution room:
     self.rrsp = RRSP(initial_year=self.initial_year,
                      owner=self.person,
                      balance=Decimal(1000),
                      contribution_room=Decimal(100),
                      high_precision=Decimal)
     # Another RRSP, linked to the first one:
     # (For testing LinkedLimitAccount nodes)
     self.rrsp2 = RRSP(initial_year=self.initial_year,
                       owner=self.person,
                       high_precision=Decimal)
     # A TFSA with a $100 balance and $1000 in contribution room:
     self.tfsa = TFSA(initial_year=self.initial_year,
                      owner=self.person,
                      balance=Decimal(100),
                      contribution_room=Decimal(1000),
                      high_precision=Decimal)
     # Another TFSA, linked to the first one:
     self.tfsa2 = TFSA(initial_year=self.initial_year,
                       owner=self.person,
                       high_precision=Decimal)
     # A taxable account with $0 balance (and no contribution limit)
     self.taxable_account = TaxableAccount(initial_year=self.initial_year,
                                           owner=self.person,
                                           balance=Decimal(0),
                                           high_precision=Decimal)
     # A $100 debt with no interest and a $10 min. payment:
     self.debt = Debt(initial_year=self.initial_year,
                      owner=self.person,
                      balance=Decimal(100),
                      minimum_payment=Decimal(10),
                      high_precision=Decimal)
     # Contribute to RRSP, then TFSA, then taxable
     self.priority_ordered = [self.rrsp, self.tfsa, self.taxable_account]
     # Contribute 50% to RRSP, 25% to TFSA, and 25% to taxable
     self.priority_weighted = {
         self.rrsp: Decimal(0.5),
         self.tfsa: Decimal(0.25),
         self.taxable_account: Decimal(0.25)
     }
     # Contribute to RRSP and TFSA (50-50) until both are full, with
     # the remainder to taxable accounts.
     self.priority_nested = [{
         self.rrsp: Decimal(0.5),
         self.tfsa: Decimal(0.5)
     }, self.taxable_account]
Esempio n. 5
0
    def setUp_decimal(self):
        """ Builds default strategies/persons/etc. with Decimal inputs. """
        # pylint: disable=invalid-name
        # This name is based on `setUp`, which doesn't follow Pylint's rules
        # pylint: enable=invalid-name

        # Use a default settings object:
        # (This is conditional so that subclasses can assign their own
        # settings object before calling super().setUp())
        if not hasattr(self, 'settings'):
            self.settings = Settings()

        # To simplify tests, modify Settings so that forecasts are
        # just 2 years with easy-to-predict contributions ($1000/yr)
        self.settings.num_years = 2
        self.settings.living_expenses_strategy = (
            LivingExpensesStrategy.strategy_const_contribution)
        self.settings.living_expenses_base_amount = Decimal(1000)

        # Allow subclasses to use subclasses of Forecaster by assigning
        # to forecaster_type
        if not hasattr(self, 'forecaster_type'):
            self.forecaster_type = Forecaster

        # Build default `SubForecast` inputs based on `settings`:
        self.initial_year = self.settings.initial_year
        self.scenario = Scenario(
            inflation=Decimal(self.settings.inflation),
            stock_return=Decimal(self.settings.stock_return),
            bond_return=Decimal(self.settings.bond_return),
            other_return=Decimal(self.settings.other_return),
            management_fees=Decimal(self.settings.management_fees),
            initial_year=self.settings.initial_year,
            num_years=self.settings.num_years)
        self.living_expenses_strategy = LivingExpensesStrategy(
            strategy=self.settings.living_expenses_strategy,
            base_amount=Decimal(self.settings.living_expenses_base_amount),
            rate=Decimal(self.settings.living_expenses_rate),
            inflation_adjust=self.scenario.inflation_adjust)
        self.saving_strategy = TransactionStrategy(
            strategy=self.settings.saving_strategy,
            weights={
                year: Decimal(val)
                for (year, val) in self.settings.saving_weights.items()
            })
        self.withdrawal_strategy = TransactionStrategy(
            strategy=self.settings.withdrawal_strategy,
            weights={
                year: Decimal(val)
                for (year, val) in self.settings.withdrawal_weights.items()
            })
        self.allocation_strategy = AllocationStrategy(
            strategy=self.settings.allocation_strategy,
            min_equity=Decimal(self.settings.allocation_min_equity),
            max_equity=Decimal(self.settings.allocation_max_equity),
            target=Decimal(self.settings.allocation_target),
            standard_retirement_age=(
                self.settings.allocation_std_retirement_age),
            risk_transition_period=self.settings.allocation_risk_trans_period,
            adjust_for_retirement_plan=(
                self.settings.allocation_adjust_retirement))
        self.debt_payment_strategy = DebtPaymentStrategy(
            strategy=self.settings.debt_payment_strategy,
            high_precision=Decimal)
        self.tax_treatment = Tax(
            tax_brackets={
                year: {
                    Decimal(lower): Decimal(upper)
                }
                for (year, vals) in self.settings.tax_brackets.items()
                for (lower, upper) in vals.items()
            },
            personal_deduction={
                year: Decimal(val)
                for (year,
                     val) in self.settings.tax_personal_deduction.items()
            },
            credit_rate={
                year: Decimal(val)
                for (year, val) in self.settings.tax_credit_rate.items()
            },
            inflation_adjust=self.scenario.inflation_adjust,
            high_precision=Decimal)

        # Now build some Ledger objects to test against:
        # A person making $10,000/yr
        self.person = Person(initial_year=self.initial_year,
                             name="Test 1",
                             birth_date="1 January 1980",
                             retirement_date="31 December 2040",
                             gross_income=Decimal(10000),
                             raise_rate=Decimal(0),
                             spouse=None,
                             tax_treatment=self.tax_treatment,
                             high_precision=Decimal)
        # An account with $1000 in it (and no interest)
        self.account = Account(owner=self.person,
                               balance=Decimal(1000),
                               high_precision=Decimal)
        # A debt with a $100 balance (and no interest)
        self.debt = Debt(owner=self.person,
                         balance=Decimal(100),
                         high_precision=Decimal)

        # Init a Forecaster object here for convenience:
        self.forecaster = self.forecaster_type(settings=self.settings,
                                               high_precision=Decimal)
Esempio n. 6
0
    def setUp(self):
        """ Builds default strategies, persons, etc. """
        # Use a default settings object:
        # (This is conditional so that subclasses can assign their own
        # settings object before calling super().setUp())
        if not hasattr(self, 'settings'):
            self.settings = Settings()

        # To simplify tests, modify Settings so that forecasts are
        # just 2 years with easy-to-predict contributions ($1000/yr)
        self.settings.num_years = 2
        self.settings.living_expenses_strategy = (
            LivingExpensesStrategy.strategy_const_contribution)
        self.settings.living_expenses_base_amount = 1000

        # Allow subclasses to use subclasses of Forecaster by assigning
        # to forecaster_type
        if not hasattr(self, 'forecaster_type'):
            self.forecaster_type = Forecaster

        # Build default `SubForecast` inputs based on `settings`:
        self.initial_year = self.settings.initial_year
        self.scenario = Scenario(inflation=self.settings.inflation,
                                 stock_return=self.settings.stock_return,
                                 bond_return=self.settings.bond_return,
                                 other_return=self.settings.other_return,
                                 management_fees=self.settings.management_fees,
                                 initial_year=self.settings.initial_year,
                                 num_years=self.settings.num_years)
        self.living_expenses_strategy = LivingExpensesStrategy(
            strategy=self.settings.living_expenses_strategy,
            base_amount=self.settings.living_expenses_base_amount,
            rate=self.settings.living_expenses_rate,
            inflation_adjust=self.scenario.inflation_adjust)
        self.saving_strategy = TransactionStrategy(
            strategy=self.settings.saving_strategy,
            weights=self.settings.saving_weights)
        self.withdrawal_strategy = TransactionStrategy(
            strategy=self.settings.withdrawal_strategy,
            weights=self.settings.withdrawal_weights)
        self.allocation_strategy = AllocationStrategy(
            strategy=self.settings.allocation_strategy,
            min_equity=self.settings.allocation_min_equity,
            max_equity=self.settings.allocation_max_equity,
            target=self.settings.allocation_target,
            standard_retirement_age=(
                self.settings.allocation_std_retirement_age),
            risk_transition_period=self.settings.allocation_risk_trans_period,
            adjust_for_retirement_plan=(
                self.settings.allocation_adjust_retirement))
        self.debt_payment_strategy = DebtPaymentStrategy(
            strategy=self.settings.debt_payment_strategy)
        self.tax_treatment = Tax(
            tax_brackets=self.settings.tax_brackets,
            personal_deduction=self.settings.tax_personal_deduction,
            credit_rate=self.settings.tax_credit_rate,
            inflation_adjust=self.scenario.inflation_adjust)

        # Now build some Ledger objects to test against:
        # A person making $10,000/yr
        self.person = Person(initial_year=self.initial_year,
                             name="Test 1",
                             birth_date="1 January 1980",
                             retirement_date="31 December 2040",
                             gross_income=10000,
                             raise_rate=0,
                             spouse=None,
                             tax_treatment=self.tax_treatment)
        # An account with $1000 in it (and no interest)
        self.account = Account(owner=self.person, balance=1000)
        # A debt with a $100 balance (and no interest)
        self.debt = Debt(owner=self.person, balance=100)

        # Init a Forecaster object here for convenience:
        self.forecaster = self.forecaster_type(settings=self.settings)
Esempio n. 7
0
class TestDebtPaymentStrategies(TestCaseTransactions):
    """ Tests the strategies of the `DebtPaymentStrategy` class.

    In particular, this class tests various payments using various
    strategies on a stock set of (multiple) debts.
    """
    def setUp(self):
        initial_year = 2000
        person = Person(initial_year,
                        'Testy McTesterson',
                        1980,
                        retirement_date=2045)

        self.timing = Timing({Decimal(0.5): 1})

        # These accounts have different rates:
        self.debt_big_high_interest = Debt(
            person,
            balance=Money(1000),
            rate=1,
            minimum_payment=Money(100),
            accelerated_payment=Money('Infinity'))
        self.debt_small_low_interest = Debt(
            person,
            balance=Money(100),
            rate=0,
            minimum_payment=Money(10),
            accelerated_payment=Money('Infinity'))
        self.debt_medium = Debt(person,
                                balance=Money(500),
                                rate=0.5,
                                minimum_payment=Money(50),
                                accelerated_payment=Money('Infinity'))

        self.debts = {
            self.debt_big_high_interest, self.debt_medium,
            self.debt_small_low_interest
        }

        self.max_payments = {
            debt: self.max_payment({debt})
            for debt in self.debts
        }
        self.min_payments = {
            debt: self.min_payment({debt})
            for debt in self.debts
        }

        self.strategy_avalanche = DebtPaymentStrategy(
            DebtPaymentStrategy.strategy_avalanche)
        self.strategy_snowball = DebtPaymentStrategy(
            DebtPaymentStrategy.strategy_snowball)

        self.excess = Money(10)

    def min_payment(self, debts, timing=None):
        """ Finds the minimum payment *from savings* for `accounts`. """
        if timing is None:
            timing = self.timing
        return sum(
            sum(debt.min_inflows(timing=timing).values()) for debt in debts)

    def max_payment(self, debts, timing=None):
        """ Finds the maximum payment *from savings* for `debts`. """
        if timing is None:
            timing = self.timing
        return sum(
            sum(debt.max_inflows(timing=timing).values()) for debt in debts)

    def make_available(self, total, timing=None):
        """ Generates an `available` dict of cashflows. """
        if timing is None:
            timing = self.timing
        normalization = sum(timing.values())
        return {
            when: total * weight / normalization
            for when, weight in timing.items()
        }

    def test_snowball_min_payment(self):
        """ Test strategy_snowball with the minimum payment only. """
        # Inflows will exactly match minimum payments:
        total = self.min_payment(self.debts)
        available = self.make_available(total)
        results = self.strategy_snowball(self.debts, available)
        for debt in self.debts:
            self.assertTransactions(results[debt], debt.minimum_payment)

    def test_snowball_less_than_min(self):
        """ Test strategy_snowball with less than the minimum payments. """
        # DebtPaymentStrategy (like TransactionStrategy) always
        # allocates no more than the amount available.
        # This test confirms that:
        # 1)    The sum of amounts allocated is equal to `total`
        # 2)    The order of accounts is maintained (i.e. smallest
        #       debts repaid first)
        # To do this, we set `total` to a non-zero smaller than the
        # smallest debt's minimum payment.
        total = self.min_payments[self.debt_small_low_interest] / 2
        available = self.make_available(total)
        results = self.strategy_snowball(self.debts, available)
        # Entire repayment should go to the smallest debt:
        self.assertTransactions(results[self.debt_small_low_interest], total)
        # Remaining debts should receive no payments:
        if self.debt_medium in results:
            self.assertTransactions(results[self.debt_medium], Money(0))
        if self.debt_medium in results:
            self.assertTransactions(results[self.debt_big_high_interest],
                                    Money(0))

    def test_snowball_basic(self):
        """ Test strategy_snowball with a little more than min payments. """
        # The smallest debt should be paid first.
        total = self.min_payment(self.debts) + self.excess
        available = self.make_available(total)
        results = self.strategy_snowball(self.debts, available)
        # The smallest debt should be partially repaid:
        self.assertTransactions(
            results[self.debt_small_low_interest],
            self.debt_small_low_interest.minimum_payment + self.excess)
        # The medium-sized debt should get its minimum payments:
        self.assertTransactions(results[self.debt_medium],
                                self.min_payments[self.debt_medium])
        # The largest debt should get its minimum payments:
        self.assertTransactions(results[self.debt_big_high_interest],
                                self.min_payments[self.debt_big_high_interest])

    def test_snowball_close_one(self):
        """ Test strategy_snowball payments to close one debt. """
        # Pay more than the first-paid debt will accomodate.
        # The excess should go to the next-paid debt (medium).
        total = self.min_payment(self.debts - {self.debt_small_low_interest})
        total += self.max_payment({self.debt_small_low_interest})
        total += self.excess
        available = self.make_available(total)
        results = self.strategy_snowball(self.debts, available)
        # The smallest debt should be fully repaid:
        self.assertTransactions(
            results[self.debt_small_low_interest],
            self.max_payments[self.debt_small_low_interest])
        # The medium-sized debt should be partially repaid:
        self.assertTransactions(results[self.debt_medium],
                                self.debt_medium.minimum_payment + self.excess)
        # The largest debt should get its minimum payments:
        self.assertTransactions(results[self.debt_big_high_interest],
                                self.min_payments[self.debt_big_high_interest])

    def test_snowball_close_two(self):
        """ Test strategy_snowball with payments to close 2 debts. """
        # Pay more than the first and second-paid debts will accomodate.
        # The self.excess should go to the next-paid debt.
        total = self.min_payment({self.debt_big_high_interest})
        total += self.max_payment(self.debts - {self.debt_big_high_interest})
        total += self.excess
        available = self.make_available(total)
        results = self.strategy_snowball(self.debts, available)
        # The smallest debt should be fully repaid:
        self.assertTransactions(
            results[self.debt_small_low_interest],
            self.max_payments[self.debt_small_low_interest])
        # The medium-size debt should be fully repaid:
        self.assertTransactions(results[self.debt_medium],
                                self.max_payments[self.debt_medium])
        # The largest debt should be partially repaid:
        self.assertTransactions(
            results[self.debt_big_high_interest],
            self.debt_big_high_interest.minimum_payment + self.excess)

    def test_snowball_close_all(self):
        """ Test strategy_snowball with payments to close all debts. """
        # Contribute more than the total max.
        total = self.max_payment(self.debts) + self.excess
        available = self.make_available(total)
        results = self.strategy_snowball(self.debts, available)
        # Each debt should be fully repaid:
        for debt in self.debts:
            self.assertTransactions(results[debt], self.max_payments[debt])

    def test_avalanche_min_payment(self):
        """ Test strategy_avalanche with minimum payments only. """
        total = self.min_payment(self.debts)
        available = self.make_available(total)
        results = self.strategy_avalanche(self.debts, available)
        for debt in self.debts:
            self.assertTransactions(results[debt], debt.minimum_payment)

    def test_avalanche_less_than_min(self):
        """ Test strategy_avalanche with less than the minimum payments. """
        # DebtPaymentStrategy (like TransactionStrategy) always
        # allocates no more than the amount available.
        # This test confirms that:
        # 1)    The sum of amounts allocated is equal to `total`
        # 2)    The order of accounts is maintained (i.e.
        #       highest-interest debts repaid first)
        # To do this, we set `total` to a non-zero smaller than the
        # highest-interest debt's minimum payment.
        total = self.min_payments[self.debt_big_high_interest] / 2
        available = self.make_available(total)
        results = self.strategy_avalanche(self.debts, available)
        # Entire repayment should go to the highest-interest debt:
        self.assertTransactions(results[self.debt_big_high_interest], total)
        # Remaining debts should receive no payments:
        if self.debt_medium in results:
            self.assertTransactions(results[self.debt_medium], Money(0))
        if self.debt_medium in results:
            self.assertTransactions(results[self.debt_small_low_interest],
                                    Money(0))

    def test_avalanche_basic(self):
        """ Test strategy_avalanche with a bit more than min payments. """
        # The highest-interest debt should be paid first.
        total = self.min_payment(self.debts) + self.excess
        available = self.make_available(total)
        results = self.strategy_avalanche(self.debts, available)
        self.assertTransactions(
            results[self.debt_big_high_interest],
            self.debt_big_high_interest.minimum_payment + self.excess)
        self.assertTransactions(results[self.debt_medium],
                                self.debt_medium.minimum_payment)
        self.assertTransactions(results[self.debt_small_low_interest],
                                self.debt_small_low_interest.minimum_payment)

    def test_avalanche_close_one(self):
        """ Test strategy_avalanche with payments to close one debt. """
        # Pay more than the first-paid debt will accomodate.
        # The excess should go to the next-paid debt (medium).
        total = self.min_payment(self.debts - {self.debt_big_high_interest})
        total += self.max_payment({self.debt_big_high_interest})
        total += self.excess
        available = self.make_available(total)
        results = self.strategy_avalanche(self.debts, available)
        # The high interest debt should be fully repaid:
        self.assertTransactions(results[self.debt_big_high_interest],
                                self.max_payments[self.debt_big_high_interest])
        # The medium-interest debt should be partially repaid:
        self.assertTransactions(results[self.debt_medium],
                                self.debt_medium.minimum_payment + self.excess)
        # The low-interest debt should receive the minimum payment:
        self.assertTransactions(results[self.debt_small_low_interest],
                                self.debt_small_low_interest.minimum_payment)

    def test_avalanche_close_two(self):
        """ Test strategy_avalanche with payments to close two debts. """
        # Pay more than the first and second-paid debts will accomodate.
        # The excess should go to the next-paid debt.
        total = self.min_payment({self.debt_small_low_interest})
        total += self.max_payment(self.debts - {self.debt_small_low_interest})
        total += self.excess
        available = self.make_available(total)
        results = self.strategy_avalanche(self.debts, available)
        # The high interest debt should be fully repaid:
        self.assertTransactions(results[self.debt_big_high_interest],
                                self.max_payments[self.debt_big_high_interest])
        # The medium interest debt should be fully repaid:
        self.assertTransactions(results[self.debt_medium],
                                self.max_payments[self.debt_medium])
        # The low interest debt should be partially repaid:
        self.assertTransactions(
            results[self.debt_small_low_interest],
            self.debt_small_low_interest.minimum_payment + self.excess)

    def test_avalanche_close_all(self):
        """ Test strategy_avalanche with payments to close all debts. """
        # Contribute more than the total max.
        total = self.max_payment(self.debts) + self.excess
        available = self.make_available(total)
        results = self.strategy_avalanche(self.debts, available)
        # All debts should be fully repaid:
        for debt in self.debts:
            self.assertTransactions(results[debt], self.max_payments[debt])

    def test_accel_payment_none(self):
        """ Tests payments where `accelerate_payment=Money(0)`. """
        # Don't allow accelerated payments and try to contribute more
        # than the minimum.
        self.debt_medium.accelerated_payment = Money(0)
        available = self.make_available(self.debt_medium.minimum_payment * 2)
        results = self.strategy_avalanche({self.debt_medium}, available)
        # If there's no acceleration, only the minimum is paid.
        self.assertTransactions(results[self.debt_medium],
                                self.debt_medium.minimum_payment)

    def test_accel_payment_finite(self):
        """ Tests payments with finite, non-zero `accelerate_payment`. """
        # Allow only $20 in accelerated payments and try to contribute
        # even more than that.
        self.debt_medium.accelerated_payment = Money(20)
        available = self.make_available(self.debt_medium.minimum_payment +
                                        Money(40))
        results = self.strategy_avalanche({self.debt_medium}, available)
        # Payment should be $20 more than the minimum:
        self.assertTransactions(results[self.debt_medium],
                                self.debt_medium.minimum_payment + Money(20))

    def test_accel_payment_infinity(self):
        """ Tests payments where `accelerate_payment=Money('Infinity')`. """
        # No limit on accelerated payments. Try to contribute more than
        # the debt requires to be fully repaid:
        self.debt_medium.accelerated_payment = Money("Infinity")
        available = self.make_available(
            2 * sum(self.debt_medium.max_inflows().values()))
        results = self.strategy_avalanche({self.debt_medium}, available)
        # Payments should max out money available:
        self.assertTransactions(results[self.debt_medium],
                                self.max_payments[self.debt_medium])
Esempio n. 8
0
class TestDebtMethods(unittest.TestCase):
    """ Test Debt. """
    def setUp(self):
        """ Sets up class attributes for convenience. """
        super().setUp()
        # We use caps because this is a type.
        # pylint: disable=invalid-name
        self.AccountType = Debt
        # pylint: enable=invalid-name

        # It's important to synchronize the initial years of related
        # objects, so store it here:
        self.initial_year = 2000
        # Every init requires an owner, so store that here:
        self.owner = Person(self.initial_year,
                            "test",
                            2000,
                            raise_rate={year: 1
                                        for year in range(2000, 2066)},
                            retirement_date=2065)

        # We'll also need a timing value for various tests.
        # Use two inflows, at the start and end, evenly weighted:
        self.timing = {Decimal(0): 1, Decimal(1): 1}

        # Basic Debt account:
        self.debt = Debt(self.owner,
                         balance=-1000,
                         minimum_payment=Money(10),
                         accelerated_payment=Money('Infinity'))

    def test_init_default(self):
        """ Test Debt.__init__ with default args. """
        account = self.AccountType(self.owner)
        self.assertEqual(account.minimum_payment, Money(0))
        self.assertEqual(account.accelerated_payment, Money('Infinity'))

    def test_init_explicit(self, *args, **kwargs):
        """ Test Debt.__init__ with explicit args of expected types. """
        minimum_payment = Money(100)
        accelerated_payment = Money(0)
        account = self.AccountType(self.owner,
                                   *args,
                                   minimum_payment=minimum_payment,
                                   accelerated_payment=accelerated_payment,
                                   **kwargs)
        self.assertEqual(account.minimum_payment, minimum_payment)
        self.assertEqual(account.accelerated_payment, accelerated_payment)

    def test_init_convert(self, *args, **kwargs):
        """ Test Debt.__init__ with args needing conversion. """
        minimum_payment = 100
        accelerated_payment = 10
        account = self.AccountType(self.owner,
                                   *args,
                                   minimum_payment=minimum_payment,
                                   accelerated_payment=accelerated_payment,
                                   **kwargs)
        self.assertEqual(account.minimum_payment, minimum_payment)
        self.assertEqual(account.accelerated_payment,
                         Money(accelerated_payment))

    def test_init_invalid(self, *args, **kwargs):
        """ Test Debt.__init__ with non-convertible args. """
        with self.assertRaises(decimal.InvalidOperation):
            _ = self.AccountType(self.owner,
                                 *args,
                                 minimum_payment='invalid',
                                 **kwargs)
        with self.assertRaises(decimal.InvalidOperation):
            _ = self.AccountType(self.owner,
                                 *args,
                                 accelerated_payment='invalid',
                                 **kwargs)

    def test_max_inflows_large_balance(self):
        """ Test `max_inflows` with balance greater than min. payment. """
        self.debt.minimum_payment = 100
        self.debt.balance = Money(-1000)
        result = self.debt.max_inflows(self.timing)
        # Test result by adding those transactions to the account
        # and confirming that it brings the balance to $0:
        for when, value in result.items():
            self.debt.add_transaction(value, when=when)
        balance = self.debt.balance_at_time('end')
        self.assertAlmostEqual(balance, Money(0))

    def test_max_inflows_small_balance(self):
        """ Test `max_inflows` with balance less than minimum payment. """
        self.debt.minimum_payment = 1000
        self.debt.balance = Money(-100)
        result = self.debt.max_inflows(self.timing)
        for when, value in result.items():
            self.debt.add_transaction(value, when=when)
        # Result of `max_outflows` should bring balance to $0 if
        # applied as transactions:
        self.assertAlmostEqual(self.debt.balance_at_time('end'), Money(0))

    def test_max_inflows_zero_balance(self):
        """ Test `max_inflows` with zero balance. """
        self.debt.minimum_payment = 100
        self.debt.balance = Money(0)
        result = self.debt.max_inflows(self.timing)
        for value in result.values():
            self.assertEqual(value, Money(0))

    def test_max_inflows_no_accel(self):
        """ Test `max_inflows` with zero `accelerated_payment`. """
        self.debt.minimum_payment = 100
        self.debt.balance = Money(-200)
        self.debt.accelerated_payment = 0
        result = self.debt.max_inflows(self.timing)
        # Total inflows should be limited to minimum_payment:
        self.assertEqual(sum(result.values()), self.debt.minimum_payment)

    def test_max_inflows_finite_accel(self):
        """ Test `max_inflows` with finite `accelerated_payment`. """
        self.debt.minimum_payment = 100
        self.debt.balance = Money(-200)
        self.debt.accelerated_payment = 50
        result = self.debt.max_inflows(self.timing)
        # Total inflows should be limited to min. payment + accel:
        self.assertEqual(
            sum(result.values()),
            self.debt.minimum_payment + self.debt.accelerated_payment)

    def test_max_inflows_small_inflow(self):
        """ Test `max_inflows` with small pre-existing inflows. """
        self.debt.minimum_payment = 100
        self.debt.balance = Money(-200)
        self.debt.accelerated_payment = 50
        # Add an inflow that's less than the total that we could pay:
        self.debt.add_transaction(60)
        result = self.debt.max_inflows(self.timing)
        target = (self.debt.minimum_payment + self.debt.accelerated_payment -
                  Money(60))  # Amount already added
        # Total inflows should be limited to amount remaining after
        # existing transactions, up to min. payment + accel:
        self.assertEqual(sum(result.values()), target)

    def test_max_inflows_large_inflow(self):
        """ Test `max_inflows` with inflows greater than the total max. """
        self.debt.minimum_payment = 100
        self.debt.balance = Money(-200)
        self.debt.accelerated_payment = 0
        # Add an inflow that's more than the total that we can pay:
        self.debt.add_transaction(170)
        result = self.debt.max_inflows(self.timing)
        target = Money(0)  # We can't add any more
        # The result should be $0, not a negative value:
        self.assertEqual(sum(result.values()), target)

    def test_min_inflows_large_balance(self):
        """ Test `min_inflows` with balance greater than min. payment. """
        self.debt.minimum_payment = 100
        self.debt.balance = Money(-1000)
        result = self.debt.min_inflows(self.timing)
        # Inflows should be capped at minimum payment:
        self.assertEqual(sum(result.values()), self.debt.minimum_payment)

    def test_min_inflows_small_balance(self):
        """ Test `min_inflows` with balance less than min. payment. """
        self.debt.minimum_payment = 1000
        self.debt.balance = Money(-100)
        # The resuls will be impacted by the timing of outflows, so
        # pick a specific timing here: a lump sum at end of year.
        timing = {Decimal(1): 1}
        result = self.debt.min_inflows(timing)
        # Inflows should be capped at the balance at the time the
        # transaction was made (i.e. $100):
        self.assertEqual(-sum(result.values()), self.debt.balance_at_time(1))

    def test_min_inflows_zero_balance(self):
        """ Test `min_inflows` with zero balance. """
        self.debt.minimum_payment = 100
        self.debt.balance = Money(0)
        result = self.debt.min_inflows(self.timing)
        # No inflows should be made to a fully-paid debt:
        self.assertEqual(sum(result.values()), Money(0))

    def test_min_inflows_small_inflow(self):
        """ Test `min_inflows` with small pre-existing inflows. """
        self.debt.minimum_payment = 10
        self.debt.balance = Money(-100)
        # Add inflow less than the min. payment:
        self.debt.add_transaction(5)
        result = self.debt.min_inflows(self.timing)
        # We only need to add another $5 to reach the min. payment:
        self.assertEqual(sum(result.values()), Money(5))

    def test_min_inflows_large_inflow(self):
        """ Test `min_inflows` with inflows more than the min. payment. """
        self.debt.minimum_payment = 10
        self.debt.balance = Money(-100)
        # Add inflow greater than the min. payment:
        self.debt.add_transaction(20)
        result = self.debt.min_inflows(self.timing)
        # No need to add any more payments to reach the minimum:
        self.assertEqual(sum(result.values()), Money(0))