コード例 #1
0
ファイル: test_strategy.py プロジェクト: dxcv/forecaster
    def setUp(self):
        """ Sets up variables for testing. """
        # Different groups of tests use different groups of variables.
        # We could split this class up into multiple classes (perhaps
        # one for ordered strategies and one for weighted strategies?),
        # but for now this project's practice is one test case for each
        # custom class.
        # pylint: disable=too-many-instance-attributes

        initial_year = 2000
        person = Person(
            initial_year, 'Testy McTesterson', 1980, retirement_date=2045)

        # Set up some accounts for the tests.
        self.rrsp = RRSP(
            person,
            balance=Money(200), rate=0, contribution_room=Money(200))
        self.tfsa = TFSA(
            person,
            balance=Money(100), rate=0, contribution_room=Money(100))
        self.taxable_account = TaxableAccount(
            person, balance=Money(1000), rate=0)
        self.accounts = {self.rrsp, self.tfsa, self.taxable_account}

        # Define a simple timing for transactions:
        self.timing = {Decimal(0.5): 1}

        # Build strategy for testing (in non-init tests):
        self.strategy = TransactionStrategy(
            TransactionStrategy.strategy_ordered, {
                'RRSP': 1,
                'TFSA': 2,
                'TaxableAccount': 3
            })
コード例 #2
0
ファイル: test_strategy.py プロジェクト: dxcv/forecaster
    def setUp(self):
        """ Sets up variables for testing. """
        # Vars for building accounts:
        initial_year = 2000
        person = Person(
            initial_year, 'Testy McTesterson', 1980, retirement_date=2045)

        # Set up some accounts for the tests.
        self.rrsp = RRSP(
            person,
            balance=Money(200), rate=0, contribution_room=Money(200))
        self.rrsp2 = RRSP(person, balance=Money(100), rate=0)
        self.tfsa = TFSA(
            person,
            balance=Money(100), rate=0, contribution_room=Money(100))
        self.taxable_account = TaxableAccount(
            person, balance=Money(1000), rate=0)
        self.accounts = {
            self.rrsp, self.rrsp2, self.tfsa, self.taxable_account
        }

        # Define a simple timing for transactions:
        self.timing = {Decimal(0.5): 1}

        # Build strategy for testing (in non-init tests):
        self.weights = {
            'RRSP': Decimal('0.4'),
            'TFSA': Decimal('0.3'),
            'TaxableAccount': Decimal('0.3')
        }
        self.strategy = TransactionStrategy(
            TransactionStrategy.strategy_weighted, self.weights)
コード例 #3
0
    def setUp(self):
        """ Sets up variables for testing. """

        # Vars for building accounts:
        initial_year = 2000
        person = Person(initial_year,
                        'Testy McTesterson',
                        1980,
                        retirement_date=2045)

        # Set up some accounts for the tests.
        self.rrsp = RRSP(person, balance=200, rate=0, contribution_room=200)
        self.tfsa = TFSA(person, balance=100, rate=0, contribution_room=100)
        self.taxable_account = TaxableAccount(person, balance=1000, rate=0)
        self.accounts = {self.rrsp, self.tfsa, self.taxable_account}

        # Define a simple timing for transactions:
        self.timing = {0.5: 1}

        self.max_outflow = sum(
            sum(account.max_outflows(timing=self.timing).values())
            for account in self.accounts)
        self.max_inflows = sum(
            sum(account.max_inflows(timing=self.timing).values())
            for account in self.accounts)

        # Build strategy for testing (in non-init tests):
        self.weights = {'RRSP': 0.4, 'TFSA': 0.3, 'TaxableAccount': 0.3}
        self.strategy = TransactionStrategy(
            TransactionStrategy.strategy_weighted, self.weights)
コード例 #4
0
    def setUp_decimal(self):
        """ Sets up variables for testing. """
        # pylint: disable=invalid-name
        # Pylint doesn't like `setUp_decimal`, but it's not our naming
        # convention, so don't complain to us!
        # pylint: enable=invalid-name
        initial_year = 2000
        person = Person(initial_year,
                        'Testy McTesterson',
                        1980,
                        retirement_date=2045,
                        high_precision=Decimal)

        # Set up some accounts for the tests.
        self.rrsp = RRSP(person,
                         balance=Decimal(200),
                         rate=Decimal(0),
                         contribution_room=Decimal(200),
                         high_precision=Decimal)
        self.rrsp2 = RRSP(person,
                          balance=Decimal(100),
                          rate=Decimal(0),
                          high_precision=Decimal)
        self.tfsa = TFSA(person,
                         balance=Decimal(100),
                         rate=Decimal(0),
                         contribution_room=Decimal(100),
                         high_precision=Decimal)
        self.taxable_account = TaxableAccount(person,
                                              balance=Decimal(1000),
                                              rate=Decimal(0),
                                              high_precision=Decimal)
        self.accounts = {
            self.rrsp, self.rrsp2, self.tfsa, self.taxable_account
        }

        # Define a simple timing for transactions:
        self.timing = {Decimal(0.5): Decimal(1)}

        self.max_outflow = sum(
            sum(account.max_outflows(timing=self.timing).values())
            for account in self.accounts)
        self.max_inflows = sum(
            sum(account.max_inflows(timing=self.timing).values())
            for account in self.accounts)

        # Build strategies for testing (in non-init tests):
        self.strategy = TransactionStrategy(
            TransactionStrategy.strategy_ordered, {
                'RRSP': Decimal(1),
                'TFSA': Decimal(2),
                'TaxableAccount': Decimal(3)
            },
            high_precision=Decimal)
コード例 #5
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]
コード例 #6
0
    def setUp_decimal(self):
        """ Sets up variables based on Decimal inputs. """
        # pylint: disable=invalid-name
        # Pylint doesn't like `setUp_decimal`, but it's not our naming
        # convention, so don't complain to us!
        # pylint: enable=invalid-name

        # Vars for building accounts:
        initial_year = 2000
        person = Person(initial_year,
                        'Testy McTesterson',
                        1980,
                        retirement_date=2045,
                        high_precision=Decimal)

        # Set up some accounts for the tests.
        self.rrsp = RRSP(person,
                         balance=Decimal(200),
                         rate=Decimal(0),
                         contribution_room=Decimal(200),
                         high_precision=Decimal)
        self.rrsp2 = RRSP(person,
                          balance=Decimal(100),
                          rate=Decimal(0),
                          high_precision=Decimal)
        self.tfsa = TFSA(person,
                         balance=Decimal(100),
                         rate=Decimal(0),
                         contribution_room=Decimal(100),
                         high_precision=Decimal)
        self.taxable_account = TaxableAccount(person,
                                              balance=Decimal(1000),
                                              rate=Decimal(0),
                                              high_precision=Decimal)
        self.accounts = {
            self.rrsp, self.rrsp2, self.tfsa, self.taxable_account
        }

        # Define a simple timing for transactions:
        self.timing = {Decimal(0.5): Decimal(1)}

        # Build strategy for testing (in non-init tests):
        self.weights = {
            'RRSP': Decimal(0.4),
            'TFSA': Decimal(0.3),
            'TaxableAccount': Decimal(0.3)
        }
        self.strategy = TransactionStrategy(
            TransactionStrategy.strategy_weighted,
            self.weights,
            high_precision=Decimal)
コード例 #7
0
    def setUp_decimal(self):
        """ Sets up variables based on Decimal inputs. """
        # pylint: disable=invalid-name
        # Pylint doesn't like `setUp_decimal`, but it's not our naming
        # convention, so don't complain to us!
        # pylint: enable=invalid-name

        # Different groups of tests use different groups of variables.
        # We could split this class up into multiple classes (perhaps
        # one for ordered strategies and one for weighted strategies?),
        # but for now this project's practice is one test case for each
        # custom class.
        # pylint: disable=too-many-instance-attributes

        initial_year = 2000
        person = Person(initial_year,
                        'Testy McTesterson',
                        1980,
                        retirement_date=2045,
                        high_precision=Decimal)

        # Set up some accounts for the tests.
        self.rrsp = RRSP(person,
                         balance=Decimal(200),
                         rate=Decimal(0),
                         contribution_room=Decimal(200),
                         high_precision=Decimal)
        self.tfsa = TFSA(person,
                         balance=Decimal(100),
                         rate=Decimal(0),
                         contribution_room=Decimal(100),
                         high_precision=Decimal)
        self.taxable_account = TaxableAccount(person,
                                              balance=Decimal(1000),
                                              rate=Decimal(0),
                                              high_precision=Decimal)
        self.accounts = {self.rrsp, self.tfsa, self.taxable_account}

        # Define a simple timing for transactions:
        self.timing = {Decimal(0.5): Decimal(1)}

        # Build strategy for testing (in non-init tests):
        self.strategy = TransactionStrategy(
            TransactionStrategy.strategy_ordered, {
                'RRSP': 1,
                'TFSA': 2,
                'TaxableAccount': 3
            },
            high_precision=Decimal)
コード例 #8
0
ファイル: test_strategy.py プロジェクト: dxcv/forecaster
    def setUp(self):
        """ Sets up variables for testing. """
        initial_year = 2000
        person = Person(
            initial_year, 'Testy McTesterson', 1980, retirement_date=2045)

        # Set up some accounts for the tests.
        self.rrsp = RRSP(
            person,
            balance=Money(200), rate=0, contribution_room=Money(200))
        self.rrsp2 = RRSP(person, balance=Money(100), rate=0)
        self.tfsa = TFSA(
            person,
            balance=Money(100), rate=0, contribution_room=Money(100))
        self.taxable_account = TaxableAccount(
            person, balance=Money(1000), rate=0)
        self.accounts = {
            self.rrsp, self.rrsp2, self.tfsa, self.taxable_account
        }

        # Define a simple timing for transactions:
        self.timing = {Decimal(0.5): 1}

        self.max_outflow = sum(
            sum(account.max_outflows(timing=self.timing).values())
            for account in self.accounts)
        self.max_inflows = sum(
            sum(account.max_inflows(timing=self.timing).values())
            for account in self.accounts)

        # Build strategies for testing (in non-init tests):
        self.strategy = TransactionStrategy(
            TransactionStrategy.strategy_ordered, {
                'RRSP': 1,
                'TFSA': 2,
                'TaxableAccount': 3
            })
