Coverage for tests / test_ir_swap.py: 98%

473 statements  

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

1import unittest 

2import unittest.mock 

3from datetime import datetime, timedelta 

4from dateutil.relativedelta import relativedelta 

5import numpy as np 

6import math 

7 

8 

9from rivapy.marketdata.curves import DiscountCurve 

10from rivapy.instruments.ir_swap_specification import ( 

11 IrSwapLegSpecification, 

12 IrFixedLegSpecification, 

13 IrFloatLegSpecification, 

14 IrOISLegSpecification, 

15 InterestRateSwapSpecification, 

16) 

17from rivapy.instruments.components import ConstNotionalStructure, ResettingNotionalStructure 

18from rivapy.tools.enums import DayCounterType, IrLegType, Currency, RollConvention, SecuritizationLevel, Rating, InterpolationType, ExtrapolationType 

19from rivapy.pricing.interest_rate_swap_pricing import get_projected_notionals, InterestRateSwapPricer 

20from rivapy.instruments.components import CashFlow 

21from rivapy.tools.datetools import DayCounter 

22 

23# of class InterestRateSwapPricer as Pricer 

24# #non-static methods are: 

25# price 

26# #static methods are 

27# _populate_cashflow_fixed 

28# _populate_cashflow_float 

29# _populate_cashflow_ois 

30# price_leg_pricing_data # not fully implemented - test later 

31# price_leg 

32# compute_swap_rate 

33# compute_swap_spread # not yett implemented 

34# compute_basis_spread # not yet implemented 

35 

36 

37class TestIrSwapLegSpecification(unittest.TestCase): 

38 def setUp(self): 

39 """General setup for tests with some default values e.g., dates and notional""" 

40 self.start_dates = [datetime(2024, 1, 1)] 

41 self.end_dates = [datetime(2025, 1, 1)] 

42 self.pay_dates = [datetime(2025, 1, 1)] 

43 self.notional = 1000.0 

44 

45 def test_init_and_properties(self): 

46 """Test for the initialization and properties of the IrSwapLegSpecification class""" 

47 leg = IrSwapLegSpecification( 

48 obj_id="leg1", 

49 notional=self.notional, 

50 start_dates=self.start_dates, 

51 end_dates=self.end_dates, 

52 pay_dates=self.pay_dates, 

53 currency="EUR", 

54 day_count_convention=DayCounterType.ThirtyU360, 

55 ) 

56 self.assertEqual(leg.obj_id, "leg1") 

57 self.assertEqual(leg.currency, "EUR") 

58 self.assertEqual(leg.start_dates, self.start_dates) 

59 self.assertEqual(leg.end_dates, self.end_dates) 

60 self.assertEqual(leg.pay_dates, self.pay_dates) 

61 self.assertIsInstance(leg.notional_structure, ConstNotionalStructure) 

62 

63 def test_notional_structure_setter(self): 

64 """Test for use of NotionalStructure class used in IrSwapLegSpecification. 

65 For more spepcific tests of NotionalStructure, see its own test class. 

66 

67 #TODO add uses cases of different notional structures 

68 """ 

69 ns = ConstNotionalStructure(5000.0) 

70 leg = IrSwapLegSpecification( 

71 obj_id="leg2", 

72 notional=ns, 

73 start_dates=self.start_dates, 

74 end_dates=self.end_dates, 

75 pay_dates=self.pay_dates, 

76 currency="USD", 

77 ) 

78 self.assertIs(leg.notional_structure, ns) 

79 

80 

81class TestIrFixedLegSpecification(unittest.TestCase): 

82 """Similar to TestIrSwapLegSpecification but for fixed leg specific 

83 

84 Args: 

85 unittest (_type_): _description_ 

86 """ 

87 

88 def setUp(self): 

89 self.start_dates = [datetime(2024, 1, 1)] 

90 self.end_dates = [datetime(2025, 1, 1)] 

91 self.pay_dates = [datetime(2025, 1, 1)] 

92 self.notional = 1000.0 

93 

94 def test_fixed_leg(self): 

95 leg = IrFixedLegSpecification( 

96 fixed_rate=0.01, 

97 obj_id="fixed_leg", 

98 notional=self.notional, 

99 start_dates=self.start_dates, 

100 end_dates=self.end_dates, 

101 pay_dates=self.pay_dates, 

102 currency="EUR", 

103 ) 

104 self.assertEqual(leg.leg_type, IrLegType.FIXED) 

105 self.assertAlmostEqual(leg.fixed_rate, 0.01) 

106 self.assertEqual(leg.udl_id, "") 

107 

108 

109class TestIrFloatLegSpecification(unittest.TestCase): 

110 """Similar to TestIrSwapLegSpecification but for float leg specific 

111 

112 Args: 

113 unittest (_type_): _description_ 

114 """ 

115 

116 def setUp(self): 

117 self.start_dates = [datetime(2024, 1, 1)] 

118 self.end_dates = [datetime(2025, 1, 1)] 

119 self.pay_dates = [datetime(2025, 1, 1)] 

120 self.reset_dates = [datetime(2024, 1, 1)] 

121 self.rate_start_dates = [datetime(2024, 1, 1)] 

122 self.rate_end_dates = [datetime(2025, 1, 1)] 

123 self.notional = 1000.0 

124 

125 def test_float_leg(self): 

126 leg = IrFloatLegSpecification( 

127 obj_id="float_leg", 

128 notional=self.notional, 

129 reset_dates=self.reset_dates, 

130 start_dates=self.start_dates, 

131 end_dates=self.end_dates, 

132 rate_start_dates=self.rate_start_dates, 

133 rate_end_dates=self.rate_end_dates, 

134 pay_dates=self.pay_dates, 

135 currency="USD", 

136 udl_id="SOFR", 

137 fixing_id="SOFR_FIX", 

138 spread=0.002, 

139 ) 

140 self.assertEqual(leg.leg_type, IrLegType.FLOAT) 

141 self.assertEqual(leg.udl_id, "SOFR") 

142 self.assertEqual(leg.fixing_id, "SOFR_FIX") 

143 self.assertAlmostEqual(leg.spread, 0.002) 

144 self.assertEqual(leg.reset_dates, self.reset_dates) 

145 

146 

147class TestInterestRateSwapSpecification(unittest.TestCase): 

148 """Full swap with both leg tests 

149 

150 Args: 

151 unittest (_type_): _description_ 

152 """ 

153 

154 def setUp(self): 

155 self.start_dates = [datetime(2024, 1, 1)] 

156 self.end_dates = [datetime(2025, 1, 1)] 

157 self.pay_dates = [datetime(2025, 1, 1)] 

158 self.notional = 1000.0 

159 self.fixed_leg = IrFixedLegSpecification( 

160 fixed_rate=0.01, 

161 obj_id="fixed_leg", 

162 notional=self.notional, 

163 start_dates=self.start_dates, 

164 end_dates=self.end_dates, 

165 pay_dates=self.pay_dates, 

166 currency="EUR", 

167 ) 

