Coverage for rivapy / pricing / interest_rate_swap_pricing.py: 66%

331 statements  

« prev     ^ index     » next       coverage.py v7.12.0, created at 2025-11-27 14:36 +0000

1# 2025.07.24 Hans Nguyen 

2from datetime import datetime, date, timedelta 

3from scipy.optimize import brentq 

4from rivapy.tools.interfaces import BaseDatedCurve 

5from rivapy.instruments.bond_specifications import DeterministicCashflowBondSpecification 

6from rivapy.marketdata import DiscountCurveParametrized, ConstantRate, DiscountCurve 

7from rivapy.pricing.pricing_request import PricingRequest 

8from rivapy.pricing._logger import logger 

9from rivapy.instruments.deposit_specifications import DepositSpecification 

10from rivapy.instruments.fra_specifications import ForwardRateAgreementSpecification 

11from rivapy.instruments.ir_swap_specification import ( 

12 IrFixedLegSpecification, 

13 IrFloatLegSpecification, 

14 IrOISLegSpecification, 

15 InterestRateSwapSpecification, 

16 IrSwapLegSpecification, 

17) 

18 

19# from rivapy.pricing.pricing_data import InterestRateSwapPricingData, InterestRateSwapLegPricingData, InterestRateSwapFloatLegPricingData 

20from rivapy.pricing.pricing_data import ( 

21 InterestRateSwapPricingData_rivapy, 

22 InterestRateSwapLegPricingData_rivapy, 

23 InterestRateSwapFloatLegPricingData_rivapy, 

24) 

25from rivapy.pricing.pricing_request import InterestRateSwapPricingRequest 

26from typing import List as _List, Union as _Union, Tuple, Dict, Any 

27from rivapy.tools.datetools import DayCounter 

28from rivapy.instruments.components import CashFlow 

29from rivapy.marketdata.fixing_table import FixingTable 

30from rivapy.instruments.components import * 

31import numpy as np 

32 

33 

34from rivapy.tools.enums import IrLegType 

35from rivapy.tools._validators import print_member_values 

36 

37 

38class InterestRateSwapPricer: 

39 

40 def __init__( 

41 self, 

42 val_date: _Union[date, datetime], 

43 spec: InterestRateSwapSpecification, 

44 discount_curve_pay_leg: DiscountCurve, 

45 discount_curve_receive_leg: DiscountCurve, 

46 fixing_curve_pay_leg: DiscountCurve, 

47 fixing_curve_receive_leg: DiscountCurve, 

48 fx_fwd_curve_pay_leg: DiscountCurve, # TODO FxForwardCurve ... do we need anotheer class 

49 fx_fwd_curve_receive_leg: DiscountCurve, 

50 pricing_request: InterestRateSwapPricingRequest, 

51 pricing_param: Dict = None, # mutable argument should be defaulted to none 

52 fixing_map: FixingTable = None, 

53 fx_pay_leg: float = 1.0, 

54 fx_receive_leg: float = 1.0, 

55 ): 

56 """Initializes the Interest Rate Swap Pricer with all required curves, specifications, and parameters. 

57 

58 Args: 

59 val_date (date | datetime): The valuation date for pricing the swap. This is the anchor date for all time-dependent calculations. 

60 spec (InterestRateSwapSpecification): The swap's structural details (legs, notionals, schedules, etc.). 

61 discount_curve_pay_leg (DiscountCurve): Discount curve used to present value the pay leg. 

62 discount_curve_receive_leg (DiscountCurve): Discount curve used to present value the receive leg. 

63 fixing_curve_pay_leg (DiscountCurve): Curve used to forecast forward rates for the pay leg (typically for floating legs). 

64 fixing_curve_receive_leg (DiscountCurve): Curve used to forecast forward rates for the receive leg. 

65 fx_fwd_curve_pay_leg (DiscountCurve): FX forward curve to convert the pay leg currency to the pricing currency (if applicable). 

66 fx_fwd_curve_receive_leg (DiscountCurve): FX forward curve to convert the receive leg currency to the pricing currency. 

67 pricing_request (InterestRateSwapPricingRequest): Contains the pricing type, metrics requested (e.g., PV), and other flags. Not yet used properly 

68 pricing_param (Dict, optional): Additional pricing parameters, such as day count conventions, compounding rules, etc. 

69 fixing_map (FixingTable, optional): Historical fixings for floating legs that reference past periods. 

70 fx_pay_leg (float, optional): FX rate multiplier to convert the pay leg currency to base. Default is 1.0 (i.e., same currency). 

71 fx_receive_leg (float, optional): FX rate multiplier to convert the receive leg currency to base. Default is 1.0. 

72 

73 """ 

74 

75 self._val_date = val_date 

76 self._spec = spec 

77 self._pay_leg = spec.pay_leg 

78 self._receive_leg = spec.receive_leg 

79 

80 self._discount_curve_pay_leg = discount_curve_pay_leg 

81 self._discount_curve_receive_leg = discount_curve_receive_leg 

82 

83 self._fixing_curve_pay_leg = fixing_curve_pay_leg # const std::shared_ptr<const DiscountCurve>& fixingCurvePayLeg, 

84 self._fixing_curve_receive_leg = fixing_curve_receive_leg # const std::shared_ptr<const DiscountCurve>& fixingCurveReceiveLeg, 

85 

86 self._fx_fwd_curve_pay_leg = fx_fwd_curve_pay_leg # const std::shared_ptr<const FxForwardCurve>& fxForwardCurvePayLeg, 

87 self._fx_fwd_curve_receive_leg = fx_fwd_curve_receive_leg # const std::shared_ptr<const FxForwardCurve>& fxForwardCurveReceiveLeg, 

88 

89 self._pricing_request = pricing_request # const PricingRequest& pricingRequest, 

90 if pricing_param is None: 

91 pricing_param = {} 

92 self._pricing_param = pricing_param # std::shared_ptr<const InterestRateSwapPricingParameter> pricingParam, 

93 self._fixing_map = fixing_map # std::shared_ptr<const FixingTable> fixingMap, 

94 

95 self._fx_pay_leg = fx_pay_leg # double fxPayLeg, 

96 self._fx_receive_leg = fx_receive_leg # double fxReceiveLeg) 

97 

98 self._pricing_param = pricing_param 

99 

100 @staticmethod 

101 def _populate_cashflows_fix( 

102 val_date: _Union[date, datetime], 

103 fixed_leg_spec: IrFixedLegSpecification, 

104 discount_curve: DiscountCurve, 

105 # forward_curve: DiscountCurve, 

106 fx_forward_curve: DiscountCurve, 

107 fixing_map: FixingTable, 

108 set_rate: bool = False, 

109 desired_rate: float = 1.0, 

110 ) -> _List[CashFlow]: 