コード例 #9
0
ファイル: test_tax.py プロジェクト: dxcv/forecaster
    def setUp(self):
        """ Sets up mutable variables for each test call. """
        # Set to default province:
        self.province = 'BC'
        self.tax = TaxCanada(self.inflation_adjustments, province='BC')

        # Set up some people to test on:
        # Person1 makes $100,000/yr, has a taxable account with $500,000
        # taxable income, and an RRSP with $500,000 in taxable income.
        self.person1 = Person(
            self.initial_year, "Tester 1", self.initial_year - 20,
            retirement_date=self.initial_year + 45, gross_income=100000)
        self.taxable_account1 = TaxableAccount(
            owner=self.person1,
            acb=0, balance=Money(1000000), rate=Decimal('0.05'), nper=1)
        self.taxable_account1.add_transaction(-Money(1000000), when='start')
        # NOTE: by using an RRSP here, a pension income tax credit will
        # be applied by TaxCanadaJurisdiction. Be aware of this if you
        # want to test this output against a generic Tax object with
        # Canadian brackets.
        self.rrsp = RRSP(
            self.person1,
            inflation_adjust=self.inflation_adjustments,
            contribution_room=0,
            balance=Money(500000), rate=Decimal('0.05'), nper=1)
        self.rrsp.add_transaction(-Money(500000), when='start')

        # Person2 makes $50,000/yr and has a taxable account with
        # $5000 taxable income.
        self.person2 = Person(
            self.initial_year, "Tester 2", self.initial_year - 18,
            retirement_date=self.initial_year + 47, gross_income=50000)
        self.taxable_account2 = TaxableAccount(
            owner=self.person2,
            acb=0, balance=Money(10000), rate=Decimal('0.05'), nper=1)
        self.taxable_account2.add_transaction(-Money(10000), when='start')
コード例 #10
0
ファイル: test_strategy.py プロジェクト: dxcv/forecaster
class TestTransactionStrategyOrderedMult(TestCaseTransactions):
    """ Tests TransactionStrategy.strategy_ordered with account groups.
    In particular, this test case includes multiple accounts of the same
    type (e.g. two RRSPs) to ensure that accounts that share a weighting
    are handled properly.
    """

    def setUp(self):
        """ Sets up variables for testing. """
        initial_year = 2000
        person = Person(
            initial_year, 'Testy McTesterson', 1980, retirement_date=2045)

        # Set up some accounts for the tests.
        self.rrsp = RRSP(
            person,
            balance=Money(200), rate=0, contribution_room=Money(200))
        self.rrsp2 = RRSP(person, balance=Money(100), rate=0)
        self.tfsa = TFSA(
            person,
            balance=Money(100), rate=0, contribution_room=Money(100))
        self.taxable_account = TaxableAccount(
            person, balance=Money(1000), rate=0)
        self.accounts = {
            self.rrsp, self.rrsp2, self.tfsa, self.taxable_account
        }

        # Define a simple timing for transactions:
        self.timing = {Decimal(0.5): 1}

        self.max_outflow = sum(
            sum(account.max_outflows(timing=self.timing).values())
            for account in self.accounts)
        self.max_inflows = sum(
            sum(account.max_inflows(timing=self.timing).values())
            for account in self.accounts)

        # Build strategies for testing (in non-init tests):
        self.strategy = TransactionStrategy(
            TransactionStrategy.strategy_ordered, {
                'RRSP': 1,
                'TFSA': 2,
                'TaxableAccount': 3
            })

    def test_out_basic(self):
        """ Test strategy_ordered with multiple RRSPs, small outflows. """
        available = make_available(Money(-150), self.timing)
        results = self.strategy(available, self.accounts)
        # Confirm that the total of all outflows sums up to `-$150`,
        # which should be fully allocated to accounts:
        self.assertAccountTransactionsTotal(results, Money(-150))
        self.assertAlmostEqual(
            sum(results[self.rrsp].values())
            + sum(results[self.rrsp2].values()),
            Money(-150))

    def test_out_empty_three(self):
        """ Test strategy_ordered with multiple RRSPs, empty 3 accounts. """
        available = make_available(Money(-400), self.timing)
        results = self.strategy(available, self.accounts)
        # Confirm that the total of all outflows sums up to -$400, which
        # should be fully allocated to accounts:
        self.assertAccountTransactionsTotal(results, Money(-400))
        self.assertTransactions(results[self.rrsp], Money(-200))
        self.assertTransactions(results[self.rrsp2], Money(-100))
        self.assertTransactions(results[self.tfsa], Money(-100))

    def test_out_empty_all(self):
        """ Test strategy_ordered with multiple RRSPs, empty all accounts. """
        # Try to withdraw more than all accounts combined contain:
        val = self.max_outflow * 2
        available = make_available(Money(val), self.timing)
        results = self.strategy(available, self.accounts)
        # Ensure that the correct amount is withdrawn in total; should
        # be the amount available in the accounts (i.e. their balances):
        self.assertAccountTransactionsTotal(
            results,
            -sum(account.balance for account in self.accounts))
        # Confirm balances for each account:
        self.assertTransactions(results[self.rrsp], self.rrsp.max_outflows())
        self.assertTransactions(results[self.rrsp2], self.rrsp2.max_outflows())
        self.assertTransactions(results[self.tfsa], self.tfsa.max_outflows())
        self.assertTransactions(
            results[self.taxable_account], self.taxable_account.max_outflows())

    def test_in_basic(self):
        """ Test strategy_ordered with multiple RRSPs, small inflows. """
        # Amount contributed is more than the RRSPs can receive:
        val = self.rrsp.contribution_room + Money(50)
        available = make_available(Money(val), self.timing)
        results = self.strategy(available, self.accounts)

        # Confirm that the total amount contributed to the RRSPs is
        # equal to their (shared) contribution room.
        # If it exceeds that limit, then it's likely that their
        # contribution room sharing isn't being respected.
        self.assertAlmostEqual(
            sum(results[self.rrsp].values())
            + sum(results[self.rrsp2].values()),
            self.rrsp.contribution_room)
        # The remainder should be contributed to the TFSA:
        self.assertTransactions(results[self.tfsa], Money(50))
コード例 #11
0
ファイル: test_tax.py プロジェクト: ChrisCScott/forecaster
    def setUp(self):
        self.initial_year = 2000
        # Build 100 years of inflation adjustments with steadily growing
        # adjustment factors. First, pick a nice number (ideally a power
        # of 2 to avoid float precision issues); inflation_adjustment
        # will grow by adding the inverse (1/n) of this number annually
        growth_factor = 32
        year_range = range(self.initial_year, self.initial_year + 100)
        self.inflation_adjustments = {
            year: 1 + (year - self.initial_year) / growth_factor
            for year in year_range
        }
        # For convenience, store the year where inflation has doubled
        # the nominal value of money
        self.double_year = self.initial_year + growth_factor
        # Build some brackets with nice round numbers:
        self.tax_brackets = {self.initial_year: {0: 0.1, 100: 0.2, 10000: 0.3}}
        # For convenience, build a sorted, type-converted array of
        # each of the tax bracket thresholds:
        self.brackets = sorted({
            key: self.tax_brackets[self.initial_year][key]
            for key in self.tax_brackets[self.initial_year].keys()
        })
        # For convenience in testing, build an accum dict that
        # corresponds to the tax brackets above.
        self.accum = {self.initial_year: {0: 0, 100: 10, 10000: 1990}}
        self.personal_deduction = {self.initial_year: 100}
        self.credit_rate = {self.initial_year: 0.1}

        self.tax = Tax(self.tax_brackets,
                       inflation_adjust=self.inflation_adjustments,
                       personal_deduction=self.personal_deduction,
                       credit_rate=self.credit_rate)

        # Set up a simple person with no account-derived taxable income
        self.person = Person(self.initial_year,
                             "Tester",
                             self.initial_year - 25,
                             retirement_date=self.initial_year + 40,
                             gross_income=0)

        # Set up two people, spouses, on which to do more complex tests
        self.person1 = Person(self.initial_year,
                              "Tester 1",
                              self.initial_year - 20,
                              retirement_date=self.initial_year + 45,
                              gross_income=100000)
        self.person2 = Person(self.initial_year,
                              "Tester 2",
                              self.initial_year - 22,
                              retirement_date=self.initial_year + 43,
                              gross_income=50000)

        # Give the first person two accounts, one taxable and one
        # tax-deferred. Withdraw the entirety from the taxable account,
        # so that we don't need to worry about tax on unrealized growth:
        self.taxable_account1 = TaxableAccount(owner=self.person1,
                                               acb=0,
                                               balance=50000,
                                               rate=0.05,
                                               nper=1)
        self.taxable_account1.add_transaction(-50000, when='start')
        self.rrsp = RRSP(owner=self.person1,
                         inflation_adjust=self.inflation_adjustments,
                         contribution_room=0,
                         balance=10000,
                         rate=0.05,
                         nper=1)
        # Employment income is fully taxable, and only half of capital
        # gains (the income from the taxable account) is taxable:
        self.person1_taxable_income = (self.person1.gross_income +
                                       self.taxable_account1.balance / 2)

        # Give the second person two accounts, one taxable and one
        # non-taxable. Withdraw the entirety from the taxable account,
        # so that we don't need to worry about tax on unrealized growth,
        # and withdraw a bit from the non-taxable account (which should
        # have no effect on taxable income):
        self.taxable_account2 = TaxableAccount(owner=self.person2,
                                               acb=0,
                                               balance=20000,
                                               rate=0.05,
                                               nper=1)
        self.taxable_account2.add_transaction(-20000, when='start')
        self.tfsa = TFSA(owner=self.person2, balance=50000, rate=0.05, nper=1)
        self.tfsa.add_transaction(-20000, when='start')
        # Employment income is fully taxable, and only half of capital
        # gains (the income from the taxable account) is taxable:
        self.person2_taxable_income = (self.person2.gross_income +
                                       self.taxable_account2.balance / 2)
