def value_at_time(value, rate, now='start', time='end', nper=1, *, high_precision=None): """ Returns the present (or future) value. Args: value (Money): The (nominal) value to be converted. rate (Decimal): The rate of growth (e.g. inflation) now (Decimal): The time associated with the nominal value, expressed using `when_conv` syntax. time (Decimal): The time to which the nominal value is to be converted, expressed using `when_conv` syntax. nper (Decimal): The number of compounding periods for growth. Returns: A Money object representing the present value (if now > time) or the future value (if now < time) of `value`. """ return value * accumulation_function( when_conv(time, high_precision=high_precision) - when_conv(now, high_precision=high_precision), rate, nper, high_precision=high_precision)
def time_to_balance(self, value, when=Decimal(0)): """ Returns the time required to grow to a given balance. If `when` is provided, this method returns the earliest time at or after `when` when the balance has reached `value`. This method is transaction-aware; a given balance may be reached more than once if there are inflows/outflows. Args: value (Money): The balance to grow to. when (Decimal): Only balances reached on or after `when` are considered. Optional. """ # Convert `when` to avoid type errors. when = when_conv(when) # We'll base all calculations at `when`, including the value # of `balance`. Do this even for `when=0`, since there may # be a transaction at the start of the year that isn't # reflected by `balance` but is incorporated in # `balance_at_time`. balance = self.balance_at_time(when) # Determine when we'll reach the desired amount, assuming # no further transactions: time = when + time_to_value(self.rate, balance, value, nper=self.nper) # Now look ahead to the next transaction and, if it happens # before `time`, recurse onto that transaction's timing: next_transaction = min( (key for key in self.transactions if key > when), default=time) if next_transaction < time: time = self.time_to_balance(value, next_transaction) return time
def balance_at_time(self, time, transactions=None): """ Returns the balance at a point in time. Args: when (Decimal, str): The time at which the account's balance is to be determined. transactions (dict[Decimal, float]): If provided, the result of this method will be determined as if the account also had these transactions recorded against it. """ # We need to convert `time` to enable the comparison in the dict # comprehension in the for loop below. time = when_conv(time, high_precision=self.high_precision) # Find the future value (at t=time) of the initial balance. # This doesn't include any transactions of their growth. balance = value_at_time(self.balance, self.rate, 'start', time, nper=self.nper, high_precision=self.high_precision) # Combine the recorded and input transactions, if provided: if transactions is not None: transactions = copy(transactions) add_transactions(transactions, self.transactions) # Otherwise simply use the account's recorded transactions: else: transactions = self.transactions # Add in the future value of each transaction (except that that # happen after `time`). for when in [w for w in transactions if w <= time]: balance += value_at_time(transactions[when], self.rate, when, time, nper=self.nper, high_precision=self.high_precision) return balance
def add_transaction(self, value, when='end'): """ Adds a transaction to the account. Args: value (float): The value of the transaction. Positive values are inflows and negative values are outflows. when (float, Decimal, str): The timing of the transaction. Must be in the range [0,1] or be a suitable str input, as described in the documentation for `when_conv`. Raises: decimal.InvalidOperation: Transactions must be convertible to type Money and `when` must be convertible to type Decimal. ValueError: `when` must be in [0,1] """ when = when_conv(when, high_precision=self.high_precision) # NOTE: If `value` is intended to be a special Money type # (like PyMoney), attempt conversion here # Simultaneous transactions are modelled as one sum, self.transactions[when] += value
def add_transaction(self, value, when='end'): """ Adds a transaction to the account. Args: value (Money): The value of the transaction. Positive values are inflows and negative values are outflows. when (float, Decimal, str): The timing of the transaction. Must be in the range [0,1] or be a suitable str input, as described in the documentation for `when_conv`. Raises: decimal.InvalidOperation: Transactions must be convertible to type Money and `when` must be convertible to type Decimal. ValueError: `when` must be in [0,1] """ when = when_conv(when) # Try to cast non-Money objects to type Money if not isinstance(value, Money): value = Money(value) # Simultaneous transactions are modelled as one sum, self.transactions[when] += value
def __contains__(self, key): when = when_conv(key) return when in self._transactions
def __contains__(self, key): when = when_conv(key, high_precision=self.high_precision) return when in self._transactions
def test_when_conv_invalid(self): """ Tests `when_conv` on an invalid input. """ with self.assertRaises(decimal.InvalidOperation): _ = when_conv('invalid input')
def test_when_conv_str(self): """ Tests `when_conv` on a non-magic str input. """ when = when_conv('1') self.assertEqual(when, Decimal(1))
def test_when_conv_end(self): """ Tests `when_conv` on 'end'. """ when = when_conv('end') self.assertEqual(when, Decimal(1))
def test_when_conv_start(self): """ Tests `when_conv` on 'start'. """ when = when_conv('start') self.assertEqual(when, Decimal(0))
def test_when_conv_simple(self): """ Tests `when_conv` on a simple, single-valued input. """ when = when_conv(1) self.assertEqual(when, Decimal(1))