111 """Generate a list of CashFlow objects, each with the cashflow amount for the given accrual period 

112 and additional information added to describe the cashflow. For the fixed leg of a swap. 

113 If desired_rate = 1.0, this is to calculate the annuity, for the calculation of fair swap rate. 

114 

115 Args: 

116 val_date (_Union[date, datetime]): valuation date 

117 fixed_leg_spec (IrFixedLegSpecification): specification of the fixed leg of the IR Swap 

118 discount_curve (DiscountCurve): The discount curve used for discounting to calculated the present value 

119 fx_forward_curve (DiscountCurve): fxCurve used for currency conversion for applicable swap (not yet implemented) 

120 fixing_map (FixingTable): Fixing map of historial values (not yet implemented) 

121 fixing_grace_period (float): 

122 set_rate (bool, optional): Flag to manually set a Fixed rate. Defaults to False. 

123 spread (float, optional): Desired rate value. Defaults to 1 (which then calculates annuity). 

124 

125 Returns: 

126 _List[CashFlow]: list of CashFlow objects. 

127 """ 

128 

129 # overwrite fixed interest if desired 

130 fixed_rate = fixed_leg_spec.fixed_rate 

131 if set_rate: 

132 fixed_rate = desired_rate # if desired_rate = 1.0, this is to calculate the annuity, for the calculation of fair swap rate. 

133 

134 # init output storage object 

135 entries = [] 

136 dcc = DayCounter(discount_curve.daycounter) 

137 

138 # get projected notionals 

139 leg_notional_structure = fixed_leg_spec.get_NotionalStructure() 

140 # define number of cashflows 

141 num_of_start_dates = len(fixed_leg_spec.start_dates) 

142 

143 notionals = get_projected_notionals( 

144 val_date=val_date, 

145 notional_structure=leg_notional_structure, 

146 start_period=0, 

147 end_period=num_of_start_dates, 

148 fx_forward_curve=fx_forward_curve, 

149 fixing_map=fixing_map, 

150 ) # output is a lsit of floats 

151 

152 for i in range(len(notionals)): 

153 

154 notional_start_date = leg_notional_structure.get_pay_date_start(i) 

155 notional_end_date = leg_notional_structure.get_pay_date_end(i) 

156 

157 if notional_start_date: # i.e. not None or empty 

158 # add an intional notional OUTFLOW or not 

159 notional_entry = CashFlow() 

160 notional_entry.pay_date = notional_start_date 

161 

162 if val_date <= notional_entry.pay_date: # TODO recheck this business logic 

163 notional_entry.discount_factor = discount_curve.value(val_date, notional_entry.pay_date) 

164 else: 

165 notional_entry.discount_factor = 0.0 

166 

167 notional_entry.pay_amount = -1 * notionals[i] 

168 notional_entry.present_value = notional_entry.pay_amount * notional_entry.discount_factor 

169 notional_entry.notional_cashflow = True 

170 entries.append(notional_entry) 

171 

172 entry = CashFlow() 

173 entry.start_date = fixed_leg_spec.start_dates[i] 

174 entry.end_date = fixed_leg_spec.end_dates[i] 

175 entry.pay_date = fixed_leg_spec.pay_dates[i] 

176 entry.notional = notionals[i] 

177 entry.rate = fixed_rate 

178 entry.interest_yf = dcc.yf(entry.start_date, entry.end_date) # gives back SINGLE yearfraction 

179 if val_date < entry.pay_date: 

180 entry.discount_factor = discount_curve.value(val_date, entry.pay_date) 

181 else: 

182 entry.discount_factor = 0.0 

183 entry.interest_amount = entry.notional * entry.rate * entry.interest_yf 

184 entry.pay_amount = entry.interest_amount 

185 entry.present_value = entry.pay_amount * entry.discount_factor 

186 

187 # setting main cashflow value 

188 entry.val = entry.pay_amount 

189 entry.interest_cashflow = True 

190 entries.append(entry) 

191 

192 # # TEMPORARY TEST - inthe case of constant notional structure, but i want a final notional cashflow like a bond 

193 # if i == len(notionals) - 1: # this checks for the last entry 

194 # notional_end_date = entry.end_date 

195 

196 if notional_end_date: 

197 # add an intional notional INFLOW or not 

198 notional_entry = CashFlow() 

199 notional_entry.pay_date = notional_end_date 

200 

201 if val_date <= notional_entry.pay_date: # TODO recheck this business logic 

202 notional_entry.discount_factor = discount_curve.value(val_date, notional_entry.pay_date) 

203 else: 

204 notional_entry.discount_factor = 0.0 

205 

206 notional_entry.pay_amount = notionals[i] # positive 

207 notional_entry.present_value = notional_entry.pay_amount * notional_entry.discount_factor 

208 notional_entry.notional_cashflow = True 

209 entries.append(notional_entry) 

210 

211 return entries # a LIST of ENTRY objects, where each object has the PV 

212 

213 @staticmethod 

214 def _populate_cashflows_float( 

215 val_date: _Union[date, datetime], 

216 float_leg_spec: IrFloatLegSpecification, 

217 discount_curve: DiscountCurve, 

218 forward_curve: DiscountCurve, 

219 fx_forward_curve: DiscountCurve, 

220 fixing_map: FixingTable, 

221 fixing_grace_period: float, 

222 set_spread: bool = False, 

223 spread: float = None, 

224 ) -> _List[CashFlow]: 

225 """Generate a list of CashFlow objects, each with the cashflow amount for the given accrual period 

226 and additional information added to descrive the cashflow. 

227 

228 Args: 

229 val_date (_Union[date, datetime]): valuation date 

230 float_leg_spec (IrFloatLegSpecification): specification of the floating leg of the IR Swap 

231 discount_curve (DiscountCurve): The discount curve used for discounting to calculated the present value 

232 forward_curve (DiscountCurve): forward curve used to determine the forward rate to calculate the interest 

233 fx_forward_curve (DiscountCurve): fxCurve used for currency conversion for applicable swap (not yet implemented) 

234 fixing_map (FixingTable): Fixing map of historial values (not yet implemented) 

235 fixing_grace_period (float): 

236 setSpread (bool, optional): Flag to manually set a spread value. Defaults to False. 

237 spread (float, optional): Desired spread value. Defaults to None. 

238 

239 Raises: 

240 ValueError: No underlying index ID found in fixing table 

241 

242 Returns: 

243 _List[CashFlow]: list of CashFlow objects. 

244 """ 

245 

246 fixing_grace_period_dt = timedelta(days=fixing_grace_period) 

247 

248 entries = [] 

249 udl = float_leg_spec.udl_id 

250 

251 # swap day count convention 

252 dcc = DayCounter(discount_curve.daycounter) 

253 # rate day count convention # note that the specification also has dcc but without the curve... 

254 rate_dcc = DayCounter(float_leg_spec.rate_day_count_convention) 

255 

256 # overwrite spread if desired 

257 leg_spread = float_leg_spec.spread 

258 if set_spread: 

259 leg_spread = spread 

260 

261 # obtain the notionals and create a list of notionals to be used for each cashflow 

262 leg_notional_structure = float_leg_spec.get_NotionalStructure() 

263 # define the number of cashflows 

264 num_of_start_dates = len(float_leg_spec.start_dates) 

265 

