Skip to content

Commit efd019e

Browse files
committed
Added some swap classes
1 parent fa66d3a commit efd019e

File tree

8 files changed

+1344
-54
lines changed

8 files changed

+1344
-54
lines changed

financepy/products/rates/ibor_dual_curve.py

Lines changed: 580 additions & 0 deletions
Large diffs are not rendered by default.
Lines changed: 377 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,377 @@
1+
##############################################################################
2+
# Copyright (C) 2018, 2019, 2020 Dominic O'Kane
3+
##############################################################################
4+
5+
from typing import Union
6+
7+
import numpy as np
8+
import pandas as pd
9+
10+
from ...utils.error import FinError
11+
from ...utils.date import Date
12+
from ...utils.global_vars import G_SMALL
13+
from ...utils.day_count import DayCountTypes
14+
from ...utils.frequency import FrequencyTypes, annual_frequency
15+
from ...utils.calendar import CalendarTypes, DateGenRuleTypes
16+
from ...utils.calendar import Calendar, BusDayAdjustTypes
17+
from ...utils.helpers import check_argument_types, label_to_string
18+
from ...utils.math import ONE_MILLION
19+
from ...utils.global_types import SwapTypes
20+
from ...market.curves.discount_curve import DiscountCurve
21+
22+
from .swap_fixed_leg import SwapFixedLeg
23+
from .swap_float_leg import SwapFloatLeg
24+
25+
26+
##########################################################################
27+
28+
29+
class IborFixedFloatSwap:
30+
"""Class for managing a standard Fixed vs IBOR swap. This is a contract
31+
in which a fixed payment leg is exchanged for a series of floating rates
32+
payments linked to some IBOR index rate. There is no exchange of principal.
33+
The contract is entered into at zero initial cost. The contract lasts from
34+
a start date to a specified maturity date.
35+
36+
The floating rate is not known fully until the end of the preceding payment
37+
period. It is set in advance and paid in arrears.
38+
39+
The value of the contract is the NPV of the two cpn streams. Discounting
40+
is done on a supplied discount curve which is separate from the curve from
41+
which the implied index rates are extracted."""
42+
43+
def __init__(
44+
self,
45+
effective_dt: Date, # Date interest starts to accrue
46+
term_dt_or_tenor: Union[Date, str], # Date contract ends
47+
fixed_leg_type: SwapTypes,
48+
fixed_cpn: float, # Fixed cpn (annualised)
49+
fixed_freq_type: FrequencyTypes,
50+
fixed_dc_type: DayCountTypes,
51+
notional: float = ONE_MILLION,
52+
float_spread: float = 0.0,
53+
float_freq_type: FrequencyTypes = FrequencyTypes.QUARTERLY,
54+
float_dc_type: DayCountTypes = DayCountTypes.THIRTY_E_360,
55+
cal_type: CalendarTypes = CalendarTypes.WEEKEND,
56+
bd_type: BusDayAdjustTypes = BusDayAdjustTypes.FOLLOWING,
57+
dg_type: DateGenRuleTypes = DateGenRuleTypes.BACKWARD,
58+
):
59+
"""Create an interest rate swap contract giving the contract start
60+
date, its maturity, fixed cpn, fixed leg frequency, fixed leg day
61+
count convention and notional. The floating leg parameters have default
62+
values that can be overwritten if needed. The start date is contractual
63+
and is the same as the settlement date for a new swap. It is the date
64+
on which interest starts to accrue. The end of the contract is the
65+
termination date. This is not adjusted for business days. The adjusted
66+
termination date is called the maturity date. This is calculated."""
67+
68+
check_argument_types(self.__init__, locals())
69+
70+
if isinstance(term_dt_or_tenor, Date):
71+
self.termination_dt = term_dt_or_tenor
72+
else:
73+
self.termination_dt = effective_dt.add_tenor(term_dt_or_tenor)
74+
75+
calendar = Calendar(cal_type)
76+
self.maturity_dt = calendar.adjust(self.termination_dt, bd_type)
77+
78+
if effective_dt > self.maturity_dt:
79+
raise FinError("Start date after maturity date")
80+
81+
self.effective_dt = effective_dt
82+
83+
float_leg_type = SwapTypes.PAY
84+
if fixed_leg_type == SwapTypes.PAY:
85+
float_leg_type = SwapTypes.RECEIVE
86+
87+
payment_lag = 0
88+
principal = 0.0
89+
90+
self.fixed_leg = SwapFixedLeg(
91+
effective_dt,
92+
self.termination_dt,
93+
fixed_leg_type,
94+
fixed_cpn,
95+
fixed_freq_type,
96+
fixed_dc_type,
97+
notional,
98+
principal,
99+
payment_lag,
100+
cal_type,
101+
bd_type,
102+
dg_type,
103+
)
104+
105+
self.float_leg = SwapFloatLeg(
106+
effective_dt,
107+
self.termination_dt,
108+
float_leg_type,
109+
float_spread,
110+
float_freq_type,
111+
float_dc_type,
112+
notional,
113+
principal,
114+
payment_lag,
115+
cal_type,
116+
bd_type,
117+
dg_type,
118+
)
119+
120+
###########################################################################
121+
122+
def get_fixed_rate(self):
123+
"""
124+
easy read access to the coupon (fixed rate)
125+
"""
126+
return self.fixed_leg.cpn
127+
128+
###########################################################################
129+
130+
def set_fixed_rate(self, new_rate: float):
131+
"""
132+
Sometimes we need to reset the coupon (fixed rate)
133+
This function updates caches that depend on it
134+
"""
135+
self.fixed_leg.cpn = new_rate
136+
self.fixed_leg.generate_payments()
137+
138+
###########################################################################
139+
140+
def set_fixed_rate_to_atm(
141+
self,
142+
valuation_date: Date,
143+
discount_curve: DiscountCurve,
144+
index_curve: DiscountCurve = None,
145+
first_fixing: float = None,
146+
):
147+
"""
148+
Reset fixed rate to atm given curve(s). returns the new atm
149+
"""
150+
atm = self.swap_rate(valuation_date, discount_curve, index_curve, first_fixing)
151+
self.set_fixed_rate(atm)
152+
return atm
153+
154+
###########################################################################
155+
156+
def value(
157+
self,
158+
value_dt: Date,
159+
discount_curve: DiscountCurve,
160+
index_curve: DiscountCurve = None,
161+
first_fixing_rate=None,
162+
pv_only=True,
163+
):
164+
"""Value the interest rate swap on a value date given a single Ibor
165+
discount curve."""
166+
167+
if index_curve is None:
168+
index_curve = discount_curve
169+
170+
fixed_leg_results = self.fixed_leg.value(
171+
value_dt, discount_curve, pv_only=pv_only
172+
)
173+
174+
float_leg_results = self.float_leg.value(
175+
value_dt,
176+
discount_curve,
177+
index_curve,
178+
first_fixing_rate,
179+
pv_only=pv_only,
180+
)
181+
182+
if pv_only:
183+
value = fixed_leg_results + float_leg_results
184+
return value
185+
else:
186+
value = fixed_leg_results[0] + float_leg_results[0]
187+
cashflow_report = pd.concat(
188+
[fixed_leg_results[1], float_leg_results[1]], ignore_index=True
189+
)
190+
191+
return value, cashflow_report
192+
193+
###########################################################################
194+
195+
def valuation_details(
196+
self,
197+
valuation_date: Date,
198+
discount_curve: DiscountCurve,
199+
index_curve: DiscountCurve = None,
200+
first_fixing_rate=None,
201+
):
202+
"""
203+
A long-hand method that returns various details relevant to valuation in
204+
a dictionary
205+
Slower than value(...) so should not be used when performance is important
206+
207+
We want the output dictionary to have the same labels for different bechmarks
208+
(depos, fras, swaps) because we want to present them together so please
209+
do not stick new outputs into one of them only
210+
"""
211+
if index_curve is None:
212+
index_curve = discount_curve
213+
214+
fixed_leg_value = self.fixed_leg.value(valuation_date, discount_curve)
215+
216+
float_leg_value = self.float_leg.value(
217+
valuation_date, discount_curve, index_curve, first_fixing_rate
218+
)
219+
220+
value = fixed_leg_value + float_leg_value
221+
pv01 = np.abs(fixed_leg_value / self.fixed_leg.cpn / self.fixed_leg.notional)
222+
pay_receive_float = -1 if self.float_leg.leg_type == SwapTypes.PAY else 1
223+
swap_rate = float_leg_value / self.float_leg.notional / pv01 / pay_receive_float
224+
225+
# VP: There is significant amount of confusion here with swap_type vs notional.
226+
is_payers = (
227+
self.fixed_leg.leg_type == SwapTypes.PAY and self.fixed_leg.notional > 0
228+
) or (
229+
self.fixed_leg.leg_type == SwapTypes.RECEIVE and self.fixed_leg.notional < 0
230+
)
231+
232+
pvbp_sign = 1 if is_payers else -1
233+
234+
out = {
235+
"type": type(self).__name__,
236+
"start_dt": self.effective_dt,
237+
"maturity_dt": self.maturity_dt,
238+
"dc_type": self.fixed_leg.dc_type.name,
239+
"fixed_leg_type": self.fixed_leg.leg_type.name,
240+
"fixed_freq_type": self.fixed_leg.freq_type.name,
241+
"notional": self.fixed_leg.notional,
242+
"contract_rate": self.fixed_leg.cpn,
243+
"market_rate": swap_rate,
244+
"spot_pvbp": pv01 * pvbp_sign,
245+
"fwd_pvbp": pv01 * pvbp_sign / discount_curve.df(self.effective_dt),
246+
"unit_value": value / self.fixed_leg.notional,
247+
"value": value,
248+
# ignoring bus day adj type, calendar, etc for now
249+
}
250+
return out
251+
252+
###########################################################################
253+
254+
def pv01(self, value_dt, discount_curve):
255+
"""Calculate the value of 1 basis point coupon on the fixed leg."""
256+
257+
pv = self.fixed_leg.value(value_dt, discount_curve)
258+
pv01 = pv / self.fixed_leg.cpn / self.fixed_leg.notional
259+
# Needs to be positive even if it is a payer leg
260+
pv01 = np.abs(pv01)
261+
return pv01
262+
263+
###########################################################################
264+
265+
def swap_rate(
266+
self,
267+
value_dt: Date,
268+
discount_curve: DiscountCurve,
269+
index_curve: DiscountCurve = None,
270+
first_fixing: float = None,
271+
):
272+
"""Calculate the fixed leg cpn that makes the swap worth zero.
273+
If the valuation date is before the swap payments start then this
274+
is the forward swap rate as it starts in the future. The swap rate
275+
is then a forward swap rate and so we use a forward discount
276+
factor. If the swap fixed leg has begun then we have a spot
277+
starting swap. The swap rate can also be calculated in a dual curve
278+
approach but in this case the first fixing on the floating leg is
279+
needed."""
280+
281+
pv01 = self.pv01(value_dt, discount_curve)
282+
283+
if abs(pv01) < G_SMALL:
284+
raise FinError("PV01 is zero. Cannot compute swap rate.")
285+
286+
float_leg_pv = self.float_leg.value(
287+
value_dt, discount_curve, index_curve, first_fixing
288+
)
289+
290+
float_leg_pv /= self.float_leg.notional
291+
292+
# Make sure we get the sign right
293+
if self.float_leg.leg_type == SwapTypes.PAY:
294+
float_leg_pv = -float_leg_pv
295+
296+
cpn = float_leg_pv / pv01
297+
return cpn
298+
299+
###########################################################################
300+
301+
def cash_settled_pv01(self, value_dt, flat_swap_rate, freq_type):
302+
"""Calculate the forward value of an annuity of a forward starting
303+
swap using a single flat discount rate equal to the swap rate. This is
304+
used in the pricing of a cash-settled swaption in the IborSwaption
305+
class. This method does not affect the standard valuation methods."""
306+
307+
m = annual_frequency(freq_type)
308+
309+
if m == 0:
310+
raise FinError("Frequency cannot be zero.")
311+
312+
# The swap may have started in the past but we can only value
313+
# payments that have occurred after the valuation date.
314+
start_index = 0
315+
while self.fixed_leg.payment_dts[start_index] < value_dt:
316+
start_index += 1
317+
318+
# If the swap has yet to settle then we do not include the
319+
# start date of the swap as a cpn payment date.
320+
if value_dt <= self.effective_dt:
321+
start_index = 1
322+
323+
# Now PV fixed leg flows.
324+
flat_pv01 = 0.0
325+
df = 1.0
326+
alpha = 1.0 / m
327+
328+
for _ in self.fixed_leg.payment_dts[start_index:]:
329+
df = df / (1.0 + alpha * flat_swap_rate)
330+
flat_pv01 += df * alpha
331+
332+
return flat_pv01
333+
334+
###########################################################################
335+
336+
def print_fixed_leg_pv(self):
337+
"""Prints the fixed leg amounts without any valuation details. Shows
338+
the dates and sizes of the promised fixed leg flows."""
339+
340+
self.fixed_leg.print_valuation()
341+
342+
###########################################################################
343+
344+
def print_float_leg_pv(self):
345+
"""Prints the fixed leg amounts without any valuation details. Shows
346+
the dates and sizes of the promised fixed leg flows."""
347+
348+
self.float_leg.print_valuation()
349+
350+
###########################################################################
351+
352+
def print_payments(self):
353+
"""Prints the fixed leg amounts without any valuation details. Shows
354+
the dates and sizes of the promised fixed leg flows."""
355+
356+
self.fixed_leg.print_payments()
357+
self.float_leg.print_payments()
358+
359+
###########################################################################
360+
361+
def __repr__(self):
362+
363+
s = label_to_string("OBJECT TYPE", type(self).__name__)
364+
s += self.fixed_leg.__repr__()
365+
s += "\n"
366+
s += self.float_leg.__repr__()
367+
return s
368+
369+
###########################################################################
370+
371+
def _print(self):
372+
"""Print a list of the unadjusted cpn payment dates used in
373+
analytic calculations for the bond."""
374+
print(self)
375+
376+
377+
########################################################################################

financepy/products/rates/swaps/IborIborSwap.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
from ....utils import DayCount, DayCountTypes
1010
from ....utils import FrequencyTypes
1111
from ....utils import CalendarTypes, DateGenRuleTypes
12-
from ....utils import Calendar, BusDayAdjustTypes
12+
from ....utils import BusDayAdjustTypes
1313
from ....utils import Schedule
1414
from ....utils import label_to_string, check_argument_types
1515
from ....utils import ONE_MILLION
@@ -173,7 +173,7 @@ def float_leg_value(
173173
self._float_total_pv = []
174174
self._first_fixing_rate = first_fixing_rate
175175

176-
basis = DayCount(self.float_dc_type)
176+
basis = DayCount(self._float_dc_type)
177177

178178
# The swap may have started in the past but we can only value
179179
# payments that have occurred after the start date.

0 commit comments

Comments
 (0)