def test_len(self): """ Tests `Scenario.__len__`. """ # Some variables that we'll use multiple times: num_years = 100 initial_year = 2000 # Test a constant scenario scenario = Scenario(inflation=0, stock_return=0, bond_return=0, other_return=0, management_fees=0, initial_year=initial_year, num_years=num_years) self.assertEqual(len(scenario), num_years) # Test a scenario built from lists/constants scenario = Scenario(initial_year=initial_year, num_years=num_years, inflation=[0 for _ in range(0, num_years)], stock_return=[0 for _ in range(0, num_years)], bond_return=[0 for _ in range(0, num_years)]) self.assertEqual(len(scenario), num_years) # Test a scenario built from dicts: num_years = 100 scenario = Scenario( initial_year=initial_year, num_years=num_years, inflation={ year: 0 for year in range(initial_year, initial_year + num_years) }, stock_return={ year: 0 for year in range(initial_year, initial_year + num_years - 1) }, bond_return=0, other_return=0, management_fees=0) self.assertEqual(len(scenario), num_years) # Test a scenario built from dicts and (longer) lists: num_years = 100 scenario = Scenario( initial_year=initial_year, num_years=num_years, inflation={ year: 0 for year in range(initial_year, initial_year + num_years) }, stock_return={ year: 0 for year in range(initial_year, initial_year + num_years - 1) }, bond_return=[0 for _ in range(0, num_years)], other_return=0, management_fees=0) self.assertEqual(len(scenario), num_years)
def setUpClass(cls): """ Set up default values for tests """ cls.initial_year = 2000 # Set up a simple `Scenario` with constant values cls.constant_initial_year = cls.initial_year cls.constant_inflation = 0.02 cls.constant_stock_return = 0.07 cls.constant_bond_return = 0.04 cls.constant_other_return = 0.03 cls.constant_management_fees = 0.01 cls.constant_num_years = 100 cls.constant_scenario = Scenario( inflation=cls.constant_inflation, stock_return=cls.constant_stock_return, bond_return=cls.constant_bond_return, other_return=cls.constant_other_return, management_fees=cls.constant_management_fees, initial_year=cls.constant_initial_year, num_years=100) # Set up a `Scenario` with varying elements, # including at least one 0 in each list cls.varying_initial_year = cls.initial_year cls.varying_num_years = 100 cls.varying_inflation = [0] cls.varying_stock_return = [0] cls.varying_bond_return = [0] cls.varying_other_return = [0] cls.varying_management_fees = [0] # Values will jump around, but will generally increase in magnitude for i in range(cls.varying_num_years - 1): cls.varying_inflation.append(0.0025 * i) cls.varying_stock_return.append(pow(-1, i) * 0.01 * i) cls.varying_bond_return.append(pow(-1, i + 1) * 0.005 * i) cls.varying_other_return.append(pow(-1, i) * 0.001 * i) cls.varying_management_fees.append(0.0002 * i) cls.varying_scenario = Scenario( inflation=cls.varying_inflation, stock_return=cls.varying_stock_return, bond_return=cls.varying_bond_return, other_return=cls.varying_other_return, management_fees=cls.varying_management_fees, initial_year=cls.varying_initial_year, num_years=cls.varying_num_years) cls.scenarios = [cls.constant_scenario, cls.varying_scenario] # Add a scenario of all 0 values cls.scenarios.append( Scenario(inflation=0, stock_return=0, bond_return=0, other_return=0, management_fees=0, initial_year=cls.constant_initial_year, num_years=10)) for _ in range(10): # add some random scenarios to the test set cls.scenarios.append(cls.get_random_scenario())
def setUp(self): """ Builds a default Person for testing of complex objects. """ # Build inputs to Person: self.settings = Settings() # default settings object self.initial_year = self.settings.initial_year # convenience 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.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) # Build a Person object to test against later: 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)
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)
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 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 get_random_scenario(initial_year=None, length=None): """ Returns a random `Scenario`. The `Scenario` has length `length` and starts in `initial_year`. Each parameter is randomized if not provided. """ rand = Random() # If inputs aren't given, randomly choose reasonable values. if initial_year is None: initial_year = rand.randint(2000, 2100) if length is None: length = rand.randint(2, 200) # Initialize empty lists inflation = [] stock_return = [] bond_return = [] other_return = [] management_fees = [] # Build lists for each variable using a reasonable range. # inflation: [0, 30%] # stock_return: [-50%, 50%] # bond_return: [-25%, 25%] # other_return: [-10%, 10%] # management_fees: [0.15%, 2%] for _ in range(length): inflation.append(rand.random() * 0.3) stock_return.append(rand.random() - 0.5) bond_return.append(rand.random() * 0.5 - 0.5) other_return.append(rand.random() * 0.2 - 0.1) management_fees.append(rand.random() * 0.0185 + 0.0015) return Scenario(initial_year=initial_year, num_years=length, inflation=inflation, stock_return=stock_return, bond_return=bond_return, other_return=other_return, management_fees=management_fees)
def test_init(self): """ Tests `Scenario.__init__()` and basic properties. Also tests `inflation()`, `stock_return()`, and `bond_return()` """ # Test initialization with scalar values. scenario = Scenario(inflation=0, stock_return=0, bond_return=0, other_return=0, management_fees=0, initial_year=2000, num_years=100) # Confirm we can pull 100 (identical) years from the scenario for i in range(100): year = i + 2000 self.assertEqual(scenario.inflation[year], 0) self.assertEqual(scenario.stock_return[year], 0) self.assertEqual(scenario.bond_return[year], 0) self.assertEqual(scenario.other_return[year], 0) self.assertEqual(scenario.management_fees[year], 0) self.assertEqual(scenario.initial_year, 2000) # Test the varying scenario next, using list inputs scenario = Scenario(self.varying_initial_year, self.varying_num_years, inflation=self.varying_inflation, stock_return=self.varying_stock_return, bond_return=self.varying_bond_return, other_return=self.varying_other_return, management_fees=self.varying_management_fees) # Do an elementwise comparison to confirm initialization worked for i in range(self.varying_num_years): year = i + self.varying_initial_year self.assertEqual(scenario.inflation[year], self.varying_inflation[i]) self.assertEqual(scenario.stock_return[year], self.varying_stock_return[i]) self.assertEqual(scenario.bond_return[year], self.varying_bond_return[i]) self.assertEqual(scenario.other_return[year], self.varying_other_return[i]) self.assertEqual(scenario.management_fees[year], self.varying_management_fees[i]) self.assertEqual(scenario.initial_year, self.varying_initial_year) # Construct a varying scenario from dicts instead of lists years = list( range(self.varying_initial_year, self.varying_initial_year + len(self.varying_inflation))) inflation = dict(zip(years, self.varying_inflation)) stock_return = dict(zip(years, self.varying_stock_return)) bond_return = dict(zip(years, self.varying_bond_return)) other_return = dict(zip(years, self.varying_other_return)) management_fees = dict(zip(years, self.varying_management_fees)) # Do an elementwise comparison to confirm initialization worked for year in years: self.assertEqual(scenario.inflation[year], inflation[year]) self.assertEqual(scenario.stock_return[year], stock_return[year]) self.assertEqual(scenario.bond_return[year], bond_return[year]) self.assertEqual(scenario.other_return[year], other_return[year]) self.assertEqual(scenario.management_fees[year], management_fees[year]) self.assertEqual(scenario.initial_year, self.varying_initial_year) # Mix constant, list, and dict inputs scenario = Scenario(initial_year=self.initial_year, num_years=len(self.varying_stock_return), inflation=0.02, stock_return=self.varying_stock_return, bond_return=bond_return, other_return=Decimal(0.03), management_fees=self.varying_management_fees) for i in range(scenario.num_years): year = i + scenario.initial_year self.assertEqual(scenario.inflation[year], 0.02) self.assertEqual(scenario.stock_return[year], self.varying_stock_return[i]) self.assertEqual(scenario.bond_return[year], bond_return[year]) self.assertEqual(scenario.other_return[year], Decimal(0.03)) self.assertEqual(scenario.management_fees[year], self.varying_management_fees[i]) self.assertEqual(scenario.initial_year, self.initial_year)
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)