266 notionals = get_projected_notionals( 

267 val_date=val_date, 

268 notional_structure=leg_notional_structure, 

269 start_period=0, 

270 end_period=num_of_start_dates, 

271 fx_forward_curve=fx_forward_curve, 

272 fixing_map=fixing_map, 

273 ) # output is a list of floats 

274 

275 for i in range(len(notionals)): 

276 

277 notional_start_date = leg_notional_structure.get_pay_date_start(i) 

278 notional_end_date = leg_notional_structure.get_pay_date_end(i) 

279 

280 if notional_start_date: # i.e. not None or empty 

281 # add an initial notional OUTFLOW or not 

282 notional_entry = CashFlow() 

283 notional_entry.pay_date = notional_start_date 

284 

285 if val_date <= notional_entry.pay_date: # TODO recheck this business logic 

286 notional_entry.discount_factor = discount_curve.value(val_date, notional_entry.pay_date) 

287 else: 

288 notional_entry.discount_factor = 0.0 

289 

290 notional_entry.pay_amount = -1 * notionals[i] 

291 notional_entry.present_value = notional_entry.pay_amount * notional_entry.discount_factor 

292 notional_entry.notional_cashflow = True 

293 entries.append(notional_entry) 

294 

295 entry = CashFlow() 

296 entry.start_date = float_leg_spec.start_dates[i] 

297 entry.end_date = float_leg_spec.end_dates[i] 

298 entry.pay_date = float_leg_spec.pay_dates[i] 

299 entry.notional = notionals[i] 

300 entry.interest_yf = dcc.yf(entry.start_date, entry.end_date) 

301 rate_yf = rate_dcc.yf(float_leg_spec.rate_start_dates[i], float_leg_spec.rate_end_dates[i]) 

302 # # DEBUG TODO REMOVE 

303 # print(f"notional {i}:{notionals[i]} for {entry.end_date}") 

304 if val_date <= float_leg_spec.reset_dates[i]: 

305 # print("swap calculating fwd_rate for floating leg") # DEBUG 11.2025 TODO REMOVE 

306 # print(f"rate_start={float_leg_spec.rate_start_dates[i]}, rate_end={float_leg_spec.rate_end_dates[i]}") 

307 # print( 

308 # f"DF_start={forward_curve.value(val_date, float_leg_spec.rate_start_dates[i])}, DF_end={forward_curve.value(val_date, float_leg_spec.rate_end_dates[i])}" 

309 # ) 

310 # print(f"value_fwd={forward_curve.value_fwd(val_date, float_leg_spec.rate_start_dates[i], float_leg_spec.rate_end_dates[i])}") 

311 

312 fwd_rate = forward_curve.value_fwd(val_date, float_leg_spec.rate_start_dates[i], float_leg_spec.rate_end_dates[i]) 

313 entry.rate = leg_spread + (1.0 / fwd_rate - 1.0) / rate_yf 

314 # print(f"leg_spread: {leg_spread} rate_yf: {rate_yf} fwd_rate: {fwd_rate} calculated rate: {entry.rate} ") 

315 else: 

316 # print("GOT JERE INSTEAD : val_date:", val_date, " cf restet date:", float_leg_spec.reset_dates[i]) 

317 fixing = fixing_map.get_fixing(udl, float_leg_spec.reset_dates[i]) 

318 if fixing is None: # i.e. no fixing available 

319 

320 if val_date - float_leg_spec.reset_dates[i] > fixing_grace_period_dt: 

321 raise ValueError(f"Missing fixing for {udl} on {float_leg_spec.reset_dates[i]}") 

322 

323 else: 

324 # fix value of payment i in future based on current discount curve and a period between 

325 # valDate and valDate+length of original period (workaround if fixing is not available) 

326 # taken from pyvacon 

327 if entry.pay_date >= val_date: # TODO understand the logic 

328 time_delta = entry.end_date - entry.start_date 

329 fixing = (1.0 / forward_curve.value_fwd(val_date, val_date, val_date + time_delta) - 1) / entry.interest_yf 

330 

331 entry.rate = fixing + spread 

332 

333 if val_date <= entry.pay_date: 

334 entry.discount_factor = discount_curve.value(val_date, entry.pay_date) 

335 else: 

336 entry.discount_factor = 0.0 

337 

338 # given rate, notional, and yf, calc interest 

339 entry.interest_amount = entry.notional * entry.rate * entry.interest_yf 

340 

341 # scale by forward rate #TODO # required for FX swap... 

342 if val_date <= entry.end_date: 

343 entry.pay_amount = entry.interest_amount / forward_curve.value_fwd(val_date, entry.end_date, entry.pay_date) 

344 else: 

345 if entry.pay_date >= val_date: 

346 entry.pay_amount = entry.interest_amount / forward_curve.value_fwd(val_date, val_date, entry.pay_date) 

347 else: 

348 entry.pay_amount = 0.0 

349 

350 # given total cashflow amount - discount it 

351 # #DEBUG TODO 

352 # print(f"forward rate: {entry.rate}") 

353 # print(f"delta_t: {entry.interest_yf}") 

354 # print(f"discount_factor: {entry.discount_factor}") 

355 entry.present_value = entry.pay_amount * entry.discount_factor 

356 entry.interest_cashflow = True 

357 entries.append(entry) 

358 

359 # # TEMPORARY TEST - inthe case of constant notional structure, but i want a final notional cashflow like a bond 

360 # if i == len(notionals) - 1: # this checks for the last entry 

361 # notional_end_date = entry.end_date 

362 

363 if notional_end_date: 

364 # add an intional notional INFLOW or not 

365 notional_entry = CashFlow() 

366 notional_entry.pay_date = notional_end_date 

367 

368 if val_date <= notional_entry.pay_date: # TODO recheck this business logic 

369 notional_entry.discount_factor = discount_curve.value(val_date, notional_entry.pay_date) 

370 else: 

371 notional_entry.discount_factor = 0.0 

372 

373 notional_entry.pay_amount = notionals[i] # positive 

374 notional_entry.present_value = notional_entry.pay_amount * notional_entry.discount_factor 

375 notional_entry.notional_cashflow = True 

376 entries.append(notional_entry) 

377 

378 return entries 

379 

380 @staticmethod 

381 def _populate_cashflows_ois( 

382 val_date: _Union[date, datetime], 

383 ois_leg_spec: IrOISLegSpecification, 

384 discount_curve: DiscountCurve, 

385 forward_curve: DiscountCurve, 

386 fx_forward_curve: DiscountCurve, 

387 fixing_map: FixingTable, 

388 fixing_grace_period: float, 

389 set_spread: bool = False, 

390 spread: float = None, 

391 ) -> _List[CashFlow]: 