コード例 #12
0
class TestTransactionTraversalMethods(TestCaseTransactions):
    """ A test case for non-strategy method of TransactionStrategy. """
    def setUp(self):
        """ Sets up variables for testing. """
        self.initial_year = 2000
        self.person = Person(self.initial_year,
                             "Test",
                             "1 January 1980",
                             retirement_date="1 January 2030")
        # An RRSP with a $1000 balance and $100 in contribution room:
        self.rrsp = RRSP(initial_year=self.initial_year,
                         owner=self.person,
                         balance=1000,
                         contribution_room=100)
        # Another RRSP, linked to the first one:
        # (For testing LinkedLimitAccount nodes)
        self.rrsp2 = RRSP(initial_year=self.initial_year, owner=self.person)
        # A TFSA with a $100 balance and $1000 in contribution room:
        self.tfsa = TFSA(initial_year=self.initial_year,
                         owner=self.person,
                         balance=100,
                         contribution_room=1000)
        # Another TFSA, linked to the first one:
        self.tfsa2 = TFSA(initial_year=self.initial_year, owner=self.person)
        # A taxable account with $0 balance (and no contribution limit)
        self.taxable_account = TaxableAccount(initial_year=self.initial_year,
                                              owner=self.person,
                                              balance=0)
        # A $100 debt with no interest and a $10 min. payment:
        self.debt = Debt(initial_year=self.initial_year,
                         owner=self.person,
                         balance=100,
                         minimum_payment=10)
        # 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: 0.5,
            self.tfsa: 0.25,
            self.taxable_account: 0.25
        }
        # Contribute to RRSP and TFSA (50-50) until both are full, with
        # the remainder to taxable accounts.
        self.priority_nested = [{
            self.rrsp: 0.5,
            self.tfsa: 0.5
        }, self.taxable_account]

    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]

    def test_init_basic(self):
        """ Test __init__ with explicit arguments. """
        strategy = TransactionTraversal(priority=self.priority_ordered)
        self.assertEqual(strategy.priority, self.priority_ordered)

    def test_call_basic(self):
        """ Test __call__ with a one-node priority tree. """
        strategy = TransactionTraversal(priority=self.taxable_account)
        available = {0.5: 100}
        transactions = strategy(available)
        # All $100 will go to the single account:
        self.assertTransactions(transactions[self.taxable_account], 100)

    def test_ordered_basic(self):
        """ Contribute to the first account of an ordered list. """
        # Contribute $100 to RRSP, then TFSA, then taxable.
        strategy = TransactionTraversal(priority=self.priority_ordered)
        available = {0.5: 100}
        transactions = strategy(available)
        # $100 will go to RRSP, $0 to TFSA, $0 to taxable:
        self.assertTransactions(transactions[self.rrsp], 100)
        if self.tfsa in transactions:
            self.assertTransactions(transactions[self.tfsa], 0)
        if self.taxable_account in transactions:
            self.assertTransactions(transactions[self.taxable_account], 0)

    def test_weighted_basic(self):
        """ Contribute to each account proportionate to its weight. """
        # Contribute $100 to the accounts.
        strategy = TransactionTraversal(priority=self.priority_weighted)
        available = {0.5: 100}
        transactions = strategy(available)
        # $50 will go to RRSP, $25 to TFSA, $25 to taxable:
        self.assertTransactions(transactions[self.rrsp], 50)
        self.assertTransactions(transactions[self.tfsa], 25)
        self.assertTransactions(transactions[self.taxable_account], 25)

    def test_nested_basic(self):
        """ Test with weighted dict nested in ordered list. """
        # Contribute $100 to the accounts.
        strategy = TransactionTraversal(priority=self.priority_nested)
        available = {0.5: 100}
        transactions = strategy(available)
        # $50 will go to RRSP, $50 to TFSA, $0 to taxable:
        self.assertTransactions(transactions[self.rrsp], 50)
        self.assertTransactions(transactions[self.tfsa], 50)
        if self.taxable_account in transactions:
            self.assertTransactions(transactions[self.taxable_account], 0)

    def test_ordered_overflow_partial(self):
        """ Contribute to first and second accounts of an ordered list. """
        # Contribute $200 to RRSP, then TFSA, then taxable.
        strategy = TransactionTraversal(priority=self.priority_ordered)
        available = {0.5: 200}
        transactions = strategy(available)
        # $100 will go to RRSP, $100 to TFSA, $0 to taxable:
        self.assertTransactions(transactions[self.rrsp], 100)
        self.assertTransactions(transactions[self.tfsa], 100)
        if self.taxable_account in transactions:
            self.assertTransactions(transactions[self.taxable_account], 0)

    def test_weighted_overflow_partial(self):
        """ Max out one weighted account, contribute overflow to others. """
        # Contribute $400 to the accounts.
        strategy = TransactionTraversal(priority=self.priority_weighted)
        available = {0.5: 400}
        transactions = strategy(available)
        # $100 will go to RRSP (maxed), $150 to TFSA, $150 to taxable:
        self.assertTransactions(transactions[self.rrsp], 100)
        self.assertTransactions(transactions[self.tfsa], 150)
        self.assertTransactions(transactions[self.taxable_account], 150)

    def test_nested_overflow_partial(self):
        """ Max out one nested account, contribute overflow to neighbor. """
        # Contribute $400 to the accounts.
        strategy = TransactionTraversal(priority=self.priority_nested)
        available = {0.5: 400}
        transactions = strategy(available)
        # $100 will go to RRSP (maxed), $300 to TFSA, $0 to taxable:
        self.assertTransactions(transactions[self.rrsp], 100)
        self.assertTransactions(transactions[self.tfsa], 300)
        if self.taxable_account in transactions:
            self.assertTransactions(transactions[self.taxable_account], 0)

    def test_link_group_ordered(self):
        """ Contribute to ordered accounts sharing max inflow limit. """
        priority = [self.rrsp, self.rrsp2, self.taxable_account]
        strategy = TransactionTraversal(priority=priority)
        # Contribute $200 to the accounts:
        available = {0.5: 200}
        transactions = strategy(available)
        # $100 will go to self.rrsp, $0 to rrsp2, and $100 to taxable:
        self.assertTransactions(transactions[self.rrsp], 100)
        if self.rrsp2 in transactions:
            self.assertTransactions(transactions[self.rrsp2], 0)
        self.assertTransactions(transactions[self.taxable_account], 100)

    def test_link_weighted_nested_1(self):
        """ Contribute to weighted accounts sharing max inflow limit. """
        priority = [{self.rrsp: 0.5, self.rrsp2: 0.5}, self.taxable_account]
        strategy = TransactionTraversal(priority=priority)
        # Contribute $200 to the accounts:
        available = {0.5: 200}
        transactions = strategy(available)
        # $50 will go to self.rrsp, $50 to rrsp2, and $100 to taxable:
        self.assertTransactions(transactions[self.rrsp], 50)
        self.assertTransactions(transactions[self.rrsp2], 50)
        self.assertTransactions(transactions[self.taxable_account], 100)

    def test_limit_ordered(self):
        """ Limit contributions with per-node limit in ordered tree. """
        # Limit debt contributions to $100
        # (rather than $1000 max. contribution)
        limits = LimitTuple(max_inflow=100)
        limit_node = TransactionNode(self.debt, limits=limits)
        priority = [self.rrsp, limit_node, self.taxable_account]
        strategy = TransactionTraversal(priority=priority)
        # Contribute $300 to the accounts:
        available = {0.5: 300}
        transactions = strategy(available)
        # $100 will go to each account:
        self.assertTransactions(transactions[self.rrsp], 100)
        self.assertTransactions(transactions[self.debt], 100)
        self.assertTransactions(transactions[self.taxable_account], 100)

    def test_limit_weighted(self):
        """ Limit contributions with per-node limit in weighted tree. """
        # Limit debt contributions to $50
        # (rather than $1000 max. contribution)
        limits = LimitTuple(max_inflow=50)
        limit_node = TransactionNode(self.debt, limits=limits)
        priority = {self.rrsp: 1, limit_node: 1}
        strategy = TransactionTraversal(priority=priority)
        # Contribute $150 to the accounts:
        available = {0.5: 150}
        transactions = strategy(available)
        # $50 will go to debt. Remaining $100 will go to RRSP:
        self.assertTransactions(transactions[self.debt], 50)
        self.assertTransactions(transactions[self.rrsp], 100)

    def test_limit_weight_link_1_small(self):
        """ Linked account with limit in weighted tree; small inflow. """
        # This test looks at this structure:
        #       {}
        #      /| \
        #     / |  \
        #    R1 R2  A
        # R1/R2 are a group with a shared limit of $100.
        # _R1_ has a per-node limit of $10. All nodes have equal weight.
        # This should result in any contributions over the limit being
        # redistributed to R2 and A (up to the linked group limit, in
        # the case of R2).
        limits = LimitTuple(max_inflow=10)
        limit_node = TransactionNode(self.rrsp, limits=limits)
        priority = {limit_node: 1, self.rrsp2: 1, self.taxable_account: 1}
        strategy = TransactionTraversal(priority=priority)
        # Contribute $110 to the accounts:
        available = {0.5: 110}
        transactions = strategy(available)
        # $10 will go to rrsp and $100 will be split equally between
        # the two other accounts, for a total of $50 each:
        self.assertTransactions(transactions[self.rrsp], 10)
        self.assertTransactions(transactions[self.rrsp2], 50)
        self.assertTransactions(transactions[self.taxable_account], 50)

    def test_limit_weight_link_1_large(self):
        """ Linked account with limit in weighted tree; large inflow. """
        # This test looks at the same structure as
        # `test_limit_weighted_link_small`, except that enough money
        # is contributed to hit the group limit for R2 (as well as the
        # per-node limit for R1)
        limits = LimitTuple(max_inflow=10)
        limit_node = TransactionNode(self.rrsp, limits=limits)
        priority = {limit_node: 1, self.rrsp2: 1, self.taxable_account: 1}
        strategy = TransactionTraversal(priority=priority)
        # Contribute $210 to the accounts:
        available = {0.5: 210}
        transactions = strategy(available)
        # $10 will go to rrsp and $200 will be split between the two
        # other accounts - i.e. $90 to rrsp2 and $110 to taxable
        # (because rrsp/rrsp2 share a $100 limit):
        self.assertTransactions(transactions[self.rrsp], 10)
        self.assertTransactions(transactions[self.rrsp2], 90)
        self.assertTransactions(transactions[self.taxable_account], 110)

    def test_limit_weight_link_2_small(self):
        """ Limit in weighted tree with linked accounts; small inflow. """
        # This test looks at this structure:
        #       {}
        #      /| \
        #     / |  \
        #    R1 R2  A
        # R1/R2 are a group. _A_ has a per-node limit.
        # This should result in any contributions over the limit being
        # redistributed to R1 and R2 (up to the linked group limit).
        limits = LimitTuple(max_inflow=10)
        limit_node = TransactionNode(self.taxable_account, limits=limits)
        priority = {self.rrsp: 1, self.rrsp2: 1, limit_node: 1}
        strategy = TransactionTraversal(priority=priority)
        # Contribute $60 to the accounts:
        available = {0.5: 60}
        transactions = strategy(available)
        # $10 will go to taxable and $50 will be split between the two
        # linked accounts - i.e. $25 to each of rrsp and rrsp2:
        self.assertTransactions(transactions[self.rrsp], 25)
        self.assertTransactions(transactions[self.rrsp2], 25)
        self.assertTransactions(transactions[self.taxable_account], 10)

    def test_limit_weight_link_2_large(self):
        """ Limit in weighted tree with linked accounts; large inflow. """
        # This test looks at the same structure as
        # `test_limit_weight_link_2_small`, except that enough money
        # is contributed to hit the group limit for R1/R2
        limits = LimitTuple(max_inflow=10)
        limit_node = TransactionNode(self.taxable_account, limits=limits)
        priority = {self.rrsp: 1, self.rrsp2: 1, limit_node: 1}
        strategy = TransactionTraversal(priority=priority)
        # Contribute $300 to the accounts:
        available = {0.5: 300}
        transactions = strategy(available)
        # $10 will go to taxable and $50 go to each of rrsp and rrsp2:
        # (This is less than the full $300 because the various accounts
        # hit their limits at $110 of inflows)
        self.assertTransactions(transactions[self.rrsp], 50)
        self.assertTransactions(transactions[self.rrsp2], 50)
        self.assertTransactions(transactions[self.taxable_account], 10)

    def test_link_basic(self):
        """ A weighted root with two linked children. """
        # This test looks at this structure:
        #       {}
        #      /  \
        #     /    \
        #    R1     R2
        # R1/R2 are a group. Trying to contribute more than the group
        # allows should result in only the total contribution room
        # being contributed across both accounts.
        priority = {self.rrsp: 0.5, self.rrsp2: 0.5}
        strategy = TransactionTraversal(priority=priority)
        # Contribute $200 (i.e. more than the joint contribution room
        # of the two RRSPs, which is $100):
        available = {0.5: 200}
        transactions = strategy(available)
        # $50 should go to each account:
        self.assertTransactions(transactions[self.rrsp], 50)
        self.assertTransactions(transactions[self.rrsp2], 50)

    def test_link_weighted_overflow(self):
        """ A weighted root with two linked children and one other. """
        # This test looks at this structure:
        #       {}
        #      / |\
        #     /  | \
        #    R1 R2  T
        # If R1/R2 are a group (and R1 and R2 have equal weights) then
        # contributing more than the group can receive will send the
        # overflow to T.
        priority = {self.rrsp: 1, self.rrsp2: 1, self.taxable_account: 1}
        strategy = TransactionTraversal(priority=priority)
        # Contribute $200:
        available = {0.5: 200}
        transactions = strategy(available)
        # $50 should go to each RRSP, with remaining $100 to taxable:
        self.assertTransactions(transactions[self.rrsp], 50)
        self.assertTransactions(transactions[self.rrsp2], 50)
        self.assertTransactions(transactions[self.taxable_account], 100)

    def test_link_weighted_max(self):
        """ A weighted root with two linked children and one other. """
        # This test looks at this structure:
        #       {}
        #      /  \
        #     /    \
        #    R1     R2
        # If R1/R2 are a group (and R1 and R2 have equal weights) then
        # contributing more than the group can receive should result
        # in R1 and R2 getting equal inflows.
        priority = {self.rrsp: 1, self.rrsp2: 1}
        strategy = TransactionTraversal(priority=priority)
        # Contribute $200:
        available = {0.5: 200}
        transactions = strategy(available)
        # $50 should go to each RRSP:
        self.assertTransactions(transactions[self.rrsp], 50)
        self.assertTransactions(transactions[self.rrsp2], 50)

    def test_link_order_equal(self):
        """ Two linked groups, each under both children of the root. """
        # This test looks at this structure:
        #       {}
        # (50%)/  \(50%)
        #     /    \
        #   []      []
        #  /  \    /  \
        # R1  T2  T1  R2
        # If R1/R2 and T1/T2 are groups with equal contribution room,
        # R2 and T2 shouldn't ever get a contribution.
        priority = {(self.rrsp, self.tfsa2): 1, (self.tfsa, self.rrsp2): 1}
        # Ensure RRSP and TFSA have equal contribution room:
        self.tfsa.contribution_room = self.rrsp.contribution_room
        strategy = TransactionTraversal(priority=priority)
        # Contribute $200 (enough to fill both groups):
        available = {0.5: 200}
        transactions = strategy(available)
        # $100 should go to each of rrsp and tfsa (which are 1st on each
        # side of the root node), with no more going to rrsp2 or tfsa2:
        self.assertTransactions(transactions[self.rrsp], 100)
        self.assertTransactions(transactions[self.tfsa], 100)
        self.assertTransactions(transactions[self.rrsp2], 0)
        self.assertTransactions(transactions[self.tfsa2], 0)

    def test_link_order_unequal(self):
        """ Two linked groups, each under both children of the root. """
        # This test looks at this structure:
        #       {}
        # (50%)/  \(50%)
        #     /    \
        #   []      []
        #  /  \    /  \
        # R1  T2  T1  R2
        # If R1/R2 and T1/T2 are groups with unequal contribution room
        # (WLOG, say R1 > T1), then R2 will get additional contributions
        priority = {(self.rrsp, self.tfsa2): 1, (self.tfsa, self.rrsp2): 1}
        # Ensure RRSP has more contribution room than TFSA:
        self.tfsa.contribution_room = 50
        strategy = TransactionTraversal(priority=priority)
        # Contribute $150 (enough to fill both groups):
        available = {0.5: 150}
        transactions = strategy(available)
        # $75 should go to rrsp, $50 to tfsa, and $25 to rrsp2:
        self.assertTransactions(transactions[self.rrsp], 75)
        self.assertTransactions(transactions[self.tfsa], 50)
        self.assertTransactions(transactions[self.rrsp2], 25)
        if self.tfsa2 in transactions:
            self.assertTransactions(transactions[self.tfsa2], 0)

    def test_link_weighted_nested_2(self):
        """ A weighted root with weighted children with common groups. """
        # This test looks at this structure:
        #       {}
        # (50%)/  \(50%)
        #     /    \
        #   {}      {}
        #  /  \    /  \
        # R1  T2  T1  R2
        # If R1/R2 and T1/T2 are groups and all links are equal-weighted
        # then all accounts will get equal contributions (up to each
        # group's contribution limit)
        left_child = TransactionNode({self.rrsp: 1, self.tfsa2: 1})
        right_child = TransactionNode({self.tfsa: 1, self.rrsp2: 1})
        # Need to wrap nested dicts, since they aren't hashable.
        priority = {left_child: 1, right_child: 1}
        # Ensure RRSP has more contribution room than TFSA:
        self.tfsa.contribution_room = self.rrsp.contribution_room
        strategy = TransactionTraversal(priority=priority)
        # Contribute $200 (enough to fill both groups):
        available = {0.5: 200}
        transactions = strategy(available)
        # $50 should go to each account:
        self.assertTransactions(transactions[self.rrsp], 50)
        self.assertTransactions(transactions[self.tfsa], 50)
        self.assertTransactions(transactions[self.rrsp2], 50)
        self.assertTransactions(transactions[self.tfsa2], 50)

    def test_link_nested_basic(self):
        """ A weighted root with weighted children with common groups. """
        # This test looks at this structure:
        #       {}
        # (50%)/  \(50%)
        #     /    \
        #   R1      []
        #          /  \
        #         R2   T
        # If R1/R2 is a group then contributing slightly more than their
        # shared contribution room could have two possible reasonable
        # results:
        # 1) R1 and R2 are contributed to equally, with the excess to T.
        # 2) R1 receives more than R2 so that both children of the root
        #    are balanced, with the excess to T.
        # Past implementations opted for #1, but current implementations
        # achieve the (preferred) behaviour of #2. Test for that:
        priority = {self.rrsp: 1, (self.rrsp2, self.taxable_account): 1}
        strategy = TransactionTraversal(priority=priority)
        # Contribute $200 (enough to fill RRSPs with $100 left over):
        available = {0.5: 200}
        transactions = strategy(available)
        # $100 should go to rrsp, none to rrsp2, and balance to taxable:
        self.assertTransactions(transactions[self.rrsp], 100)
        if self.rrsp2 in transactions:
            self.assertTransactions(transactions[self.rrsp2], 0)
        self.assertTransactions(transactions[self.taxable_account], 100)

    def test_link_nested_hidden(self):
        """ A weighted root with weighted children with common groups. """
        # This test looks at this structure:
        #       {}
        # (50%)/  \(50%)
        #     /    \
        #   R1      []
        #          /  \
        #         T    R2
        # If R1/R2 is a group then contributing slightly more than their
        # shared contribution room should result in R1 receiving all
        # of the group's contribution room, with the rest to T:
        priority = {self.rrsp: 1, (self.taxable_account, self.rrsp2): 1}
        strategy = TransactionTraversal(priority=priority)
        # Contribute $200 (enough to fill RRSPs with $100 left over):
        available = {0.5: 200}
        transactions = strategy(available)
        # $100 should go to rrsp, with balance to taxable:
        self.assertTransactions(transactions[self.rrsp], 100)
        if self.rrsp2 in transactions:
            self.assertTransactions(transactions[self.rrsp2], 0)
        self.assertTransactions(transactions[self.taxable_account], 100)

    def test_assign_mins(self):
        """ Assign minimum inflows without throwing off total inflows. """
        # Simple scenario: two accounts that take $10 in min. inflows:
        priority = [self.rrsp, self.debt]
        strategy = TransactionTraversal(priority=priority)
        # Contribute the debt's minimum payment ($10):
        available = {0.5: 10}
        transactions = strategy(available)
        # Exactly $10 should go to `debt`, with none going to `rrsp`,
        # even though `rrsp` comes first in the priority tree:
        self.assertTransactions(transactions[self.debt], 10)
        if self.rrsp in transactions:
            self.assertTransactions(transactions[self.rrsp], 0)

    def test_assign_mins_out(self):
        """ Assign minimum outflows without throwing off total outflows. """
        # RRSPs have min outflows (if converted to an RRIF), so use
        # one of those:
        self.person.birth_date = self.initial_year - 72
        self.rrsp.convert_to_rrif(year=self.initial_year - 1)
        priority = [self.rrsp]
        strategy = TransactionTraversal(priority=priority)
        # Withdraw more than min_outflow_limit and less than balance:
        total = (self.rrsp.min_outflow_limit - self.rrsp.balance) / 2
        available = {0.5: total}
        transactions = strategy(available)
        # Exactly the `total` amount should be withdrawn:
        self.assertTransactions(transactions[self.rrsp], total)

    def test_assign_mins_out_overflow(self):
        """ Assign minimum outflows without throwing off total outflows. """
        # RRSPs have min outflows (if converted to an RRIF), so use
        # one of those:
        self.person.birth_date = self.initial_year - 72
        self.rrsp.convert_to_rrif(year=self.initial_year - 1)
        priority = [self.rrsp]
        strategy = TransactionTraversal(priority=priority)
        # Try to withdraw more than the account allows:
        total = -2 * self.rrsp.balance
        available = {0.5: total}
        transactions = strategy(available)
        # Withdrawals should not exceed the RRSP's max outflows:
        self.assertTransactions(transactions[self.rrsp],
                                self.rrsp.max_outflows(timing=available))

    def test_assign_mins_unbalanced(self):
        """ Assign min inflows to some accounts and not to others.

        This example was part of an old test for WithdrawalForecast and
        gave rise to unexpected results. Rather than test it indirectly
        there, test for it explicitly here.

        The basic problem we're checking for is where we have multiple
        accounts with different min_inflow (or min_outflow) limits.
        TransactionTraversal will assign those transactions first, which
        don't necessarily align with the weights of a weighted node.
        When going on to the second traversal (for max_inflow or
        max_outflow), that imbalance in the prior transactions needs to
        be accounted for.
        """
        # Set up an RRSP that's been converted to an RRIF (so that it
        # has minimum withdrawals).
        self.person.birth_date = self.initial_year - 72
        self.rrsp.convert_to_rrif(year=self.initial_year - 1)
        # Use balances that were used by TestWithdrawalForecast:
        self.rrsp.balance = 6000
        self.taxable_account.balance = 60000
        # We want to withdraw $20,000 from these accounts, with
        # $3000 from the RRSP and $17000 from the taxable account.
        priority = {self.rrsp: 3000, self.taxable_account: 17000}
        # Use the same `available` dict as in TestWithdrawalForecast,
        # with $2000 in inflows and $22,000 in outflows, for a net need
        # of $20,000 in withdrawals to make up the shortfall:
        available = {0.25: 1000, 0.5: -11000, 0.75: 1000, 1: -11000}
        strategy = TransactionTraversal(priority=priority)
        # Generate the transactions:
        transactions = strategy(available)
        # Confirm that the $20,000 is distributed as in `priority`:
        self.assertAccountTransactionsTotal(transactions, -20000)
        self.assertTransactions(transactions[self.rrsp], -3000)
        self.assertTransactions(transactions[self.taxable_account], -17000)

    def test_decimal(self):
        """ A weighted root with weighted children with common groups. """
        # Convert values to Decimal:
        self.setUp_decimal()

        # This test is based on test_link_weighted_nested_2
        left_child = TransactionNode({self.rrsp: 1, self.tfsa2: 1})
        right_child = TransactionNode({self.tfsa: 1, self.rrsp2: 1})
        # Need to wrap nested dicts, since they aren't hashable.
        priority = {left_child: 1, right_child: 1}
        # Ensure RRSP has more contribution room than TFSA:
        self.tfsa.contribution_room = self.rrsp.contribution_room
        strategy = TransactionTraversal(priority=priority,
                                        high_precision=Decimal)
        # Contribute $200 (enough to fill both groups):
        available = {Decimal(0.5): Decimal(200)}
        transactions = strategy(available)
        # $50 should go to each account:
        self.assertTransactions(transactions[self.rrsp], Decimal(50))
        self.assertTransactions(transactions[self.tfsa], Decimal(50))
        self.assertTransactions(transactions[self.rrsp2], Decimal(50))
        self.assertTransactions(transactions[self.tfsa2], Decimal(50))
