Coverage for tests / test_fras.py: 99%

97 statements  

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

1# hnguyen, 2024-09-08 

2# unit tests for Forward rate agreement specification class 

3import math 

4from rivapy.tools.holidays_compat import ECB # ?? 

5 

6import unittest 

7import datetime as dt 

8from matplotlib import dates 

9import numpy as np 

10from rivapy.instruments.fra_specifications import ForwardRateAgreementSpecification 

11from rivapy.marketdata.curves import DiscountCurve 

12from rivapy.pricing.fra_pricing import ForwardRateAgreementPricer 

13from rivapy.pricing.bond_pricing import DeterministicCashflowPricer 

14from rivapy.tools.datetools import DayCounter, roll_day 

15from rivapy.tools.enums import ( 

16 DayCounterType, 

17 InterpolationType, 

18 RollConvention, 

19 SecuritizationLevel, 

20 Currency, 

21 Rating, 

22 Instrument, 

23) 

24 

25 

26class TestForwardRateAgreementSpecification(unittest.TestCase): 

27 

28 # Set up FRA 

29 ccy = "EUR" 

30 fra_rate = 0.04 

31 ref_date = dt.datetime(2023, 1, 28) 

32 start_date = dt.datetime(2023, 7, 28) 

33 end_date = dt.datetime(2023, 10, 28) 

34 

35 mat_date = ref_date + dt.timedelta(days=365) 

36 

37 fra = ForwardRateAgreementSpecification( 

38 obj_id="dummy_id", 

39 trade_date=ref_date, 

40 notional=1000.0, 

41 rate=fra_rate, 

42 start_date=start_date, 

43 end_date=end_date, 

44 udlID="dummy_underlying_index", 

45 rate_start_date=start_date, 

46 rate_end_date=end_date, 

47 day_count_convention="Act360", 

48 rate_day_count_convention="Act360", 

49 currency=ccy, 

50 spot_days=1, 

51 payment_days=1, 

52 issuer="dummy_issuer", 

53 securitization_level="NONE", 

54 ) 

55 

56 def setUp(self): 

57 """Common setup for tests""" 

58 self.trade_date = dt.date(2024, 1, 1) 

59 self.maturity_date = dt.date(2024, 12, 31) 

60 self.start_date = dt.date(2024, 3, 1) 

61 self.end_date = dt.date(2024, 9, 1) 

62 

63 self.fra = ForwardRateAgreementSpecification( 

64 obj_id="fra_001", 

65 trade_date=self.trade_date, 

66 maturity_date=self.maturity_date, 

67 notional=1_000_000, 

68 rate=0.02, 

69 start_date=self.start_date, 

70 end_date=self.end_date, 

71 udlID="EURIBOR_3M", # at the moment, dummy, not used in pricing YET 

72 rate_start_date=self.start_date - dt.timedelta(days=2), 

73 rate_end_date=self.end_date - dt.timedelta(days=2), 

74 calendar=ECB(years=range(2024, 2025)), 

75 currency="EUR", 

76 issuer="TestBank", 

77 securitization_level=SecuritizationLevel.NONE, 

78 rating=Rating.A, 

79 payment_days=2, 

80 ) 

81 

82 def test_initialization(self): 

83 self.assertEqual(self.fra.obj_id, "fra_001") 

84 self.assertEqual(self.fra.notional, 1_000_000) 

85 self.assertEqual(self.fra.rate, 0.02) 

86 self.assertEqual(self.fra.trade_date, self.trade_date) 

87 self.assertEqual(self.fra.maturity_date, self.maturity_date) 

88 self.assertEqual(self.fra.start_date, self.start_date) 

89 self.assertEqual(self.fra.end_date, self.end_date) 

90 self.assertEqual(self.fra.currency, "EUR") 

91 self.assertEqual(self.fra.issuer, "TestBank") 

92 self.assertEqual(self.fra.securitization_level, "NONE") 

93 self.assertEqual(self.fra.rating, "A") 

94 self.assertEqual(self.fra.payment_days, 2) 

95 

96 def test_to_dict(self): 

97 d = self.fra._to_dict() 

98 self.assertIsInstance(d, dict) 

99 self.assertEqual(d["obj_id"], "fra_001") 

100 self.assertEqual(d["currency"], "EUR") 

101 self.assertEqual(d["notional"], 1_000_000) 

102 

103 def test_property_setters(self): 

104 self.fra.rate = 0.05 

105 self.assertEqual(self.fra.rate, 0.05) 

106 

107 self.fra.notional = 5000 

108 self.assertEqual(self.fra.notional, 5000) 

109 

110 self.fra.issuer = "NewBank" 

111 self.assertEqual(self.fra.issuer, "NewBank") 

112 

113 self.fra.rating = Rating.B 

114 self.assertEqual(self.fra.rating, "B") 