392 """Generate a list of CashFlow objects, each with the cashflow amount for the given accrual period 

393 and additional information added to describe the cashflow. To be used for OIS leg specifications. 

394 In this case, the daily rates are calculated and compounded to give the total interest for the accrual period. 

395 

396 Args: 

397 val_date (_Union[date, datetime]): valuation date 

398 float_leg_spec (IrFloatLegSpecification): specification of the floating leg of the IR Swap 

399 discount_curve (DiscountCurve): The discount curve used for discounting to calculated the present value 

400 forward_curve (DiscountCurve): forward curve used to determine the forward rate to calculate the interest 

401 fx_forward_curve (DiscountCurve): fxCurve used for currency conversion for applicable swap (not yet implemented) 

402 fixing_map (FixingTable): Fixing map of historial values (not yet implemented) 

403 fixing_grace_period (float): 

404 setSpread (bool, optional): Flag to manually set a spread value. Defaults to False. 

405 spread (float, optional): Desired spread value. Defaults to None. 

406 

407 Raises: 

408 ValueError: No underlying index ID found in fixing table 

409 

410 Returns: 

411 _List[CashFlow]: list of CashFlow objects. 

412 """ 

413 entries = [] # output container to be returned 

414 udl = ois_leg_spec.udl_id # get the ID of the underlying 

415 

416 # Parameters to consider 

417 # const std::vector<std::vector<boost::posix_time::ptime>>& dailyRateStartDates = oisLeg->getDailyRateStartDates(); 

418 # const std::vector<std::vector<boost::posix_time::ptime>>& dailyRateEndDates = oisLeg->getDailyRateEndDates(); 

419 # const std::vector<std::vector<boost::posix_time::ptime>>& dailyResetDates = oisLeg->getDailyResetDates(); 

420 # const std::vector<boost::posix_time::ptime>& startDates = oisLeg->getStartDates(); 

421 # const std::vector<boost::posix_time::ptime>& endDates = oisLeg->getEndDates(); 

422 # const std::vector<boost::posix_time::ptime>& payDates = oisLeg->getPayDates(); 

423 # std::shared_ptr<const NotionalStructure> notionalStructure = leg->getNotionalStructure(); 

424 # std::vector<double> notionals(leg->getStartDates().size()); 

425 

426 # swap day count convention 

427 dcc = DayCounter(discount_curve.daycounter) 

428 # rate day count convention # note that the specification also has dcc but without the curve... 

429 rate_dcc = DayCounter(ois_leg_spec.rate_day_count_convention) 

430 

431 # overwrite spread if desired 

432 leg_spread = ois_leg_spec.spread 

433 if set_spread: 

434 leg_spread = spread 

435 

436 # obtain the notionals and create a list of notionals to be used for each cashflow 

437 leg_notional_structure = ois_leg_spec.get_NotionalStructure() 

438 # define the number of cashflows 

439 num_of_start_dates = len(ois_leg_spec.start_dates) 

440 

441 notionals = get_projected_notionals( 

442 val_date=val_date, 

443 notional_structure=leg_notional_structure, 

444 start_period=0, 

445 end_period=num_of_start_dates, 

446 fx_forward_curve=fx_forward_curve, 

447 fixing_map=fixing_map, 

448 ) # output is a list of floats 

449 

450 # obtained from ois_leg_spec # expect 2D structure 

451 daily_rate_start_dates = ois_leg_spec.rate_start_dates 

452 # [[0],[1]] # i:start datet fo accrual period j:rate start days within the accrual period # e.g i:accrual over 3M, j:reset daily 

453 daily_rate_end_dates = ois_leg_spec.rate_end_dates 

454 daily_rate_reset_dates = ois_leg_spec.rate_reset_dates 

455 

456 # for each notional, but essentially each accrual period 

457 for i in range(len(notionals)): 

458 

459 notional_start_date = leg_notional_structure.get_pay_date_start(i) 

460 notional_end_date = leg_notional_structure.get_pay_date_end(i) 

461 

462 if notional_start_date: # i.e. not None or empty 

463 # add an initial notional OUTFLOW or not 

464 notional_entry = CashFlow() 

465 notional_entry.pay_date = notional_start_date 

466 

467 if val_date <= notional_entry.pay_date: # TODO recheck this business logic 

468 notional_entry.discount_factor = discount_curve.value(val_date, notional_entry.pay_date) 

469 else: 

470 notional_entry.discount_factor = 0.0 

471 

472 notional_entry.pay_amount = -1 * notionals[i] 

473 notional_entry.present_value = notional_entry.pay_amount * notional_entry.discount_factor 

474 notional_entry.notional_cashflow = True 

475 entries.append(notional_entry) 

476 

477 entry = CashFlow() 

478 entry.start_date = ois_leg_spec.start_dates[i] 

479 entry.end_date = ois_leg_spec.end_dates[i] 

480 entry.pay_date = ois_leg_spec.pay_dates[i] 

481 entry.notional = notionals[i] 

482 entry.interest_yf = dcc.yf(entry.start_date, entry.end_date) 

483 

484 # loop over each resetting datet, i.e. daily to calculate the daily compounded interest to use to calculae coupon for this time period/entry 

485 accDf = 1.0 # accrual discount factor 

486 

487 for j in range(len(daily_rate_start_dates[i])): 

488 

489 rate_yf = rate_dcc.yf(daily_rate_start_dates[i][j], daily_rate_end_dates[i][j]) # should be a day 

490 

491 if daily_rate_reset_dates[i][j] >= val_date: # rate not yet fixed 

492 daily_fwd = forward_curve.value_fwd(val_date, daily_rate_start_dates[i][j], daily_rate_end_dates[i][j]) 

493 daily_rate = leg_spread + (1.0 / daily_fwd - 1.0) / rate_yf 

494 

495 else: # rate already fixed 

496 

497 fixing = fixing_map.get_fixing(udl, daily_rate_reset_dates[i][j]) 

498 

499 if np.isnan(fixing): # math.isnan 

500 if (val_date - daily_rate_reset_dates[i][j]) > fixing_grace_period: 

501 raise RuntimeError(f"Fixing for udl {udl}, date {daily_rate_reset_dates[i][j]} not provided") 

502 else: 

503 if entry.pay_date >= val_date: 

504 # fix value of payment i in future based on current discount curve and a period between valDate and valDate+length of original period (workaround if fixing is not available) 

505 time_delta = entry.end_date - entry.start_date # do we need? we assume daily... 

506 fixing = ( 

507 1.0 / forward_curve.value_fwd(val_date, daily_rate_start_dates[i][j], daily_rate_end_dates[i][j]) - 1.0 

508 ) / rate_yf 

509 

510 daily_rate = leg_spread + fixing 

511 

512 accDf *= 1.0 + daily_rate * rate_yf # compounded daily, hence the multiplication 

513 # accDf now calulated 

514 

515 rate_yf = rate_dcc.yf(daily_rate_start_dates[i][0], daily_rate_end_dates[i][-1]) # total accrual period Yf 

516 entry.rate = (accDf - 1.0) / rate_yf # the -1 gives then just the interest portion of the compounded daily 

517 

518 if val_date <= entry.pay_date: 

519 entry.discount_factor = discount_curve.value(val_date, entry.pay_date) 

520 else: 

521 entry.discount_factor = 0.0 

522 

523 # given rate, notional, and yf, calc interest 

524 entry.interest_amount = entry.notional * entry.rate * entry.interest_yf 

