コード例 #1
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)
コード例 #2
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}})
        # 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)
コード例 #3
0
    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)
コード例 #4
0
ファイル: test_person.py プロジェクト: ChrisCScott/forecaster
 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())
コード例 #5
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)}})
        # 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)
コード例 #6
0
 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)
コード例 #7
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)
コード例 #8
0
ファイル: test_base.py プロジェクト: ChrisCScott/forecaster
    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)
コード例 #9
0
ファイル: test_link.py プロジェクト: dxcv/forecaster
    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))
コード例 #10
0
    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)
コード例 #11
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 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)
コード例 #12
0
    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)
コード例 #13
0
    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)
コード例 #14
0
    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)
コード例 #15
0
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)
コード例 #16
0
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))
コード例 #17
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)
コード例 #18
0
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)})