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 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())))