115 

116 self.fra.currency = Currency.USD # should be fine despite the warning 

117 self.assertEqual(self.fra.currency, "USD") 

118 

119 def test_invalid_notional_raises(self): 

120 with self.assertRaises(ValueError): 

121 self.fra.notional = -1000 

122 

123 def test_ins_type(self): 

124 self.assertEqual(self.fra.ins_type(), Instrument.FRA) 

125 

126 def test_get_end_date_alias(self): 

127 self.assertEqual(self.fra.get_end_date(), self.maturity_date) 

128 

129 def test_create_sample_reproducibility(self): 

130 samples1 = ForwardRateAgreementSpecification._create_sample(3, seed=42, ref_date=self.trade_date) 

131 samples2 = ForwardRateAgreementSpecification._create_sample(3, seed=42, ref_date=self.trade_date) 

132 

133 self.assertEqual(len(samples1), 3) 

134 self.assertEqual(len(samples2), 3) 

135 # Same seed → same first instrument spec 

136 # self.assertEqual(samples1[0]["notional"], samples2[0]["notional"]) 

137 # self.assertEqual(samples1[0]["currency"], samples2[0]["currency"]) 

138 

139 # def test_create_sample_structure(self): 

140 # samples = ForwardRateAgreementSpecification._create_sample(2, seed=1, ref_date=self.trade_date) 

141 # self.assertIsInstance(samples, list) 

142 # self.assertIn("trade_date", samples[0]) 

143 # self.assertIn("maturity_date", samples[0]) 

144 # self.assertIn("currency", samples[0]) 

145 

146 ####################################################### 

147 # Tests for Pricing 

148 def test_fra_cf_implied_rate(self): 

149 # setting up necessary curves 

150 # discount curve 

151 object_id = "TEST_DC" 

152 dsc_rate = 0.01 

153 days_to_maturity = [1, 180, 365, 720, 3 * 365, 4 * 365, 10 * 365] 

154 dates = [self.ref_date + dt.timedelta(days=d) for d in days_to_maturity] 

155 df = [math.exp(-d / 365.0 * dsc_rate) for d in days_to_maturity] 

156 dc = DiscountCurve(id=object_id, refdate=self.ref_date, dates=dates, df=df, interpolation=InterpolationType.LINEAR) 

157 

158 # Fixing curve 

159 object_id = "TEST_fwd" 

160 fwd_rate = 0.05 

161 fwd_df = [math.exp(-d / 365.0 * fwd_rate) for d in days_to_maturity] 

162 fwd_dc = DiscountCurve(id=object_id, refdate=self.ref_date, dates=dates, df=fwd_df, interpolation=InterpolationType.LINEAR) 

163 

164 fra_pricer = ForwardRateAgreementPricer(self.ref_date, self.fra, dc, fwd_dc) 

165 

166 # Manually calculate expected cashflows 'manually' for comparison 

167 dcc_rate = DayCounter(fwd_dc.daycounter) 

168 fwdrateDF = fwd_dc.value_fwd(self.ref_date, self.fra._rate_start_date, self.fra._rate_end_date) 

169 dt_rate = dcc_rate.yf(self.fra._rate_start_date, self.fra._rate_end_date) 

170 fwdrate = (1.0 / fwdrateDF - 1) / dt_rate 

171 

172 # using instrument daycount convention to calculate delta t for cf amount calculation and discounting 

173 dcc = DayCounter(self.fra.day_count_convention) 

174 # avoid shadowing the datetime module alias `dt` imported at module level 

175 # (assignment to `dt` would make `dt` local in this function and cause 

176 # UnboundLocalError when `dt.timedelta` is used earlier) 

177 year_frac = dcc.yf(self.fra._start_date, self.fra._end_date) 

178 amount = self.fra._notional * (fwdrate - self.fra._rate) * year_frac 

179 cf = amount / (1 + fwdrate * year_frac) 

180 fair_rate = (1.0 / fwdrateDF - 1) / dt_rate 

181 self.assertEqual(fra_pricer._fra_spec, self.fra) 

182 self.assertEqual(fra_pricer._val_date, self.ref_date) 

183 self.assertEqual(fra_pricer._discount_curve, dc) 

184 self.assertEqual(fra_pricer._forward_curve, fwd_dc) 

185 

186 self.assertEqual( 

187 fra_pricer.get_expected_cashflows(self.fra, self.ref_date, fwd_dc), 

188 [(roll_day(self.fra._start_date, self.fra._calendar, self.fra._business_day_convention, settle_days=self.fra._payment_days), cf)], 

189 ) 

190 self.assertEqual(fra_pricer.compute_fair_rate(self.ref_date, self.fra, fwd_dc), fair_rate) 

191 

192 

193if __name__ == "__main__": 

194 unittest.main()