168 self.float_leg = IrFloatLegSpecification( 

169 obj_id="float_leg", 

170 notional=self.notional, 

171 reset_dates=self.start_dates, 

172 start_dates=self.start_dates, 

173 end_dates=self.end_dates, 

174 rate_start_dates=self.start_dates, 

175 rate_end_dates=self.end_dates, 

176 pay_dates=self.pay_dates, 

177 currency="USD", 

178 udl_id="SOFR", 

179 fixing_id="SOFR_FIX", 

180 spread=0.002, 

181 ) 

182 

183 def test_swap_specification(self): 

184 issue_date = datetime(2024, 1, 1) 

185 maturity_date = datetime(2025, 1, 1) 

186 spec = InterestRateSwapSpecification( 

187 obj_id="swap1", 

188 notional=self.notional, 

189 issue_date=issue_date, 

190 maturity_date=maturity_date, 

191 pay_leg=self.fixed_leg, 

192 receive_leg=self.float_leg, 

193 currency="EUR", 

194 day_count_convention=DayCounterType.ThirtyU360, 

195 business_day_convention=RollConvention.FOLLOWING, 

196 issuer="TestIssuer", 

197 securitization_level=SecuritizationLevel.NONE, 

198 rating=Rating.NONE, 

199 ) 

200 self.assertEqual(spec.obj_id, "swap1") 

201 self.assertEqual(spec.issue_date, issue_date) 

202 self.assertEqual(spec.maturity_date, maturity_date) 

203 self.assertEqual(spec.pay_leg, self.fixed_leg) 

204 self.assertEqual(spec.receive_leg, self.float_leg) 

205 self.assertEqual(spec.currency, "EUR") 

206 self.assertEqual(spec.issuer, "TestIssuer") 

207 self.assertEqual(spec.securitization_level, SecuritizationLevel.to_string(SecuritizationLevel.NONE)) 

208 self.assertEqual(spec.rating, Rating.to_string(Rating.NONE)) 

209 self.assertIsInstance(spec.notional_structure, ConstNotionalStructure) 

210 

211 def test_get_fixed_and_float_leg(self): 

212 spec = InterestRateSwapSpecification( 

213 obj_id="swap2", 

214 notional=self.notional, 

215 issue_date=datetime(2024, 1, 1), 

216 maturity_date=datetime(2025, 1, 1), 

217 pay_leg=self.fixed_leg, 

218 receive_leg=self.float_leg, 

219 ) 

220 self.assertIs(spec.get_fixed_leg(), self.fixed_leg) 

221 self.assertIs(spec.get_float_leg(), self.float_leg) 

222 

223 def test_get_fixed_leg_error(self): 

224 # Both legs fixed should raise 

225 fixed_leg2 = IrFixedLegSpecification( 

226 fixed_rate=0.01, 

227 obj_id="fixed_leg2", 

228 notional=self.notional, 

229 start_dates=self.start_dates, 

230 end_dates=self.end_dates, 

231 pay_dates=self.pay_dates, 

232 currency="EUR", 

233 ) 

234 spec = InterestRateSwapSpecification( 

235 obj_id="swap3", 

236 notional=self.notional, 

237 issue_date=datetime(2024, 1, 1), 

238 maturity_date=datetime(2025, 1, 1), 

239 pay_leg=self.fixed_leg, 

240 receive_leg=fixed_leg2, 

241 ) 

242 with self.assertRaises(ValueError): 

243 spec.get_fixed_leg() 

244 

245 def test_get_float_leg_error(self): 

246 # Both legs fixed should raise 

247 fixed_leg2 = IrFixedLegSpecification( 

248 fixed_rate=0.01, 

249 obj_id="fixed_leg2", 

250 notional=self.notional, 

251 start_dates=self.start_dates, 

252 end_dates=self.end_dates, 

253 pay_dates=self.pay_dates, 

254 currency="EUR", 

255 ) 

256 spec = InterestRateSwapSpecification( 

257 obj_id="swap4", 

258 notional=self.notional, 

259 issue_date=datetime(2024, 1, 1), 

260 maturity_date=datetime(2025, 1, 1), 

261 pay_leg=self.fixed_leg, 

262 receive_leg=fixed_leg2, 

263 ) 

264 with self.assertRaises(ValueError): 

265 spec.get_float_leg() 

266 

267 

268####################################################### 

269# Tests for Pricing 

270 

271 

272class TestIRSwapSpecificationPricing(unittest.TestCase): 

273 """Test suite for pricing functionality of interest rate swaps. 

274 Note OIS is modelled after a plain IRS except with its own pricing function 

275 for the OIS/float leg. 

276 

277 Args: 

278 unittest (_type_): _description_ 

279 """ 

280 

281 def setUp(self): 

282 """Setup of default values used throughout pricing""" 

283 # by hand calculations to compare to 

284 # #discount curve 

285 # ttm = [0.5, 1.0, 1.5] 

286 # rates = [0.1, 0.105, 0.11] 

287 # N = 100 # Notional 

288 # m = 2 # compounding frequency i.e. every half year 

289 

290 # ref_date = datetime(2017, 1, 1) 

291 

292 # days_to_maturity = [180, 360, 540] 

293 # dates = [ref_date + timedelta(days=d) for d in days_to_maturity] 

294 # df = [math.exp(-r * t) for r, t in zip(rates, ttm)] 

295 

296 # Discount curve - we use these discount factors to get the present values of both the fixed and floating leg as well as 

297 object_id = "TEST_DC" 

298 refdatedc = datetime(2017, 1, 1) 

299 days_to_maturity = [180, 360, 540] 

300 dates_dc = [refdatedc + timedelta(days=d) for d in days_to_maturity] 

301 # discount factors from constant rate 

302 rates = [0.10, 0.105, 0.11] 

303 df = [math.exp(-r * d / 360) for r, d in zip(rates, days_to_maturity)] 

304 dc = DiscountCurve( 

305 id=object_id, refdate=refdatedc, dates=dates_dc, df=df, interpolation=InterpolationType.LINEAR, extrapolation=ExtrapolationType.LINEAR 

306 ) 

307 

308 dcc = "Act360" 

309 ccy = "EUR" 

310 # Create the vectors defining the statdates, enddates, paydates and reset dates 

311 refdate = datetime(2017, 1, 1) 

312 days_to_maturity = [0, 180, 360, 540] 

313 dates = [refdate + timedelta(d) for d in days_to_maturity] 

314 

315 startdates = dates[:-1] 

316 enddates = dates[1:] 

317 paydates = enddates 

318 resetdates = startdates 

319 

320 fixed_leg = IrFixedLegSpecification( 

321 fixed_rate=0.08, 

322 obj_id="dummy_fixed_leg", 

323 notional=100.0, 

324 start_dates=startdates, 

325 end_dates=enddates, 

326 pay_dates=paydates, 

327 currency=ccy, 

328 day_count_convention=dcc, 

329 ) 

330 

331 spread = 0.00 

332 self.notional_amount = 100 

333 ns = ConstNotionalStructure(self.notional_amount) 

334 

335 float_leg = IrFloatLegSpecification( 

336 obj_id="dummy_float_leg", 

337 notional=ns, 

338 reset_dates=resetdates, 

339 start_dates=startdates, 

340 end_dates=enddates, 

341 rate_start_dates=startdates, 

342 rate_end_dates=enddates, 

343 pay_dates=paydates, 

344 currency=ccy, 

345 udl_id="test_udl_id", 

346 fixing_id="test_fixing_id", 

347 day_count_convention=dcc, 

348 spread=spread, 

349 ) 