525 entry.pay_amount = entry.interest_amount 

526 

527 # # scale by forward rate???? #TODO 

528 # if val_date <= entry.end_date: 

529 # entry.pay_amount = entry.interest_amount / forward_curve.value_fwd(val_date, entry.end_date, entry.pay_date) 

530 # else: 

531 # if entry.pay_date >= val_date: 

532 # entry.pay_amount = entry.interest_amount / forward_curve.value_fwd(val_date, val_date, entry.pay_date) 

533 # else: 

534 # entry.pay_amount = 0.0 

535 

536 # given total cashflow amount - discount it 

537 # #DEBUG TODO 

538 # print(f"forward rate: {entry.rate}") 

539 # print(f"delta_t: {entry.interest_yf}") 

540 # print(f"discount_factor: {entry.discount_factor}") 

541 entry.present_value = entry.pay_amount * entry.discount_factor 

542 entry.interest_cashflow = True 

543 entries.append(entry) 

544 

545 # # TEMPORARY TEST - inthe case of constant notional structure, but i want a final notional cashflow like a bond 

546 # if i == len(notionals) - 1: # this checks for the last entry 

547 # notional_end_date = entry.end_date 

548 

549 if notional_end_date: 

550 # add an intenional notional INFLOW or not 

551 notional_entry = CashFlow() 

552 notional_entry.pay_date = notional_end_date 

553 

554 if val_date <= notional_entry.pay_date: # TODO recheck this business logic 

555 notional_entry.discount_factor = discount_curve.value(val_date, notional_entry.pay_date) 

556 else: 

557 notional_entry.discount_factor = 0.0 

558 

559 notional_entry.pay_amount = notionals[i] # positive 

560 notional_entry.present_value = notional_entry.pay_amount * notional_entry.discount_factor 

561 notional_entry.notional_cashflow = True 

562 entries.append(notional_entry) 

563 

564 return entries 

565 

566 @staticmethod 

567 def price_leg_pricing_data(val_date, pricing_data: InterestRateSwapLegPricingData_rivapy, param): 

568 """Pricing a single Leg using Pricing Data architecture (not yet fully integrated, to be done once architecture clarified) 

569 

570 Args: 

571 val_date (_type_): _description_ 

572 pricing_data (InterestRateSwapLegPricingData): float leg or base leg pricing data ... 

573 param (_type_): extra parameters (not yet used) 

574 

575 Raises: 

576 ValueError: _description_ 

577 

578 Returns: 

579 _type_: _description_ 

580 """ 

581 # get leg info from PricingData ->spec 

582 leg_spec = pricing_data.spec # what kind of leg? 

583 

584 if leg_spec.leg_type == IrLegType.FIXED: 

585 if pricing_data.desired_rate is not None: 

586 set_rate = True 

587 else: 

588 set_rate = False 

589 cashflow_table = InterestRateSwapPricer._populate_cashflows_fix( 

590 val_date, 

591 pricing_data.spec, 

592 pricing_data.discount_curve, 

593 pricing_data.forward_curve, 

594 pricing_data.fixing_map, 

595 set_rate=set_rate, 

596 desired_rate=pricing_data.desired_rate, 

597 ) 

598 elif leg_spec.leg_type == IrLegType.FLOAT: 

599 if pricing_data.spread is not None: 

600 set_spread = True 

601 else: 

602 set_spread = False 

603 

604 # REMOVE THIS TEST? 

605 if isinstance(pricing_data, InterestRateSwapFloatLegPricingData_rivapy): 

606 cashflow_table = InterestRateSwapPricer._populate_cashflows_float( 

607 val_date, 

608 pricing_data.spec, 

609 pricing_data.discount_curve, 

610 pricing_data.forward_curve, 

611 pricing_data.fixing_curve, 

612 pricing_data.fixing_map, 

613 pricing_data.fixing_grace_period, 

614 set_spread=set_spread, 

615 spread=pricing_data.spread, 

616 ) 

617 

618 else: 

619 raise ValueError("pricing data is not of type 'InterestRateSwapFloatLegPricingData_rivapy' ") # TODO UPDATE 

620 

621 # elif leg_spec.leg_type == IrLegType.OIS: 

622 # InterestRateSwapPricer._populate_cashflows_ois 

623 else: 

624 raise ValueError(f"Unknown leg type {leg_spec.type}") 

625 

626 PV = 0 

627 for entry in cashflow_table: 

628 

629 PV += entry.present_value 

630 

631 # return PV 

632 return PV * pricing_data.fx_rate 

633 

634 @staticmethod 

635 def price_leg( 

636 val_date, 

637 discount_curve: DiscountCurve, 

638 forward_curve: DiscountCurve, 

639 fxForward_curve: DiscountCurve, 

640 spec: _Union[IrFixedLegSpecification, IrFloatLegSpecification], 

641 fixing_map: FixingTable = None, 

642 fixing_grace_period: float = 0, 

643 pricing_params: dict = {"set_rate": False, "desired_rate": 1.0}, 

644 ): 

645 """Price a leg of a IR swap making distinctions for floating and fixed legs as well as OIS legs. 

646 

647 Args: 

648 val_date (_type_): Valuation date 

649 discount_curve (DiscountCurve): discount curve for discounting cashflows 

650 forward_curve (DiscountCurve): forward curve for the forward rate calculations 

651 fxForward_curve (DiscountCurve): fx forward curve for currency conversions for applicable instruments (not yet implemented) 

652 spec (_Union[IrFixedLegSpecification, IrFloatLegSpecification]): Specification object for the leg to be priced 

653 fixing_map (FixingTable, optional): Historical fixing data (not yet implemented). Defaults to None. 

654 fixing_grace_period (float, optional): . Defaults to 0. 

655 pricing_params (_type_, optional): Additional parameters needed for swap pricing. Defaults to {"set_rate": False, "desired_rate": 1.0}. 

656 

657 Raises: 

658 ValueError: if unknown leg type is passed 

659 

660 Returns: 

661 float: return the aggregated Present value of the leg of the swap. 

662 """ 

663 

664 # get leg info from PricingData ->spec 

665 leg_spec = spec # what kind of leg? 

666 

667 if leg_spec.leg_type == IrLegType.FIXED: 

668 # print("generating cashflow table for FIXED leg") 

669 set_rate = pricing_params["set_rate"] # if we want to set the rate 

670 desired_rate = pricing_params["desired_rate"] 

671 cashflow_table = InterestRateSwapPricer._populate_cashflows_fix( 

672 val_date, leg_spec, discount_curve, forward_curve, fixing_map, set_rate, desired_rate 

673 ) 

674 elif leg_spec.leg_type == IrLegType.FLOAT: 

675 # print("generating cashflow table for FLOAT leg") 

676 cashflow_table = InterestRateSwapPricer._populate_cashflows_float( 

677 val_date, leg_spec, discount_curve, forward_curve, fxForward_curve, fixing_map, fixing_grace_period 

678 ) # TODO implement spread 

679 

680 elif leg_spec.leg_type == IrLegType.OIS: 

