Skip to content

Commit d4491be

Browse files
committed
Added reset flows method for bonds
1 parent 757a193 commit d4491be

File tree

3 files changed

+97
-33
lines changed

3 files changed

+97
-33
lines changed

financepy/products/bonds/bond.py

Lines changed: 57 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
# GET THE COUPON AND THE ACCRUED INTEREST EQUALS THE COUPON.
2222
########################################################################################
2323

24+
from typing import List
2425
from enum import Enum
2526
import numpy as np
2627
from scipy import optimize
@@ -157,7 +158,8 @@ def __init__(
157158
self.bd_type = bd_type
158159
self.dg_type = dg_type
159160

160-
self._calculate_unadjusted_cpn_dts()
161+
self._calculate_cpn_dts()
162+
self._calculate_payment_dts()
161163
self._calculate_flow_amounts()
162164

163165
self._pcd = None
@@ -170,7 +172,7 @@ def __init__(
170172

171173
####################################################################################
172174

173-
def _calculate_unadjusted_cpn_dts(self):
175+
def _calculate_cpn_dts(self):
174176
"""Determine the unadjusted bond coupon dts. Note that for analytical
175177
calculations these are not usually adjusted and so may fall on a
176178
weekend or holiday.
@@ -203,23 +205,20 @@ def _calculate_payment_dts(self):
203205
bus_day_adj_type = BusDayAdjustTypes.FOLLOWING
204206
calendar = Calendar(self.cal_type)
205207

206-
self._calculate_unadjusted_cpn_dts()
207-
208-
self.payment_dts = []
209-
210208
# Expect at least an issue date and a maturity date - if not - problem
211209
if len(self.cpn_dts) < 2:
212210
raise FinError("Cannot calculate payment dts with one payment")
213211

212+
self.payment_dts = []
213+
214214
# I do not adjust the first date as it is the issue date
215215
self.payment_dts.append(self.cpn_dts[0])
216216

217217
for cpn_dt in self.cpn_dts[1:]:
218218
pmt_dt = calendar.adjust(cpn_dt, bus_day_adj_type)
219-
220219
self.payment_dts.append(pmt_dt)
221220

222-
###########################################################################
221+
####################################################################################
223222

224223
def _calculate_flow_amounts(self):
225224
"""Determine the bond cash flow payment amounts without principal.
@@ -231,7 +230,37 @@ def _calculate_flow_amounts(self):
231230
cpn = self.cpn / self.freq
232231
self.flow_amounts.append(cpn)
233232

234-
###########################################################################
233+
####################################################################################
234+
235+
def reset_flows(
236+
self, cpn_dts: List[Date], payment_dts: List[Date], flow_amounts: np.ndarray
237+
):
238+
"""Set the flows of the bond externally. Coupon dates are for accrued
239+
while payment dates are calendar adjusted. Flows are on payment dates"""
240+
241+
n_cpn_dts = len(cpn_dts)
242+
n_payment_dts = len(payment_dts)
243+
n_flows = len(flow_amounts)
244+
245+
if n_cpn_dts != n_payment_dts:
246+
raise FinError("Number of coupon dates not equal to number payments")
247+
248+
if n_cpn_dts != n_flows:
249+
raise FinError("Number of coupon dates not equal to number flows")
250+
251+
for i in range(1, n_cpn_dts):
252+
253+
if cpn_dts[i] < cpn_dts[i - 1]:
254+
raise FinError("Coupon dates not in order")
255+
256+
if payment_dts[i] < payment_dts[i - 1]:
257+
raise FinError("Payment dates not in order")
258+
259+
self.cpn_dts = cpn_dts
260+
self.payment_dts = payment_dts
261+
self.flow_amounts = flow_amounts
262+
263+
####################################################################################
235264

236265
def dirty_price_from_ytm(
237266
self,
@@ -275,7 +304,7 @@ def dirty_price_from_ytm(
275304

276305
# n is the number of flows after the next coupon
277306
n = 0
278-
for dt in self.cpn_dts:
307+
for dt in self.payment_dts:
279308
if dt > settle_dt:
280309
n += 1
281310
n = n - 1
@@ -292,7 +321,6 @@ def dirty_price_from_ytm(
292321
term3 = (c / f) * v * v * (1.0 - v ** (n - 1)) / (1.0 - v)
293322
term4 = v**n
294323
dp = (v ** (self.alpha)) * (term1 + term2 + term3 + term4)
295-
# print(term1, term2, term3, term4, v, self.alpha, dp)
296324
elif convention == YTMCalcType.US_TREASURY:
297325
if n == 0:
298326
dp = (v ** (self.alpha)) * (1.0 + c / f)
@@ -365,7 +393,7 @@ def forward_price(
365393
accrued_forward = self.accrued_interest(forward_dt)
366394

367395
fv_cpns = 0.0
368-
for dt, amt in zip(self.cpn_dts[1:], self.flow_amounts[1:]):
396+
for dt, amt in zip(self.payment_dts[1:], self.flow_amounts[1:]):
369397
if settle_dt < dt <= forward_dt:
370398
t_cpn_to_forward, _, _ = dc.year_frac(dt, forward_dt)
371399
fv_cpns += amt * self.par * (1 + repo_rate * t_cpn_to_forward)
@@ -664,14 +692,14 @@ def dirty_price_from_discount_curve(
664692
df = 1.0
665693
df_settle_dt = discount_curve.df(settle_dt)
666694

667-
dt = self.cpn_dts[1]
695+
dt = self.payment_dts[1]
668696
if dt > settle_dt:
669697
df = discount_curve.df(dt)
670698
flow = self.cpn / self.freq
671699
pv = flow * df
672700
px += pv * pay_first_cpn
673701

674-
for dt in self.cpn_dts[2:]:
702+
for dt in self.payment_dts[2:]:
675703

676704
# coupons paid on a settlement date are paid to the seller
677705
if dt > settle_dt:
@@ -820,7 +848,7 @@ def asset_swap_spread(
820848
pv_ibor = 0.0
821849
prev_dt = self._pcd
822850

823-
for dt in self.cpn_dts[1:]:
851+
for dt in self.payment_dts[1:]:
824852

825853
# coupons paid on a settlement date are paid to the seller
826854
if dt > settle_dt:
@@ -901,7 +929,7 @@ def dirty_price_from_oas(
901929
df_adjusted = 1.0
902930

903931
pv = 0.0
904-
for dt in self.cpn_dts[1:]:
932+
for dt in self.payment_dts[1:]:
905933

906934
# coupons paid on a settlement date are paid to the seller
907935
if dt > settle_dt:
@@ -999,7 +1027,7 @@ def dirty_price_from_survival_curve(
9991027
defaulting_pv_pay_start = 0.0
10001028
defaulting_pv_pay_end = 0.0
10011029

1002-
for dt in self.cpn_dts[1:]:
1030+
for dt in self.payment_dts[1:]:
10031031

10041032
# coupons paid on a settlement date are paid to the seller
10051033
if dt > settle_dt:
@@ -1068,7 +1096,7 @@ def calc_ror(
10681096
"""
10691097
buy_price = self.dirty_price_from_ytm(begin_dt, begin_ytm, convention)
10701098
sell_price = self.dirty_price_from_ytm(end_dt, end_ytm, convention)
1071-
dts_cfs = zip(self.cpn_dts, self.flow_amounts)
1099+
dts_cfs = zip(self.payment_dts, self.flow_amounts)
10721100

10731101
# The coupon or par payments on buying date belong to the buyer. The
10741102
# coupon or par payments on selling date are given to the new buyer.
@@ -1121,13 +1149,20 @@ def print_payments(self, settle_dt: Date, face: float = 100):
11211149
flow = face * self.cpn / self.freq
11221150
flow_str = ""
11231151

1124-
for dt in self.cpn_dts[1:-1]:
1152+
n_flows = len(self.cpn_dts)
1153+
1154+
for i in range(0, n_flows):
1155+
11251156
# coupons paid on a settlement date are paid to the seller
1126-
if dt > settle_dt:
1127-
flow_str += "%12s %12.5f \n" % (dt, flow)
1157+
cpn_dt = self.cpn_dts[i]
1158+
pmt_dt = self.payment_dts[i]
1159+
flow = self.flow_amounts[i]
1160+
1161+
if cpn_dt > settle_dt:
1162+
flow_str += "%12s %12s %12.5f \n" % (cpn_dt, pmt_dt, flow)
11281163

11291164
redemption_amount = face + flow
1130-
flow_str += "%12s %12.5f \n" % (self.cpn_dts[-1], redemption_amount)
1165+
flow_str += "%12s %12s %12.5f \n" % (cpn_dt, pmt_dt, redemption_amount)
11311166

11321167
print(flow_str)
11331168

financepy/products/fx/fx_double_one_touch_option.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@
2424

2525

2626
@njit(fastmath=True, parallel=True, cache=True)
27-
def _p_touch_bb_parallel(
27+
def _p_double_touch_bb_parallel(
2828
S0: float,
2929
L: float,
3030
U: float,
@@ -468,7 +468,9 @@ def value_mc(
468468
steps = int(max(1, num_steps_per_year * T))
469469

470470
# Estimate touch probability with Brownian bridge (parallel)
471-
p_touch = _p_touch_bb_parallel(S0, L, U, mu, sigma, T, steps, num_paths, seed)
471+
p_touch = _p_double_touch_bb_parallel(
472+
S0, L, U, mu, sigma, T, steps, num_paths, seed
473+
)
472474

473475
if self.option_type == DoubleBarrierTypes.KNOCK_OUT:
474476
# Double no-touch

golden_tests/TestFinBond.py

Lines changed: 36 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -513,21 +513,46 @@ def test_bond_ex_dividend():
513513

514514
def test_bond_payment_dates():
515515

516+
issue_dt = Date(1, 1, 2020)
517+
mat_dt = Date(1, 1, 2023)
518+
cpn = 0.05
519+
ytm = 0.05
520+
face = 100.0
521+
522+
# Use auto generated schedule
516523
bond = Bond(
517-
issue_dt=Date(7, 6, 2021),
518-
maturity_dt=Date(7, 6, 2031),
519-
coupon=0.0341,
524+
issue_dt=issue_dt,
525+
maturity_dt=mat_dt,
526+
coupon=cpn,
520527
freq_type=FrequencyTypes.ANNUAL,
521528
dc_type=DayCountTypes.ACT_ACT_ISDA,
529+
cal_type=CalendarTypes.UNITED_STATES,
522530
)
523-
bond._calculate_payment_dts()
524531

525-
DEBUG = False
532+
settle_dt = issue_dt.add_months(3)
533+
# bond.print_payments(settle_dt)
526534

527-
if DEBUG:
528-
print(bond.flow_amounts)
529-
print(bond.cpn_dts)
530-
print(bond._payment_dts)
535+
accrued = bond.accrued_interest(settle_dt, face)
536+
dirty_price = bond.dirty_price_from_ytm(settle_dt, ytm)
537+
clean_price = dirty_price - accrued
538+
test_cases.print(settle_dt, dirty_price, accrued, clean_price)
539+
# print(settle_dt, dirty_price, accrued, clean_price)
540+
541+
# Use manual schedule where I make payment dates equal coupon dates even weekends
542+
cpn_dts = [Date(1, 1, 2020), Date(1, 1, 2021), Date(1, 1, 2022), Date(1, 1, 2023)]
543+
pmt_dts = [Date(1, 1, 2020), Date(1, 1, 2021), Date(1, 1, 2022), Date(1, 1, 2023)]
544+
flow_amts = np.array([0.0, 0.05, 0.05, 1.05])
545+
546+
bond.reset_flows(cpn_dts, pmt_dts, flow_amts)
547+
# bond.print_payments(settle_dt)
548+
549+
accrued = bond.accrued_interest(settle_dt, face)
550+
dirty_price = bond.dirty_price_from_ytm(settle_dt, ytm)
551+
clean_price = dirty_price - accrued
552+
test_cases.print(settle_dt, dirty_price, accrued, clean_price)
553+
554+
555+
# print(settle_dt, dirty_price, accrued, clean_price)
531556

532557

533558
########################################################################################
@@ -602,6 +627,8 @@ def test_bond_eom():
602627

603628
accrued_interest = bond.accrued_interest(settle_dt) # should be 8406.593406
604629

630+
# print(accrued_interest)
631+
605632

606633
########################################################################################
607634

0 commit comments

Comments
 (0)