コード例 #13
0
ファイル: test_strategy.py プロジェクト: dxcv/forecaster
class TestTransactionStrategyOrdered(TestCaseTransactions):
    """ A test case for TransactionStrategy.strategy_ordered """

    def setUp(self):
        """ Sets up variables for testing. """
        # Different groups of tests use different groups of variables.
        # We could split this class up into multiple classes (perhaps
        # one for ordered strategies and one for weighted strategies?),
        # but for now this project's practice is one test case for each
        # custom class.
        # pylint: disable=too-many-instance-attributes

        initial_year = 2000
        person = Person(
            initial_year, 'Testy McTesterson', 1980, retirement_date=2045)

        # Set up some accounts for the tests.
        self.rrsp = RRSP(
            person,
            balance=Money(200), rate=0, contribution_room=Money(200))
        self.tfsa = TFSA(
            person,
            balance=Money(100), rate=0, contribution_room=Money(100))
        self.taxable_account = TaxableAccount(
            person, balance=Money(1000), rate=0)
        self.accounts = {self.rrsp, self.tfsa, self.taxable_account}

        # Define a simple timing for transactions:
        self.timing = {Decimal(0.5): 1}

        # Build strategy for testing (in non-init tests):
        self.strategy = TransactionStrategy(
            TransactionStrategy.strategy_ordered, {
                'RRSP': 1,
                'TFSA': 2,
                'TaxableAccount': 3
            })

    # Test with inflows:

    def test_in_basic(self):
        """ Test strategy_ordered with a small amount of inflows. """
        # The amount being contributed is less than the available
        # contribution room in the top-weighted account type.
        available = make_available(Money(100), self.timing)
        results = self.strategy(available, self.accounts)
        self.assertTransactions(results[self.rrsp], Money(100))
        # Accounts with no transactions aren't guaranteed to be
        # included in the results dict:
        if self.tfsa in results:
            self.assertTransactions(results[self.tfsa], Money(0))
        if self.taxable_account in results:
            self.assertTransactions(results[self.taxable_account], Money(0))

    def test_in_fill_one(self):
        """ Test strategy_ordered with inflows to fill 1 account. """
        # Contribute more than the rrsp will accomodate.
        # The extra $50 should go to the tfsa, which is next in line.
        available = make_available(Money(250), self.timing)
        results = self.strategy(available, self.accounts)
        self.assertTransactions(results[self.rrsp], Money(200))
        self.assertTransactions(results[self.tfsa], Money(50))
        if self.taxable_account in results:
            self.assertTransactions(results[self.taxable_account], Money(0))

    def test_in_fill_two(self):
        """ Test strategy_ordered with inflows to fill 2 accounts. """
        # The rrsp and tfsa will get filled and the remainder will go to
        # the taxable account.
        available = make_available(Money(1000), self.timing)
        results = self.strategy(available, self.accounts)
        self.assertTransactions(results[self.rrsp], Money(200))
        self.assertTransactions(results[self.tfsa], Money(100))
        self.assertTransactions(results[self.taxable_account], Money(700))

    # Test with outflows:

    def test_out_basic(self):
        """ Test strategy_ordered with a small amount of outflows. """
        # The amount being withdrawn is less than the max outflow in the
        # top-weighted account type.
        available = make_available(Money(-100), self.timing)
        results = self.strategy(available, self.accounts)
        self.assertTransactions(results[self.rrsp], Money(-100))
        if self.tfsa in results:
            self.assertTransactions(results[self.tfsa], Money(0))
        if self.taxable_account in results:
            self.assertTransactions(results[self.taxable_account], Money(0))

    def test_out_empty_one(self):
        """ Test strategy_ordered with outflows to empty 1 account. """
        # Now withdraw more than the rrsp will accomodate. The extra $50
        # should come from the tfsa, which is next in line.
        available = make_available(Money(-250), self.timing)
        results = self.strategy(available, self.accounts)
        self.assertTransactions(results[self.rrsp], Money(-200))
        self.assertTransactions(results[self.tfsa], Money(-50))
        if self.taxable_account in results:
            self.assertTransactions(results[self.taxable_account], Money(0))

    def test_out_empty_two(self):
        """ Test strategy_ordered with outflows to empty 2 accounts. """
        # The rrsp and tfsa will get emptied and the remainder will go
        # to the taxable account.
        available = make_available(Money(-1000), self.timing)
        results = self.strategy(available, self.accounts)
        self.assertTransactions(results[self.rrsp], self.rrsp.max_outflows())
        self.assertTransactions(results[self.tfsa], self.tfsa.max_outflows())
        self.assertTransactions(results[self.taxable_account], Money(-700))

    def test_out_empty_all(self):
        """ Test strategy_ordered with outflows to empty all account. """
        # Try withdrawing more than all of the accounts have:
        val = sum(
            sum(account.max_outflows().values())
            for account in self.accounts
        ) * 2
        available = make_available(Money(val), self.timing)
        results = self.strategy(available, self.accounts)
        self.assertTransactions(results[self.rrsp], self.rrsp.max_outflows())
        self.assertTransactions(results[self.tfsa], self.tfsa.max_outflows())
        self.assertTransactions(
            results[self.taxable_account],
            self.taxable_account.max_outflows())

    def test_change_order(self):
        """ Test strategy_ordered works with changed order vars. """
        self.strategy.weights['RRSP'] = 2
        self.strategy.weights['TFSA'] = 1
        available = make_available(Money(100), self.timing)
        results = self.strategy(available, self.accounts)
        # Contribute the full $100 to TFSA:
        self.assertTransactions(results[self.tfsa], self.tfsa.max_inflows())
        # Remaining accounts shouldn't be contributed to:
        if self.rrsp in results:
            self.assertTransactions(results[self.rrsp], Money(0))
        if self.taxable_account in results:
            self.assertTransactions(results[self.taxable_account], Money(0))
