Coverage for tests / test_curves.py: 99%

117 statements  

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

1import unittest 

2import datetime as dt 

3import math 

4 

5from rivapy.tools.interpolate import Interpolator 

6from rivapy.tools.enums import DayCounterType, InterpolationType, ExtrapolationType 

7from rivapy.marketdata import DiscountCurve, SurvivalCurve, EquityForwardCurve 

8from rivapy.tools.datetools import DayCounter 

9 

10 

11# , delta=1e-5 ? 

12class TestDiscountCurve(unittest.TestCase): 

13 

14 # Discount Curve has 

15 # _init_ 

16 # get_dates 

17 # get_df 

18 # value 

19 # get_pyvacon_obj 

20 # value - cares about daycount convention refdates, target date, interpolation type, extrapolation type.. 

21 # at the moment, it is assumed the target date is correctly calculated before input with correct business day logic/roll convention 

22 # there is at the moment a potential issue with the roll convention and the given reference date, for now assume it is correct #TODO 

23 # plot - calls the value or value in order to plot. 

24 # TODO: consider how to wrap both value and value depending on if the interpolationType is a pyvacon or Rivapy construction respectively 

25 

26 def setUp(self): 

27 """Test data, simple linear case. Extend to more robust if requested.""" 

28 

29 # Example discount curve, with discount factors calculated based on given rates and days to maturity 

30 # first example uses day count convention ACT365FIXED 

31 

32 # base imformation 

33 self.refdate = dt.datetime(2017, 1, 1, 0, 0, 0) 

34 self.days_to_maturity = [1, 180, 365, 720, 3 * 365, 4 * 365, 10 * 365] 

35 self.rates = [-0.0065, 0.0003, 0.0059, 0.0086, 0.0101, 0.02, 0.03] 

36 self.rate = 0.03 

37 self.dates = [self.refdate + dt.timedelta(days=i) for i in self.days_to_maturity] 

38 

39 self.dsc_fac_ACT365FIXED = [math.exp(-d / 365.0 * self.rate) for d in self.days_to_maturity] 

40 

41 def test_init_success_and_getters(self): 

42 """Check initialization and getters.""" 

43 dc = DiscountCurve( 

44 "test_curve", 

45 self.refdate, 

46 self.dates, 

47 self.dsc_fac_ACT365FIXED, 

48 InterpolationType.LINEAR, 

49 ExtrapolationType.LINEAR, 

50 DayCounterType.Act365Fixed, 

51 ) 

52 self.assertEqual(dc.id, "test_curve") 

53 self.assertEqual(dc.get_dates()[0], self.refdate) 

54 self.assertEqual(dc.get_df()[0], 1.0) 

55 self.assertTrue(all(isinstance(d, dt.datetime) for d in dc.get_dates())) 

56 

57 def test_init_invalid_inputs(self): 

58 """Invalid input combinations must raise where appropriate.""" 

59 

60 # Empty dates and dfs — must raise 

61 with self.assertRaises(Exception): 

62 DiscountCurve("x", self.refdate, [], []) 

63 

64 # Length mismatch 

65 with self.assertRaises(Exception): 

66 DiscountCurve("x", self.refdate, [self.refdate + dt.timedelta(days=1)], [0.9, 0.8]) 

67 

68 # Non-enum arguments 

69 with self.assertRaises(TypeError): 

70 DiscountCurve( 

71 "x", 

72 self.refdate, 

73 [self.refdate + dt.timedelta(days=1)], 

74 [1.0], 

75 interpolation="BAD", 

76 extrapolation=ExtrapolationType.LINEAR, 

77 daycounter=DayCounterType.Act365Fixed, 

78 ) 

79 with self.assertRaises(TypeError): 

80 DiscountCurve( 

81 "x", 

82 self.refdate, 

83 [self.refdate + dt.timedelta(days=1)], 

84 [1.0], 

85 interpolation=InterpolationType.LINEAR, 

86 extrapolation="BAD", 

87 daycounter=DayCounterType.Act365Fixed, 

88 ) 

