def setUp_decimal(self): """ Builds stock variables to test with. """ # 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 self.initial_year = 2000 self.subforecast = SubForecast(self.initial_year, high_precision=Decimal) self.person = Person(initial_year=self.initial_year, name="Test", birth_date="1 January 1980", retirement_date="31 December 2045", high_precision=Decimal) # A basic account with 100% interest and no compounding: self.account1 = Account(owner=self.person, balance=100, rate=Decimal(1), nper=1, high_precision=Decimal) # Another account, same as account1: self.account2 = Account(owner=self.person, balance=100, rate=Decimal(1), nper=1, high_precision=Decimal) # Set up a dict and Account for use as `available`: self.available_dict = defaultdict(lambda: Decimal(0)) self.available_acct = Account(initial_year=self.initial_year, rate=0, high_precision=Decimal)
def setUp(self): """ Builds stock variables to test with. """ self.initial_year = 2000 # Simple tax treatment: 50% tax rate across the board. tax = Tax(tax_brackets={self.initial_year: {0: 0.5}}) # A person who is paid $1000 gross ($500 withheld): timing = Timing(frequency='BW') self.person1 = Person(initial_year=self.initial_year, name="Test 1", birth_date="1 January 1980", retirement_date="31 December 2045", gross_income=1000, tax_treatment=tax, payment_timing=timing) # A person who is paid $500 gross ($250 withheld): self.person2 = Person(initial_year=self.initial_year, name="Test 2", birth_date="1 January 1982", retirement_date="31 December 2047", gross_income=500, tax_treatment=tax, payment_timing=timing) # An account owned by person1 with $100 to withdraw self.account1 = Account(owner=self.person1, balance=100, rate=0) # An account owned by person2 with $200 to withdraw self.account2 = Account(owner=self.person2, balance=100, rate=0) # An account that belongs in some sense to both people # with $50 to withdraw self.account_joint = Account(owner=self.person1, balance=100, rate=0) self.person2.accounts.add(self.account_joint) self.forecast = TaxForecast(initial_year=self.initial_year, people={self.person1, self.person2}, tax_treatment=tax)
def setUp_decimal(self): """ Set up stock variables based on Decimal inputs. """ # Set up inflation of various rates covering 1999-2002: self.year_half = 1999 # -50% inflation (values halved) in this year self.year_1 = 2000 # baseline year; no inflation self.year_2 = 2001 # 100% inflation (values doubled) in this year self.year_10 = 2002 # Values multiplied by 10 in this year self.inflation_adjustment = { self.year_half: Decimal(0.5), self.year_1: Decimal(1), self.year_2: Decimal(2), self.year_10: Decimal(10) } # We need to provide a callable object that returns inflation # adjustments between years. Build that here: def variable_inflation(year, base_year=self.year_1): """ Returns inflation-adjustment factor between two years. """ return (self.inflation_adjustment[year] / self.inflation_adjustment[base_year]) self.variable_inflation = variable_inflation # Build all the objects we need to build an instance of # `LivingExpensesStrategy`: self.initial_year = self.year_1 # Simple tax treatment: 50% tax rate across the board. tax = Tax(tax_brackets={self.initial_year: {Decimal(0): Decimal(0.5)}}) # Set up people with $4000 gross income, $2000 net income: biweekly_timing = Timing(frequency="BW") self.person1 = Person( initial_year=self.initial_year, name="Test 1", birth_date="1 January 1980", retirement_date="31 December 2001", # next year gross_income=Decimal(1000), tax_treatment=tax, payment_timing=biweekly_timing) self.person2 = Person( initial_year=self.initial_year, name="Test 2", birth_date="1 January 1975", retirement_date="31 December 2001", # next year gross_income=Decimal(3000), tax_treatment=tax, payment_timing=biweekly_timing) self.people = {self.person1, self.person2} # Give person1 a $1000 account and person2 a $9,000 account: self.account1 = Account(owner=self.person1, balance=Decimal(1000), rate=0) self.account2 = Account(owner=self.person2, balance=Decimal(9000), rate=0)
def test_add_account(self): """ Test Person after being added as an Account owner. """ person1 = self.owner person2 = Person( self.initial_year, "Spouse", self.initial_year - 20, retirement_date=self.retirement_date, gross_income=50000, spouse=person1, tax_treatment=self.tax_treatment) # Add an account and confirm that the Person passed as owner is # updated. account1 = Account(owner=person1) account2 = Account(owner=person1) self.assertEqual(person1.accounts, {account1, account2}) self.assertEqual(person2.accounts, set())
def setUp_decimal(self): """ Builds stock variables to test with. """ # 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 self.initial_year = 2000 # Simple tax treatment: 50% tax rate across the board. tax = Tax(tax_brackets={self.initial_year: {Decimal(0): Decimal(0.5)}}) # A person who is paid $1000 gross ($500 withheld): timing = Timing(frequency='BW', high_precision=Decimal) self.person1 = Person(initial_year=self.initial_year, name="Test 1", birth_date="1 January 1980", retirement_date="31 December 2045", gross_income=Decimal(1000), tax_treatment=tax, payment_timing=timing, high_precision=Decimal) # A person who is paid $500 gross ($250 withheld): self.person2 = Person(initial_year=self.initial_year, name="Test 2", birth_date="1 January 1982", retirement_date="31 December 2047", gross_income=Decimal(500), tax_treatment=tax, payment_timing=timing, high_precision=Decimal) # An account owned by person1 with $100 to withdraw self.account1 = Account(owner=self.person1, balance=Decimal(100), rate=Decimal(0), high_precision=Decimal) # An account owned by person2 with $200 to withdraw self.account2 = Account(owner=self.person2, balance=Decimal(100), rate=Decimal(0), high_precision=Decimal) # An account that belongs in some sense to both people # with $50 to withdraw self.account_joint = Account(owner=self.person1, balance=Decimal(100), rate=Decimal(0), high_precision=Decimal) self.person2.accounts.add(self.account_joint) self.forecast = TaxForecast(initial_year=self.initial_year, people={self.person1, self.person2}, tax_treatment=tax, high_precision=Decimal)
def setUp(self): """ Builds stock variables to test with. """ self.initial_year = 2000 self.subforecast = SubForecast(self.initial_year) self.person = Person(initial_year=self.initial_year, name="Test", birth_date="1 January 1980", retirement_date="31 December 2045") # A basic account with 100% interest and no compounding: self.account1 = Account(owner=self.person, balance=100, rate=1, nper=1) # Another account, same as account1: self.account2 = Account(owner=self.person, balance=100, rate=1, nper=1) # Set up a dict and Account for use as `available`: self.available_dict = defaultdict(lambda: 0) self.available_acct = Account(initial_year=self.initial_year, rate=0)
def setUp(self): """ Builds stock variables to test with. """ self.initial_year = 2000 # Simple tax treatment: 50% tax rate across the board. tax = Tax(tax_brackets={ self.initial_year: {0: 0.5}}) # Accounts need an owner: timing = Timing(frequency='BW') self.person = Person( initial_year=self.initial_year, name="Test", birth_date="1 January 1980", retirement_date="31 December 1999", # last year gross_income=5200, tax_treatment=tax, payment_timing=timing) # We want at least two accounts which are withdrawn from # in different orders depending on the strategy. self.account = Account( owner=self.person, balance=60000) # $60,000 <- BIGGER! self.rrsp = canada.accounts.RRSP( owner=self.person, contribution_room=1000, balance=6000) # $6,000 # Assume there are $2000 in inflows and $22,000 in outflows, # for a net need of $20,000: self.available = { 0.25: 1000, 0.5: -11000, 0.75: 1000, 1: -11000 } # Now we can set up the big-ticket items: self.strategy = TransactionStrategy( strategy=TransactionStrategy.strategy_ordered, weights={"RRSP": 1, "Account": 2}) self.forecast = WithdrawalForecast( initial_year=self.initial_year, people={self.person}, accounts={self.account, self.rrsp}, transaction_strategy=self.strategy) # Set up another forecast for testing withholding behaviour: self.withholding_account = WithholdingAccount( owner=self.person, balance=100000) self.withholding_strategy = TransactionStrategy( strategy=TransactionStrategy.strategy_ordered, weights={"WithholdingAccount": 1}) self.withholding_forecast = WithdrawalForecast( initial_year=self.initial_year, people={self.person}, accounts={self.withholding_account}, transaction_strategy=self.withholding_strategy)
def setUp_decimal(self): """ Sets up variables based on Decimal inputs. """ # We use caps because this is a type. # pylint: disable=invalid-name self.AccountType = Account # pylint: enable=invalid-name # It's important to synchronize the initial years of related # objects, so store it here: self.initial_year = 2000 # Every init requires an owner, so store that here: self.scenario = Scenario(inflation=Decimal(0), stock_return=Decimal(1), bond_return=Decimal(0.5), other_return=Decimal(0), management_fees=Decimal(0.03125), initial_year=self.initial_year, num_years=100) self.allocation_strategy = AllocationStrategy( strategy=AllocationStrategy.strategy_n_minus_age, min_equity=Decimal(0.5), max_equity=Decimal(0.5), target=Decimal(0.5), standard_retirement_age=65, risk_transition_period=20, adjust_for_retirement_plan=False) self.owner = Person( self.initial_year, "test", 2000, raise_rate={year: Decimal(1) for year in range(2000, 2066)}, retirement_date=2065) # We'll also need a timing value for various tests. # Use two inflows, at the start and end, evenly weighted: self.timing = {Decimal(0): Decimal(1), Decimal(1): Decimal(1)} # Inheriting classes should assign to self.account with an # instance of an appropriate subclass of Account. self.account = Account(self.owner, balance=Decimal(100), rate=Decimal(1.0), high_precision=Decimal)
def setUp(self): """ Sets up variables for testing LinkedLimitAccount """ super().setUp() # Set up some stock owners and accounts for testing: self.person1 = Person(initial_year=2000, name="Test One", birth_date="1 January 1980", retirement_date="31 December 2030") self.person2 = Person(initial_year=2000, name="Test One", birth_date="1 January 1980", retirement_date="31 December 2030") self.token1 = "token1" self.token2 = "token2" self.account1 = Account(owner=self.person1) self.account2 = Account(owner=self.person2) # Build a link to test against, for convenience: self.link = AccountLink(link=(self.person1, self.token1))
def setUp_decimal(self): """ Builds stock 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 self.initial_year = 2000 # Simple tax treatment: 50% tax rate across the board. tax = Tax(tax_brackets={self.initial_year: { Decimal(0): Decimal(0.5) }}, high_precision=Decimal) # Accounts need an owner: timing = Timing(frequency='BW', high_precision=Decimal) self.person = Person(initial_year=self.initial_year, name="Test", birth_date="1 January 1980", retirement_date="31 December 2045", gross_income=Decimal(5200), tax_treatment=tax, payment_timing=timing, high_precision=Decimal) # We want at least two accounts which are contributed to # in different orders depending on the strategy. self.account = Account(owner=self.person, high_precision=Decimal) self.rrsp = canada.accounts.RRSP(owner=self.person, contribution_room=Decimal(1000), high_precision=Decimal) # Track money available for use by the forecast: self.available = defaultdict(lambda: Decimal(0)) for i in range(26): # biweekly inflows from employment self.available[Decimal(0.5 + i) / 26] = Decimal(150) for i in range(12): # monthly living expenses and reductions: self.available[Decimal(i) / 12] -= Decimal(75) # The result: $3000 available self.total_available = sum(self.available.values()) # Now we can set up the big-ticket items: # Use an ordered strategy by default: self.strategy = TransactionTraversal([self.account, self.rrsp], high_precision=Decimal) self.forecast = SavingForecast( initial_year=self.initial_year, retirement_accounts={self.account, self.rrsp}, debt_accounts=set(), transaction_strategy=self.strategy, high_precision=Decimal)
def setUp(self): """ Builds stock variables to test with. """ self.initial_year = 2000 # Simple tax treatment: 50% tax rate across the board. tax = Tax(tax_brackets={self.initial_year: {0: 0.5}}) # Accounts need an owner: timing = Timing(frequency='BW') self.person = Person(initial_year=self.initial_year, name="Test", birth_date="1 January 1980", retirement_date="31 December 2045", gross_income=5200, tax_treatment=tax, payment_timing=timing) # We want at least two accounts which are contributed to # in different orders depending on the strategy. self.account = Account(owner=self.person) self.rrsp = canada.accounts.RRSP(owner=self.person, contribution_room=1000) # Track money available for use by the forecast: self.available = defaultdict(lambda: 0) for i in range(26): # biweekly inflows from employment self.available[(0.5 + i) / 26] = 150 for i in range(12): # monthly living expenses and reductions: self.available[i / 12] -= 75 # The result: $3000 available self.total_available = sum(self.available.values()) # Now we can set up the big-ticket items: # Use an ordered strategy by default: self.strategy = TransactionTraversal([self.account, self.rrsp]) self.forecast = SavingForecast( initial_year=self.initial_year, retirement_accounts={self.account, self.rrsp}, debt_accounts=set(), transaction_strategy=self.strategy)
def setUp_decimal(self): """ Builds default strategies/persons/etc. with Decimal inputs. """ # pylint: disable=invalid-name # This name is based on `setUp`, which doesn't follow Pylint's rules # pylint: enable=invalid-name # Use a default settings object: # (This is conditional so that subclasses can assign their own # settings object before calling super().setUp()) if not hasattr(self, 'settings'): self.settings = Settings() # To simplify tests, modify Settings so that forecasts are # just 2 years with easy-to-predict contributions ($1000/yr) self.settings.num_years = 2 self.settings.living_expenses_strategy = ( LivingExpensesStrategy.strategy_const_contribution) self.settings.living_expenses_base_amount = Decimal(1000) # Allow subclasses to use subclasses of Forecaster by assigning # to forecaster_type if not hasattr(self, 'forecaster_type'): self.forecaster_type = Forecaster # Build default `SubForecast` inputs based on `settings`: self.initial_year = self.settings.initial_year self.scenario = Scenario( inflation=Decimal(self.settings.inflation), stock_return=Decimal(self.settings.stock_return), bond_return=Decimal(self.settings.bond_return), other_return=Decimal(self.settings.other_return), management_fees=Decimal(self.settings.management_fees), initial_year=self.settings.initial_year, num_years=self.settings.num_years) self.living_expenses_strategy = LivingExpensesStrategy( strategy=self.settings.living_expenses_strategy, base_amount=Decimal(self.settings.living_expenses_base_amount), rate=Decimal(self.settings.living_expenses_rate), inflation_adjust=self.scenario.inflation_adjust) self.saving_strategy = TransactionStrategy( strategy=self.settings.saving_strategy, weights={ year: Decimal(val) for (year, val) in self.settings.saving_weights.items() }) self.withdrawal_strategy = TransactionStrategy( strategy=self.settings.withdrawal_strategy, weights={ year: Decimal(val) for (year, val) in self.settings.withdrawal_weights.items() }) self.allocation_strategy = AllocationStrategy( strategy=self.settings.allocation_strategy, min_equity=Decimal(self.settings.allocation_min_equity), max_equity=Decimal(self.settings.allocation_max_equity), target=Decimal(self.settings.allocation_target), standard_retirement_age=( self.settings.allocation_std_retirement_age), risk_transition_period=self.settings.allocation_risk_trans_period, adjust_for_retirement_plan=( self.settings.allocation_adjust_retirement)) self.debt_payment_strategy = DebtPaymentStrategy( strategy=self.settings.debt_payment_strategy, high_precision=Decimal) self.tax_treatment = Tax( tax_brackets={ year: { Decimal(lower): Decimal(upper) } for (year, vals) in self.settings.tax_brackets.items() for (lower, upper) in vals.items() }, personal_deduction={ year: Decimal(val) for (year, val) in self.settings.tax_personal_deduction.items() }, credit_rate={ year: Decimal(val) for (year, val) in self.settings.tax_credit_rate.items() }, inflation_adjust=self.scenario.inflation_adjust, high_precision=Decimal) # Now build some Ledger objects to test against: # A person making $10,000/yr self.person = Person(initial_year=self.initial_year, name="Test 1", birth_date="1 January 1980", retirement_date="31 December 2040", gross_income=Decimal(10000), raise_rate=Decimal(0), spouse=None, tax_treatment=self.tax_treatment, high_precision=Decimal) # An account with $1000 in it (and no interest) self.account = Account(owner=self.person, balance=Decimal(1000), high_precision=Decimal) # A debt with a $100 balance (and no interest) self.debt = Debt(owner=self.person, balance=Decimal(100), high_precision=Decimal) # Init a Forecaster object here for convenience: self.forecaster = self.forecaster_type(settings=self.settings, high_precision=Decimal)
def setUp(self): """ Builds default strategies, persons, etc. """ # Use a default settings object: # (This is conditional so that subclasses can assign their own # settings object before calling super().setUp()) if not hasattr(self, 'settings'): self.settings = Settings() # To simplify tests, modify Settings so that forecasts are # just 2 years with easy-to-predict contributions ($1000/yr) self.settings.num_years = 2 self.settings.living_expenses_strategy = ( LivingExpensesStrategy.strategy_const_contribution) self.settings.living_expenses_base_amount = 1000 # Allow subclasses to use subclasses of Forecaster by assigning # to forecaster_type if not hasattr(self, 'forecaster_type'): self.forecaster_type = Forecaster # Build default `SubForecast` inputs based on `settings`: self.initial_year = self.settings.initial_year self.scenario = Scenario(inflation=self.settings.inflation, stock_return=self.settings.stock_return, bond_return=self.settings.bond_return, other_return=self.settings.other_return, management_fees=self.settings.management_fees, initial_year=self.settings.initial_year, num_years=self.settings.num_years) self.living_expenses_strategy = LivingExpensesStrategy( strategy=self.settings.living_expenses_strategy, base_amount=self.settings.living_expenses_base_amount, rate=self.settings.living_expenses_rate, inflation_adjust=self.scenario.inflation_adjust) self.saving_strategy = TransactionStrategy( strategy=self.settings.saving_strategy, weights=self.settings.saving_weights) self.withdrawal_strategy = TransactionStrategy( strategy=self.settings.withdrawal_strategy, weights=self.settings.withdrawal_weights) self.allocation_strategy = AllocationStrategy( strategy=self.settings.allocation_strategy, min_equity=self.settings.allocation_min_equity, max_equity=self.settings.allocation_max_equity, target=self.settings.allocation_target, standard_retirement_age=( self.settings.allocation_std_retirement_age), risk_transition_period=self.settings.allocation_risk_trans_period, adjust_for_retirement_plan=( self.settings.allocation_adjust_retirement)) self.debt_payment_strategy = DebtPaymentStrategy( strategy=self.settings.debt_payment_strategy) self.tax_treatment = Tax( tax_brackets=self.settings.tax_brackets, personal_deduction=self.settings.tax_personal_deduction, credit_rate=self.settings.tax_credit_rate, inflation_adjust=self.scenario.inflation_adjust) # Now build some Ledger objects to test against: # A person making $10,000/yr self.person = Person(initial_year=self.initial_year, name="Test 1", birth_date="1 January 1980", retirement_date="31 December 2040", gross_income=10000, raise_rate=0, spouse=None, tax_treatment=self.tax_treatment) # An account with $1000 in it (and no interest) self.account = Account(owner=self.person, balance=1000) # A debt with a $100 balance (and no interest) self.debt = Debt(owner=self.person, balance=100) # Init a Forecaster object here for convenience: self.forecaster = self.forecaster_type(settings=self.settings)
def setUp(self): """ Builds stock variables to test with. """ self.initial_year = 2000 # We will occasionally need to swap out subforecasts when # we want them to have no effect (e.g. no withdrawals because # we're not yet retired). Use null_forecast for that: self.null_forecast = DummyForecast(self.initial_year) # Paid $100 at the start of each month self.income_forecast_dummy = DummyForecast( self.initial_year, {when / 12: 100 for when in range(12)}) self.income_forecast_dummy.people = None # Spend $70 on living expenses at the start of each month self.living_expenses_forecast_dummy = DummyForecast( self.initial_year, {when / 12: -70 for when in range(12)}) # Contribute the balance ($30/mo, $360/yr): self.saving_forecast_dummy = DummyForecast( self.initial_year, {when + 1 / 12: -30 for when in range(12)}) # Withdraw $300 at the start and middle of the year: self.withdrawal_forecast_dummy = DummyForecast(self.initial_year, { 0: 300, 0.5: 300 }) # Refund for $100 next year: self.tax_forecast_dummy = DummyForecast(self.initial_year) self.tax_forecast_dummy.tax_adjustment = 100 self.tax_forecast_dummy.tax_refund_timing = Timing('start') # Also build a real ContributionForecast so that we can # test cash flows into accounts according to the overall # Forecast: # Simple tax rate: 50% on all income: tax = Tax(tax_brackets={self.initial_year: {0: 0.5}}) # One person, to own the account: timing = Timing(frequency='BW') self.person = Person(initial_year=self.initial_year, name="Test", birth_date="1 January 1980", retirement_date="31 December 2045", gross_income=5200, tax_treatment=tax, payment_timing=timing) # An account for savings to go to: self.account = Account(owner=self.person) # A strategy is required, but since there's only # one account the result will always be the same: self.strategy = TransactionTraversal(priority=[self.account]) self.saving_forecast = SavingForecast( initial_year=self.initial_year, retirement_accounts={self.account}, debt_accounts=set(), transaction_strategy=self.strategy) # Now assign `people`, `accounts`, and `debts` attrs to # appropriate subforecasts so that Forecast can retrieve # them: self.income_forecast_dummy.people = {self.person} self.saving_forecast_dummy.debt_accounts = set() self.withdrawal_forecast_dummy.accounts = {self.account} # Also add these to the null forecast, since it could be # substituted for any of the above dummy forecasts: self.null_forecast.people = self.income_forecast_dummy.people self.null_forecast.accounts = self.withdrawal_forecast_dummy.accounts self.null_forecast.debt_accounts = ( self.saving_forecast_dummy.debt_accounts) # Forecast depends on SubForecasts having certain properties, # so add those here: self.income_forecast_dummy.net_income = (sum( self.income_forecast_dummy.transactions.values())) self.living_expenses_forecast_dummy.living_expenses = (sum( self.living_expenses_forecast_dummy.transactions.values())) self.withdrawal_forecast_dummy.gross_withdrawals = (sum( self.withdrawal_forecast_dummy.transactions.values())) self.tax_forecast_dummy.tax_owing = 600 # Add the same properties to the null forecast, since it # could be substituted for any of the above: self.null_forecast.net_income = self.income_forecast_dummy.net_income self.null_forecast.living_expenses = ( self.living_expenses_forecast_dummy.living_expenses) self.null_forecast.gross_withdrawals = ( self.withdrawal_forecast_dummy.gross_withdrawals) self.null_forecast.tax_owing = self.tax_forecast_dummy.tax_owing # Finally, we need a Scenario to build a Forecast. # This is the simplest possible: 1 year, no growth. self.scenario = Scenario(self.initial_year, num_years=1)
class TestForecast(unittest.TestCase): """ Tests Forecast. """ def setUp(self): """ Builds stock variables to test with. """ self.initial_year = 2000 # We will occasionally need to swap out subforecasts when # we want them to have no effect (e.g. no withdrawals because # we're not yet retired). Use null_forecast for that: self.null_forecast = DummyForecast(self.initial_year) # Paid $100 at the start of each month self.income_forecast_dummy = DummyForecast( self.initial_year, {when / 12: 100 for when in range(12)}) self.income_forecast_dummy.people = None # Spend $70 on living expenses at the start of each month self.living_expenses_forecast_dummy = DummyForecast( self.initial_year, {when / 12: -70 for when in range(12)}) # Contribute the balance ($30/mo, $360/yr): self.saving_forecast_dummy = DummyForecast( self.initial_year, {when + 1 / 12: -30 for when in range(12)}) # Withdraw $300 at the start and middle of the year: self.withdrawal_forecast_dummy = DummyForecast(self.initial_year, { 0: 300, 0.5: 300 }) # Refund for $100 next year: self.tax_forecast_dummy = DummyForecast(self.initial_year) self.tax_forecast_dummy.tax_adjustment = 100 self.tax_forecast_dummy.tax_refund_timing = Timing('start') # Also build a real ContributionForecast so that we can # test cash flows into accounts according to the overall # Forecast: # Simple tax rate: 50% on all income: tax = Tax(tax_brackets={self.initial_year: {0: 0.5}}) # One person, to own the account: timing = Timing(frequency='BW') self.person = Person(initial_year=self.initial_year, name="Test", birth_date="1 January 1980", retirement_date="31 December 2045", gross_income=5200, tax_treatment=tax, payment_timing=timing) # An account for savings to go to: self.account = Account(owner=self.person) # A strategy is required, but since there's only # one account the result will always be the same: self.strategy = TransactionTraversal(priority=[self.account]) self.saving_forecast = SavingForecast( initial_year=self.initial_year, retirement_accounts={self.account}, debt_accounts=set(), transaction_strategy=self.strategy) # Now assign `people`, `accounts`, and `debts` attrs to # appropriate subforecasts so that Forecast can retrieve # them: self.income_forecast_dummy.people = {self.person} self.saving_forecast_dummy.debt_accounts = set() self.withdrawal_forecast_dummy.accounts = {self.account} # Also add these to the null forecast, since it could be # substituted for any of the above dummy forecasts: self.null_forecast.people = self.income_forecast_dummy.people self.null_forecast.accounts = self.withdrawal_forecast_dummy.accounts self.null_forecast.debt_accounts = ( self.saving_forecast_dummy.debt_accounts) # Forecast depends on SubForecasts having certain properties, # so add those here: self.income_forecast_dummy.net_income = (sum( self.income_forecast_dummy.transactions.values())) self.living_expenses_forecast_dummy.living_expenses = (sum( self.living_expenses_forecast_dummy.transactions.values())) self.withdrawal_forecast_dummy.gross_withdrawals = (sum( self.withdrawal_forecast_dummy.transactions.values())) self.tax_forecast_dummy.tax_owing = 600 # Add the same properties to the null forecast, since it # could be substituted for any of the above: self.null_forecast.net_income = self.income_forecast_dummy.net_income self.null_forecast.living_expenses = ( self.living_expenses_forecast_dummy.living_expenses) self.null_forecast.gross_withdrawals = ( self.withdrawal_forecast_dummy.gross_withdrawals) self.null_forecast.tax_owing = self.tax_forecast_dummy.tax_owing # Finally, we need a Scenario to build a Forecast. # This is the simplest possible: 1 year, no growth. self.scenario = Scenario(self.initial_year, num_years=1) def setUp_decimal(self): """ Builds stock variables to test with. """ # 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 self.initial_year = 2000 # We will occasionally need to swap out subforecasts when # we want them to have no effect (e.g. no withdrawals because # we're not yet retired). Use null_forecast for that: self.null_forecast = DummyForecast(self.initial_year) # Paid $100 at the start of each month self.income_forecast_dummy = DummyForecast( self.initial_year, {Decimal(when) / 12: Decimal(100) for when in range(12)}) self.income_forecast_dummy.people = None # Spend $70 on living expenses at the start of each month self.living_expenses_forecast_dummy = DummyForecast( self.initial_year, {Decimal(when) / 12: Decimal(-70) for when in range(12)}) # Contribute the balance ($30/mo, $360/yr): self.saving_forecast_dummy = DummyForecast( self.initial_year, {Decimal(when + 1) / 12: Decimal(-30) for when in range(12)}) # Withdraw $300 at the start and middle of the year: self.withdrawal_forecast_dummy = DummyForecast( self.initial_year, { Decimal(0): Decimal(300), Decimal(0.5): Decimal(300) }) # Refund for $100 next year: self.tax_forecast_dummy = DummyForecast(self.initial_year) self.tax_forecast_dummy.tax_adjustment = Decimal(100) self.tax_forecast_dummy.tax_refund_timing = Timing( 'start', high_precision=Decimal) # Also build a real ContributionForecast so that we can # test cash flows into accounts according to the overall # Forecast: # Simple tax rate: 50% on all income: tax = Tax(tax_brackets={self.initial_year: { Decimal(0): Decimal(0.5) }}, high_precision=Decimal) # One person, to own the account: timing = Timing(frequency='BW', high_precision=Decimal) self.person = Person(initial_year=self.initial_year, name="Test", birth_date="1 January 1980", retirement_date="31 December 2045", gross_income=Decimal(5200), tax_treatment=tax, payment_timing=timing, high_precision=Decimal) # An account for savings to go to: self.account = Account(owner=self.person, high_precision=Decimal) # A strategy is required, but since there's only # one account the result will always be the same: self.strategy = TransactionTraversal(priority=[self.account], high_precision=Decimal) self.saving_forecast = SavingForecast( initial_year=self.initial_year, retirement_accounts={self.account}, debt_accounts=set(), transaction_strategy=self.strategy, high_precision=Decimal) # Now assign `people`, `accounts`, and `debts` attrs to # appropriate subforecasts so that Forecast can retrieve # them: self.income_forecast_dummy.people = {self.person} self.saving_forecast_dummy.debt_accounts = set() self.withdrawal_forecast_dummy.accounts = {self.account} # Also add these to the null forecast, since it could be # substituted for any of the above dummy forecasts: self.null_forecast.people = self.income_forecast_dummy.people self.null_forecast.accounts = self.withdrawal_forecast_dummy.accounts self.null_forecast.debt_accounts = ( self.saving_forecast_dummy.debt_accounts) # Forecast depends on SubForecasts having certain properties, # so add those here: self.income_forecast_dummy.net_income = (sum( self.income_forecast_dummy.transactions.values())) self.living_expenses_forecast_dummy.living_expenses = (sum( self.living_expenses_forecast_dummy.transactions.values())) self.withdrawal_forecast_dummy.gross_withdrawals = (sum( self.withdrawal_forecast_dummy.transactions.values())) self.tax_forecast_dummy.tax_owing = Decimal(600) # Add the same properties to the null forecast, since it # could be substituted for any of the above: self.null_forecast.net_income = self.income_forecast_dummy.net_income self.null_forecast.living_expenses = ( self.living_expenses_forecast_dummy.living_expenses) self.null_forecast.gross_withdrawals = ( self.withdrawal_forecast_dummy.gross_withdrawals) self.null_forecast.tax_owing = self.tax_forecast_dummy.tax_owing # Finally, we need a Scenario to build a Forecast. # This is the simplest possible: 1 year, no growth. self.scenario = Scenario(self.initial_year, num_years=1) def test_update_available(self): """ Test the mechanics of the update_available method. We don't want to test the underlying SubForecast classes, so just use end-to-end dummies. """ # A 1-year forecast with no withdrawals. Should earn $1200 # in income, spend $840 on living expenses, save the remaining # $360, and withdraw $600. forecast = Forecast( income_forecast=self.income_forecast_dummy, living_expenses_forecast=self.living_expenses_forecast_dummy, saving_forecast=self.saving_forecast_dummy, withdrawal_forecast=self.withdrawal_forecast_dummy, tax_forecast=self.tax_forecast_dummy, scenario=self.scenario) results = [ sum(forecast.income_forecast.available_in.values(), 0), sum(forecast.living_expenses_forecast.available_in.values()), sum(forecast.saving_forecast.available_in.values()), sum(forecast.withdrawal_forecast.available_in.values()), sum(forecast.tax_forecast.available_in.values()), sum(forecast.tax_forecast.available_out.values()) ] target = [0, 1200, 360, 0, 600, 600] for first, second in zip(results, target): self.assertAlmostEqual(first, second, places=2) def test_multi_year(self): """ Tests a multi-year forecast. """ # Build a two-year forecast. Should contribute $360 each year. # No tax refunds or withdrawals. self.scenario = Scenario(self.initial_year, 2) self.tax_forecast_dummy.tax_adjustment = 0 forecast = Forecast( income_forecast=self.income_forecast_dummy, living_expenses_forecast=self.living_expenses_forecast_dummy, # NOTE: Saving forecast is not a dummy because we # want to actually contribute to savings accounts: saving_forecast=self.saving_forecast, withdrawal_forecast=self.null_forecast, tax_forecast=self.tax_forecast_dummy, scenario=self.scenario) results = [ # pylint: disable=no-member # Pylint has trouble with attributes added via metaclass forecast.principal_history[self.initial_year], forecast.principal_history[self.initial_year + 1], self.account.balance_at_time('end') ] target = [0, 360, 720] for first, second in zip(results, target): self.assertAlmostEqual(first, second, places=2) def test_refund(self): """ Tests tax refund carryovers """ # Set up a forecast where we receive a $100 refund in the middle # of year 2, with no other transactions: self.scenario.num_years = 2 self.tax_forecast_dummy.tax_adjustment = 100 trans_time = 0.5 self.tax_forecast_dummy.tax_refund_timing = Timing(trans_time) forecast = Forecast(income_forecast=self.null_forecast, living_expenses_forecast=self.null_forecast, saving_forecast=self.null_forecast, withdrawal_forecast=self.null_forecast, tax_forecast=self.tax_forecast_dummy, scenario=self.scenario) # Now confirm that the refund was in fact received: self.assertEqual(forecast.available[trans_time], 100) # And confirm that there were no other non-zero transactions: self.assertTrue( all(value == 0 for timing, value in forecast.available.items() if timing != trans_time)) def test_payment(self): """ Tests tax payment carryovers """ # Set up a forecast where we pay $100 in taxes owing in the # middle of year 2, with no other transactions: self.scenario.num_years = 2 self.tax_forecast_dummy.tax_adjustment = -100 trans_time = 0.5 self.tax_forecast_dummy.tax_payment_timing = Timing(trans_time) forecast = Forecast(income_forecast=self.null_forecast, living_expenses_forecast=self.null_forecast, saving_forecast=self.null_forecast, withdrawal_forecast=self.null_forecast, tax_forecast=self.tax_forecast_dummy, scenario=self.scenario) # Now confirm that the refund was in fact received: self.assertEqual(forecast.available[trans_time], -100) # And confirm that there were no other non-zero transactions: self.assertTrue( all(value == 0 for timing, value in forecast.available.items() if timing != trans_time)) def test_decimal(self): """ Tests a Forecast with Decimal inputs. """ # Convert values to Decimal: self.setUp_decimal() # This test is based on test_multi_year: # Build a two-year forecast. Should contribute $360 each year. # No tax refunds or withdrawals. self.scenario = Scenario(self.initial_year, 2) self.tax_forecast_dummy.tax_adjustment = Decimal(0) forecast = Forecast( income_forecast=self.income_forecast_dummy, living_expenses_forecast=self.living_expenses_forecast_dummy, # NOTE: Saving forecast is not a dummy because we # want to actually contribute to savings accounts: saving_forecast=self.saving_forecast, withdrawal_forecast=self.null_forecast, tax_forecast=self.tax_forecast_dummy, scenario=self.scenario, high_precision=Decimal) results = [ # pylint: disable=no-member # Pylint has trouble with attributes added via metaclass forecast.principal_history[self.initial_year], forecast.principal_history[self.initial_year + 1], self.account.balance_at_time('end') ] target = [Decimal(0), Decimal(360), Decimal(720)] for first, second in zip(results, target): self.assertAlmostEqual(first, second, places=2)
class TestAccountMethods(unittest.TestCase): """ A test suite for the `Account` class. For each Account subclass, create a test case that subclasses from this (or an intervening subclass). Then, in the setUp method, assign to class attributes `args`, and/or `kwargs` to determine which arguments will be prepended, postpended, or added via keyword when an instance of the subclass is initialized. Don't forget to also assign the subclass your're testing to `self.AccountType`, and to run `super().setUp()` at the top! This way, the methods of this class will still be called even for subclasses with mandatory positional arguments. You should still override the relevant methods to test subclass-specific logic (e.g. if the subclass modifies the treatment of the `rate` attribute based on an init arg, you'll want to test that by overriding `test_rate`) """ def setUp(self): """ Sets up some class-specific variables for calling methods. """ # We use caps because this is a type. # pylint: disable=invalid-name self.AccountType = Account # pylint: enable=invalid-name # It's important to synchronize the initial years of related # objects, so store it here: self.initial_year = 2000 # Every init requires an owner, so store that here: self.scenario = Scenario(inflation=0, stock_return=1, bond_return=0.5, other_return=0, management_fees=0.03125, initial_year=self.initial_year, num_years=100) self.allocation_strategy = AllocationStrategy( strategy=AllocationStrategy.strategy_n_minus_age, min_equity=Decimal(0.5), max_equity=Decimal(0.5), target=Decimal(0.5), standard_retirement_age=65, risk_transition_period=20, adjust_for_retirement_plan=False) self.owner = Person(self.initial_year, "test", 2000, raise_rate={year: 1 for year in range(2000, 2066)}, retirement_date=2065) # We'll also need a timing value for various tests. # Use two inflows, at the start and end, evenly weighted: self.timing = {Decimal(0): 1, Decimal(1): 1} # Inheriting classes should assign to self.account with an # instance of an appropriate subclass of Account. self.account = Account(self.owner, balance=100, rate=1.0) def test_init_basic(self, *args, **kwargs): """ Tests Account.__init__ """ # Basic test: All correct values, check for equality and type owner = self.owner balance = Money(0) rate = 1.0 nper = 1 # This is the easiest case to test initial_year = self.initial_year account = self.AccountType(owner, *args, balance=balance, rate=rate, nper=nper, **kwargs) # Test primary attributes # pylint: disable=no-member # Pylint is confused by members added by metaclass self.assertEqual(account.balance_history, {initial_year: balance}) self.assertEqual(account.rate_history, {initial_year: rate}) self.assertEqual(account.transactions_history, {initial_year: {}}) self.assertEqual(account.balance, balance) self.assertEqual(account.rate, rate) self.assertEqual(account.nper, 1) self.assertEqual(account.initial_year, initial_year) self.assertEqual(account.this_year, initial_year) # Check types # pylint: disable=no-member # Pylint is confused by members added by metaclass self.assertTrue(type_check(account.balance_history, {int: Money})) self.assertIsInstance(account.balance, Money) self.assertTrue(type_check(account.rate_history, {int: Decimal})) self.assertIsInstance(account.rate, Decimal) self.assertIsInstance(account.nper, int) self.assertIsInstance(account.initial_year, int) def test_init_rate_function(self, *args, **kwargs): """ Tests using a function as an input for arg `rate`. """ # Infer the rate from the account owner's asset allocation # (which is 50% stocks, 50% bonds, with 75% return overall) # pylint: disable=no-member # Pylint is confused by members added by metaclass balance = 0 rate = self.allocation_strategy.rate_function(self.owner, self.scenario) account = self.AccountType(self.owner, *args, balance=balance, rate=rate, **kwargs) self.assertEqual(account.balance_history, {self.initial_year: balance}) self.assertEqual(account.transactions_history, {self.initial_year: {}}) self.assertEqual(account.balance, balance) self.assertEqual(account.rate, Decimal(0.75)) self.assertEqual(account.rate_history, {self.initial_year: Decimal(0.75)}) self.assertEqual(account.transactions, {}) self.assertEqual(account.nper, 1) self.assertEqual(account.initial_year, self.initial_year) self.assertEqual(account.rate_callable, rate) def test_init_type_conversion(self, *args, **kwargs): """ Tests using (Decimal-convertible) strings as input. """ balance = "0" rate = "1.0" nper = 'A' initial_year = self.initial_year account = self.AccountType(self.owner, *args, balance=balance, rate=rate, nper=nper, **kwargs) # pylint: disable=no-member # Pylint is confused by members added by metaclass self.assertEqual(account.balance_history, {initial_year: Money(0)}) self.assertEqual(account.rate_history, {initial_year: 1}) self.assertEqual(account.transactions_history, {initial_year: {}}) self.assertEqual(account.balance, Money(0)) self.assertEqual(account.rate, 1) self.assertEqual(account.nper, 1) self.assertEqual(account.initial_year, initial_year) # Check types for conversion self.assertIsInstance(account.balance_history[initial_year], Money) self.assertIsInstance(account.rate_history[initial_year], Decimal) self.assertIsInstance(account.nper, int) self.assertIsInstance(account.initial_year, int) def test_init_invalid_balance(self, *args, **kwargs): """ Test Account.__init__ with invalid balance input. """ # Let's test invalid Decimal conversions next. # (BasicContext causes most Decimal-conversion errors to raise # exceptions. Invalid input will raise InvalidOperation) decimal.setcontext(decimal.BasicContext) # Test with values not convertible to Decimal with self.assertRaises(decimal.InvalidOperation): account = self.AccountType(self.owner, *args, balance="invalid input", **kwargs) # In some contexts, Decimal returns NaN instead of raising an error if account.balance == Money("NaN"): raise decimal.InvalidOperation() def test_init_invalid_rate(self, *args, **kwargs): """ Test Account.__init__ with invalid rate input. """ decimal.setcontext(decimal.BasicContext) with self.assertRaises(decimal.InvalidOperation): account = self.AccountType(self.owner, *args, balance=0, rate="invalid input", **kwargs) # `rate` is not callable if we use non-callable input # pylint: disable=comparison-with-callable if account.rate == Decimal("NaN"): raise decimal.InvalidOperation() # pylint: enable=comparison-with-callable def test_init_invalid_owner(self, *args, **kwargs): """ Test Account.__init__ with invalid rate input. """ # Finally, test passing an invalid owner: with self.assertRaises(TypeError): _ = self.AccountType("invalid owner", *args, **kwargs) def test_add_trans_in_range(self): """ Test 'when' values inside of the range [0,1]. """ self.account.add_transaction(1, when=0) self.assertEqual(self.account.transactions[Decimal(0)], Money(1)) self.account.add_transaction(1, when=0.5) self.assertEqual(self.account.transactions[Decimal(0.5)], Money(1)) self.account.add_transaction(1, when=1) self.assertEqual(self.account.transactions[Decimal(1)], Money(1)) def test_add_trans_out_range(self): """ Test 'when' values outside of the range [0,1]. """ # All of these should raise exceptions. with self.assertRaises(ValueError): # test negative self.account.add_transaction(1, when=-1) with self.assertRaises(ValueError): # test positive self.account.add_transaction(1, when=2) def test_returns(self, *args, **kwargs): """ Tests Account.returns and Account.returns_history. """ # Account with $1 balance and 100% non-compounded growth. # Should have returns of $1 in its first year: account = self.AccountType(self.owner, *args, balance=1, rate=1.0, nper=1, **kwargs) # pylint: disable=no-member # Pylint is confused by members added by metaclass self.assertEqual(account.returns, Money(1)) # $1 return self.assertEqual(account.returns_history, {self.initial_year: Money(1)}) def test_returns_next_year(self, *args, **kwargs): """ Tests Account.returns after calling next_year. """ # Account with $1 balance and 100% non-compounded growth. # Should have returns of $2 in its second year: account = self.AccountType(self.owner, *args, balance=1, rate=1.0, nper=1, **kwargs) account.next_year() # pylint: disable=no-member # Pylint is confused by members added by metaclass self.assertEqual(account.returns_history, { self.initial_year: Money(1), self.initial_year + 1: Money(2) }) self.assertEqual(account.returns, Money(2)) def test_next(self, *args, **kwargs): """ Tests next_year with basic scenario. """ # Simple account: Start with $1, apply 100% growth once per # year, no transactions. Should yield a new balance of $2. account = self.AccountType(self.owner, *args, balance=1, rate=1.0, nper=1, **kwargs) account.next_year() self.assertEqual(account.balance, Money(2)) def test_next_no_growth(self, *args, **kwargs): """ Tests next_year with no growth. """ # Start with $1 and apply 0% growth. account = self.AccountType(self.owner, *args, balance=1, rate=0, **kwargs) account.next_year() self.assertEqual(account.balance, Money(1)) def test_next_cont_growth(self, *args, **kwargs): """ Tests next_year with continuous growth. """ account = self.AccountType(self.owner, *args, balance=1, rate=1, nper='C', **kwargs) account.next_year() self.assertAlmostEqual(account.balance, Money(math.e), 3) def test_next_disc_growth(self, *args, **kwargs): """ Tests next_year with discrete (monthly) growth. """ account = self.AccountType(self.owner, *args, balance=1, rate=1, nper='M', **kwargs) account.next_year() self.assertAlmostEqual(account.balance, Money((1 + 1 / 12)**12), 3) def test_next_basic_trans(self, *args, **kwargs): """ Tests next_year with a mid-year transaction. """ # Start with $1 (which grows to $2), contribute $2 mid-year. # NOTE: The growth of the $2 transaction is not well-defined, # since it occurs mid-compounding-period. However, the output # should be sensible. In particular, it should grow by $0-$1. # So check to confirm that the result is in the range [$4, $5] account = self.AccountType(self.owner, *args, balance=1, rate=1.0, nper=1, **kwargs) account.add_transaction(Money(2), when='0.5') account.next_year() self.assertGreaterEqual(account.balance, Money(4)) self.assertLessEqual(account.balance, Money(5)) def test_next_no_growth_trans(self, *args, **kwargs): """ Tests next_year with no growth and a transaction. """ # Start with $1, add $2, and apply 0% growth. account = self.AccountType(self.owner, *args, balance=1, rate=0, nper=1, **kwargs) account.add_transaction(Money(2), when='0.5') account.next_year() self.assertEqual(account.balance, Money(3)) def test_next_cont_growth_trans(self, *args, **kwargs): """ Tests next_year with continuous growth and a transaction. """ # This can be calculated from P = P_0 * e^rt account = self.AccountType(self.owner, *args, balance=1, rate=1, nper='C', **kwargs) account.add_transaction(Money(2), when='0.5') next_val = Money(1 * math.e + 2 * math.e**0.5) account.next_year() self.assertAlmostEqual(account.balance, next_val, 5) def test_next_disc_growth_trans(self, *args, **kwargs): """ Tests next_year with discrete growth and a transaction. """ # The $2 transaction happens at the start of a compounding # period, so behaviour is well-defined. It should grow by a # factor of (1 + r/n)^nt, for n = 12 (monthly) and t = 0.5 account = self.AccountType(self.owner, *args, balance=1, rate=1, nper='M', **kwargs) account.add_transaction(Money(2), when='0.5') next_val = Money((1 + 1 / 12)**(12) + 2 * (1 + 1 / 12)**(12 * 0.5)) account.next_year() self.assertAlmostEqual(account.balance, next_val, 5) def test_add_trans(self, *args, **kwargs): """ Tests add_transaction. """ # Start with an empty account and add a transaction. # pylint: disable=no-member # Pylint is confused by members added by metaclass account = self.AccountType(self.owner, *args, **kwargs) self.assertEqual(account.transactions_history, {self.initial_year: {}}) account.add_transaction(Money(1), when='end') self.assertEqual(account.transactions_history, {self.initial_year: { 1: Money(1) }}) self.assertEqual(account.transactions, {1: Money(1)}) self.assertEqual(account.inflows(), Money(1)) def test_add_trans_mult_diff_time(self, *args, **kwargs): """ Tests add_transaction with transactions at different times. """ account = self.AccountType(self.owner, *args, **kwargs) account.add_transaction(Money(1), 'start') account.add_transaction(Money(2), 1) self.assertEqual(account.transactions, {0: Money(1), 1: Money(2)}) self.assertEqual(account.inflows(), Money(3)) self.assertEqual(account.outflows(), Money(0)) def test_add_trans_mult_same_time(self, *args, **kwargs): """ Tests add_transaction with transactions at the same time. """ account = self.AccountType(self.owner, *args, **kwargs) account.add_transaction(Money(1), 'start') account.add_transaction(Money(1), 0) self.assertEqual(account.transactions, {0: Money(2)}) self.assertEqual(account.inflows(), Money(2)) self.assertEqual(account.outflows(), Money(0)) def test_add_trans_diff_in_out(self, *args, **kwargs): """ Tests add_transaction with in- and outflows at different times. """ account = self.AccountType(self.owner, *args, **kwargs) account.add_transaction(Money(1), 'start') account.add_transaction(Money(-2), 'end') self.assertEqual(account.transactions, {0: Money(1), 1: Money(-2)}) self.assertEqual(account.inflows(), Money(1)) self.assertEqual(account.outflows(), Money(-2)) def test_add_trans_same_in_out(self, *args, **kwargs): """ Tests add_transaction with simultaneous inflows and outflows. """ # NOTE: Consider whether this behaviour (i.e. simultaneous flows # being combined into one net flow) should be revised. account = self.AccountType(self.owner, *args, **kwargs) account.add_transaction(Money(1), 'start') account.add_transaction(Money(-2), 'start') self.assertEqual(account.transactions, {0: Money(-1)}) self.assertEqual(account.inflows(), 0) self.assertEqual(account.outflows(), Money(-1)) # TODO: Test add_transactions again after performing next_year # (do this recursively?) def test_max_outflows_constant(self, *args, **kwargs): """ Test max_outflows with constant-balance account """ # Simple scenario: $100 in a no-growth account with no # transactions. Should return $100 for any point in time. account = self.AccountType(self.owner, *args, balance=100, rate=0, nper=1, **kwargs) result = account.max_outflows(self.timing) for when, value in result.items(): account.add_transaction(value, when=when) # Result of `max_outflows` should bring balance to $0 if # applied as transactions: self.assertAlmostEqual(account.balance_at_time('end'), Money(0)) def test_max_outflows_negative(self, *args, **kwargs): """ Test max_outflows with negative-balance account. """ # Try with negative balance - should return $0 account = self.AccountType(self.owner, *args, balance=-100, rate=1, nper=1, **kwargs) result = account.max_outflows(self.timing) for value in result.values(): self.assertAlmostEqual(value, Money(0), places=4) def test_max_outflows_simple(self, *args, **kwargs): """ Test max_outflows with simple growth, no transactions """ # $100 in account that grows to $200 in one compounding period. # No transactions. account = self.AccountType(self.owner, *args, balance=100, rate=1, nper=1, **kwargs) result = account.max_outflows(self.timing) for when, value in result.items(): account.add_transaction(value, when=when) # Result of `max_outflows` should bring balance to $0 if # applied as transactions: self.assertAlmostEqual(account.balance_at_time('end'), Money(0), places=4) def test_max_outflows_simple_trans(self, *args, **kwargs): """ Test max_outflows with simple growth and transactions """ # $100 in account that grows linearly by 100%. Add $100 # transactions at the start and end of the year. account = self.AccountType(self.owner, *args, balance=100, rate=1, nper=1, **kwargs) account.add_transaction(100, when='start') account.add_transaction(100, when='end') result = account.max_outflows(self.timing) for when, value in result.items(): account.add_transaction(value, when=when) # Result of `max_outflows` should bring balance to $0 if # applied as transactions: self.assertAlmostEqual(account.balance_at_time('end'), Money(0), places=4) def test_max_outflows_neg_to_pos(self, *args, **kwargs): """ Test max_outflows going from neg. to pos. balance """ # Try with a negative starting balance and a positive ending # balance. With -$100 start and 200% interest compounding at # t=0.5, balance should be -$200 at t=0.5. Add $200 transaction # at t=0.5 so balance = 0 and another transaction at t='end' so # balance = $100. account = self.AccountType(self.owner, *args, balance=-200, rate=2.0, nper=2, **kwargs) account.add_transaction(100, when='start') account.add_transaction(200, when='0.5') account.add_transaction(100, when='end') result = account.max_outflows(self.timing) for when, value in result.items(): account.add_transaction(value, when=when) # Result of `max_outflows` should bring balance to $0 if # applied as transactions: self.assertAlmostEqual(account.balance_at_time('end'), Money(0), places=4) def test_max_outflows_compound_disc(self, *args, **kwargs): """ Test max_outflows with discrete compounding """ # Test compounding. First: discrete compounding, once at the # halfway point. Add a $100 transaction at when=0.5 just to be # sure. account = self.AccountType(self.owner, *args, balance=100, rate=1, nper=2, **kwargs) account.add_transaction(100, when=0.5) # Three evenly-weighted transactions for some extra complexity: timing = {Decimal(0): 1, Decimal(0.5): 1, Decimal(1): 1} result = account.max_outflows(timing) for when, value in result.items(): account.add_transaction(value, when=when) # Result of `max_outflows` should bring balance to $0 if # applied as transactions: self.assertAlmostEqual(account.balance_at_time('end'), Money(0), places=4) def test_max_outflows_compound_cont(self, *args, **kwargs): """ Test max_outflows with continuous compounding. """ # Now to test continuous compounding. Add a $100 transaction at # when=0.5 just to be sure. account = self.AccountType(self.owner, *args, balance=100, rate=1, nper='C', **kwargs) account.add_transaction(100, when='0.5') # Three evenly-weighted transactions, for extra complexity: timing = {Decimal(0): 1, Decimal(0.5): 1, Decimal(1): 1} result = account.max_outflows(timing) for when, value in result.items(): account.add_transaction(value, when=when) # Result of `max_outflows` should bring balance to $0 if # applied as transactions: self.assertAlmostEqual(account.balance_at_time('end'), Money(0), places=4) def test_max_inflows_pos(self, *args, **kwargs): """ Test max_inflows with positive balance """ # This method should always return Money('Infinity') account = self.AccountType(self.owner, *args, balance=100, **kwargs) result = account.max_inflows(self.timing) for value in result.values(): self.assertEqual(value, Money('Infinity')) def test_max_inflows_neg(self, *args, **kwargs): """ Test max_inflows with negative balance """ # This method should always return Money('Infinity') account = self.AccountType(self.owner, *args, balance=-100, **kwargs) result = account.max_inflows(self.timing) for value in result.values(): self.assertEqual(value, Money('Infinity')) def test_min_outflows_pos(self, *args, **kwargs): """ Test Account.min_outflow with positive balance """ # This method should always return $0 account = self.AccountType(self.owner, *args, balance=100, **kwargs) result = account.min_outflows(self.timing) for value in result.values(): self.assertAlmostEqual(value, Money(0), places=4) def test_min_outflows_neg(self, *args, **kwargs): """ Test min_outflow with negative balance """ account = self.AccountType(self.owner, *args, balance=-100, **kwargs) result = account.min_outflows(self.timing) for value in result.values(): self.assertAlmostEqual(value, Money(0), places=4) def test_min_inflows_pos(self, *args, **kwargs): """ Test min_inflow with positive balance """ # This method should always return $0 account = self.AccountType(self.owner, *args, balance=100, **kwargs) result = account.min_inflows(self.timing) for value in result.values(): self.assertAlmostEqual(value, Money(0), places=4) def test_min_inflows_neg(self, *args, **kwargs): """ Test min_inflows with negative balance """ account = self.AccountType(self.owner, *args, balance=-100, **kwargs) result = account.min_inflows(self.timing) for value in result.values(): self.assertAlmostEqual(value, Money(0), places=4) def test_max_outflows_example(self, *args, **kwargs): """ Test max_outflows with its docstring example. """ account = self.AccountType(self.owner, *args, balance=100, rate=1, nper=1, **kwargs) # This is taken straight from the docstring example: result = account.max_outflows({0: 1, 1: 1}) target = {0: Money(-200) / 3, 1: Money(-200) / 3} self.assertEqual(result.keys(), target.keys()) for timing in result: self.assertAlmostEqual(result[timing], target[timing], places=4) def test_taxable_income_gain(self, *args, **kwargs): """ Test Account.taxable_income with gains in account """ # This method should return the growth in the account, which is # $200 at the start of the period. (The $100 withdrawal at the # end of the period doesn't affect taxable income.) account = self.AccountType(self.owner, *args, balance=100, rate=1.0, **kwargs) account.add_transaction(100, when='start') account.add_transaction(-100, when='end') self.assertEqual(account.taxable_income, Money(200)) def test_taxable_income_loss(self, *args, **kwargs): """ Test Account.taxable_income with losses in account """ # Losses are not taxable: account = self.AccountType(self.owner, *args, balance=-100, rate=1.0, **kwargs) account.add_transaction(100, when='start') account.add_transaction(-100, when='end') self.assertEqual(account.taxable_income, Money(0)) def test_tax_withheld_pos(self, *args, **kwargs): """ Test Account.tax_withheld with positive balance. """ # This method should always return $0 account = self.AccountType(self.owner, *args, balance=100, rate=1.0, **kwargs) account.add_transaction(100, when='start') account.add_transaction(-100, when='end') self.assertEqual(account.tax_withheld, Money(0)) def test_tax_withheld_neg(self, *args, **kwargs): """ Test Account.tax_withheld with negative balance. """ account = self.AccountType(self.owner, *args, balance=-100, rate=1.0, **kwargs) account.add_transaction(100, when='start') account.add_transaction(-100, when='end') self.assertEqual(account.tax_withheld, Money(0)) def test_tax_credit_pos(self, *args, **kwargs): """ Test Account.tax_credit with positive balance """ # This method should always return $0, regardless of balance, # inflows, or outflows account = self.AccountType(self.owner, *args, balance=100, rate=1.0, **kwargs) account.add_transaction(100, when='start') account.add_transaction(-100, when='end') self.assertEqual(account.tax_credit, Money(0)) def test_tax_credit_neg(self, *args, **kwargs): """ Test Account.tax_credit with negative balance """ # Test with negative balance account = self.AccountType(self.owner, *args, balance=-100, rate=1.0, **kwargs) account.add_transaction(100, when='start') account.add_transaction(-100, when='end') self.assertEqual(account.tax_credit, Money(0)) def test_tax_deduction_pos(self, *args, **kwargs): """ Test Account.tax_deduction with positive balance """ # This method should always return $0, regardless of balance, # inflows, or outflows account = self.AccountType(self.owner, *args, balance=100, rate=1.0, **kwargs) account.add_transaction(100, when='start') account.add_transaction(-100, when='end') self.assertEqual(account.tax_deduction, Money(0)) def test_tax_deduction_neg(self, *args, **kwargs): """ Test Account.tax_deduction with negative balance """ # Test with negative balance account = self.AccountType(self.owner, *args, balance=-100, rate=1.0, **kwargs) account.add_transaction(100, when='start') account.add_transaction(-100, when='end') self.assertEqual(account.tax_deduction, Money(0))
def setUp_decimal(self): """ Builds stock variables to test with. """ # 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 self.initial_year = 2000 # Simple tax treatment: 50% tax rate across the board. tax = Tax(tax_brackets={ self.initial_year: {Decimal(0): Decimal(0.5)}}, high_precision=Decimal) # Accounts need an owner: timing = Timing(frequency='BW',high_precision=Decimal) self.person = Person( initial_year=self.initial_year, name="Test", birth_date="1 January 1980", retirement_date="31 December 1999", # last year gross_income=Decimal(5200), tax_treatment=tax, payment_timing=timing, high_precision=Decimal) # We want at least two accounts which are withdrawn from # in different orders depending on the strategy. self.account = Account( owner=self.person, balance=Decimal(60000), # $60,000 <- BIGGER! high_precision=Decimal) self.rrsp = canada.accounts.RRSP( owner=self.person, contribution_room=Decimal(1000), balance=Decimal(6000), # $6,000 high_precision=Decimal) # Assume there are $2000 in inflows and $22,000 in outflows, # for a net need of $20,000: self.available = { Decimal(0.25): Decimal(1000), Decimal(0.5): Decimal(-11000), Decimal(0.75): Decimal(1000), Decimal(1): Decimal(-11000) } # Now we can set up the big-ticket items: self.strategy = TransactionStrategy( strategy=TransactionStrategy.strategy_ordered, weights={"RRSP": Decimal(1), "Account": Decimal(2)}) self.forecast = WithdrawalForecast( initial_year=self.initial_year, people={self.person}, accounts={self.account, self.rrsp}, transaction_strategy=self.strategy, high_precision=Decimal) # Set up another forecast for testing withholding behaviour: self.withholding_account = WithholdingAccount( owner=self.person, balance=Decimal(100000), high_precision=Decimal) self.withholding_strategy = TransactionStrategy( strategy=TransactionStrategy.strategy_ordered, weights={"WithholdingAccount": Decimal(1)}, high_precision=Decimal) self.withholding_forecast = WithdrawalForecast( initial_year=self.initial_year, people={self.person}, accounts={self.withholding_account}, transaction_strategy=self.withholding_strategy, high_precision=Decimal)
class TestSubForecast(unittest.TestCase): """ Tests Subforecast. """ def setUp(self): """ Builds stock variables to test with. """ self.initial_year = 2000 self.subforecast = SubForecast(self.initial_year) self.person = Person(initial_year=self.initial_year, name="Test", birth_date="1 January 1980", retirement_date="31 December 2045") # A basic account with 100% interest and no compounding: self.account1 = Account(owner=self.person, balance=100, rate=1, nper=1) # Another account, same as account1: self.account2 = Account(owner=self.person, balance=100, rate=1, nper=1) # Set up a dict and Account for use as `available`: self.available_dict = defaultdict(lambda: 0) self.available_acct = Account(initial_year=self.initial_year, rate=0) def setUp_decimal(self): """ Builds stock variables to test with. """ # 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 self.initial_year = 2000 self.subforecast = SubForecast(self.initial_year, high_precision=Decimal) self.person = Person(initial_year=self.initial_year, name="Test", birth_date="1 January 1980", retirement_date="31 December 2045", high_precision=Decimal) # A basic account with 100% interest and no compounding: self.account1 = Account(owner=self.person, balance=100, rate=Decimal(1), nper=1, high_precision=Decimal) # Another account, same as account1: self.account2 = Account(owner=self.person, balance=100, rate=Decimal(1), nper=1, high_precision=Decimal) # Set up a dict and Account for use as `available`: self.available_dict = defaultdict(lambda: Decimal(0)) self.available_acct = Account(initial_year=self.initial_year, rate=0, high_precision=Decimal) def test_transaction_basic(self): """ Tests that transactions are saved correctly. """ # Receive cash at start of year: self.available_dict[0] = 100 # Move all $100 to account1 right away: self.subforecast.add_transaction(value=100, timing='start', from_account=self.available_dict, to_account=self.account1) # Transactions should be recorded against both available_dict # and account1: self.assertEqual(self.subforecast.transactions[self.available_dict], {0: -100}) self.assertEqual(self.subforecast.transactions[self.account1], {0: 100}) def test_transaction_delay(self): """ Tests that delayed transactions are saved correctly. """ # Receive cash mid-year: self.available_acct.add_transaction(value=100, when=0.5) # Try to move $100 in cash to account1 at the start of the year # (i.e. before cash is actually on-hand): self.subforecast.add_transaction(value=100, timing='start', from_account=self.available_acct, to_account=self.account2) result = self.subforecast.transactions target = {self.available_acct: {0.5: -100}, self.account2: {0.5: 100}} # Transaction should be delayed until mid-year: self.assertEqual(result, target) def test_transaction_none(self): """ Tests that transactions against None are saved correctly. """ # Move $100 in cash (which comes from the untracked pool None) # to account2 at the start of the year: self.subforecast.add_transaction(value=100, timing='start', from_account=None, to_account=self.account2) # Transactions should be recorded against both: self.assertEqual(self.subforecast.transactions, { None: { 0: -100 }, self.account2: { 0: 100 } }) def test_add_trans_basic(self): """ Moves $100 from available to an account. """ # Receive cash at start of year: self.available_dict[0] = 100 # Move all $100 to account1 right away: self.subforecast.add_transaction(value=100, timing='start', from_account=self.available_dict, to_account=self.account1) # The $100 inflow at the start time should be reduced to $0: self.assertEqual(self.available_dict[0], 0) # A $100 transaction should be added to the account: self.assertEqual(self.account1.transactions[0], 100) def test_add_trans_basic_acct(self): """ Moves $100 from available to an account. """ # Receive cash at start of year: self.available_acct.add_transaction(value=100, when='start') # Move all $100 to account1 right away: self.subforecast.add_transaction(value=100, timing='start', from_account=self.available_acct, to_account=self.account2) # No more money should be available: self.assertEqual(self.available_acct.transactions[0], 0) # A $100 transaction should be added to account2: self.assertEqual(self.account2.transactions[0], 100) def test_add_trans_tnsfr_acct(self): """ Moves $100 from one account to another. """ # Receive cash at start of year: self.account1.transactions[0] = 100 # Move $100 from account1 to account2 right away: self.subforecast.add_transaction(value=100, timing='start', from_account=self.account1, to_account=self.account2) # The $100 inflow at the start time should be reduced to $0: self.assertEqual(self.account1.transactions[0], 0) # A $100 transaction should be added to account2: self.assertEqual(self.account2.transactions[0], 100) def test_add_trans_delay(self): """ Transaction that should be shifted to later time. """ # Try to move $100 in cash to account1 at the start of the year, # when cash isn't actually available until mid-year: self.available_dict[0.5] = 100 self.subforecast.add_transaction(value=100, timing='start', from_account=self.available_dict, to_account=self.account2) # Transaction should be recorded against existing transaction # at timing=0.5, resulting in no net transaction: self.assertEqual(self.available_dict[0.5], 0) # A $100 transaction should be added to account2: self.assertEqual(self.account2.transactions[0.5], 100) def test_add_trans_delay_acct(self): """ Transaction that should be shifted to later time. """ # Receive cash mid-year: self.available_acct.add_transaction(value=100, when=0.5) # Try to move $100 in cash to account1 at the start of the year # (i.e. before cash is actually on-hand): self.subforecast.add_transaction(value=100, timing='start', from_account=self.available_acct, to_account=self.account2) # Transactions should be delayed until mid-year: self.assertEqual(self.available_acct.transactions[0.5], 0) # A $100 transaction should be added to account2: self.assertEqual(self.account2.transactions[0.5], 100) def test_add_trans_small_in(self): """ Multiple small inflows and one large outflow. """ # Receive $100 spread across 2 transactions: self.available_dict[0] = 50 self.available_dict[0.5] = 50 # Move $100 in cash to account2 at mid-year: self.subforecast.add_transaction(value=100, timing=0.5, from_account=self.available_dict, to_account=self.account2) # Transaction should occur on-time: self.assertEqual((self.available_dict[0], self.available_dict[0.5]), (50, -50)) # A $100 transaction should be added to account2: self.assertEqual(self.account2.transactions[0.5], 100) def test_add_trans_future_neg(self): """ Transaction that would cause future negative balance. """ # Want to have $100 available at timing=0.5 and at timing=1, # but with >$100 in-between: self.available_dict[0] = 50 self.available_dict[0.5] = 50 self.available_dict[0.75] = -50 self.available_dict[1] = 50 # Try to move $100 in cash to account2 at mid-year: self.subforecast.add_transaction(value=100, timing=0.5, from_account=self.available_dict, to_account=self.account2) # Transaction should be delayed to year-end: self.assertEqual(self.available_dict[1], -50) # A $100 transaction should be added to account2: self.assertEqual(self.account2.transactions[1], 100) def test_add_trans_acct_growth(self): """ Account growth allows outflow after insufficient inflows. """ # Receive $100 at the start. It will grow to $150 by mid-year: self.account1.transactions[0] = 100 # Move $150 in cash to account2 at mid-year: self.subforecast.add_transaction(value=150, timing=0.5, from_account=self.account1, to_account=self.account2) # Check net transaction flows of available cash: self.assertEqual( (self.account1.transactions[0], self.account1.transactions[0.5]), (100, -150)) # A $150 transaction should be added to account2: self.assertEqual(self.account2.transactions[0.5], 150) def test_add_trans_acct_growth_btwn(self): """ Account growth allows outflow between inflows. """ # Receive $100 at the start. It will grow to $150 by mid-year: self.account1.transactions[0] = 100 # Add another inflow at the end. This shouldn't change anything: self.account1.transactions[1] = 100 # Move $150 in cash to account2 at mid-year: self.subforecast.add_transaction(value=150, timing=0.5, from_account=self.account1, to_account=self.account2) # Check net transaction flows of available cash: self.assertEqual( (self.account1.transactions[0], self.account1.transactions[0.5]), (100, -150)) # A $150 transaction should be added to account2: self.assertEqual(self.account2.transactions[0.5], 150) def test_add_trans_shortfall(self): """ Transaction that must cause a negative balance. """ # Want to withdraw $100 when this amount will not be available # at any point in time: self.available_dict[0] = 50 self.available_dict[1] = 49 # Try to move $100 in cash to account2 at mid-year: self.subforecast.add_transaction(value=100, timing=0.5, from_account=self.available_dict, to_account=self.account2) # Transaction should occur immediately: self.assertEqual(self.available_dict[0.5], -100) # A $100 transaction should be added to account2: self.assertEqual(self.account2.transactions[0.5], 100) def test_add_trans_shortfall_acct(self): """ Transaction that must cause a negative balance. """ # Want to withdraw $100 when this amount will not be available # at any point in time: self.available_acct.transactions[0] = 50 self.available_acct.transactions[1] = 49 # Try to move $100 in cash to account2 at mid-year: self.subforecast.add_transaction(value=100, timing=0.5, from_account=self.available_acct, to_account=self.account2) # Transaction should occur immediately: self.assertEqual(self.available_acct.transactions[0.5], -100) # A $100 transaction should be added to account2: self.assertEqual(self.account2.transactions[0.5], 100) def test_add_trans_strict(self): """ Add transaction with strict timing. """ # Move $100 in cash to account1 at the start of the year. # Cash isn't actually available until mid-year, but use strict # timing to force it through: self.available_dict[0.5] = 100 self.subforecast.add_transaction(value=100, timing='start', from_account=self.available_dict, to_account=self.account2, strict_timing=True) # Transaction should done at the time requested: self.assertEqual(self.available_dict[0], -100) # A $100 transaction should be added to account2: self.assertEqual(self.account2.transactions[0], 100) def test_add_trans_timing_basic(self): """ Add a number of transactions based on a Timing object. """ # Add $50 at when=0.5 and $50 at when=1 timing = Timing(when=1, frequency=2) self.subforecast.add_transaction(value=100, timing=timing, to_account=self.available_dict) # Confirm monies were added at the times noted above: self.assertEqual(self.available_dict, {0.5: 50, 1: 50}) def test_decimal(self): """ Tests Subforecast with Decimal inputs. """ # Convert values to Decimal: self.setUp_decimal() # This test is based on `test_transaction_basic` # Receive cash at start of year: self.available_dict[0] = Decimal(100) # Move all $100 to account1 right away: self.subforecast.add_transaction(value=Decimal(100), timing='start', from_account=self.available_dict, to_account=self.account1) # Transactions should be recorded against both available_dict # and account1: self.assertEqual(self.subforecast.transactions[self.available_dict], {Decimal(0): Decimal(-100)}) self.assertEqual(self.subforecast.transactions[self.account1], {Decimal(0): Decimal(100)})