コード例 #14
0
    def setUp_decimal(self):  # pylint: disable=invalid-name
        """ Sets up mutable variables for each test call. """
        # Set up constants:
        self.initial_year = 2000
        self.constants = constants.ConstantsCanada(high_precision=Decimal)

        # Modify constants to make math easier:
        # Build some brackets with nice round numbers:
        self.constants.TAX_BRACKETS = {
            'Federal': {
                self.initial_year: {
                    Decimal(0): Decimal('0.1'),
                    Decimal(100): Decimal('0.2'),
                    Decimal(10000): Decimal('0.3')
                }
            },
            'BC': {
                self.initial_year: {
                    Decimal(0): Decimal('0.25'),
                    Decimal(1000): Decimal('0.5'),
                    Decimal(100000): Decimal('0.75')
                }
            }
        }
        self.constants.TAX_PERSONAL_DEDUCTION = {
            'Federal': {
                self.initial_year: Decimal('100')
            },
            'BC': {
                self.initial_year: Decimal('1000')
            }
        }
        self.constants.TAX_CREDIT_RATE = {
            'Federal': {
                self.initial_year: Decimal('0.1')
            },
            'BC': {
                self.initial_year: Decimal('0.25')
            }
        }
        self.constants.TAX_PENSION_CREDIT = {
            'Federal': {
                self.initial_year: Decimal('100')
            },
            'BC': {
                self.initial_year: Decimal('1000')
            }
        }
        # It's convenient (and accurate!) to use the same values
        # for the spousal amount and the personal deduction:
        self.constants.TAX_SPOUSAL_AMOUNT = (
            self.constants.TAX_PERSONAL_DEDUCTION)

        # Build 100 years of inflation adjustments.
        growth_factor = Decimal(32)
        year_range = range(self.initial_year, self.initial_year + 100)
        self.inflation_adjustments = {
            year: 1 + (year - self.initial_year) / growth_factor
            for year in year_range
        }

        # Set to default province:
        self.province = 'BC'
        self.tax = TaxCanada(self.inflation_adjustments,
                             province='BC',
                             constants=self.constants)

        # Set up some people to test on:
        # Person1 makes $100,000/yr, has a taxable account with $500,000
        # taxable income, and an RRSP with $500,000 in taxable income.
        self.person1 = Person(self.initial_year,
                              "Tester 1",
                              self.initial_year - 20,
                              retirement_date=self.initial_year + 45,
                              gross_income=100000)
        self.taxable_account1 = TaxableAccount(owner=self.person1,
                                               acb=0,
                                               balance=Decimal(1000000),
                                               rate=Decimal('0.05'),
                                               nper=1)
        self.taxable_account1.add_transaction(-Decimal(1000000), when='start')
        # NOTE: by using an RRSP here, a pension income tax credit will
        # be applied by TaxCanadaJurisdiction. Be aware of this if you
        # want to test this output against a generic Tax object with
        # Canadian brackets.
        self.rrsp = RRSP(self.person1,
                         inflation_adjust=self.inflation_adjustments,
                         contribution_room=0,
                         balance=Decimal(500000),
                         rate=Decimal('0.05'),
                         nper=1,
                         constants=self.constants)
        self.rrsp.add_transaction(-Decimal(500000), when='start')

        # Person2 makes $50,000/yr and has a taxable account with
        # $5000 taxable income.
        self.person2 = Person(self.initial_year,
                              "Tester 2",
                              self.initial_year - 18,
                              retirement_date=self.initial_year + 47,
                              gross_income=50000)
        self.taxable_account2 = TaxableAccount(owner=self.person2,
                                               acb=0,
                                               balance=Decimal(10000),
                                               rate=Decimal('0.05'),
                                               nper=1)
        self.taxable_account2.add_transaction(-Decimal(10000), when='start')