89 with self.assertRaises(TypeError): 

90 DiscountCurve( 

91 "x", 

92 self.refdate, 

93 [self.refdate + dt.timedelta(days=1)], 

94 [1.0], 

95 interpolation=InterpolationType.LINEAR, 

96 extrapolation=ExtrapolationType.LINEAR, 

97 daycounter="BAD", 

98 ) 

99 

100 # First date before refdate 

101 with self.assertRaises(Exception): 

102 DiscountCurve( 

103 "x", 

104 self.refdate, 

105 [self.refdate - dt.timedelta(days=1)], 

106 [1.0], 

107 InterpolationType.LINEAR, 

108 ExtrapolationType.LINEAR, 

109 DayCounterType.Act365Fixed, 

110 ) 

111 

112 # Instead of expecting an exception, assert correct behavior: 

113 dc = DiscountCurve( 

114 "x", 

115 self.refdate, 

116 [self.refdate + dt.timedelta(days=1)], 

117 [0.9], 

118 InterpolationType.LINEAR, 

119 ExtrapolationType.LINEAR, 

120 DayCounterType.Act365Fixed, 

121 ) 

122 # constructor should have prepended (refdate, 1.0) 

123 dates = dc.get_dates() 

124 dfs = dc.get_df() 

125 self.assertEqual(dates[0], self.refdate) 

126 self.assertEqual(dfs[0], 1.0) 

127 self.assertEqual(dates[1], self.refdate + dt.timedelta(days=1)) 

128 self.assertAlmostEqual(dfs[1], 0.9) 

129 

130 # Non-monotonic or duplicate dates 

131 with self.assertRaises(Exception): 

132 DiscountCurve( 

133 "x", 

134 self.refdate, 

135 [self.refdate + dt.timedelta(days=1), self.refdate + dt.timedelta(days=1)], 

136 [1.0, 0.99], 

137 InterpolationType.LINEAR, 

138 ExtrapolationType.LINEAR, 

139 DayCounterType.Act365Fixed, 

140 ) 

141 

142 def test_get_dates_and_get_df(self): 

143 dc = DiscountCurve( 

144 "Test_DC_ACT365FIXED", 

145 self.refdate, 

146 self.dates, 

147 self.dsc_fac_ACT365FIXED, 

148 InterpolationType.LINEAR, 

149 ExtrapolationType.LINEAR, 

150 DayCounterType.Act365Fixed, 

151 ) 

152 dates = dc.get_dates() 

153 dfs = dc.get_df() 

154 self.assertEqual(len(dates), len(dfs)) 

155 self.assertEqual(dates[0], self.refdate) 

156 self.assertEqual(dfs[0], 1.0) 

157 

158 def test_value_and_extrapolation_cases(self): 

159 """Replicates and extends original test_value logic.""" 

160 dc_linear = DiscountCurve( 

161 "Test_DC_ACT365FIXED", 

162 self.refdate, 

163 self.dates, 

164 self.dsc_fac_ACT365FIXED, 

165 InterpolationType.LINEAR, 

166 ExtrapolationType.LINEAR, 

167 DayCounterType.Act365Fixed, 

168 ) 

169 

170 df1 = dc_linear.value(self.refdate, self.refdate + dt.timedelta(days=90)) 

171 df2 = dc_linear.value(self.refdate, self.refdate + dt.timedelta(days=180)) 

172 fwd_df = dc_linear.value(self.refdate + dt.timedelta(days=90), self.refdate + dt.timedelta(days=180)) 

173 self.assertAlmostEqual(df1, 0.9926568878362608, delta=1e-5) 

174 self.assertAlmostEqual(df2, 0.9853143806626516, delta=1e-5) 

175 self.assertAlmostEqual(fwd_df, df2 / df1, delta=1e-5) 

176 

177 # Linear extrapolation 

178 df_extrap1 = dc_linear.value(self.refdate, self.refdate + dt.timedelta(days=10 * 365 + 10)) 