350 

351 # in my other example, was an OIS with 6M tenor, and 6M maturity. with only 1 "interval" 

352 # here we will have more if using these startdates and enddates 

353 res = IrOISLegSpecification.ois_scheduler_2D(startdates, enddates) 

354 daily_rate_start_dates = res[0] # 2D list: coupon i -> list of daily starts 

355 daily_rate_end_dates = res[1] # 2D list: coupon i -> list of daily ends 

356 daily_rate_reset_dates = res[2] # 2D list: coupon i -> list of reset dates 

357 ois_pay_dates = res[3] 

358 

359 ois_leg = IrOISLegSpecification( 

360 obj_id="dummy_ois_leg", 

361 notional=ns, 

362 rate_reset_dates=daily_rate_reset_dates, # reflects the overnight nature of OIS 

363 start_dates=startdates, # same as before 

364 end_dates=enddates, # same as before 

365 rate_start_dates=daily_rate_start_dates, # reflects the overnight nature of OIS 

366 rate_end_dates=daily_rate_end_dates, # reflects the overnight nature of OIS 

367 pay_dates=ois_pay_dates, 

368 currency=ccy, 

369 udl_id="test_udl_id", 

370 fixing_id="test_fixing_id", 

371 day_count_convention=dcc, 

372 rate_day_count_convention=dcc, 

373 spread=spread, 

374 ) 

375 

376 maturity_date = refdate + timedelta(600) 

377 # ir_swap = InterestRateSwapSpecification('TEST_SWAP', 'DBK', 'COLLATERALIZED', 'EUR', paydates[-1], fixedleg, floatleg) 

378 ir_swap = InterestRateSwapSpecification( 

379 obj_id="dummy_swap_6m", 

380 notional=ns, 

381 issue_date=refdate, 

382 maturity_date=maturity_date, 

383 pay_leg=fixed_leg, 

384 receive_leg=float_leg, 

385 currency=ccy, 

386 day_count_convention=dcc, 

387 issuer="dummy_issuer", 

388 securitization_level="COLLATERALIZED", 

389 ) 

390 

391 oi_swap = InterestRateSwapSpecification( 

392 obj_id="dummy_ois_6M", 

393 notional=ns, 

394 issue_date=refdate, 

395 maturity_date=maturity_date, 

396 pay_leg=fixed_leg, 

397 receive_leg=ois_leg, 

398 currency=ccy, 

399 day_count_convention=dcc, 

400 issuer="dummy_issuer", 

401 securitization_level="COLLATERALIZED", 

402 ) 

403 

404 self.refdate = refdate 

405 self.start_dates = startdates 

406 self.end_dates = enddates 

407 self.reset_dates = resetdates 

408 self.pay_dates = paydates 

409 self.maturity_date = maturity_date 

410 

411 self.day_count_convention = dcc 

412 self.ccy = ccy 

413 self.dc = dc 

414 self.dc_rates = rates 

415 self.dc_dates = dates_dc 

416 self.dc_df = df 

417 

418 self.ns = ns 

419 

420 self.fixed_leg = fixed_leg 

421 self.float_leg = float_leg 

422 self.ois_leg = ois_leg 

423 self.ir_swap = ir_swap 

424 self.oi_swap = oi_swap 

425 

426 @unittest.mock.patch("rivapy.pricing.interest_rate_swap_pricing.DayCounter.get") 

427 @unittest.mock.patch("rivapy.pricing.interest_rate_swap_pricing.DayCounter") 

428 @unittest.mock.patch("rivapy.pricing.interest_rate_swap_pricing.get_projected_notionals") 

429 def test_populate_cashflow_fixed(self, mock_get_notionals, mock_daycounter, mock_daycounter_get): 

430 """ 

431 Test to output correct present value calculation of FIXED leg of an IR swap. 

432 

433 We provide a fixed IR leg specification 

434 We contrtol the output of finding forward rates from a discount curve through mock so that we are 

435 independent of this helper function. 

436 We control the yearfraction also for ease of calculation and verification. 

437 

438 

439 """ 

440 notional_amount = self.notional_amount 

441 fixed_rate = 0.08 

442 # Arrange 

443 mock_get_notionals.return_value = [notional_amount] 

444 

445 mock_instance = unittest.mock.Mock() 

446 mock_instance.yf.return_value = 1.0 

447 mock_daycounter_get.return_value = mock_instance 

448 

449 mock_daycounter.return_value.yf.return_value = 1.0 # just assume year fraction will be 1 for ease of calc 

450 

451 # Mock discount curve 

452 discount_curve = unittest.mock.Mock() 

453 discount_curve.daycounter = "Act360" 

454 discount_curve.value.return_value = 0.95 # talk the discount factor to be 0.95 for control 

455 

456 print("DEBUG: DayCounter mock:", mock_daycounter) 

457 print("DEBUG: DayCounter instance:", mock_daycounter.return_value) 

458 print("DEBUG: DayCounter.yf mock:", mock_daycounter.return_value.yf) 

459 print("DEBUG: DayCounter.yf.return_value:", mock_daycounter.return_value.yf.return_value) 

460 # Act 

461 result = InterestRateSwapPricer._populate_cashflows_fix( 

462 self.refdate, 

463 fixed_leg_spec=self.fixed_leg, 

464 discount_curve=discount_curve, 

465 fx_forward_curve=unittest.mock.Mock(), # since they are not required 

466 fixing_map=unittest.mock.Mock(), # since they are not required for calculation 

467 set_rate=False, 

468 desired_rate=0.05, # will not be accessed if set_rate = False 

469 ) 

470 

471 # Assert 

472 self.assertEqual(len(result), 1) 

473 

474 # Check the interest cashflow 

475 interest_cf = [cf for cf in result if getattr(cf, "interest_cashflow", False)][0] 

476 print("DEBUG: entry.interest_yf =", interest_cf.interest_yf, type(interest_cf.interest_yf)) 

477 

478 # DEBUG PRINTS 

479 print("DEBUG: interest_yf =", interest_cf.interest_yf, type(interest_cf.interest_yf)) 

480 print("DEBUG: interest_amount =", interest_cf.interest_amount, type(interest_cf.interest_amount)) 

481 print("DEBUG: present_value =", interest_cf.present_value, type(interest_cf.present_value)) 

482 print("DEBUG: discount_factor =", interest_cf.discount_factor, type(interest_cf.discount_factor)) 

483 

484 self.assertAlmostEqual(interest_cf.interest_amount, notional_amount * fixed_rate * 1.0) # 8 

485 self.assertAlmostEqual(interest_cf.present_value, 8 * 0.95) 

486 

487 # Not yet implemented in populate_fixed fully 

488 # # Check notional outflow 

489 # outflow_cf = [cf for cf in result if getattr(cf, "notional_cashflow", False) and cf.pay_amount < 0][0] 

490 # self.assertEqual(outflow_cf.pay_amount, -notional_amount) 

491 

492 # # Check notional inflow 

493 # inflow_cf = [cf for cf in result if getattr(cf, "notional_cashflow", False) and cf.pay_amount > 0][0] 

494 # self.assertEqual(inflow_cf.pay_amount, notional_amount) 

495 