コード例 #15
0
ファイル: test_strategy.py プロジェクト: dxcv/forecaster
class TestTransactionStrategyWeightedLink(TestCaseTransactions):
    """ Tests TransactionStrategy.strategy_weighted with linked accounts
    
    This test case includes multiple linked accounts (e.g. two RRSPs)
    to ensure that accounts that share a weighting are handled properly.
    """

    def setUp(self):
        """ Sets up variables for testing. """
        # Vars for building accounts:
        initial_year = 2000
        person = Person(
            initial_year, 'Testy McTesterson', 1980, retirement_date=2045)

        # Set up some accounts for the tests.
        self.rrsp = RRSP(
            person,
            balance=Money(200), rate=0, contribution_room=Money(200))
        self.rrsp2 = RRSP(person, balance=Money(100), rate=0)
        self.tfsa = TFSA(
            person,
            balance=Money(100), rate=0, contribution_room=Money(100))
        self.taxable_account = TaxableAccount(
            person, balance=Money(1000), rate=0)
        self.accounts = {
            self.rrsp, self.rrsp2, self.tfsa, self.taxable_account
        }

        # Define a simple timing for transactions:
        self.timing = {Decimal(0.5): 1}

        # Build strategy for testing (in non-init tests):
        self.weights = {
            'RRSP': Decimal('0.4'),
            'TFSA': Decimal('0.3'),
            'TaxableAccount': Decimal('0.3')
        }
        self.strategy = TransactionStrategy(
            TransactionStrategy.strategy_weighted, self.weights)

    def test_out_basic(self):
        """ Test strategy_weighted with multiple RRSPs, small outflows. """
        # Amount withdrawn is less than the balance of each account.
        val = Money(
            max(sum(account.max_outflows().values())
                for account in self.accounts))
        available = make_available(Money(val), self.timing)
        results = self.strategy(available, self.accounts)
        # Sum up results for each account for convenience:
        results_totals = {
            account: sum(transactions.values())
            for account, transactions in results.items()}
        # Confirm that the total of all outflows sums up to `val`, which
        # should be fully allocated to accounts:
        self.assertAccountTransactionsTotal(results, val)
        # Confirm RRSPs' shared weight is respected:
        self.assertAlmostEqual(
            results_totals[self.rrsp] + results_totals[self.rrsp2],
            val * self.weights['RRSP'])
        # Confirm that money is withdrawn from each RRSP, but don't
        # put constraints on how much:
        self.assertLess(results_totals[self.rrsp], Money(0))
        self.assertLess(results_totals[self.rrsp2], Money(0))
        # Confirm that remaining accounts have expected amounts:
        self.assertTransactions(results[self.tfsa], val * self.weights['TFSA'])
        self.assertTransactions(
            results[self.taxable_account],
            val * self.weights['TaxableAccount'])

    def test_out_empty_one(self):
        """ Test strategy_weighted with multiple RRSPs, empty 1 account. """
        # Withdraw enough to exceed the balance of one account (the
        # TFSA, in this case, as it has the smallest balance):
        threshold = (
            sum(self.tfsa.max_outflows().values())
            / self.weights['TFSA'])
        val = Money(threshold - Money(50))
        available = make_available(Money(val), self.timing)
        results = self.strategy(available, self.accounts)
        # Sum up results for each account for convenience:
        results_totals = {
            account: sum(transactions.values())
            for account, transactions in results.items()}
        # Confirm that the total of all outflows sums up to `val`, which
        # should be fully allocated to accounts:
        self.assertAccountTransactionsTotal(results, val)
        # Confirm each account has the expected set of transactions:
        self.assertTransactions(results[self.tfsa], self.tfsa.max_outflows())
        # The excess (i.e. the amount that would ordinarily be
        # contributed to the TFSA but can't due to contribution room
        # limits) should also be split between RRSPs and the TFSA
        # proportionately to their relative weights.
        self.assertAlmostEqual(
            results_totals[self.rrsp] + results_totals[self.rrsp2],
            results_totals[self.taxable_account]
            * self.weights['RRSP'] / self.weights['TaxableAccount'])

    def test_out_empty_three(self):
        """ Test strategy_weighted with mult. RRSPs, empty 3 accounts. """
        # Try withdrawing just a little less than the total available
        # balance. This will clear out the RRSPs and TFSA and leave the
        # remainder in the taxable account, since the taxable account
        # has a much larger balance and roughly similar weight:
        val = sum(
            sum(account.max_outflows().values())
            for account in self.accounts
        ) + Money(50)
        available = make_available(Money(val), self.timing)
        results = self.strategy(available, self.accounts)
        # Sum up results for each account for convenience:
        # Confirm that the total of all outflows sums up to `val`, which
        # should be fully allocated to accounts:
        self.assertAccountTransactionsTotal(results, val)
        # Also confirm that the smaller accounts get emptied:
        self.assertTransactions(results[self.rrsp], self.rrsp.max_outflows())
        self.assertTransactions(results[self.rrsp2], self.rrsp2.max_outflows())
        self.assertTransactions(results[self.tfsa], self.tfsa.max_outflows())
        # And confirm that the largest account is not-quite-filled:
        self.assertTransactions(
            results[self.taxable_account],
            sum(self.taxable_account.max_outflows().values()) + Money(50))

    def test_out_empty_all(self):
        """ Test strategy_weighted with mult. RRSPs, empty all accounts. """
        # Try withdrawing more than the accounts have
        val = sum(
            sum(account.max_outflows().values())
            for account in self.accounts
        ) - Money(50)
        available = make_available(Money(val), self.timing)
        results = self.strategy(available, self.accounts)
        # Confirm each account has the expected set of transactions:
        self.assertTransactions(results[self.rrsp], self.rrsp.max_outflows())
        self.assertTransactions(results[self.tfsa], self.tfsa.max_outflows())
        self.assertTransactions(
            results[self.taxable_account],
            self.taxable_account.max_outflows())

    def test_in_basic(self):
        """ Test strategy_weighted with multiple RRSPs, small inflows. """
        # Amount contributed is more than the RRSPs can receive:
        val = self.rrsp.contribution_room / self.weights['RRSP'] + Money(50)
        available = make_available(Money(val), self.timing)
        results = self.strategy(available, self.accounts)
        # Sum up results for each account for convenience:
        results_totals = {
            account: sum(transactions.values())
            for account, transactions in results.items()}
        # Confirm that the total of all outflows sums up to `val`, which
        # should be fully allocated to accounts:
        self.assertAccountTransactionsTotal(results, val)

        # Confirm that the total amount contributed to the RRSPs is
        # equal to their (shared) contribution room.
        # If it exceeds that limit, then it's likely that their
        # contribution room sharing isn't being respected.
        self.assertAlmostEqual(
            results_totals[self.rrsp] + results_totals[self.rrsp2],
            self.rrsp.contribution_room)
