Source code for dcf.curves.creditcurve

# -*- 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 Sunday, 22 May 2022
# Website:  https://github.com/sonntagsgesicht/dcf
# License:  Apache License 2.0 (see LICENSE file)


from sys import float_info

from .curve import RateCurve
from dcf.compounding import continuous_compounding, continuous_rate
from dcf.interpolation import constant, linear_scheme, log_linear_scheme, \
    log_linear_rate_scheme


[docs]class CreditCurve(RateCurve): r"""Base class of credit curve classes All credit curves share the same three fundamental methodological methods * |CreditCurve().get_survival_prob()| * |CreditCurve().get_flat_intensity()| * |CreditCurve().get_hazard_rate()| All subclasses differ only in data types for storage and interpolation. """ _FORWARD_TENOR = '1Y' @staticmethod def _get_storage_value(curve, x): raise NotImplementedError() def __init__(self, domain=(), data=(), interpolation=None, origin=None, day_count=None, forward_tenor=None): if isinstance(domain, RateCurve): # if argument is a curve add extra curve points to domain # for better approximation if data: raise TypeError("If first argument is %s, " "data argument must not be given." % domain.__class__.__name__) data = domain domain = sorted(set(list(data.domain) + [max(data.domain) + '1y'])) super(CreditCurve, self).__init__(domain, data, interpolation, origin, day_count, forward_tenor)
[docs] def get_survival_prob(self, start, stop=None): r"""survival probability of credit curve :param start: start point in time $t_0$ of period :param stop: end point $t_1$ of period (optional, if not given $t_0$ will be **origin** and $t_1$ taken from **start**) :return: survival probability $sv(t_0, t_1)$ for period $t_0$ to $t_1$ Assume an uncertain event $\chi$, e.g. occurrence of a credit default event such as a loan borrower failing to fulfill the obligation to pay back interest or redemption. Let $\iota_\chi$ be the point in time when the event $\chi$ happens. Then the survival probability $sv(t_0, t_1)$ is the probability of not occurring $\chi$ until $t_1$ if $\chi$ didn't happen until $t_0$, i.e. $$sv(t_0, t_1) = 1 - P(t_0 < \iota_\chi \leq t_1)$$ * similar to |InterestRateCurve().get_discount_factor()| """ if stop is None: return self.get_survival_prob(self.origin, start) return self._get_compounding_factor(start, stop)
[docs] def get_flat_intensity(self, start, stop=None): r"""intensity value of credit curve :param start: start point in time $t_0$ of intensity :param stop: end point $t_1$ of intensity (optional, if not given $t_0$ will be **origin** and $t_1$ taken from **start**) :return: intensity $\lambda(t_0, t_1)$ The intensity $\lambda(t_0, t_1)$ relates to survival probabilities by $$sv(t_0, t_1) = exp(-\lambda(t_0, t_1) \cdot \tau(t_0, t_1)).$$ * similar to |InterestRateCurve().get_zero_rate()| """ if stop is None: return self.get_flat_intensity(self.origin, start) return self._get_compounding_rate(start, stop)
[docs] def get_hazard_rate(self, start): r"""hazard rate of credit curve :param start: point in time $t$ of hazard rate :return: hazard rate $hz(t)$ The hazard rate $hz(t)$ relates to intensities by $$\lambda(t_0, t_1) = \int_{t_0}^{t_1} hz(t)\ dt.$$ * similar to |InterestRateCurve().get_short_rate()| """ return self._get_hazard_rate(start)
def _get_hazard_rate(self, start): # aka get_short_rate if start < min(self.domain): return self.get_hazard_rate(min(self.domain)) if max(self.domain) <= start: return self.get_hazard_rate( max(self.domain) - self._TIME_SHIFT) previous = max(d for d in self.domain if d <= start) follow = min(d for d in self.domain if start < d) if not previous <= start <= follow: raise AssertionError() if not previous < follow: raise AssertionError(list(map(str, (previous, start, follow)))) return self.get_flat_intensity(previous, follow)
[docs]class ProbabilityCurve(CreditCurve): r"""base class of probability based credit curve classes""" def __init__(self, domain=(), data=(), interpolation=None, origin=None, day_count=None, forward_tenor=None): # validate probabilities if not isinstance(data, RateCurve): data = [max(float_info.min, min(d, 1. - float_info.min)) for d in data] if not all(data): raise ValueError('Found non positive survival probabilities.') # if argument is a curve add extra curve points to domain # for better approximation if isinstance(domain, RateCurve): if data: raise TypeError("If first argument is %s, " "data argument must not be given." % domain.__class__.__name__) data = domain origin = data.origin if origin is None else origin domain = sorted(set(list(data.domain) + [origin + '1d', max(data.domain) + '1y'])) super(ProbabilityCurve, self).__init__(domain, data, interpolation, origin, day_count, forward_tenor)
[docs]class SurvivalProbabilityCurve(ProbabilityCurve): r"""Interest rate curve storing and interpolating data as discount factor $$sv(0, t)=y_t$$ """ _INTERPOLATION = log_linear_rate_scheme @staticmethod def _get_storage_value(curve, x): return curve.get_survival_prob(curve.origin, x) def _get_compounding_factor(self, start, stop): if start is self.origin: return self(stop) if start == stop: return 1. if 2 * float_info.min <= self(start) else 0. return self(stop) / self(start) def _get_compounding_rate(self, start, stop): if start == stop == self.origin: # intensity proxi at origin stop = min(d for d in self.domain if self.origin < d) # todo: # calc left extrapolation (for linear zero rate interpolation) return super(SurvivalProbabilityCurve, self)._get_compounding_rate( start, stop)
[docs]class DefaultProbabilityCurve(SurvivalProbabilityCurve): r"""Credit curve storing and interpolating data as default probability $$pd(0, t)=1-sv(0, t)=y_t$$ """ _INTERPOLATION = log_linear_rate_scheme @staticmethod def _get_storage_value(curve, x): return curve.get_survival_prob(curve.origin, x) def __init__(self, domain=(), data=(), interpolation=None, origin=None, day_count=None, forward_tenor=None): if not isinstance(data, RateCurve): data = [1. - d for d in data] super(DefaultProbabilityCurve, self).__init__(domain, data, interpolation, origin, day_count, forward_tenor)
[docs]class FlatIntensityCurve(CreditCurve): r"""Credit curve storing and interpolating data as intensities $$\lambda(t)=y_t$$ """ _INTERPOLATION = linear_scheme @staticmethod def _get_storage_value(curve, x): return curve.get_flat_intensity(curve.origin, x) def _get_compounding_rate(self, start, stop): if start == stop == self.origin: return self(self.origin) if start is self.origin: return self(stop) if start == stop: return self._get_compounding_rate( start, start + self.__class__._time_shift) s = self(start) * self.day_count(self.origin, start) e = self(stop) * self.day_count(self.origin, stop) t = self.day_count(start, stop) return (e - s) / t
[docs]class HazardRateCurve(CreditCurve): r"""Credit curve storing and interpolating data as hazard rate $$hz(t)=y_t$$ """ _INTERPOLATION = constant @staticmethod def _get_storage_value(curve, x): return curve.get_hazard_rate(x) def _get_compounding_rate(self, start, stop): if start == stop: return self(start) current = start rate = 0.0 step = self._TIME_SHIFT while current + step < stop: rate += self(current) * self.day_count(current, current + step) current += step rate += self(current) * self.day_count(current, stop) return rate / self.day_count(start, stop) def _get_hazard_rate(self, start): # aka get_short_rate return self(start)
[docs]class MarginalSurvivalProbabilityCurve(ProbabilityCurve): r"""Credit curve storing and interpolating data as intensities $$sv(t, t+\tau^*)=y_t$$ """ _INTERPOLATION = log_linear_scheme @staticmethod def _get_storage_value(curve, x): return curve.get_survival_prob(x, x + curve.forward_tenor) def _get_compounding_factor(self, start, stop): if start == stop: return 1. if 2 * float_info.min <= self(start) else 0. current = start df = 1.0 step = self.forward_tenor while current + step < stop: df *= self(current) if 2 * float_info.min <= self(current) else 0. current += step if 2 * float_info.min <= self(current): r = continuous_rate(self(current), self.day_count(current, current + step)) df *= continuous_compounding(r, self.day_count(current, stop)) else: df *= 0. return df def _get_hazard_rate(self, start): # aka get_short_rate if start < min(self.domain): return self.get_hazard_rate(min(self.domain)) if max(self.domain) <= start: return self.get_flat_intensity( max(self.domain), max(self.domain) + self._TIME_SHIFT) previous = max(d for d in self.domain if d <= start) follow = min(d for d in self.domain if start < d) if not previous < follow: raise AssertionError(list(map(str, (previous, start, follow)))) if not previous <= start <= follow: raise AssertionError(list(map(str, (previous, start, follow)))) return self.get_flat_intensity(previous, follow)
[docs]class MarginalDefaultProbabilityCurve(MarginalSurvivalProbabilityCurve): r"""Credit curve storing and interpolating data as marginal default probability $$pd(t, t+\tau^*)=1-sv(t, t+\tau^*)=y_t$$ """ _INTERPOLATION = log_linear_scheme @staticmethod def _get_storage_value(curve, x): return curve.get_survival_prob(x, x + curve.forward_tenor) def __init__(self, domain=(), data=(), interpolation=None, origin=None, day_count=None, forward_tenor=None): if not isinstance(data, RateCurve): data = [1. - d for d in data] super(MarginalDefaultProbabilityCurve, self).__init__( domain, data, interpolation, origin, day_count, forward_tenor)