496 # Verify mocks 

497 mock_get_notionals.assert_called_once() 

498 mock_daycounter.return_value.yf.assert_called_once() 

499 

500 @unittest.mock.patch("rivapy.pricing.interest_rate_swap_pricing.get_projected_notionals") 

501 @unittest.mock.patch("rivapy.pricing.interest_rate_swap_pricing.DayCounter") 

502 def test_cashflows_fix_notional_start_end(self, mock_daycounter_class, mock_get_notionals): 

503 """ 

504 Test _populate_cashflows_fix generates entries for both start and end notional cashflows. 

505 """ 

506 

507 # --- Arrange --- 

508 val_date = datetime(2025, 1, 1) 

509 

510 # Mock fixed leg spec 

511 fixed_leg_spec = unittest.mock.Mock() 

512 fixed_leg_spec.fixed_rate = 0.05 

513 fixed_leg_spec.start_dates = [val_date, val_date + timedelta(days=180)] 

514 fixed_leg_spec.end_dates = [val_date + timedelta(days=180), val_date + timedelta(days=360)] 

515 fixed_leg_spec.pay_dates = [val_date + timedelta(days=180), val_date + timedelta(days=360)] 

516 

517 # Mock Notional structure with start/end dates 

518 notional_structure = unittest.mock.Mock() 

519 notional_structure.get_pay_date_start.side_effect = [val_date, val_date + timedelta(days=180)] 

520 notional_structure.get_pay_date_end.side_effect = [val_date + timedelta(days=180), val_date + timedelta(days=360)] 

521 fixed_leg_spec.get_NotionalStructure.return_value = notional_structure 

522 

523 # Mock projected notionals 

524 mock_get_notionals.return_value = [100.0, 200.0] 

525 

526 # Mock DiscountCurve 

527 discount_curve = unittest.mock.Mock() 

528 discount_curve.value.side_effect = lambda val, pay: 0.95 # always 0.95 

529 

530 # Mock fx_forward_curve 

531 fx_forward_curve = unittest.mock.Mock() 

532 

533 # Mock DayCounter 

534 mock_daycounter_instance = unittest.mock.Mock() 

535 mock_daycounter_instance.yf.side_effect = lambda start, end: (end - start).days / 360.0 

536 mock_daycounter_class.return_value = mock_daycounter_instance 

537 

538 # Mock FixingMap (not used) 

539 fixing_map = unittest.mock.Mock() 

540 

541 # --- Act --- 

542 from rivapy.pricing.interest_rate_swap_pricing import InterestRateSwapPricer 

543 

544 entries = InterestRateSwapPricer._populate_cashflows_fix( 

545 val_date=val_date, 

546 fixed_leg_spec=fixed_leg_spec, 

547 discount_curve=discount_curve, 

548 fx_forward_curve=fx_forward_curve, 

549 fixing_map=fixing_map, 

550 set_rate=False, 

551 ) 

552 

553 # --- Assert --- 

554 # There should be 2 start + 2 end notional cashflows + 2 interest cashflows = 6 

555 self.assertEqual(len(entries), 6) 

556 

557 # Check first notional start/outflow 

558 start_cf = entries[0] 

559 self.assertTrue(getattr(start_cf, "notional_cashflow", False)) 

560 self.assertEqual(start_cf.pay_amount, -100.0) 

561 self.assertEqual(start_cf.discount_factor, 0.95) 

562 self.assertAlmostEqual(start_cf.present_value, -95.0) 

563 

564 # Check first interest cashflow 

565 interest_cf = entries[1] 

566 self.assertTrue(getattr(interest_cf, "interest_cashflow", False)) 

567 self.assertEqual(interest_cf.notional, 100.0) 

568 expected_interest = 100.0 * 0.05 * ((180) / 360) # rate * yf 

569 self.assertAlmostEqual(interest_cf.interest_amount, expected_interest) 

570 self.assertAlmostEqual(interest_cf.present_value, expected_interest * 0.95) 

571 

572 # Check first notional end/inflow 

573 end_cf = entries[2] 

574 self.assertTrue(getattr(end_cf, "notional_cashflow", False)) 

575 self.assertEqual(end_cf.pay_amount, 100.0) 

576 self.assertEqual(end_cf.discount_factor, 0.95) 

577 self.assertAlmostEqual(end_cf.present_value, 95.0) 

578 

579 # Optionally, check second cashflow sequence similarly 

580 start_cf2, interest_cf2, end_cf2 = entries[3], entries[4], entries[5] 

581 self.assertEqual(start_cf2.pay_amount, -200.0) 

582 self.assertEqual(end_cf2.pay_amount, 200.0) 

583 self.assertAlmostEqual(interest_cf2.interest_amount, 200.0 * 0.05 * (180 / 360)) 

584 

585 @unittest.mock.patch("rivapy.pricing.interest_rate_swap_pricing.DayCounter.get") 

586 @unittest.mock.patch("rivapy.pricing.interest_rate_swap_pricing.DayCounter") 

587 @unittest.mock.patch("rivapy.pricing.interest_rate_swap_pricing.get_projected_notionals") 

588 def test_populate_cashflow_float(self, mock_get_notionals, mock_daycounter, mock_daycounter_get): 

589 """ 

590 Test correct present value calculation of a FLOAT leg of an IR swap. 

591 """ 

592 

593 # --- ARRANGE --- 

594 notional_amount = self.notional_amount 

595 leg_spread = 0.00 # 1% 

596 fwd_floating_rate = 0.05 # 5% forward rate 

597 

598 # Mock notionals 

599 mock_get_notionals.return_value = [notional_amount] 

600 

601 # Mock daycounter for year fractions 

602 mock_instance = unittest.mock.Mock() 

603 mock_instance.yf.return_value = 1.0 

604 mock_daycounter_get.return_value = mock_instance 

605 mock_daycounter.return_value.yf.return_value = 1.0 

606 

607 # Mock discount curve 

608 discount_curve = unittest.mock.Mock() 

609 discount_curve.daycounter = "Act360" 

610 discount_curve.value.return_value = 0.95 

611 

612 # Mock forward curve 

613 forward_curve = unittest.mock.Mock() 

614 fwd_factor = 1.0 / (1.0 + fwd_floating_rate) 

615 forward_curve.value_fwd.return_value = fwd_factor # given fwd_floating_rate = (1/fwd_factor -1 )/yf 

616 # Mock fx curve (not used) 

617 fx_forward_curve = unittest.mock.Mock() 

618 

619 # Mock fixing map (not used) 

620 fixing_map = unittest.mock.Mock() 

621 

622 # Create a minimal float leg spec #simpler than what we predefined in setup 

623 float_leg = unittest.mock.Mock() 

624 float_leg.start_dates = [datetime(2025, 1, 1)] 

625 float_leg.end_dates = [datetime(2026, 1, 1)] 

626 float_leg.pay_dates = [datetime(2026, 1, 1)] 

627 float_leg.reset_dates = [datetime(2025, 1, 1)] 

628 float_leg.rate_start_dates = [datetime(2025, 1, 1)] 

629 float_leg.rate_end_dates = [datetime(2026, 1, 1)] 

630 float_leg.udl_id = "LIBOR3M" 

631 float_leg.spread = leg_spread 