コード例 #16
0
ファイル: test_strategy.py プロジェクト: dxcv/forecaster
class TestTransactionStrategyWeighted(TestCaseTransactions):
    """ A test case for TransactionStrategy.strategy_weighted. """

    def setUp(self):
        """ Sets up variables for testing. """

        # Vars for building accounts:
        initial_year = 2000
        person = Person(
            initial_year, 'Testy McTesterson', 1980, retirement_date=2045)

        # Set up some accounts for the tests.
        self.rrsp = RRSP(
            person,
            balance=Money(200), rate=0, contribution_room=Money(200))
        self.tfsa = TFSA(
            person,
            balance=Money(100), rate=0, contribution_room=Money(100))
        self.taxable_account = TaxableAccount(
            person, balance=Money(1000), rate=0)
        self.accounts = {self.rrsp, self.tfsa, self.taxable_account}

        # Define a simple timing for transactions:
        self.timing = {Decimal(0.5): 1}

        self.max_outflow = sum(
            sum(account.max_outflows(timing=self.timing).values())
            for account in self.accounts)
        self.max_inflows = sum(
            sum(account.max_inflows(timing=self.timing).values())
            for account in self.accounts)

        # Build strategy for testing (in non-init tests):
        self.weights = {
            'RRSP': Decimal('0.4'),
            'TFSA': Decimal('0.3'),
            'TaxableAccount': Decimal('0.3')
        }
        self.strategy = TransactionStrategy(
            TransactionStrategy.strategy_weighted, self.weights)

    # Test with outflows:

    def test_out_basic(self):
        """ Test strategy_weighted with small amount of outflows. """
        # Amount withdrawn is smaller than the balance of each account.
        val = max(
            sum(account.max_outflows().values())
            for account in self.accounts)
        available = make_available(Money(val), self.timing)
        results = self.strategy(available, self.accounts)
        # Confirm that the total of all outflows sums up to `val`, which
        # should be fully allocated to accounts:
        self.assertAccountTransactionsTotal(results, val)
        # Confirm each account gets the expected total transactions:
        self.assertTransactions(results[self.rrsp], val * self.weights['RRSP'])
        self.assertTransactions(results[self.tfsa], val * self.weights['TFSA'])
        self.assertTransactions(
            results[self.taxable_account],
            val * self.weights['TaxableAccount'])

    def test_out_one_empty(self):
        """ Test strategy_weighted with outflows to empty 1 account. """
        # Now withdraw enough to exceed the TFSA's balance, plus a bit.
        threshold = (
            sum(self.tfsa.max_outflows().values())
            / self.weights['TFSA'])
        val = Money(threshold - Money(50))
        available = make_available(Money(val), self.timing)
        results = self.strategy(available, self.accounts)
        # Sum up results for each account for convenience:
        results_totals = {
            account: sum(transactions.values())
            for account, transactions in results.items()}
        # Confirm that the total of all outflows sums up to `val`, which
        # should be fully allocated to accounts:
        self.assertAccountTransactionsTotal(results, val)
        # Confirm each account gets the expected total transactions:
        self.assertTransactions(results[self.tfsa], self.tfsa.max_outflows())
        self.assertAlmostEqual(
            results_totals[self.rrsp] / self.weights['RRSP'],
            results_totals[self.taxable_account]
            / self.weights['TaxableAccount'])

    def test_out_two_empty(self):
        """ Test strategy_weighted with outflows to empty 2 accounts. """
        # Withdraw just a little less than the total available balance.
        # This will clear out the RRSP and TFSA.
        val = sum(
            sum(account.max_outflows().values())
            for account in self.accounts
        ) + Money(50)
        available = make_available(Money(val), self.timing)
        results = self.strategy(available, self.accounts)
        # Confirm that the total of all outflows sums up to `val`, which
        # should be fully allocated to accounts:
        self.assertAccountTransactionsTotal(results, val)
        # Confirm each account gets the expected total transactions:
        self.assertTransactions(results[self.rrsp], self.rrsp.max_outflows())
        self.assertTransactions(results[self.tfsa], self.tfsa.max_outflows())
        self.assertTransactions(
            results[self.taxable_account],
            sum(self.taxable_account.max_outflows().values())
            + Money(50))

    def test_out_all_empty(self):
        """ Test strategy_weighted with outflows to empty all accounts. """
        # Withdraw more than the accounts have:
        val = sum(
            sum(account.max_outflows().values())
            for account in self.accounts
        ) - Money(50)
        available = make_available(Money(val), self.timing)
        results = self.strategy(available, self.accounts)
        # Confirm each account gets the expected total transactions:
        self.assertTransactions(results[self.rrsp], self.rrsp.max_outflows())
        self.assertTransactions(results[self.tfsa], self.tfsa.max_outflows())
        self.assertTransactions(
            results[self.taxable_account],
            self.taxable_account.max_outflows())

    def test_in_basic(self):
        """ Test strategy_weighted with a small amount of inflows. """
        # The amount being contributed is less than the available
        # contribution room for each account
        val = min(
            sum(account.max_inflows().values())
            for account in self.accounts)
        available = make_available(Money(val), self.timing)
        results = self.strategy(available, self.accounts)
        # Confirm that the total of all outflows sums up to `val`, which
        # should be fully allocated to accounts:
        self.assertAccountTransactionsTotal(results, val)
        # Confirm accounts have separate total transaction values:
        self.assertTransactions(results[self.rrsp], val * self.weights['RRSP'])
        self.assertTransactions(results[self.tfsa], val * self.weights['TFSA'])
        self.assertTransactions(
            results[self.taxable_account],
            val * self.weights['TaxableAccount'])

    def test_in_fill_one(self):
        """ Test strategy_weighted with inflows to fill 1 account. """
        # Now contribute enough to exceed the TFSA's contribution room.
        # The excess (i.e. the amount that would be contributed to the
        # TFSA but can't because of its lower contribution room) should
        # be redistributed to the other accounts proportionately to
        # their relative weights:
        threshold = (
            sum(self.tfsa.max_inflows().values()) / self.weights['TFSA'])
        val = Money(threshold + Money(50))
        available = make_available(Money(val), self.timing)
        results = self.strategy(available, self.accounts)
        # Confirm that the total of all outflows sums up to `val`, which
        # should be fully allocated to accounts:
        self.assertAccountTransactionsTotal(results, val)

        self.assertTransactions(results[self.tfsa], self.tfsa.max_inflows())
        self.assertTransactions(
            results[self.rrsp],
            sum(results[self.taxable_account].values())
            * self.weights['RRSP'] / self.weights['TaxableAccount'])

    def test_in_fill_two(self):
        """ Test strategy_weighted with inflows to fill 2 accounts. """
        # Contribute a lot of money - the rrsp and tfsa will get
        # filled and the remainder will go to the taxable account.
        threshold = max(
            sum(self.rrsp.max_inflows().values()) / self.weights['RRSP'],
            sum(self.tfsa.max_inflows().values()) / self.weights['TFSA'])
        val = threshold + Money(50)
        available = make_available(Money(val), self.timing)
        results = self.strategy(available, self.accounts)
        # Confirm that the total of all outflows sums up to `val`, which
        # should be fully allocated to accounts:
        self.assertAccountTransactionsTotal(results, val)
        # Confirm accounts have expected transactions:
        self.assertTransactions(results[self.rrsp], self.rrsp.max_inflows())
        self.assertTransactions(results[self.tfsa], self.tfsa.max_inflows())
        self.assertTransactions(
            results[self.taxable_account],
            val - (
                sum(self.rrsp.max_inflows().values())
                + sum(self.tfsa.max_inflows().values())))
