Source code for dcf.cashflows.cashflow

# -*- coding: utf-8 -*-

# dcf
# ---
# A Python library for generating discounted cashflows.
#
# Author:   sonntagsgesicht, based on a fork of Deutsche Postbank [pbrisk]
# Version:  0.7, copyright Tuesday, 31 May 2022
# Website:  https://github.com/sonntagsgesicht/dcf
# License:  Apache License 2.0 (see LICENSE file)


from collections import OrderedDict
from inspect import signature
from warnings import warn

from ..plans import DEFAULT_AMOUNT

from .payoffs import FixedCashFlowPayOff, RateCashFlowPayOff


[docs]class CashFlowList(object): _cashflow_details = 'cashflow', 'pay date' @property def table(self): """ cashflow details as list of tuples """ # print(tabulate(cf.table, headers='firstrow')) # for pretty print header, table = list(), list() for d in self.domain: payoff = self._flows.get(d, 0.) if hasattr(payoff, 'details'): fwd = getattr(self, 'forward_curve', None) details = payoff.details(fwd) details['pay date'] = d else: details = {'cashflow': float(payoff), 'pay date': d} for k in self.__class__._cashflow_details: if k in details and k not in header: header.append(k) table.append(tuple(details.get(h, '') for h in header)) return [tuple(header)] + table @property def domain(self): """ payment date list """ return self._domain @property def origin(self): """ cashflow list start date """ if self._origin is None and self._domain: return self._domain[0] return self._origin @property def kwargs(self): """returns constructor arguments as ordered dictionary (under construction) """ warn('%s().kwargs is under construction' % self.__class__.__name__) kw = OrderedDict() for name in signature(self.__class__).parameters: attr = None if name == 'amount_list': attr = tuple(self._flows[d] for d in self.domain) if name == 'payment_date_list': attr = self.domain attr = getattr(self, '_' + name, attr) if isinstance(attr, (list, tuple)): attr = tuple(getattr(a, 'kwargs', a) for a in attr) attr = tuple(getattr(a, '__name__', a) for a in attr) attr = getattr(attr, 'kwargs', attr) attr = getattr(attr, '__name__', attr) if attr is not None: kw[name] = attr return kw
[docs] def payoff(self, date): """dictionary of payoffs with pay_date keys""" if isinstance(date, (tuple, list)): return tuple(self.payoff(i) for i in date) return self._flows.get(date, None)
def __init__(self, payment_date_list=(), amount_list=(), origin=None): """ basic cashflow list object :param domain: list of cashflow dates :param data: list of cashflow amounts :param origin: origin of object, i.e. start date of the cashflow list as a product Basicly |CashFlowList()| works like a read-only dictionary with payment dates as keys. And the |CashFlowList().domain| property holds the payment date list. >>> from dcf import CashFlowList >>> cf_list = CashFlowList([0, 1], [-100., 100.]) >>> cf_list.domain (0, 1) In order to get cashflows >>> cf_list[0] -100.0 >>> cf_list[cf_list.domain] (-100.0, 100.0) This works even for dates without cashflow >>> cf_list[-1, 0 , 1, 2] (0.0, -100.0, 100.0, 0.0) """ if isinstance(amount_list, (int, float)): amount_list = [amount_list] * len(payment_date_list) if not len(amount_list) == len(payment_date_list): msg = f"{self.__class__.__name__} arguments " \ f"`payment_date_list` and `amount_list` " \ f"must have same length." raise ValueError(msg) self._origin = origin self._domain = tuple(payment_date_list) self._flows = dict(zip(payment_date_list, amount_list)) def __getitem__(self, item): if isinstance(item, (tuple, list)): return tuple(self[i] for i in item) else: payoff = self._flows.get(item, 0.) if not isinstance(payoff, (int, float)): _ = None if hasattr(self, 'payoff_model'): _ = self.payoff_model elif hasattr(self, 'forward_curve'): _ = self.forward_curve payoff = payoff(_) return payoff def __call__(self, _=None): flows = list() for item in self.domain: payoff = self._flows.get(item, 0.) if not isinstance(payoff, (int, float)): if _ is None: if hasattr(self, 'payoff_model'): _ = self.payoff_model elif hasattr(self, 'forward_curve'): _ = self.forward_curve payoff = payoff(_) flows.append(payoff) return CashFlowList(self.domain, flows, self._origin) def __add__(self, other): for k in self._flows: self._flows[k].__add__(other) def __sub__(self, other): for k in self._flows: self._flows[k].__sub__(other) def __mul__(self, other): for k in self._flows: self._flows[k].__mul__(other) def __truediv__(self, other): for k in self._flows: self._flows[k].__truediv__(other) def __str__(self): inner = tuple() if self.domain: s, e = self.domain[0], self.domain[-1] inner = f'[{s!r} ... {e!r}]', \ f'[{self._flows[s]!r} ... {self._flows[e]!r}]' kw = self.kwargs kw.pop('amount_list', ()) kw.pop('payment_date_list', ()) inner += tuple(f"{k!s}={v!r}" for k, v in kw.items()) s = self.__class__.__name__ + '(' + ', '.join(inner) + ')' return s def __repr__(self): s = self.__class__.__name__ + '()' if self.domain: fill = ',\n' + ' ' * (len(s) - 1) kw = self.kwargs inner = \ str(kw.pop('payment_date_list', ())), \ str(kw.pop('amount_list', ())) inner += tuple(f"{k!s}={v!r}" for k, v in kw.items()) s = self.__class__.__name__ + '(' + fill.join(inner) + ')' return s
[docs]class CashFlowLegList(CashFlowList): """ MultiCashFlowList """ @property def legs(self): """ list of |CashFlowList| """ return list(self._legs) def __init__(self, legs): """ container class for CashFlowList :param legs: list of |CashFlowList| """ for leg in legs: if not isinstance(leg, (CashFlowList, RateCashFlowList)): cls = self.__class__.__name__, leg.__class__.__name__ raise ValueError("Legs %s of can be either `CashFlowList` " "or `RateCashFlowList` but not %s." % cls) self._legs = legs domains = tuple(tuple(leg.domain) for leg in self._legs) domain = list(sorted(set().union(*domains))) origin = min(leg.origin for leg in self._legs) super().__init__(domain, [0] * len(domain), origin=origin) def __getitem__(self, item): """ getitem does re-calc float cash flows and does not use store notional values """ if isinstance(item, (tuple, list)): return tuple(self[i] for i in item) else: return sum( float(leg[item]) for leg in self._legs if item in leg.domain) def __add__(self, other): for leg in self._legs: leg.__add__(other) def __sub__(self, other): for leg in self._legs: leg.__sub__(other) def __mul__(self, other): for leg in self._legs: leg.__mul__(other) def __truediv__(self, other): for leg in self._legs: leg.__truediv__(other)
[docs]class FixedCashFlowList(CashFlowList): _header_keys = 'cashflow', 'pay date' def __init__(self, payment_date_list, amount_list=DEFAULT_AMOUNT, origin=None): """ basic cashflow list object :param payment_date_list: list of cashflow payment dates :param amount_list: list of cashflow amounts :param origin: origin of object, i.e. start date of the cashflow list as a product """ if isinstance(payment_date_list, CashFlowList): amount_list = payment_date_list[payment_date_list.domain] origin = origin or getattr(payment_date_list, '_origin', None) payment_date_list = payment_date_list.domain if isinstance(amount_list, (int, float)): amount_list = [amount_list] * len(payment_date_list) payoff_list = tuple(FixedCashFlowPayOff(amount=a) for a in amount_list) super().__init__(payment_date_list, payoff_list, origin=origin)
[docs]class RateCashFlowList(CashFlowList): """ list of cashflows by interest rate payments """ _cashflow_details = 'cashflow', 'pay date', 'notional', \ 'start date', 'end date', 'year fraction', \ 'fixed rate', 'forward rate', 'fixing date', 'tenor' def __init__(self, payment_date_list, amount_list=DEFAULT_AMOUNT, origin=None, day_count=None, fixing_offset=None, pay_offset=None, fixed_rate=0., forward_curve=None): r""" list of interest rate cashflows :param payment_date_list: pay dates, assuming that pay dates agree with end dates of interest accrued period :param amount_list: notional amounts :param origin: start date of first interest accrued period :param day_count: day count convention :param fixing_offset: time difference between interest rate fixing date and interest period payment date :param pay_offset: time difference between interest period end date and interest payment date :param fixed_rate: agreed fixed rate :param forward_curve: interest rate curve for forward estimation Let $t_0$ be the list **origin** and $t_i$ $i=1, \dots n$ the **payment_date_list** with $N_i$ $i=1, \dots n$ the notional **amount_list**. Moreover, let $\tau$ be the **day_count** function, $c$ the **fixed_rate** and $f$ the **forward_curve**. Then, the rate cashflow $cf_i$ payed at time $t_i$ will be with $s_i = t_{i-1} - \delta$, $e_i = t_i -\delta$ as well as $d_i = s_i - \epsilon$ for **pay_offset** $\delta$ and **fixing_offset** $\epsilon$, $$cf_i = N_i \cdot \tau(s_i,e_i) \cdot (c + f(d_i)).$$ Note, the **pay_offset** $\delta$ is not applied in case of the first cashflow, then $s_1=t_0$. """ if isinstance(amount_list, (int, float)): amount_list = [amount_list] * len(payment_date_list) if origin is not None: start_dates = [origin] start_dates.extend(payment_date_list[:-1]) elif origin is None and len(payment_date_list) > 1: step = payment_date_list[1] - payment_date_list[0] start_dates = [payment_date_list[0] - step] start_dates.extend(payment_date_list[:-1]) elif payment_date_list: start_dates = payment_date_list payoff_list = list() for s, e, a in zip(start_dates, payment_date_list, amount_list): if pay_offset: e -= pay_offset s -= pay_offset payoff = RateCashFlowPayOff( start=s, end=e, day_count=day_count, fixing_offset=fixing_offset, amount=a, fixed_rate=fixed_rate ) payoff_list.append(payoff) super().__init__(payment_date_list, payoff_list, origin=origin) self.forward_curve = forward_curve r""" cashflow forward curve to derive float rates $f$ """ @property def fixed_rate(self): fixed_rates = tuple(cf.fixed_rate for cf in self._flows.values()) if len(set(fixed_rates)) == 1: return fixed_rates[0] @fixed_rate.setter def fixed_rate(self, value): for cf in self._flows.values(): cf.fixed_rate = value