681 cashflow_table = InterestRateSwapPricer._populate_cashflows_ois( 

682 val_date, leg_spec, discount_curve, forward_curve, fxForward_curve, fixing_map, fixing_grace_period 

683 ) 

684 # TODO implement spread 

685 # 2025.10.28 As of now, analytical OIS swap rate calculation does not require full cashflow table as we can calculate 

686 # the fair swap rate directly from discount factors. The method is kept as a fallback for more complex OIS legs that may require full cashflow simulation. 

687 

688 else: 

689 raise ValueError(f"Unknown leg type {leg_spec.type}") 

690 

691 # inside price_leg just after cashflow construction 

692 # - DEBUG 11.2025 

693 # print("\n=== DEBUG FLOAT LEG ===") 

694 # total_pv = 0.0 

695 # for i, cf in enumerate(cashflow_table): 

696 # print( 

697 # f"[{i}] pay_date={cf.pay_date}, " 

698 # f"notional={cf.notional:.2f}, " 

699 # f"rate={cf.rate:.6f}, " 

700 # f"yf={cf.interest_yf:.6f}, " 

701 # f"pay_amt={cf.pay_amount:.6f}, " 

702 # f"DF={cf.discount_factor:.6f}, " 

703 # f"PV={cf.present_value:.6f}" 

704 # ) 

705 # total_pv += cf.present_value 

706 # print(f"--- Total leg PV = {total_pv:.6f}\n") 

707 

708 PV = 0 

709 # print("----------------------------------------------------------") 

710 # print("DEBUG: price leg pv values") # DEBUG TODO REMOVE 

711 for entry in cashflow_table: 

712 # print_member_values(entry) 

713 # print(entry.present_value) 

714 PV += entry.present_value 

715 

716 return PV 

717 # return PV* pricing_data.fx_rate 

718 

719 def price(self): 

720 """price a full swap, with a pay leg and a receive leg in the context of 

721 pricing container and pricer having been initlialized with required inputs.""" 

722 

723 # # PricingResults& results, -> implement also 

724 # val_date = self._val_date # const boost::posix_time::ptime& valDate, 

725 # discount_curve_pay_leg = self. # const std::shared_ptr<const DiscountCurve>& discountCurvePayLeg, 

726 # discount_curve_receive_leg= self. # const std::shared_ptr<const DiscountCurve>& discountCurveReceiveLeg, 

727 # fixing_curve_pay_leg = # const std::shared_ptr<const DiscountCurve>& fixingCurvePayLeg, 

728 # fixing_curve_receive_leg= # const std::shared_ptr<const DiscountCurve>& fixingCurveReceiveLeg, 

729 # fx_fwd_curve_pay_leg = # const std::shared_ptr<const FxForwardCurve>& fxForwardCurvePayLeg, 

730 # fx_fwd_curve_receive_leg = # const std::shared_ptr<const FxForwardCurve>& fxForwardCurveReceiveLeg, 

731 # interest_rate_swap_spec = # const std::shared_ptr<const InterestRateSwapSpecification>& spec, 

732 # pricing_request = # const PricingRequest& pricingRequest, 

733 # pricing_param = # std::shared_ptr<const InterestRateSwapPricingParameter> pricingParam, 

734 # fixing_map = # std::shared_ptr<const FixingTable> fixingMap, 

735 # fx_pay_leg = # double fxPayLeg, 

736 # fx_receive_leg = # double fxReceiveLeg) 

737 

738 # unit in days 

739 fixing_grace_period = self._pricing_param["fixing_grace_period"] 

740 

741 # TODO check structure again.... 

742 # price = InterestRateSwapPricer.price_leg(valDate, discountCurveReceiveLeg, fixingCurveReceiveLeg, fxForwardCurveReceiveLeg, spec->getReceiveLeg(), fixingMap, fixingGracePeriod) * fxReceiveLeg; 

743 # price -= InterestRateSwapPricer.price_leg(valDate, discountCurvePayLeg, fixingCurvePayLeg, fxForwardCurvePayLeg, spec->getPayLeg(), fixingMap, fixingGracePeriod) * fxPayLeg; 

744 

745 # 

746 aggregated_price = InterestRateSwapPricer.price_leg( 

747 self._val_date, 

748 discount_curve=self._discount_curve_receive_leg, 

749 forward_curve=self._fixing_curve_receive_leg, 

750 fxForward_curve=self._fx_fwd_curve_receive_leg, 

751 spec=self._receive_leg, 

752 fixing_map=self._fixing_map, 

753 fixing_grace_period=fixing_grace_period, 

754 ) 

755 aggregated_price -= InterestRateSwapPricer.price_leg( 

756 self._val_date, 

757 discount_curve=self._discount_curve_pay_leg, 

758 forward_curve=self._fixing_curve_receive_leg, 

759 fxForward_curve=self._fx_fwd_curve_receive_leg, 

760 spec=self._pay_leg, 

761 fixing_map=self._fixing_map, 

762 fixing_grace_period=fixing_grace_period, 

763 ) 

764 # results.setPrice(price); 

765 # aggregated_price is already discounted to present value inside the price_leg method 

766 return aggregated_price 

767 

768 # static method also then? 

769 @staticmethod 

770 def compute_swap_rate( 

771 ref_date: _Union[date, datetime], 

772 discount_curve: DiscountCurve, 

773 fixing_curve: DiscountCurve, 

774 float_leg: IrFloatLegSpecification, 

775 fixed_leg: IrFixedLegSpecification, 

776 fixing_map: FixingTable = None, 

777 pricing_params: dict = {"fixing_grace_period": 0.0}, 

778 ) -> float: 

779 """To calculate the fair swap rate, i.e. the fixed rate(r*) such that 

780 PV_fixed(r*) = PV_float 

781 where 

782 PV_fixed(r*) = r* * Annuity 

783 where 

784 Annuity = sum(notional_i * DF_i * YF_i*) 

785 

786 Fast path for OIS floating leg: use analytical OIS formula 

787 so we do not simulate daily compounding each time inside the solver. 

788 

789 Args: 

790 ref_date (_Union[date, datetime]): reference date 

791 discount_curve (DiscountCurve): discoutn curve to determine present value 

792 fixing_curve (DiscountCurve): curve used for fwd rates for the floating leg 

793 float_leg (IrFloatLegSpecification): IR swap float leg specification 

794 fixed_leg (IrFixedLegSpecification): IR swap fix leg specification 

795 fixing_map (FixingTable, optional): historical fixing data (TODO). Defaults to None. 

796 fixing_grace_period (int, optional): . Defaults to 0. 

797 

798 Returns: 

799 float: fair rate for the swap 

800 """ 

801 

802 fixing_grace_period = pricing_params["fixing_grace_period"] 

803 

804 # ----------------------------- 

805 # Fast analytical OIS path: 

806 # - If the floating leg is OIS, compute the fixed rate (par swap rate) 

807 # analytically using discount factors (P) on the fixed leg payment dates: 

808 # R = (1 - P(T_N)) / sum_i alpha_i * P(T_i) 