632 float_leg.rate_day_count_convention = "Act360" 

633 float_leg.get_NotionalStructure.return_value.get_pay_date_start.return_value = datetime(2025, 1, 1) 

634 float_leg.get_NotionalStructure.return_value.get_pay_date_end.return_value = datetime(2026, 1, 1) 

635 

636 val_date = datetime(2025, 1, 1) 

637 

638 # --- ACT --- 

639 result = InterestRateSwapPricer._populate_cashflows_float( 

640 val_date=val_date, 

641 float_leg_spec=float_leg, 

642 discount_curve=discount_curve, 

643 forward_curve=forward_curve, 

644 fx_forward_curve=fx_forward_curve, 

645 fixing_map=fixing_map, 

646 fixing_grace_period=0.0, 

647 set_spread=False, 

648 spread=None, 

649 ) 

650 

651 # --- ASSERT --- 

652 # There should be 3 cashflows: initial notional outflow, interest cashflow, end notional inflow 

653 self.assertEqual(len(result), 3) 

654 

655 # Check interest cashflow 

656 interest_cf = [cf for cf in result if getattr(cf, "interest_cashflow", False)][0] 

657 

658 print(" - - - DEBUG - - - ") 

659 for attr, value in vars(interest_cf).items(): 

660 print(attr, value) 

661 

662 # expected_rate = leg_spread + (1.0 / fwd_rate - 1.0) / 1.0 # as per your formula 

663 expected_interest = notional_amount * fwd_floating_rate * 1.0 # yf = 1 

664 expected_pv = expected_interest * 0.95 / fwd_factor # discount factor 

665 

666 self.assertAlmostEqual(interest_cf.rate, fwd_floating_rate) 

667 self.assertAlmostEqual(interest_cf.interest_amount, expected_interest) 

668 self.assertAlmostEqual(interest_cf.present_value, expected_pv) 

669 

670 # Check mocks called 

671 mock_get_notionals.assert_called_once() 

672 mock_daycounter.return_value.yf.assert_called() 

673 forward_curve.value_fwd.assert_called() 

674 discount_curve.value.assert_called() 

675 

676 @unittest.mock.patch("rivapy.pricing.interest_rate_swap_pricing.get_projected_notionals") 

677 @unittest.mock.patch("rivapy.pricing.interest_rate_swap_pricing.DayCounter") 

678 def test_cashflows_float_notional_start_end(self, mock_daycounter_class, mock_get_notionals): 

679 """ 

680 Test _populate_cashflows_float generates entries for both start and end notional cashflows, 

681 including interest cashflows with mocked forward rates. 

682 """ 

683 

684 # --- Arrange --- 

685 val_date = datetime(2025, 1, 1) 

686 

687 # Mock floating leg spec 

688 float_leg_spec = unittest.mock.Mock() 

689 float_leg_spec.udl_id = "LIBOR3M" 

690 float_leg_spec.spread = 0.01 

691 float_leg_spec.start_dates = [val_date, val_date + timedelta(days=180)] 

692 float_leg_spec.end_dates = [val_date + timedelta(days=180), val_date + timedelta(days=360)] 

693 float_leg_spec.pay_dates = [val_date + timedelta(days=180), val_date + timedelta(days=360)] 

694 float_leg_spec.rate_start_dates = [val_date, val_date + timedelta(days=180)] 

695 float_leg_spec.rate_end_dates = [val_date + timedelta(days=180), val_date + timedelta(days=360)] 

696 float_leg_spec.reset_dates = [val_date - timedelta(days=1), val_date + timedelta(days=180)] 

697 float_leg_spec.rate_day_count_convention = "ACT/360" 

698 

699 # Mock Notional structure with start/end dates 

700 notional_structure = unittest.mock.Mock() 

701 notional_structure.get_pay_date_start.side_effect = [val_date, val_date + timedelta(days=180)] 

702 notional_structure.get_pay_date_end.side_effect = [val_date + timedelta(days=180), val_date + timedelta(days=360)] 

703 float_leg_spec.get_NotionalStructure.return_value = notional_structure 

704 

705 # Mock projected notionals 

706 mock_get_notionals.return_value = [100.0, 200.0] 

707 

708 # Mock DiscountCurve 

709 discount_curve = unittest.mock.Mock() 

710 discount_curve.value.side_effect = lambda val, pay: 0.95 # always 0.95 

711 

712 # Mock ForwardCurve 

713 forward_curve = unittest.mock.Mock() 

714 forward_curve.value_fwd.side_effect = lambda val, start, end: 0.02 # constant forward rate 

715 

716 # Mock fx_forward_curve (not used) 

717 fx_forward_curve = unittest.mock.Mock() 

718 

719 # Mock DayCounter 

720 mock_daycounter_instance = unittest.mock.Mock() 

721 mock_daycounter_instance.yf.side_effect = lambda start, end: (end - start).days / 360.0 

722 mock_daycounter_class.return_value = mock_daycounter_instance 

723 

724 # Mock FixingMap (return None to force using forward rate logic) 

725 fixing_map = unittest.mock.Mock() 

726 fixing_map.get_fixing.return_value = None 

727 

728 # --- Act --- 

729 from rivapy.pricing.interest_rate_swap_pricing import InterestRateSwapPricer 

730 

731 entries = InterestRateSwapPricer._populate_cashflows_float( 

732 val_date=val_date, 

733 float_leg_spec=float_leg_spec, 

734 discount_curve=discount_curve, 

735 forward_curve=forward_curve, 

736 fx_forward_curve=fx_forward_curve, 

737 fixing_map=fixing_map, 

738 fixing_grace_period=30, 

739 set_spread=False, 

740 spread=0.0, 

741 ) 

742 

743 # --- Assert --- 

744 # There should be 2 start + 2 end notional cashflows + 2 interest cashflows = 6 

745 self.assertEqual(len(entries), 6) 

746 

747 # --- First cashflow sequence --- 

748 start_cf = entries[0] 

749 interest_cf = entries[1] 

750 end_cf = entries[2] 

751 

752 # Notional start 

753 self.assertTrue(getattr(start_cf, "notional_cashflow", False)) 

754 self.assertEqual(start_cf.pay_amount, -100.0) 

755 self.assertEqual(start_cf.discount_factor, 0.95) 

756 self.assertAlmostEqual(start_cf.present_value, -95.0) 

757 

758 # Interest cashflow 

759 self.assertTrue(getattr(interest_cf, "interest_cashflow", False)) 

760 self.assertEqual(interest_cf.notional, 100.0) 

761 expected_interest = 100.0 * (1.0 / 0.02 - 1.0) / ((180) / 360) * ((180) / 360) + 0.01 * 100.0 * (180 / 360) 

762 # Simplified calculation here, in practice it should match the formula in the code 

763 self.assertAlmostEqual(interest_cf.interest_amount, interest_cf.notional * interest_cf.rate * interest_cf.interest_yf) 

764 self.assertAlmostEqual(interest_cf.present_value, interest_cf.pay_amount * 0.95) 

765 

766 # Notional end 

767 self.assertTrue(getattr(end_cf, "notional_cashflow", False)) 

768 self.assertEqual(end_cf.pay_amount, 100.0) 

769 self.assertEqual(end_cf.discount_factor, 0.95) 