179 df_extrap2 = dc_linear.value(self.refdate, self.refdate + dt.timedelta(days=10 * 365 + 60)) 

180 self.assertAlmostEqual(df_extrap1, 0.7401510872751634, delta=1e-5) 

181 self.assertAlmostEqual(df_extrap2, 0.7368154202423907, delta=1e-5) 

182 

183 # Constant extrapolation 

184 dc_const = DiscountCurve( 

185 "Test_DC_ACT365FIXED", 

186 self.refdate, 

187 self.dates, 

188 self.dsc_fac_ACT365FIXED, 

189 InterpolationType.LINEAR, 

190 ExtrapolationType.CONSTANT, 

191 DayCounterType.Act365Fixed, 

192 ) 

193 df_const1 = dc_const.value(self.refdate, self.refdate + dt.timedelta(days=10 * 365 + 10)) 

194 df_const2 = dc_const.value(self.refdate, self.refdate + dt.timedelta(days=10 * 365 + 60)) 

195 self.assertAlmostEqual(df_const1, df_const2, delta=1e-10) 

196 

197 # Extrapolation NONE -> should raise ValueError 

198 dc_none = DiscountCurve( 

199 "Test_DC_ACT365FIXED", 

200 self.refdate, 

201 self.dates, 

202 self.dsc_fac_ACT365FIXED, 

203 InterpolationType.LINEAR, 

204 ExtrapolationType.NONE, 

205 DayCounterType.Act365Fixed, 

206 ) 

207 with self.assertRaises(ValueError): 

208 dc_none.value(self.refdate, self.refdate + dt.timedelta(days=4000)) 

209 

210 # TODO: 

211 # Tests with other DCC 

212 

213 # ActAct, LINEAR, LINEAR 

214 # ActAct, LINEAR, CONSTANT 

215 # ActAct, LINEAR, NONE 

216 # Act360, LINEAR, LINEAR 

217 # Act360, LINEAR, CONSTANT 

218 # Act360, LINEAR, NONE 

219 # 30U360, LINEAR, LINEAR 

220 # 30U360, LINEAR, CONSTANT 

221 # 30U360, LINEAR, NONE 

222 

223 # TODO: TEST LIST of given dates 

224 

225 def test_value_rate_and_yf(self): 

226 dc = DiscountCurve( 

227 "Test_DC_ACT365FIXED", 

228 self.refdate, 

229 self.dates, 

230 self.dsc_fac_ACT365FIXED, 

231 InterpolationType.LINEAR, 

232 ExtrapolationType.LINEAR, 

233 DayCounterType.Act365Fixed, 

234 ) 

235 d = self.refdate + dt.timedelta(days=365) 

236 rate = dc.value_rate(self.refdate, d) 

237 df = dc.value(self.refdate, d) 

238 expected = -math.log(df) / DayCounter(DayCounterType.Act365Fixed).yf(self.refdate, d) 

239 self.assertAlmostEqual(rate, expected, delta=1e-12) 

240 

241 df_yf = dc.value_yf(0.5) 

242 self.assertIsInstance(df_yf, float) 

243 

244 def test_value_fwd_and_fwd_rate(self): 

245 dc = DiscountCurve( 

246 "Test_DC_ACT365FIXED", 

247 self.refdate, 

248 self.dates, 

249 self.dsc_fac_ACT365FIXED, 

250 InterpolationType.LINEAR, 

251 ExtrapolationType.LINEAR, 

252 DayCounterType.Act365Fixed, 

253 ) 

254 

255 d1 = self.refdate + dt.timedelta(days=365) 

256 d2 = self.refdate + dt.timedelta(days=730) 

257 fwd_df = dc.value_fwd(self.refdate, d1, d2) 

258 self.assertTrue(fwd_df < 1.0) 

259 fwd_rate = dc.value_fwd_rate(self.refdate, d1, d2) 

