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)
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 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 })
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)
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)
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)
def setUp(self): """ Sets up class to use Canadian default values. """ # Override settings/forecaster types to use Canadian subclasses. # (This is conditional so that subclasses can assign their own # objects before calling super().setUp()) if not hasattr(self, 'settings'): self.settings = SettingsCanada() if not hasattr(self, 'forecaster_type'): self.forecaster_type = ForecasterCanada # Let the superclass handle setup: super().setUp() self.constants = constants.ConstantsCanada() # Override tax_treatment to use TaxCanada object: self.tax_treatment = TaxCanada( inflation_adjust=self.scenario.inflation_adjust, province=self.settings.tax_province, constants=self.constants) # The AccountTransactionStrategy settings for ForecasterCanada # don't include an Account object; replace it with an # otherwise-identical TaxableAccount, which is represented in # the settings. self.account = TaxableAccount( owner=self.person, balance=self.account.balance, rate=self.account.rate, nper=self.account.nper)
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 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 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')
def setUp_decimal(self): """ Sets up class to use Canadian default values. """ # This handles almost everything: super().setUp_decimal() self.settings = SettingsCanada(high_precision=Decimal) self.constants = constants.ConstantsCanada(high_precision=Decimal) # Override tax_treatment to use TaxCanada object: self.tax_treatment = TaxCanada( inflation_adjust=self.scenario.inflation_adjust, province=self.settings.tax_province, high_precision=Decimal, constants=self.constants) # The AccountTransactionStrategy settings for ForecasterCanada # don't include an Account object; replace it with an # otherwise-identical TaxableAccount, which is represented in # the settings. self.account = TaxableAccount( owner=self.person, balance=self.account.balance, rate=self.account.rate, nper=self.account.nper, high_precision=Decimal)
class TestTax(unittest.TestCase): """ Tests the `Tax` class. """ # We save a number of attributes for convenience in testing later # on. We could refactor, but it would complicate the tests, which # would be worse. # pylint: disable=too-many-instance-attributes 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) def setUp_decimal(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: { Decimal(0): Decimal('0.1'), Decimal('100'): Decimal('0.2'), Decimal('10000'): Decimal('0.3') } } # For convenience, build a sorted, type-converted array of # each of the tax bracket thresholds: self.brackets = sorted({ Decimal(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: { Decimal(0): Decimal('0'), Decimal('100'): Decimal('10'), Decimal('10000'): Decimal('1990') } } self.personal_deduction = {self.initial_year: Decimal('100')} self.credit_rate = {self.initial_year: Decimal('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=Decimal(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=Decimal(100000)) self.person2 = Person(self.initial_year, "Tester 2", self.initial_year - 22, retirement_date=self.initial_year + 43, gross_income=Decimal(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=Decimal(0), balance=Decimal(50000), rate=Decimal('0.05'), nper=Decimal(1)) self.taxable_account1.add_transaction(Decimal(-50000), when='start') self.rrsp = RRSP(owner=self.person1, inflation_adjust=self.inflation_adjustments, contribution_room=Decimal(0), balance=Decimal(10000), rate=Decimal('0.05'), nper=Decimal(1)) # Employment income is fully taxable, and only half of capital # gains (the income from the taxable account) is taxable: self.person1_taxable_income = Decimal(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=Decimal(0), balance=Decimal(20000), rate=Decimal('0.05'), nper=Decimal(1)) self.taxable_account2.add_transaction(Decimal(-20000), when='start') self.tfsa = TFSA(owner=self.person2, balance=Decimal(50000), rate=Decimal('0.05'), nper=Decimal(1)) self.tfsa.add_transaction(Decimal(-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 = Decimal(self.person2.gross_income + self.taxable_account2.balance / 2) def test_init_optional(self): """ Test Tax.__init__ with all arguments, including optional. """ tax = Tax(self.tax_brackets, inflation_adjust=self.inflation_adjustments, personal_deduction=self.personal_deduction, credit_rate=self.credit_rate) for year in self.tax_brackets: self.assertEqual(tax.tax_brackets(year), self.tax_brackets[year]) self.assertEqual(tax.accum(year), self.accum[year]) self.assertEqual(tax.personal_deduction(year), self.personal_deduction[year]) self.assertEqual(tax.credit_rate(year), self.credit_rate[year]) self.assertTrue(callable(tax.inflation_adjust)) def test_init_basic(self): """ Test Tax.__init__ with only mandatory arguments. """ tax = Tax(self.tax_brackets, inflation_adjust=self.inflation_adjustments) for year in self.tax_brackets: self.assertEqual(tax.tax_brackets(year), self.tax_brackets[year]) self.assertEqual(tax.accum(year), self.accum[year]) self.assertTrue(callable(tax.inflation_adjust)) self.assertEqual(tax.personal_deduction(self.initial_year), 0) self.assertEqual(tax.credit_rate(self.initial_year), 1) def test_income_0_money(self): """ Call Test on $0 income. """ # $0 should return $0 in tax owing. This is the easiest test. income = 0 self.assertEqual(self.tax(income, self.initial_year), 0) def test_income_0_person(self): """ Test tax on a person with $0 income. """ # $0 should return $0 in tax owing. This is the easiest test. self.person.gross_income = 0 self.assertEqual(self.tax(self.person, self.initial_year), 0) def test_income_under_deduction(self): """ Test tax on person with income under personal deduction. """ self.person.gross_income = ( self.personal_deduction[self.initial_year] / 2) # Should return $0 self.assertEqual(self.tax(self.person, self.initial_year), 0) def test_income_at_deduction(self): """ Call Test on income equal to the personal deduction. """ self.person.gross_income = self.personal_deduction[self.initial_year] # Should return $0 self.assertEqual(self.tax(self.person, self.initial_year), 0) def test_income_in_bracket_1_money(self): """ Call Test on income mid-way into the lowest tax bracket. """ # NOTE: brackets[0] is $0; we need something between brackets[0] # and brackets[1]) income = self.brackets[1] / 2 self.assertEqual( self.tax(income, self.initial_year), income * self.tax_brackets[self.initial_year][self.brackets[0]]) def test_income_in_bracket_1_person(self): """ Call Test on income mid-way into the lowest tax bracket. """ # NOTE: brackets[0] is $0; we need something between brackets[0] # and brackets[1]) self.person.gross_income = (self.brackets[1] / 2 + self.personal_deduction[self.initial_year]) self.assertEqual( self.tax(self.person, self.initial_year), (self.person.gross_income - self.personal_deduction[self.initial_year]) * self.tax_brackets[self.initial_year][self.brackets[0]]) def test_income_at_bracket_1_money(self): """ Call Test on income equal to the lowest tax bracket. """ # Try a value that's at the limit of the lowest tax # bracket (NOTE: brackets are inclusive, so brackets[1] is # entirely taxed at the rate associated with brackets[0]) income = self.brackets[1] self.assertEqual(self.tax(income, self.initial_year), self.accum[self.initial_year][self.brackets[1]]) def test_income_at_bracket_1_person(self): """ Call Test on income equal to the lowest tax bracket. """ # Try a value that's at the limit of the lowest tax # bracket (NOTE: brackets are inclusive, so brackets[1] is # entirely taxed at the rate associated with brackets[0]) self.person.gross_income = (self.brackets[1] + self.personal_deduction[self.initial_year]) self.assertEqual(self.tax(self.person, self.initial_year), self.accum[self.initial_year][self.brackets[1]]) def test_income_in_bracket_2_money(self): """ Call Test on income mid-way into the second tax bracket. """ # Find a value that's mid-way into the next (second) bracket. # Assuming a person deduction of $100 and tax rates bounded at # $0, $100 and $10000 with 10%, 20%, and 30% rates, this gives: # Tax on first $100: $0 # Tax on next $100: $10 # Tax on remaining: 20% of remaining # For a $5150 amount, this works out to tax of $1000. income = (self.brackets[1] + self.brackets[2]) / 2 self.assertEqual( self.tax(income, self.initial_year), self.accum[self.initial_year][self.brackets[1]] + ((self.brackets[1] + self.brackets[2]) / 2 - self.brackets[1]) * self.tax_brackets[self.initial_year][self.brackets[1]]) def test_income_in_bracket_2_person(self): """ Call Test on income mid-way into the second tax bracket. """ # Find a value that's mid-way into the next (second) bracket. # Assuming a person deduction of $100 and tax rates bounded at # $0, $100 and $10000 with 10%, 20%, and 30% rates, this gives: # Tax on first $100: $0 # Tax on next $100: $10 # Tax on remaining: 20% of remaining # For a $5150 amount, this works out to tax of $1000. self.person.gross_income = ((self.brackets[1] + self.brackets[2]) / 2 + self.personal_deduction[self.initial_year]) target = (self.accum[self.initial_year][self.brackets[1]] + ( (self.brackets[1] + self.brackets[2]) / 2 - self.brackets[1]) * self.tax_brackets[self.initial_year][self.brackets[1]]) self.assertEqual(self.tax(self.person, self.initial_year), target) def test_income_at_bracket_2_money(self): """ Call Test on income equal to the second tax bracket. """ # Try again for a value that's at the limit of the second tax # bracket (NOTE: brackets are inclusive, so brackets[2] is # entirely taxed at the rate associated with brackets[1]) income = self.brackets[2] self.assertEqual( self.tax(income, self.initial_year), self.accum[self.initial_year][self.brackets[1]] + (self.brackets[2] - self.brackets[1]) * self.tax_brackets[self.initial_year][self.brackets[1]]) def test_income_at_bracket_2_person(self): """ Call Test on income equal to the second tax bracket. """ # Try again for a value that's at the limit of the second tax # bracket (NOTE: brackets are inclusive, so brackets[2] is # entirely taxed at the rate associated with brackets[1]) self.person.gross_income = (self.brackets[2] + self.personal_deduction[self.initial_year]) self.assertEqual( self.tax(self.person, self.initial_year), self.accum[self.initial_year][self.brackets[1]] + (self.brackets[2] - self.brackets[1]) * self.tax_brackets[self.initial_year][self.brackets[1]]) def test_income_in_bracket_3_money(self): """ Call Test on income in the highest tax bracket. """ # Find a value that's somewhere in the highest (unbounded) bracket. bracket = max(self.brackets) income = bracket * 2 self.assertEqual( self.tax(income, self.initial_year), self.accum[self.initial_year][bracket] + bracket * self.tax_brackets[self.initial_year][bracket]) def test_income_in_bracket_3_person(self): """ Call Test on income in the highest tax bracket. """ # Find a value that's somewhere in the highest (unbounded) bracket. bracket = max(self.brackets) self.person.gross_income = (bracket * 2 + self.personal_deduction[self.initial_year]) self.assertEqual( self.tax(self.person, self.initial_year), self.accum[self.initial_year][bracket] + bracket * self.tax_brackets[self.initial_year][bracket]) def test_taxpayer_single(self): """ Call test on a single taxpayer. """ # The tax paid on the person's income should be the same as if # we calculated the tax directly on the money itself (after # accounting for the personal deduction amount) self.assertEqual( self.tax(self.person1, self.initial_year), self.tax( self.person1_taxable_income - self.personal_deduction[self.initial_year], self.initial_year)) # We should get a similar result on the other person: self.assertEqual( self.tax(self.person2, self.initial_year), self.tax( self.person2_taxable_income - self.personal_deduction[self.initial_year], self.initial_year)) def test_taxpayer_single_set(self): """ Call test on a set with a single taxpayer member. """ # Try with a single-member set; should return the same as # it would if calling on the person directly. self.assertEqual(self.tax({self.person1}, self.initial_year), self.tax(self.person1, self.initial_year)) def test_taxpayer_set(self): """ Call Test on a set of two non-spouse taxpayers. """ # The two test people are set up as spouses; we need to split # them up. self.person1.spouse = None self.person2.spouse = None test_result = (self.tax({self.person1, self.person2}, self.initial_year)) test_target = (self.tax(self.person1, self.initial_year) + self.tax(self.person2, self.initial_year)) self.assertEqual(test_result, test_target) def test_taxpayer_spouses(self): """ Call Test on a set of two spouse taxpayers. """ # NOTE: This test is vulnerable to breakage if special tax # credits get implemented for spouses. Watch out for that. self.assertEqual( self.tax({self.person1, self.person2}, self.initial_year), self.tax(self.person1, self.initial_year) + self.tax(self.person2, self.initial_year)) def test_inflation_adjust(self): """ Call Test on a future year with inflation effects. """ # Start with a baseline result in initial_year. Then confirm # that the tax owing on twice that amount in double_year should # be exactly double the tax owing on the baseline result. # (Anything else suggests that something is not being inflation- # adjusted properly, e.g. a bracket or a deduction) double_tax = self.tax(self.person1_taxable_income * 2, self.double_year) single_tax = self.tax(self.person1_taxable_income, self.initial_year) self.assertEqual(single_tax * 2, double_tax) def test_payment_timing(self): """ Tests `payment_timing` property. """ # `payment_timing` should have exactly one timing: 0 self.tax.payment_timing = 'start' self.assertEqual(set(self.tax.payment_timing), {0}) def test_refund_timing(self): """ Tests `refund_timing` property. """ # `refund_timing` should have exactly one timing: 0 self.tax.refund_timing = 'start' self.assertEqual(set(self.tax.refund_timing), {0}) def test_decimal(self): """ Call Test with Decimal values. """ # Convert values to Decimal: self.setUp_decimal() # This test is based on test_income_in_bracket_2_person self.person.gross_income = ((self.brackets[1] + self.brackets[2]) / 2 + self.personal_deduction[self.initial_year]) target = (self.accum[self.initial_year][self.brackets[1]] + ( (self.brackets[1] + self.brackets[2]) / 2 - self.brackets[1]) * self.tax_brackets[self.initial_year][self.brackets[1]]) self.assertEqual(self.tax(self.person, self.initial_year), target)
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)
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))
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)
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())))
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))
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')
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