770 self.assertAlmostEqual(end_cf.present_value, 95.0) 

771 

772 # --- Second cashflow sequence --- 

773 start_cf2, interest_cf2, end_cf2 = entries[3], entries[4], entries[5] 

774 

775 self.assertEqual(start_cf2.pay_amount, -200.0) 

776 self.assertTrue(getattr(interest_cf2, "interest_cashflow", False)) 

777 self.assertEqual(end_cf2.pay_amount, 200.0) 

778 

779 def populate_cashflow_ois(self): 

780 # TODO 

781 pass 

782 

783 @unittest.mock.patch.object(InterestRateSwapPricer, "price_leg") 

784 def price(self, mock_price_leg): 

785 """Simple test of swap leg aggregation ensuring it makes the expected calls to the 

786 internal function price_leg properly and aggregates properly 

787 essentially: test_price_subtracts_pay_leg_from_receive_leg 

788 """ 

789 # Arrange 

790 swap = self.ir_swap 

791 

792 # Arrange: create pricer with dummy data 

793 pricer = InterestRateSwapPricer( 

794 val_date=datetime(2025, 1, 1), 

795 spec=swap, 

796 discount_curve_pay_leg=unittest.mock.Mock(name="discount_pay"), 

797 discount_curve_receive_leg=unittest.mock.Mock(name="discount_receive"), 

798 fixing_curve_pay_leg=unittest.mock.Mock(name="fixing_pay"), 

799 fixing_curve_receive_leg=unittest.mock.Mock(name="fixing_receive"), 

800 fx_fwd_curve_pay_leg=unittest.mock.Mock(name="fx_pay"), 

801 fx_fwd_curve_receive_leg=unittest.mock.Mock(name="fx_receive"), 

802 pricing_request=unittest.mock.Mock(name="InterestRateSwapPricingRequest"), 

803 pricing_param={"fixing_grace_period": 1}, 

804 ) 

805 

806 # Stub price_leg results 

807 mock_price_leg.side_effect = [150.0, 40.0] # first call receive leg, second pay leg 

808 

809 # Act 

810 result = pricer.price() 

811 

812 # Assert 

813 self.assertEqual(result, 110.0) # 150 - 40 

814 self.assertEqual(mock_price_leg.call_count, 2) 

815 

816 def price_leg_pricing_data(self): # using the pricing data container structure 

817 pass 

818 

819 @unittest.mock.patch.object(InterestRateSwapPricer, "_populate_cashflows_fix") 

820 def test_price_fixed_leg(self, mock_populate_fix): 

821 # Arrange 

822 leg_spec = self.fixed_leg 

823 cf1 = CashFlow() 

824 cf1.present_value = 10 

825 cf2 = CashFlow() 

826 cf2.present_value = 20 

827 mock_populate_fix.return_value = [cf1, cf2] 

828 

829 # Act 

830 result = InterestRateSwapPricer.price_leg( 

831 self.refdate, 

832 discount_curve=self.dc, 

833 forward_curve=self.dc, 

834 fxForward_curve=self.dc, 

835 spec=leg_spec, 

836 # fixing_map=self.fixing_map, 

837 pricing_params={"set_rate": False, "desired_rate": 0.05}, 

838 ) 

839 

840 # Assert 

841 self.assertEqual(result, 30.0) 

842 mock_populate_fix.assert_called_once() 

843 

844 @unittest.mock.patch.object(InterestRateSwapPricer, "_populate_cashflows_float") 

845 def test_price_float_leg(self, mock_populate_float): 

846 # Arrange 

847 leg_spec = self.float_leg 

848 cf1 = CashFlow() 

849 cf1.present_value = -5 

850 cf2 = CashFlow() 

851 cf2.present_value = 20 

852 mock_populate_float.return_value = [cf1, cf2] 

853 

854 # Act 

855 result = InterestRateSwapPricer.price_leg( 

856 self.refdate, 

857 discount_curve=self.dc, 

858 forward_curve=self.dc, 

859 fxForward_curve=self.dc, 

860 spec=leg_spec, 

861 # fixing_map=self.fixing_map, 

862 pricing_params={"set_rate": False, "desired_rate": 0.05}, 

863 ) 

864 

865 # Assert 

866 self.assertEqual(result, 15.0) 

867 mock_populate_float.assert_called_once() 

868 

869 @unittest.mock.patch.object(InterestRateSwapPricer, "_populate_cashflows_ois") 

870 def test_price_ois_leg(self, mock_populate_ois): 

871 # Arrange 

872 leg_spec = self.ois_leg 

873 cf1 = CashFlow() 

874 cf1.present_value = 10 

875 cf2 = CashFlow() 

876 cf2.present_value = 40 

877 mock_populate_ois.return_value = [cf1, cf2] 

878 

879 # Act 

880 result = InterestRateSwapPricer.price_leg( 

881 self.refdate, 

882 discount_curve=self.dc, 

883 forward_curve=self.dc, 

884 fxForward_curve=self.dc, 

885 spec=leg_spec, 

886 # fixing_map=self.fixing_map, 

887 pricing_params={"set_rate": False, "desired_rate": 0.05}, 

888 ) 

889 

890 # Assert 

891 self.assertEqual(result, 50.0) 

892 mock_populate_ois.assert_called_once() 

893 

894 @unittest.mock.patch("rivapy.pricing.interest_rate_swap_pricing.InterestRateSwapPricer.price_leg") 

895 def test_compute_swap_rate(self, mock_price_leg): 

896 """ 

897 Test fair swap rate calculation: 

898 swap_rate = PV_float / Annuity_fixed 

899 

900 The function essentially calls price_leg on a fixed and floating leg, where for the 

901 fixed leg, the desired rate is set = 1 in order to calculate the Annuity instead 

902 as used for the computation of a fair swap rate st. PV_Fixed = PV_float 

903 

904 """ 

905 

906 # Arrange: control PVs from mocked price_leg 

907 mock_price_leg.side_effect = [200.0, 800.0] # float PV, fixed annuity 

908 

909 ref_date = datetime(2024, 1, 1) 

910 discount_curve = unittest.mock.Mock() 

911 fixing_curve = unittest.mock.Mock() 

912 float_leg = unittest.mock.Mock() 

913 fixed_leg = unittest.mock.Mock() 

914 fixing_map = unittest.mock.Mock() 

915 

916 # Act 

917 swap_rate = InterestRateSwapPricer.compute_swap_rate( 

918 ref_date=ref_date, 

919 discount_curve=discount_curve, 

920 fixing_curve=fixing_curve, 

921 float_leg=float_leg, 

922 fixed_leg=fixed_leg, 

923 fixing_map=fixing_map, 

924 pricing_params={ 

925 "fixing_grace_period": 0.0 

926 }, # then ensures that desired_rate = 1 in price_leg for fixed since it will go with default value 

927 ) 

928 

929 # Assert 

930 expected_rate = 200.0 / 800.0 # 0.25 

931 self.assertAlmostEqual(swap_rate, expected_rate) 

932 

933 # Verify calls 

934 self.assertEqual(mock_price_leg.call_count, 2) 

935 # First call -> float leg 

936 args_float = mock_price_leg.call_args_list[0][0] 

937 # Second call -> fixed leg 