809 # - where alpha_i is the year fraction for the fixed leg payment period i (accrual factor) 

810 # - This is equivalent to pricing the compounded overnight floating leg. 

811 # ----------------------------- 

812 try: 

813 if hasattr(float_leg, "leg_type") and float_leg.leg_type == IrLegType.OIS: 

814 

815 return InterestRateSwapPricer.compute_swap_rate_ois_analytical(ref_date, discount_curve, fixing_curve, float_leg, fixed_leg) 

816 

817 except Exception: 

818 # If anything unexpected (missing attributes) happens, fall back to generic route 

819 logger.debug("Fast OIS path failed/fell through; using generic pricing path.") 

820 

821 # if fixed_leg.obj_id == "OIS_2M_fixed_leg3": # DEBUG TEST 2025 

822 # logger.debug("OIS_2M_fixed_leg3 detected") 

823 

824 # ----------------------------- 

825 # generic path: 

826 float_leg_PV = InterestRateSwapPricer.price_leg(ref_date, discount_curve, fixing_curve, None, float_leg, fixing_map, fixing_grace_period) 

827 fixed_leg_annuity = InterestRateSwapPricer.price_leg( 

828 ref_date, discount_curve, fixing_curve, None, fixed_leg, fixing_map, fixing_grace_period, pricing_params 

829 ) 

830 

831 if fixed_leg_annuity == 0: 

832 logger.error("Fixed leg annuity is zero, cannot compute swap rate!") 

833 

834 return float_leg_PV / fixed_leg_annuity 

835 

836 # TODO 

837 def compute_swap_spread(self): 

838 # ref date 

839 # discount curve pay leg 

840 # forward curve pay leg 

841 # fx forward curve pay leg 

842 # discount curve rec leg 

843 # forward curve rec leg 

844 # fx forward curve rec leg 

845 # pay leg spec 

846 # rec leg spec 

847 # fixing map 

848 # extra param: InterestRateSwapPricingParameter 

849 # fx pay 

850 # fx rec 

851 # fixing grace period comes from the extra param 

852 

853 # convert all prices into the currency of the swap 

854 pv_pay = 0 

855 pv_rec_s0 = 1 

856 py_rec_s1 = 0 

857 # pv_pay = fxPay * price_leg(refDate, discountCurvePay, forwardCurvePay, fxForwardCurvePay, floatLegPay, fixingMap, fixingGracePeriod); 

858 # pv_rec_s0 = fxRec * price_leg(refDate, discountCurveRec, forwardCurveRec, fxForwardCurveRec, floatLegRec, fixingMap, fixingGracePeriod, true, 0.);set_spread=True, desired_spread = 0.0 #for float it is spread 

859 # py_rec_s1 = fxRec * price_leg(refDate, discountCurveRec, forwardCurveRec, fxForwardCurveRec, floatLegRec, fixingMap, fixingGracePeriod, true, 1.);set_spread=True, desired_spread = 1.0 

860 # note that the current price leg doesnt take spreads as options for the moment, it is left as a # TODO for now... 

861 return (pv_pay - pv_rec_s0) / (py_rec_s1 - pv_rec_s0) 

862 

863 # # TODO 

864 # double InterestRateSwapPricer::computeBasisSpread( 

865 # const boost::posix_time::ptime& refDate, 

866 # const std::shared_ptr<const DiscountCurve>& discountCurve, 

867 # const std::shared_ptr<const DiscountCurve>& receiveLegFixingCurve, 

868 # const std::shared_ptr<const DiscountCurve>& payLegFixingCurve, 

869 # const std::shared_ptr<const IrFloatLegSpecification>& receiveLeg, 

870 # const std::shared_ptr<const IrFloatLegSpecification>& payLeg, 

871 # const std::shared_ptr<const IrFixedLegSpecification>& fixedLeg, 

872 # std::shared_ptr<const FixingTable> fixingMap, 

873 # std::shared_ptr<const InterestRateSwapPricingParameter> param 

874 # ) 

875 # { 

876 # const boost::posix_time::time_duration& fixingGracePeriod = param->fixingGracePeriod; 

877 # double receiveLegPV = price(refDate, discountCurve, receiveLegFixingCurve, nullptr, receiveLeg, fixingMap, fixingGracePeriod); 

878 # double payLegPV = price(refDate, discountCurve, payLegFixingCurve, nullptr, payLeg, fixingMap, fixingGracePeriod); 

879 # double fixedLegPV01 = price(refDate, discountCurve, std::shared_ptr<const DiscountCurve>(), nullptr, fixedLeg, 

880 # fixingMap, fixingGracePeriod, true, 1.); # the one there means its the annuity again, the true means use the given rate (1.0) for fixed rate 

881 # return (receiveLegPV - payLegPV) / fixedLegPV01; 

882 # } 

883 @staticmethod 

884 def compute_basis_spread( 

885 ref_date: _Union[date, datetime], 

886 discount_curve: DiscountCurve, 

887 payLegFixingCurve: DiscountCurve, 

888 receiveLegFixingCurve: DiscountCurve, 

889 pay_leg: IrFloatLegSpecification, 

890 receive_leg: IrFloatLegSpecification, 

891 spread_leg: IrFixedLegSpecification, 

892 fixing_map: FixingTable = None, 

893 pricing_params: dict = {"fixing_grace_period": 0.0}, 

894 ) -> float: 

895 # ref date 

896 # discount curve 

897 # receiveLegFixingCurve 

898 # payLegFixingCurve 

899 # receiveLeg spec #floatIRspec 

900 # payLeg spec #floatIRspec 

901 # fixed leg spec # fixedIRspec 

902 # fixing grace period comes from the extra param 

903 

904 # noFxFowardCruve, set to null 

905 fixing_grace_period = pricing_params["fixing_grace_period"] 

906 

907 # price_leg(refDate, discountCurve, receiveLegFixingCurve, nullptr, receiveLeg, fixingMap, fixingGracePeriod) 

908 receive_leg_PV = InterestRateSwapPricer.price_leg( 

909 ref_date, discount_curve, receiveLegFixingCurve, None, receive_leg, fixing_map, fixing_grace_period 

910 ) 

911 # price_leg(refDate, discountCurve, payLegFixingCurve, nullptr, payLeg, fixingMap, fixingGracePeriod); 

912 pay_leg_PV = InterestRateSwapPricer.price_leg(ref_date, discount_curve, payLegFixingCurve, None, pay_leg, fixing_map, fixing_grace_period) 

913 # price_leg(refDate, discountCurve, std::shared_ptr<const DiscountCurve>(), nullptr, fixedLeg, fixingMap, fixingGracePeriod, true, 1.); 

914 fixed_leg_PV01 = InterestRateSwapPricer.price_leg( 

915 ref_date, discount_curve, payLegFixingCurve, None, spread_leg, fixing_map, fixing_grace_period, pricing_params 

916 ) # should not need a fixing curve since its a fixed leg, we default to the pay leg 

917 

918 # # for fixed, we are setting the rate to 1 

919 

920 # # - DEBUG 11.2025 

