2121# GET THE COUPON AND THE ACCRUED INTEREST EQUALS THE COUPON.
2222########################################################################################
2323
24+ from typing import List
2425from enum import Enum
2526import numpy as np
2627from 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
0 commit comments