938 args_fixed = mock_price_leg.call_args_list[1][0] 

939 

940 self.assertIs(args_float[2], fixing_curve) # forward curve used for float leg 

941 self.assertIs(args_fixed[2], fixing_curve) # forward curve also passed for fixed leg annuity 

942 

943 @unittest.mock.patch("rivapy.pricing.interest_rate_swap_pricing.InterestRateSwapPricer.price_leg") 

944 def test_compute_basis_spread(self, mock_price_leg): 

945 """ 

946 Test fair basis spread calculation: 

947 basis_spread = (PV_receive_leg - PV_pay_leg) / PV01_fixed_leg 

948 

949 The function calls price_leg three times: 

950 1. receive_leg PV (using receiveLegFixingCurve) 

951 2. pay_leg PV (using payLegFixingCurve) 

952 3. fixed_leg PV01 (using payLegFixingCurve, desired_rate=1) 

953 """ 

954 

955 # --- Arrange --- 

956 mock_price_leg.side_effect = [300.0, 250.0, 20.0] # receive_leg PV, pay_leg PV, fixed_leg PV01 

957 

958 ref_date = datetime(2024, 1, 1) 

959 

960 # create mock curves and legs 

961 discount_curve = unittest.mock.Mock(name="discount_curve") 

962 pay_fixing_curve = unittest.mock.Mock(name="pay_fixing_curve") 

963 receive_fixing_curve = unittest.mock.Mock(name="receive_fixing_curve") 

964 pay_leg = unittest.mock.Mock(name="pay_leg") 

965 receive_leg = unittest.mock.Mock(name="receive_leg") 

966 spread_leg = unittest.mock.Mock(name="spread_leg") 

967 fixing_map = unittest.mock.Mock(name="fixing_map") 

968 

969 pricing_params = {"fixing_grace_period": 0.0} 

970 

971 # --- Act --- 

972 result = InterestRateSwapPricer.compute_basis_spread( 

973 ref_date=ref_date, 

974 discount_curve=discount_curve, 

975 payLegFixingCurve=pay_fixing_curve, 

976 receiveLegFixingCurve=receive_fixing_curve, 

977 pay_leg=pay_leg, 

978 receive_leg=receive_leg, 

979 spread_leg=spread_leg, 

980 fixing_map=fixing_map, 

981 pricing_params=pricing_params, 

982 ) 

983 

984 # --- Assert --- 

985 # expected = (receive_leg_PV - pay_leg_PV) / fixed_leg_PV01 = (300 - 250) / 20 = 2.5 

986 expected_spread = (300.0 - 250.0) / 20.0 

987 self.assertAlmostEqual(result, expected_spread) 

988 

989 # Check calls 

990 self.assertEqual(mock_price_leg.call_count, 3) 

991 

992 # Check that the right arguments were passed for each call 

993 args_receive = mock_price_leg.call_args_list[0][0] 

994 args_pay = mock_price_leg.call_args_list[1][0] 

995 args_fixed = mock_price_leg.call_args_list[2][0] 

996 

997 # receive leg should use receiveLegFixingCurve 

998 self.assertIs(args_receive[2], receive_fixing_curve) 

999 self.assertIs(args_receive[4], receive_leg) 

1000 

1001 # pay leg should use payLegFixingCurve 

1002 self.assertIs(args_pay[2], pay_fixing_curve) 

1003 self.assertIs(args_pay[4], pay_leg) 

1004 

1005 # fixed leg should also use payLegFixingCurve 

1006 self.assertIs(args_fixed[2], pay_fixing_curve) 

1007 self.assertIs(args_fixed[4], spread_leg) 

1008 

1009 

1010class TestInterestRateSwapPricerInit(unittest.TestCase): 

1011 """Unit tests for InterestRateSwapPricer initialization logic.""" 

1012 

1013 def setUp(self): 

1014 self.refdate = datetime(2025, 1, 1) 

1015 self.maturity = self.refdate + timedelta(days=365) # FIX: must be later than issue_date 

1016 self.ccy = "EUR" 

1017 

1018 # --- Discount curves --- 

1019 self.dc = DiscountCurve( 

1020 id="TEST_DC", 

1021 refdate=self.refdate, 

1022 dates=[self.refdate], 

1023 df=[1.0], 

1024 interpolation=InterpolationType.LINEAR, 

1025 extrapolation=ExtrapolationType.LINEAR, 

1026 ) 

1027 

1028 # --- Legs --- 

1029 ns = ConstNotionalStructure(100) 

1030 self.fixed_leg = IrFixedLegSpecification( 

1031 fixed_rate=0.05, 

1032 obj_id="fixed_leg", 

1033 notional=100.0, 

1034 start_dates=[self.refdate], 

1035 end_dates=[self.maturity], 

1036 pay_dates=[self.maturity], 

1037 currency=self.ccy, 

1038 day_count_convention="Act360", 

1039 ) 

1040 

1041 self.float_leg = IrFloatLegSpecification( 

1042 obj_id="float_leg", 

1043 notional=ns, 

1044 reset_dates=[self.refdate], 

1045 start_dates=[self.refdate], 

1046 end_dates=[self.maturity], 

1047 rate_start_dates=[self.refdate], 

1048 rate_end_dates=[self.maturity], 

1049 pay_dates=[self.maturity], 

1050 currency=self.ccy, 

1051 udl_id="test_udl", 

1052 fixing_id="test_fixing", 

1053 day_count_convention="Act360", 

1054 spread=0.0, 

1055 ) 

1056 

1057 self.spec = InterestRateSwapSpecification( 

1058 obj_id="test_swap", 

1059 notional=ns, 

1060 issue_date=self.refdate, 

1061 maturity_date=self.maturity, 

1062 pay_leg=self.fixed_leg, 

1063 receive_leg=self.float_leg, 

1064 currency=self.ccy, 

1065 day_count_convention="Act360", 

1066 issuer="issuer", 

1067 securitization_level="COLLATERALIZED", 

1068 ) 

1069 

1070 self.pricing_request = unittest.mock.Mock(name="InterestRateSwapPricingRequest") 

1071 

1072 def test_init_success_minimal(self): 

1073 """Should initialize successfully with minimal valid inputs.""" 

1074 pricer = InterestRateSwapPricer( 

1075 val_date=self.refdate, 

1076 spec=self.spec, 

1077 discount_curve_pay_leg=self.dc, 

1078 discount_curve_receive_leg=self.dc, 

1079 fixing_curve_pay_leg=self.dc, 

1080 fixing_curve_receive_leg=self.dc, 

1081 fx_fwd_curve_pay_leg=self.dc, 

1082 fx_fwd_curve_receive_leg=self.dc, 

1083 pricing_request=self.pricing_request, 

1084 ) 

1085 

1086 self.assertEqual(pricer._val_date, self.refdate) 

1087 self.assertIs(pricer._spec, self.spec) 

1088 self.assertIs(pricer._pay_leg, self.spec.pay_leg) 

1089 self.assertIs(pricer._receive_leg, self.spec.receive_leg) 

1090 self.assertEqual(pricer._fx_pay_leg, 1.0) 

1091 self.assertEqual(pricer._fx_receive_leg, 1.0) 

1092 self.assertIsInstance(pricer._pricing_param, dict) 