921 # print("INSIDE COMPUTE BASIS SPREAD") 

922 # print("receive_leg_PV:", receive_leg_PV) 

923 # print("pay_leg_PV:", pay_leg_PV) 

924 # print("fixed_leg_PV01:", fixed_leg_PV01) 

925 # print("INSIDE COMPUTE BASIS SPREAD: receive_leg_PV:", receive_leg_PV, "pay_leg_PV:", pay_leg_PV, "fixed_leg_PV01:", fixed_leg_PV01) 

926 

927 if abs(fixed_leg_PV01) < 1e-12: 

928 raise Exception(f"fixed_leg_PV01 too small ({fixed_leg_PV01}), cannot divide — check fixed leg annuity / conventions") 

929 # - 

930 

931 return (receive_leg_PV - pay_leg_PV) / fixed_leg_PV01 

932 

933 @staticmethod 

934 def compute_swap_rate_ois_analytical( 

935 ref_date: datetime, 

936 discount_curve: DiscountCurve, 

937 forward_curve: DiscountCurve, 

938 float_leg: IrOISLegSpecification, 

939 fixed_leg: IrFixedLegSpecification, 

940 ) -> float: 

941 """ 

942 Computes the fair (par) fixed rate for an Overnight Indexed Swap (OIS) 

943 using an analytical shortcut based on discount factors. 

944 

945 This method assumes that the floating leg is a compounded overnight leg 

946 and that, under standard OIS discounting, the present value of the floating 

947 leg can be derived directly from the discount curve without simulating 

948 daily compounding. Much faster than simulating daily compounding for each. 

949 [https://btrm.org/wp-content/uploads/2024/03/BTRM-WP15_SOFR-OIS-Curve-Construction_Dec-2020.pdf] 

950 

951 The par OIS rate is computed as: 

952 

953 R = (sum_i N_i * [P(T_i) - P(T_{i+1})]) / (sum_i N_i * alpha_i * P(T_{i+1})) 

954 

955 where: 

956 - P(T_i): discount factor at period start/end, 

957 - alpha_i: accrual year fraction on the fixed leg, 

958 - N_i: notional applicable for that period. 

959 

960 Assumptions: 

961 - The floating leg is fully collateralized and discounted on the 

962 same OIS curve. 

963 - The compounded overnight rate is implied by the discount factors. 

964 - No spread, lag, or convexity correction is applied. 

965 - Notionals and accrual conventions are consistent with the given curves. 

966 - Forward curve is used only for projected notionals or FX conversions, 

967 not for rate projection. 

968 - The fixed leg is currently not explictily used as it assumes that it has 

969 the same notional structure and payment dates as the floating leg, which 

970 usually the case. 

971 

972 

973 Raises: 

974 ValueError: If the computed annuity (denominator) is zero, 

975 indicating invalid leg setup or inconsistent inputs. 

976 

977 Returns: 

978 float: The par fixed rate that equates PV_fixed = PV_float (analytical) (as a decimal, e.g. 0.025 for 2.5%). 

979 """ 

980 

981 dcc = DayCounter(discount_curve.daycounter) 

982 num_periods = len(float_leg.start_dates) 

983 

984 # get notionals using the same logic as your existing daily compounding path 

985 leg_notional_structure = float_leg.get_NotionalStructure() 

986 notionals = get_projected_notionals( 

987 val_date=ref_date, 

988 notional_structure=leg_notional_structure, 

989 start_period=0, 

990 end_period=num_periods, 

991 fx_forward_curve=forward_curve, 

992 fixing_map=FixingTable(), 

993 ) 

994 

995 start_dates = float_leg.start_dates 

996 end_dates = float_leg.end_dates 

997 # pay_dates = float_leg.pay_dates 

998 

999 pv_float = 0.0 

1000 annuity = 0.0 

1001 

1002 # use analytical compounding shortcut: 

1003 # for each coupon period, the compounded OIS rate ≈ (DF_start / DF_end - 1) / YF 

1004 for i in range(num_periods): 

1005 yf = dcc.yf(start_dates[i], end_dates[i]) 

1006 df_start = discount_curve.value(ref_date, start_dates[i]) 

1007 df_end = discount_curve.value(ref_date, end_dates[i]) 

1008 

1009 # period return implied by discount factors 

1010 # period_rate = (df_start / df_end - 1.0) / yf # for DEBUG 

1011 

1012 # PV of floating leg cashflow = notional * (DF_start - DF_end) 

1013 pv_float += notionals[i] * (df_start - df_end) 

1014 

1015 # annuity = sum of DF_end * yf * notional (denominator in swap rate) 

1016 annuity += notionals[i] * yf * df_end 

1017 

1018 if annuity == 0: 

1019 raise ValueError("Zero annuity in OIS analytical pricing") 

1020 

1021 # fair fixed rate = PV_float / Annuity 

1022 return pv_float / annuity 

1023 

1024 

1025######################################################################### 

1026# FUNCTIONS 

1027def get_projected_notionals( 

1028 val_date: _Union[date, datetime], 

1029 notional_structure: NotionalStructure, 

1030 start_period: int, 

1031 end_period: int, 

1032 fx_forward_curve: DiscountCurve, 

1033 fixing_map: FixingTable = None, 

1034) -> _List[float]: 

1035 """ 

1036 Generate a list with projected notionals, using FX forward curve if applicable, or fixing table(not yet implemented). 

1037 

1038 Args: 

1039 val_date (datetime): The valuation date. 

1040 notional_structure (NotionalStructure): The notional structure class object. 

1041 start_period (int): Start index of the period range. 

1042 end_period (int): End index of the period range (exclusive). 

1043 fx_forward_curve (FxForwardCurve): Required for resetting notionals. 

1044 fixing_map (FixingTable): Not used (yet). 

1045 """ 

1046 

1047 result = [] 

1048 

1049 # Check if this is a resetting notional structure 

1050 if isinstance(notional_structure, ResettingNotionalStructure): 

1051 if fx_forward_curve is None: 

1052 raise ValueError("No FX forward curve provided for resetting leg!") 

1053 

1054 for i in range(start_period, end_period): 

1055 fixing_date = notional_structure.get_fixing_date(i) 

1056 fx = fx_forward_curve.value(val_date, fixing_date) 

1057 result.append(notional_structure.get_amount(i) * fx) 

1058 elif isinstance(notional_structure, ConstNotionalStructure): 

1059 for i in range(start_period, end_period): 

1060 # print(i) 

1061 # print(notional_structure.get_amount(i)) 

1062 result.append(notional_structure.get_amount(0)) 

1063 else: 

1064 for i in range(start_period, end_period): 

1065 # print(i) 

1066 # print(notional_structure.get_amount(i)) 

1067 result.append(notional_structure.get_amount(i)) 

1068 

1069 return result 

1070 

1071 

1072# getPricingData 

1073 

1074 

1075# difference between func price and priceImpl??? 

1076 

1077 

1078# computeSwapSpread 

1079 

1080 

1081if __name__ == "__main__": 

1082 pass 

1083 # InterestRateSwapPricer