260 self.assertAlmostEqual(fwd_rate, -math.log(fwd_df) / DayCounter(DayCounterType.Act365Fixed).yf(d1, d2)) 

261 

262 # Value date > refdate triggers rebasement logic 

263 val_date = self.refdate + dt.timedelta(days=500) 

264 fwd_df2 = dc.value_fwd(val_date, d1, d2) 

265 self.assertIsInstance(fwd_df2, float) 

266 

267 # Value date before refdate -> should raise 

268 with self.assertRaises(Exception): 

269 dc.value_fwd(self.refdate - dt.timedelta(days=1), d1, d2) 

270 

271 def test_call_zero_rate_and_rate_for_dates(self): 

272 dc = DiscountCurve( 

273 "Test_DC_ACT365FIXED", 

274 self.refdate, 

275 self.dates, 

276 self.dsc_fac_ACT365FIXED, 

277 InterpolationType.LINEAR, 

278 ExtrapolationType.LINEAR, 

279 DayCounterType.Act365Fixed, 

280 ) 

281 

282 # Direct zero rate for year fraction 

283 z = dc(0.5) 

284 self.assertIsInstance(z, float) 

285 self.assertGreater(z, 0) 

286 

287 # Zero rate for given date/refdate 

288 d = self.refdate + dt.timedelta(days=365) 

289 z2 = dc(0.5, self.refdate, d) 

290 self.assertIsInstance(z2, float) 

291 

292 # --------------------- Error and edge case tests -------------------------- 

293 

294 def test_value_invalid_ref_before_curve_ref(self): 

295 dc = DiscountCurve( 

296 "Test_DC_ACT365FIXED", 

297 self.refdate, 

298 self.dates, 

299 self.dsc_fac_ACT365FIXED, 

300 InterpolationType.LINEAR, 

301 ExtrapolationType.LINEAR, 

302 DayCounterType.Act365Fixed, 

303 ) 

304 with self.assertRaises(Exception): 

305 dc.value(self.refdate - dt.timedelta(days=1), self.refdate + dt.timedelta(days=1)) 

306 

307 def test_value_rate_invalid_ref_before_curve_ref(self): 

308 dc = DiscountCurve( 

309 "Test_DC_ACT365FIXED", 

310 self.refdate, 

311 self.dates, 

312 self.dsc_fac_ACT365FIXED, 

313 InterpolationType.LINEAR, 

314 ExtrapolationType.LINEAR, 

315 DayCounterType.Act365Fixed, 

316 ) 

317 with self.assertRaises(Exception): 

318 dc.value_rate(self.refdate - dt.timedelta(days=1), self.refdate + dt.timedelta(days=1)) 

319 

320 # --------------------- Placeholder for HAGAN and plot --------------------- 

321 # TODO 

322 def test_value_HAGAN(self): 

323 """Basic smoke test for HAGAN interpolation type.""" 

324 dc = DiscountCurve( 

325 "Test_DC_HAGAN", 

326 self.refdate, 

327 self.dates, 

328 self.dsc_fac_ACT365FIXED, 

329 InterpolationType.HAGAN_DF, 

330 ExtrapolationType.LINEAR, 

331 DayCounterType.Act365Fixed, 

332 ) 

333 val = dc.value(self.refdate, self.refdate + dt.timedelta(days=180)) 

334 self.assertIsInstance(val, float) 

335 

336 def test_plot_value(self): 

337 """Plot test stub (if implemented in module).""" 

338 dc = DiscountCurve( 

339 "Test_DC_PLOT", 

340 self.refdate, 

341 self.dates, 

342 self.dsc_fac_ACT365FIXED, 

343 InterpolationType.LINEAR, 

344 ExtrapolationType.LINEAR, 

345 DayCounterType.Act365Fixed, 

346 ) 

347 # The class itself doesn’t define plot(), but if added, ensure it runs 

348 if hasattr(dc, "plot"): 

349 dc.plot() # Smoke test 

350 

351 

352if __name__ == "__main__": 

353 unittest.main()