1093 self.assertEqual(pricer._pricing_param, {}) 

1094 

1095 def test_init_success_with_optional_args(self): 

1096 """Should properly assign optional args like pricing_param and fixing_map.""" 

1097 mock_fixing_map = unittest.mock.Mock(name="FixingTable") 

1098 pricing_param = {"fixing_grace_period": 2} 

1099 

1100 pricer = InterestRateSwapPricer( 

1101 val_date=self.refdate, 

1102 spec=self.spec, 

1103 discount_curve_pay_leg=self.dc, 

1104 discount_curve_receive_leg=self.dc, 

1105 fixing_curve_pay_leg=self.dc, 

1106 fixing_curve_receive_leg=self.dc, 

1107 fx_fwd_curve_pay_leg=self.dc, 

1108 fx_fwd_curve_receive_leg=self.dc, 

1109 pricing_request=self.pricing_request, 

1110 pricing_param=pricing_param, 

1111 fixing_map=mock_fixing_map, 

1112 fx_pay_leg=1.2, 

1113 fx_receive_leg=0.8, 

1114 ) 

1115 

1116 self.assertEqual(pricer._pricing_param, pricing_param) 

1117 self.assertIs(pricer._fixing_map, mock_fixing_map) 

1118 self.assertEqual(pricer._fx_pay_leg, 1.2) 

1119 self.assertEqual(pricer._fx_receive_leg, 0.8) 

1120 

1121 def test_init_raises_if_missing_required_args(self): 

1122 """Should raise TypeError if required arguments are missing.""" 

1123 with self.assertRaises(TypeError): 

1124 InterestRateSwapPricer( 

1125 val_date=self.refdate, 

1126 spec=self.spec, 

1127 discount_curve_pay_leg=self.dc, 

1128 discount_curve_receive_leg=self.dc, 

1129 fixing_curve_pay_leg=self.dc, 

1130 fixing_curve_receive_leg=self.dc, 

1131 fx_fwd_curve_pay_leg=self.dc, 

1132 # missing fx_fwd_curve_receive_leg 

1133 pricing_request=self.pricing_request, 

1134 ) 

1135 

1136 def test_init_all_curves_assigned_correctly(self): 

1137 """Ensure all curve attributes are assigned to the correct legs.""" 

1138 pricer = InterestRateSwapPricer( 

1139 val_date=self.refdate, 

1140 spec=self.spec, 

1141 discount_curve_pay_leg=self.dc, 

1142 discount_curve_receive_leg=self.dc, 

1143 fixing_curve_pay_leg=self.dc, 

1144 fixing_curve_receive_leg=self.dc, 

1145 fx_fwd_curve_pay_leg=self.dc, 

1146 fx_fwd_curve_receive_leg=self.dc, 

1147 pricing_request=self.pricing_request, 

1148 ) 

1149 

1150 self.assertIs(pricer._discount_curve_pay_leg, self.dc) 

1151 self.assertIs(pricer._discount_curve_receive_leg, self.dc) 

1152 self.assertIs(pricer._fixing_curve_pay_leg, self.dc) 

1153 self.assertIs(pricer._fixing_curve_receive_leg, self.dc) 

1154 self.assertIs(pricer._fx_fwd_curve_pay_leg, self.dc) 

1155 self.assertIs(pricer._fx_fwd_curve_receive_leg, self.dc) 

1156 

1157 def test_init_pricing_param_is_new_dict(self): 

1158 """Ensure pricing_param defaults to a new dict to avoid mutable default issues.""" 

1159 pricer1 = InterestRateSwapPricer( 

1160 val_date=self.refdate, 

1161 spec=self.spec, 

1162 discount_curve_pay_leg=self.dc, 

1163 discount_curve_receive_leg=self.dc, 

1164 fixing_curve_pay_leg=self.dc, 

1165 fixing_curve_receive_leg=self.dc, 

1166 fx_fwd_curve_pay_leg=self.dc, 

1167 fx_fwd_curve_receive_leg=self.dc, 

1168 pricing_request=self.pricing_request, 

1169 ) 

1170 pricer2 = InterestRateSwapPricer( 

1171 val_date=self.refdate, 

1172 spec=self.spec, 

1173 discount_curve_pay_leg=self.dc, 

1174 discount_curve_receive_leg=self.dc, 

1175 fixing_curve_pay_leg=self.dc, 

1176 fixing_curve_receive_leg=self.dc, 

1177 fx_fwd_curve_pay_leg=self.dc, 

1178 fx_fwd_curve_receive_leg=self.dc, 

1179 pricing_request=self.pricing_request, 

1180 ) 

1181 self.assertIsNot(pricer1._pricing_param, pricer2._pricing_param) 

1182 

1183 

1184class TestGetProjectedNotionals(unittest.TestCase): 

1185 

1186 def setUp(self): 

1187 self.val_date = datetime(2025, 1, 1) 

1188 self.val_notional = 1000 

1189 

1190 def test_const_notional_structure(self): 

1191 ns = ConstNotionalStructure(1000) 

1192 result = get_projected_notionals(self.val_date, ns, 0, 3, fx_forward_curve=None) 

1193 self.assertEqual(result, [1000, 1000, 1000]) 

1194 

1195 def test_resetting_notional_structure_with_fx(self): 

1196 amounts = [100, 200, 300] 

1197 fixing_dates = [ 

1198 datetime(2025, 2, 1), 

1199 datetime(2025, 3, 1), 

1200 datetime(2025, 4, 1), 

1201 ] 

1202 ns = ResettingNotionalStructure( 

1203 ref_currency="USD", 

1204 fx_fixing_id="EUR/USD", 

1205 notionals=amounts, 

1206 pay_date_start=[datetime(2025, 1, 1)] * 3, 

1207 pay_date_end=[datetime(2025, 6, 1)] * 3, 

1208 fixing_dates=fixing_dates, 

1209 ) 

1210 # Mock FX forward curve: return 2.0 regardless of input 

1211 fx_curve = unittest.mock.Mock() 

1212 fx_curve.value.return_value = 2.0 

1213 

1214 result = get_projected_notionals(self.val_date, ns, 0, 3, fx_curve) 

1215 self.assertEqual(result, [200.0, 400.0, 600.0]) 

1216 fx_curve.value.assert_called() # ensure it was used 

1217 

1218 def test_resetting_notional_structure_without_fx_raises(self): 

1219 fixing_dates = [ 

1220 datetime(2025, 2, 1), 

1221 datetime(2025, 3, 1), 

1222 datetime(2025, 4, 1), 

1223 ] 

1224 ns = ResettingNotionalStructure( 

1225 ref_currency="USD", 

1226 fx_fixing_id="EUR/USD", 

1227 notionals=[100, 200, 300], 

1228 pay_date_start=[datetime(2025, 1, 1)] * 3, 

1229 pay_date_end=[datetime(2025, 6, 1)] * 3, 

1230 fixing_dates=fixing_dates, 

1231 ) 

1232 with self.assertRaises(ValueError): 

1233 get_projected_notionals(self.val_date, ns, 0, 1, fx_forward_curve=None) 

1234 

1235 

1236# TODO once implemented: computeSwapSpread, 

1237 

1238 

1239if __name__ == "__main__": 

1240 unittest.main()