コード例 #17
0
class TestTaxCanada(unittest.TestCase):
    """ Tests TaxCanada. """
    def setUp(self):
        """ Sets up mutable variables for each test call. """
        # Set up constants:
        self.initial_year = 2000
        self.constants = constants.ConstantsCanada()

        # Modify constants to make math easier:
        # Build some brackets with nice round numbers:
        self.constants.TAX_BRACKETS = {
            'Federal': {
                self.initial_year: {
                    0: 0.1,
                    100: 0.2,
                    10000: 0.3
                }
            },
            'BC': {
                self.initial_year: {
                    0: 0.25,
                    1000: 0.5,
                    100000: 0.75
                }
            }
        }
        self.constants.TAX_PERSONAL_DEDUCTION = {
            'Federal': {
                self.initial_year: 100
            },
            'BC': {
                self.initial_year: 1000
            }
        }
        self.constants.TAX_CREDIT_RATE = {
            'Federal': {
                self.initial_year: 0.1
            },
            'BC': {
                self.initial_year: 0.25
            }
        }
        self.constants.TAX_PENSION_CREDIT = {
            'Federal': {
                self.initial_year: 100
            },
            'BC': {
                self.initial_year: 1000
            }
        }
        # It's convenient (and accurate!) to use the same values
        # for the spousal amount and the personal deduction:
        self.constants.TAX_SPOUSAL_AMOUNT = (
            self.constants.TAX_PERSONAL_DEDUCTION)

        # Build 100 years of inflation adjustments.
        growth_factor = 32
        year_range = range(self.initial_year, self.initial_year + 100)
        self.inflation_adjustments = {
            year: 1 + (year - self.initial_year) / growth_factor
            for year in year_range
        }

        # Set to default province:
        self.province = 'BC'
        self.tax = TaxCanada(self.inflation_adjustments,
                             province='BC',
                             constants=self.constants)

        # Set up some people to test on:
        # Person1 makes $100,000/yr, has a taxable account with $500,000
        # taxable income, and an RRSP with $500,000 in taxable income.
        self.person1 = Person(self.initial_year,
                              "Tester 1",
                              self.initial_year - 20,
                              retirement_date=self.initial_year + 45,
                              gross_income=100000)
        self.taxable_account1 = TaxableAccount(owner=self.person1,
                                               acb=0,
                                               balance=1000000,
                                               rate=0.05,
                                               nper=1)
        self.taxable_account1.add_transaction(-1000000, when='start')
        # NOTE: by using an RRSP here, a pension income tax credit will
        # be applied by TaxCanadaJurisdiction. Be aware of this if you
        # want to test this output against a generic Tax object with
        # Canadian brackets.
        self.rrsp = RRSP(self.person1,
                         inflation_adjust=self.inflation_adjustments,
                         contribution_room=0,
                         balance=500000,
                         rate=0.05,
                         nper=1,
                         constants=self.constants)
        self.rrsp.add_transaction(-500000, when='start')

        # Person2 makes $50,000/yr and has a taxable account with
        # $5000 taxable income.
        self.person2 = Person(self.initial_year,
                              "Tester 2",
                              self.initial_year - 18,
                              retirement_date=self.initial_year + 47,
                              gross_income=50000)
        self.taxable_account2 = TaxableAccount(owner=self.person2,
                                               acb=0,
                                               balance=10000,
                                               rate=0.05,
                                               nper=1)
        self.taxable_account2.add_transaction(-10000, when='start')

    def setUp_decimal(self):  # pylint: disable=invalid-name
        """ Sets up mutable variables for each test call. """
        # Set up constants:
        self.initial_year = 2000
        self.constants = constants.ConstantsCanada(high_precision=Decimal)

        # Modify constants to make math easier:
        # Build some brackets with nice round numbers:
        self.constants.TAX_BRACKETS = {
            'Federal': {
                self.initial_year: {
                    Decimal(0): Decimal('0.1'),
                    Decimal(100): Decimal('0.2'),
                    Decimal(10000): Decimal('0.3')
                }
            },
            'BC': {
                self.initial_year: {
                    Decimal(0): Decimal('0.25'),
                    Decimal(1000): Decimal('0.5'),
                    Decimal(100000): Decimal('0.75')
                }
            }
        }
        self.constants.TAX_PERSONAL_DEDUCTION = {
            'Federal': {
                self.initial_year: Decimal('100')
            },
            'BC': {
                self.initial_year: Decimal('1000')
            }
        }
        self.constants.TAX_CREDIT_RATE = {
            'Federal': {
                self.initial_year: Decimal('0.1')
            },
            'BC': {
                self.initial_year: Decimal('0.25')
            }
        }
        self.constants.TAX_PENSION_CREDIT = {
            'Federal': {
                self.initial_year: Decimal('100')
            },
            'BC': {
                self.initial_year: Decimal('1000')
            }
        }
        # It's convenient (and accurate!) to use the same values
        # for the spousal amount and the personal deduction:
        self.constants.TAX_SPOUSAL_AMOUNT = (
            self.constants.TAX_PERSONAL_DEDUCTION)

        # Build 100 years of inflation adjustments.
        growth_factor = Decimal(32)
        year_range = range(self.initial_year, self.initial_year + 100)
        self.inflation_adjustments = {
            year: 1 + (year - self.initial_year) / growth_factor
            for year in year_range
        }

        # Set to default province:
        self.province = 'BC'
        self.tax = TaxCanada(self.inflation_adjustments,
                             province='BC',
                             constants=self.constants)

        # Set up some people to test on:
        # Person1 makes $100,000/yr, has a taxable account with $500,000
        # taxable income, and an RRSP with $500,000 in taxable income.
        self.person1 = Person(self.initial_year,
                              "Tester 1",
                              self.initial_year - 20,
                              retirement_date=self.initial_year + 45,
                              gross_income=100000)
        self.taxable_account1 = TaxableAccount(owner=self.person1,
                                               acb=0,
                                               balance=Decimal(1000000),
                                               rate=Decimal('0.05'),
                                               nper=1)
        self.taxable_account1.add_transaction(-Decimal(1000000), when='start')
        # NOTE: by using an RRSP here, a pension income tax credit will
        # be applied by TaxCanadaJurisdiction. Be aware of this if you
        # want to test this output against a generic Tax object with
        # Canadian brackets.
        self.rrsp = RRSP(self.person1,
                         inflation_adjust=self.inflation_adjustments,
                         contribution_room=0,
                         balance=Decimal(500000),
                         rate=Decimal('0.05'),
                         nper=1,
                         constants=self.constants)
        self.rrsp.add_transaction(-Decimal(500000), when='start')

        # Person2 makes $50,000/yr and has a taxable account with
        # $5000 taxable income.
        self.person2 = Person(self.initial_year,
                              "Tester 2",
                              self.initial_year - 18,
                              retirement_date=self.initial_year + 47,
                              gross_income=50000)
        self.taxable_account2 = TaxableAccount(owner=self.person2,
                                               acb=0,
                                               balance=Decimal(10000),
                                               rate=Decimal('0.05'),
                                               nper=1)
        self.taxable_account2.add_transaction(-Decimal(10000), when='start')

    def test_init_federal(self):
        """ Test TaxCanada.__init__ for federal jurisdiction. """
        # There's some type-conversion going on, so test the Decimal-
        # valued `amount` of the Tax's tax bracket's keys against the
        # Decimal key object of the Constants tax brackets.
        tax = TaxCanada(self.inflation_adjustments,
                        self.province,
                        constants=self.constants)
        for year in self.constants.TAX_BRACKETS['Federal']:
            self.assertEqual(tax.federal_tax.tax_brackets(year),
                             self.constants.TAX_BRACKETS['Federal'][year])
            self.assertEqual(
                tax.federal_tax.personal_deduction(year),
                self.constants.TAX_PERSONAL_DEDUCTION['Federal'][year])
            self.assertEqual(tax.federal_tax.credit_rate(year),
                             self.constants.TAX_CREDIT_RATE['Federal'][year])
        self.assertTrue(callable(tax.federal_tax.inflation_adjust))
        # Test that the default timings for CRA refunds/payments have
        # been set:
        self.assertEqual(set(tax.payment_timing),
                         {self.constants.TAX_PAYMENT_TIMING})
        self.assertEqual(set(tax.refund_timing),
                         {self.constants.TAX_REFUND_TIMING})

    def test_init_provincial(self):
        """ Test TaxCanada.__init__ for provincial jurisdiction. """
        tax = TaxCanada(self.inflation_adjustments,
                        self.province,
                        constants=self.constants)
        for year in self.constants.TAX_BRACKETS[self.province]:
            self.assertEqual(
                tax.provincial_tax.tax_brackets(year), {
                    bracket: value
                    for bracket, value in self.constants.TAX_BRACKETS[
                        self.province][year].items()
                })
            self.assertEqual(
                tax.provincial_tax.personal_deduction(year),
                self.constants.TAX_PERSONAL_DEDUCTION[self.province][year])
            self.assertEqual(
                tax.provincial_tax.credit_rate(year),
                self.constants.TAX_CREDIT_RATE[self.province][year])
        self.assertTrue(callable(tax.provincial_tax.inflation_adjust))

    def test_init_min_args(self):
        """ Test init when Omitting optional arguments. """
        tax = TaxCanada(self.inflation_adjustments, constants=self.constants)
        for year in self.constants.TAX_BRACKETS[self.province]:
            self.assertEqual(
                tax.provincial_tax.tax_brackets(year), {
                    bracket: value
                    for bracket, value in self.constants.TAX_BRACKETS[
                        self.province][year].items()
                })
            self.assertEqual(
                tax.provincial_tax.personal_deduction(year),
                self.constants.TAX_PERSONAL_DEDUCTION[self.province][year])
            self.assertEqual(
                tax.provincial_tax.credit_rate(year),
                self.constants.TAX_CREDIT_RATE[self.province][year])
        self.assertTrue(callable(tax.provincial_tax.inflation_adjust))

    def test_call_money(self):
        """ Test TaxCanada.__call__ on Decimal input """
        taxable_income = 100000
        self.assertEqual(
            self.tax(taxable_income, self.initial_year),
            self.tax.federal_tax(taxable_income, self.initial_year) +
            self.tax.provincial_tax(taxable_income, self.initial_year))

    def test_call_person(self):
        """ Test TaxCanada.__call__ on one Person input """
        self.assertEqual(
            self.tax(self.person1, self.initial_year),
            self.tax.federal_tax(self.person1, self.initial_year) +
            self.tax.provincial_tax(self.person1, self.initial_year))

    def test_call_person_set(self):
        """ Test TaxCanada.__call__ on a one-Person set input """
        # Should get the same result as for a setless Person:
        self.assertEqual(self.tax({self.person1}, self.initial_year),
                         self.tax(self.person1, self.initial_year))

    def test_call_people(self):
        """ Test TaxCanada.__call__ on a set of multiple people. """
        # The people are unrelated, so should get a result which is
        # just the sum of their tax treatments.
        self.assertEqual(
            self.tax({self.person1, self.person2}, self.initial_year),
            self.tax.federal_tax({self.person1, self.person2},
                                 self.initial_year) +
            self.tax.provincial_tax({self.person1, self.person2},
                                    self.initial_year))

    def test_spousal_tax_credit(self):
        """ Test spousal tax credit behaviour. """
        # Ensure person 2's net income is less than the federal spousal
        # amount:
        spousal_amount = (
            self.constants.TAX_SPOUSAL_AMOUNT['Federal'][self.initial_year])
        shortfall = spousal_amount / 2
        deduction = self.tax.federal_tax.deduction(self.person2,
                                                   self.initial_year)
        self.person2.gross_income = deduction + spousal_amount - shortfall

        # Ensure that there is no taxable income for person2 beyond the
        # above (to stay under spousal amount):
        self.taxable_account2.owner = self.person1

        # Get a tax treatment baseline for unrelated people:
        baseline_tax = self.tax.federal_tax({self.person1, self.person2},
                                            self.initial_year)

        # Wed the two people in holy matrimony:
        self.person1.spouse = self.person2

        # Now determine total tax liability federally:
        spousal_tax = self.tax.federal_tax({self.person1, self.person2},
                                           self.initial_year)

        # Tax should be reduced (relative to baseline) by the shortfall
        # of person2's income (relative to the spousal amount, after
        # applying deductions), scaled down by the credit rate.
        # That is, for every dollar that person2 earns _under_ the
        # spousal amount, tax is reduced by (e.g.) 15 cents (assuming
        # a credit rate of 15%)
        target = baseline_tax - (
            shortfall * self.tax.federal_tax.credit_rate(self.initial_year))

        # The different between these scenarios should be equal to
        # the amount of the spousal tax credit:
        self.assertEqual(spousal_tax, target)

    def test_pension_tax_credit(self):
        """ Test pension tax credit behaviour. """
        # TODO Implement pension tax credit, then test it.
        pass