Coverage for rivapy / marketdata / bootstrapping.py: 94%

216 statements  

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

1# 2025.07.23 Hans Nguyen 

2# Boostrapping in rivapy indepedent of pyvacon 

3 

4 

5from rivapy.marketdata._logger import logger 

6 

7 

8########## 

9# Modules 

10import copy 

11from datetime import datetime, date 

12from dateutil.relativedelta import relativedelta 

13from typing import Union as _Union, List as _List 

14 

15# from rivapy.instruments.specifications import DepositSpecification, 

16from rivapy.instruments.deposit_specifications import DepositSpecification 

17from rivapy.instruments.fra_specifications import ForwardRateAgreementSpecification 

18from rivapy.instruments.ir_swap_specification import ( 

19 InterestRateSwapSpecification, 

20 IrFixedLegSpecification, 

21 IrFloatLegSpecification, 

22 IrSwapLegSpecification, 

23 InterestRateBasisSwapSpecification, 

24) 

25from rivapy.marketdata import DiscountCurve 

26from rivapy.marketdata.fixing_table import FixingTable 

27from rivapy.tools.enums import DayCounterType, RollConvention, RollRule, InterpolationType, ExtrapolationType, Instrument 

28from rivapy.tools.datetools import DayCounter 

29 

30from scipy.optimize import brentq 

31 

32 

33# import quote calculators # TODO SUBJECT TO CHANGE based on architecture 

34from rivapy.pricing.deposit_pricing import DepositPricer 

35from rivapy.pricing.fra_pricing import ForwardRateAgreementPricer 

36from rivapy.pricing.interest_rate_swap_pricing import InterestRateSwapPricer 

37 

38 

39########## 

40# Classes 

41 

42 

43###################################################### 

44########## 

45# Functions 

46def bootstrap_curve( 

47 ref_date: _Union[date, datetime], 

48 curve_id: str, 

49 day_count_convention: _Union[DayCounterType, str], 

50 instruments: _List, 

51 quotes: _List, 

52 curves: dict = None, 

53 # discount_curve: DiscountCurve = None, 

54 # basis_curve: DiscountCurve = None, 

55 interpolation_type: InterpolationType = InterpolationType.LINEAR, 

56 extrapolation_type: ExtrapolationType = ExtrapolationType.LINEAR, 

57 tolerance: float = 1.0e-6, 

58 max_iterations: int = 10000, 

59) -> DiscountCurve: 

60 """ 

61 

62 Args: 

63 ref_date (_Union[date, datetime]): the reference for the new curve 

64 curve_id (str): Id for the new Curve 

65 day_count_convention (_Union[DayCounterType, str]): daycounter for the new curve 

66 instruments (_List): instrument specifications that are used in the calibration (deposits, FRAs, and swaps allowed atm) 

67 quotes (_List): the rate quotes for the instruments (deposit rates, FRAs and swap rates) 

68 curves (dict): curves to be used during bootstrapping such as discount curve and forward curve if given. Defaults to Empty 

69 interpolation_type (InterpolationType): interpolation method to be used by the final curve. defaults to LINEAR 

70 extrapolation_type (ExtrapolationType): extrapolation method to be used by the final curve. defaults to LINEAR 

71 tolerance (float): tolerance value used in refinement of the zero rates 

72 max_iterations (int): the maximim number of iterations (after that the bootstrapper fails) 

73 

74 

75 Returns: 

76 DiscountCurve: bootstrapped discount curve 

77 """ 

78 # print("USING BOOTSTRAPPER V1----------------########################------------") # DEBUG and REMOVE 

79 logger.info("Starting bootstrapper.") 

80 

81 # Sanity checks: 

82 logger.info("Input sanity checks") 

83 assert len(instruments) == len(quotes), "Number of quotes does not equal number of instruments." 

84 # TODO implement more input quality checks: 

85 # curves given of correct type that match instrument type - or will this be done in the "market container" class? 

86 

87 logger.info("Curve dictionary import") 

88 if curves == None: 

89 curves = {} 

90 logger.info("* curves dictionary is empty, will bootstrap single discount curve") 

91 else: 

92 logger.info("* curves dictionary provided, will bootstrap forward curve") 

93 

94 ############################################################# 

95 # initialize: 

96 logger.info("discount curve value init") 

97 

98 yc_dates = [ref_date] 

99 dfs = [1.0] 

100 

101 ############################################################# 

102 # NEW FEATURE: Handle curve extension via curves["initial_curve"] - if provided 

103 # useful for using e.g. TBS to extend existing curves 

104 initial_curve = None 

105 initial_dates_set = set() 

106 if curves is not None and "initial_curve" in curves and curves["initial_curve"] is not None: 

107 initial_curve = curves["initial_curve"] 

108 if not isinstance(initial_curve, DiscountCurve): 

109 raise Exception("initial_curve must be a DiscountCurve instance") 

110 ic_dates = initial_curve.get_dates() 

111 ic_dfs = initial_curve.get_df() 

112 if len(ic_dates) == 0 or len(ic_dfs) == 0: 

113 raise Exception("initial_curve must contain at least one date and discount factor") 

114 for d, df in zip(ic_dates, ic_dfs): 

115 if d <= ref_date: 

116 continue 

117 yc_dates.append(d) 

118 dfs.append(df) 

119 initial_dates_set.add(d) 

120 logger.info(f"Prepopulated curve with {len(initial_dates_set)} initial points from initial_curve.") 

121 

122 # init DCC 

123 dcc = DayCounter(day_count_convention) 

124 if isinstance(day_count_convention, str): # normalizes type 

125 day_count_convention = DayCounterType(day_count_convention) 

126 

127 ############################################################# 

128 # Sort instruments # Obtain dates 

129 # check for instruments with duplicate end dates # for now, through exceptiion if there is 

130 # 

131 logger.info("Duplicate instrument date filter") 

132 instruments_by_date = {} 

133 for i, inst in enumerate(instruments): 

134 end_date = inst.get_end_date() 

135 if end_date in instruments_by_date: 

136 raise Exception(f"Duplicate expiry date found: {end_date}") 

137 instruments_by_date[end_date] = (quotes[i], inst) 

138 

139 if initial_dates_set: 

140 filtered_instruments_by_date = {} 

141 for d, val in instruments_by_date.items(): 

142 if d in initial_dates_set: 

143 logger.info(f"Skipping instrument at {d} because it exists in initial_curve.") 

144 continue 

145 filtered_instruments_by_date[d] = val 

146 instruments_by_date = filtered_instruments_by_date 

147 

148 ############################################################# 

149 # Determine instrument types provided 

150 logger.info("Determine instrument types provided") 

151 ins_types = set(inst.ins_type() for inst in instruments) 

152 has_fra = Instrument.FRA in ins_types 

153 has_irs = Instrument.IRS in ins_types 

154 has_bs = Instrument.BS in ins_types 

155 has_deposit = Instrument.DEPOSIT in ins_types 

156 

157 # Determine single vs multi-curve bootstrap 

158 if "discount_curve" not in curves: 

159 # Single-curve bootstrap: create discount curve from instruments 

160 flag_multi_curve = False 

161 curves["discount_curve"] = DiscountCurve( 

162 "bootstrapped_discount", 

163 ref_date, 

164 yc_dates, 

165 dfs, 

166 interpolation_type, 

167 extrapolation_type, 

168 day_count_convention, 

169 ) 

170 # Forward curve for IRS = discount curve (if any IRS present) 

171 flag_irs_bootstrapped_as_fwd = has_irs or has_bs 

172 if flag_irs_bootstrapped_as_fwd: 

173 curves["fixing_curve"] = curves["discount_curve"] 

174 

175 else: 

176 # Multi-curve bootstrap: discount curve is provided, we bootstrap forward curve 

177 flag_multi_curve = True 

178 

179 # FRAs and IRS will build forward curve 

180 if "fixing_curve" not in curves: 

181 # Create empty forward curve placeholder 

182 curves["fixing_curve"] = DiscountCurve( 

183 "bootstrapped_forward", 

184 ref_date, 

185 yc_dates, 

186 dfs, # optionally start with empty DFs 

187 interpolation_type, 

188 extrapolation_type, 

189 day_count_convention, 

190 ) 

191 flag_irs_bootstrapped_as_fwd = False 

192 

193 # Sanity checks 

194 if has_deposit and flag_multi_curve: 

195 raise Exception("Deposits cannot be used in multi-curve bootstrapping") 

196 

197 # Logging 

198 logger.info(f"Bootstrap mode: {'multi-curve' if flag_multi_curve else 'single-curve'}") 

199 logger.info(f"Instruments present: {ins_types}") 

200 logger.info(f"IRS uses bootstrapped curve as forward: {flag_irs_bootstrapped_as_fwd}") 

201 

202 ############################################################# 

203 # # start with loglinear interpolation to obtain good initial values for all dates 

204 

205 lower = 1.0e-5 # initial bracket values for brentq 

206 upper = 5.0 

207 

208 logger.info("Sort instruments by date and start bootstrapping") 

209 for end_date in sorted(instruments_by_date): 

210 # If end_date already exists (from initial_curve), skip solving it 

211 # if end_date in yc_dates: 

212 # logger.info(f"Skipping instrument at {end_date}: already present in initial_curve.") 

213 # continue 

214 quote, inst = instruments_by_date[end_date] # use the market quote to compare with brentq 

215 yc_dates.append(end_date) # next date 

216 prev_df = dfs[-1] # previous discount factor for bracket search 

217 dfs.append(prev_df) # append a dummy value for the next date 

218 

219 # arguments to be passed to the error function for the brentq root solver 

220 ARGS = ( 

221 -1, # since we will look at the latest addition to our discount curve. 

222 dfs, 

223 yc_dates, 

224 inst, 

225 ref_date, 

226 quote, 

227 curves, 

228 InterpolationType.LINEAR_LOG, 

229 ExtrapolationType.LINEAR_LOG, 

230 day_count_convention, 

231 flag_irs_bootstrapped_as_fwd, 

232 flag_multi_curve, 

233 ) 

234 

235 try: 

236 

237 # solution = brentq(error_fn, lower, upper, ARGS, xtol=1e-6) 

238 # dfs[-1] = solution 

239 # logger.debug(f"Bootstrapped DF for {end_date}: {solution} for {inst.ins_type()}") 

240 

241 # - DEBUG 11.2025 

242 # just before calling find_bracket for the failing end_date 

243 # print("INSIDE LOOP FOR BOOTSTRAP") 

244 # print("---- DEBUG START for end_date:", end_date, "quote(raw):", quote) 

245 # print("prev_df (guess):", prev_df) 

246 

247 # # Evaluate error_fn at a few DF points (inside realistic DF support (1e-8, 1.0])) 

248 # test_dfs = [max(1e-10, prev_df * 0.5), max(1e-10, prev_df * 0.9), min(0.9999999, prev_df * 1.0), min(0.9999999, prev_df * 1.1)] 

249 # for td in test_dfs: 

250 # try: 

251 # val = error_fn( 

252 # td, 

253 # -1, 

254 # dfs, 

255 # yc_dates, 

256 # inst, 

257 # ref_date, 

258 # quote, 

259 # curves, 

260 # # InterpolationType.HAGAN_DF, 

261 # # ExtrapolationType.CONSTANT_DF, 

262 # InterpolationType.LINEAR_LOG, 

263 # ExtrapolationType.LINEAR_LOG, 

264 # day_count_convention, 

265 # flag_irs_bootstrapped_as_fwd, 

266 # flag_multi_curve, 

267 # ) 

268 # except Exception as e: 

269 # val = f"EXC:{e}" 

270 # print(f"error_fn({td:.12f}) = {val}") 

271 

272 # Also quickly check the sign/units of quote here: 

273 # print("Raw quote value (from market):", quote, "— are these bps? If so, convert: quote = quote*1e-4") 

274 # - 

275 

276 lower, upper = find_bracket(error_fn, prev_df, ARGS) 

277 # lower, upper = prev_df * 0.8, prev_df * 1.2 # this is not good enouhg to work for all cases c.f. above 

278 

279 logger.debug(f"Finding lower: {lower} and upper: {upper} bracket for root finding") 

280 

281 # - DEBUG 11.2025 

282 # print( 

283 # f"-----------------------------------------[BOOTSTRAP] Solving for DF of {end_date}, initial guess: {prev_df}, market quote: {quote}" 

284 # ) 

285 # # - DEBUG 11.2025 

286 

287 solution, result = brentq( 

288 error_fn, 

289 lower, 

290 upper, 

291 args=ARGS, 

292 xtol=1e-6, 

293 full_output=True, # <--- enables access to iteration info 

294 disp=True, # optional: prints if solver fails 

295 ) 

296 dfs[-1] = solution 

297 # Log detailed solver diagnostics 

298 logger.debug( 

299 f"Bootstrapped DF for {end_date}: {solution:.10f} " 

300 f"for {inst.ins_type()} | " 

301 f"iterations={result.iterations}, " 

302 f"function_calls={result.function_calls}, " 

303 f"converged={result.converged}" 

304 ) 

305 

306 # - DEBUG 11.2025 

307 # print(f"-----------------------------------------[BOOTSTRAP] Solved DF({end_date}) = {solution}") 

308 

309 except Exception as e: 

310 raise Exception(f"Initial bootstrap failed at {end_date}: {str(e)}") 

311 

312 # In principle, this will have produced a curve. It can be improved upon with refinement 

313 logger.info("Initial curve produced") 

314 ############################################################# 

315 # Iterative refinement with real interpolator 

316 # this is to improve the values for the whole curve, as each subsequent point is dependant on the previous ones 

317 # check for convergence: max change in zero rate estimate must be below tolerance. 

318 # max_diff = float("inf") 

319 logger.info("Iterative refinement step") 

320 max_diff = 0.0 

321 iteration = 0 

322 # --- NEW: map end_date to correct index in dfs --- for the case of extension via initial_curve creating offset 

323 end_date_to_index = {d: yc_dates.index(d) for d in yc_dates if d in instruments_by_date} 

324 while iteration < max_iterations and (max_diff > tolerance or iteration == 0): 

325 

326 total_evals = 0 # 

327 

328 # for i, end_date in enumerate(sorted(instruments_by_date), start=1): # iterate through all end dates 

329 

330 # --- Use only end_dates from instruments_by_date, get correct dfs index --- 

331 for end_date in sorted(instruments_by_date): # iterate through all end dates 

332 i = end_date_to_index[end_date] # <--- CHANGED: dynamically compute index instead of enumerate 

333 quote, inst = instruments_by_date[end_date] # use the quote to compare with brentq 

334 

335 ARGS = ( 

336 i, 

337 dfs, # At this stage, these are all the solved for discount factors 

338 yc_dates, # At this stage, this is the full list of dates of the discount curve 

339 inst, 

340 ref_date, 

341 quote, 

342 curves, 

343 interpolation_type, 

344 extrapolation_type, 

345 day_count_convention, 

346 flag_irs_bootstrapped_as_fwd, 

347 flag_multi_curve, 

348 ) 

349 

350 try: 

351 # used to determine the tolerance for brentq - scaled by discount factor and maturity and a heuristic 10% ontop to keep from over fitting 

352 tol_brent = dfs[i] * tolerance * dcc.yf(ref_date, end_date) * 0.1 

353 # print("------------------------refinement tolerance:") 

354 # print(f"{i} DF:{dfs[i]} * {tolerance} * {dcc.yf(ref_date, end_date)} * 0.1 = {tol_brent}") 

355 dfs[i] = brentq(error_fn, 0.00001, 5.0, ARGS, xtol=tol_brent) 

356 total_evals += 1 

357 logger.debug(f" refinement DF for {end_date}: {dfs[i]} for {inst.ins_type()} with total evals: {total_evals}") 

358 

359 except Exception as e: 

360 raise Exception(f"Refinement failed at {end_date}: {str(e)} total evals: {total_evals}") 

361 

362 max_diff = 0.0 

363 # Convergence check 

364 # for i, end_date in enumerate(sorted(instruments_by_date), start=1): 

365 for end_date in sorted(instruments_by_date): 

366 i = end_date_to_index[end_date] # <--- CHANGED: use mapped index 

367 

368 # calculate derivative dq/dr using finite differences 

369 # (q=quote, r=zero rate) 

370 quote, inst = instruments_by_date[end_date] 

371 yc = DiscountCurve("dummy_id", ref_date, yc_dates, dfs, interpolation_type, extrapolation_type, day_count_convention) 

372 

373 # Multi-curve logic possible logic and single curve 

374 bootstrap_context = {} # only need to do one time for base and epsilon 

375 if flag_multi_curve: 

376 # This is a forward curve — use Given discount curve for discounting 

377 bootstrap_context["flag_multi_curve"] = True 

378 curves["fixing_curve"] = yc 

379 # Keep discount_curve unchanged 

380 else: 

381 # Single-curve: updating discount curve itself 

382 curves["discount_curve"] = yc 

383 if flag_irs_bootstrapped_as_fwd: # if it is an irs instrument that needs the forward curve as well as it was not provided 

384 curves["fixing_curve"] = yc 

385 

386 q_model = get_quote(ref_date, inst, curves, bootstrap_context) # this curves dict needs to have the updated YC 

387 

388 epsilon = 1e-6 

389 dfs_perturbed = dfs.copy() 

390 dfs_perturbed[i] += epsilon # perturb only at position = i 

391 yc_perturbed = DiscountCurve( 

392 "dummy_id_perturbed", ref_date, yc_dates, dfs_perturbed, interpolation_type, extrapolation_type, day_count_convention 

393 ) 

394 

395 # Multi-curve and single curve flag logic 

396 if flag_multi_curve: 

397 # This is a forward curve — use Given discount curve for discounting 

398 curves["fixing_curve"] = yc_perturbed 

399 # Keep discount_curve unchanged 

400 else: 

401 # Single-curve: updating discount curve itself 

402 curves["discount_curve"] = yc_perturbed 

403 if flag_irs_bootstrapped_as_fwd: # if it is an irs instrument that needs the forward curve as well as it was not provided 

404 curves["fixing_curve"] = yc_perturbed 

405 

406 q_model_eps = get_quote(ref_date, inst, curves, bootstrap_context) 

407 

408 dq = (q_model_eps - q_model) / epsilon 

409 # print(f"dq: {dq}") 

410 # print(f"yf: {dcc.yf(ref_date, end_date)}") 

411 # print(f"df: {dfs[i]}") 

412 dr = abs((quote - q_model) / (dq * dcc.yf(ref_date, end_date) * dfs[i])) 

413 max_diff = max(max_diff, dr) 

414 

415 iteration += 1 

416 

417 if max_diff > tolerance: 

418 raise Exception("Bootstrapping did not converge within tolerance.") 

419 

420 # TODO adding 150Y pillar to avoid explicit extrapolation??? 

421 

422 # create final discount curve 

423 logger.info("Curve Complete and output") 

424 curve = DiscountCurve( 

425 id=curve_id, 

426 refdate=ref_date, 

427 dates=yc_dates, # populate with correct dates 

428 df=dfs, # populated with corresponding discount factors 

429 interpolation=interpolation_type, 

430 extrapolation=extrapolation_type, 

431 daycounter=day_count_convention, 

432 ) 

433 

434 return curve 

435 

436 

437# Compute Error - This method computes the diff between market quote and candidate 

438def error_fn( 

439 df_val: float, 

440 index: int, 

441 dfs: _List, 

442 yc_dates: _List, 

443 instrument_spec: _Union[DepositSpecification, ForwardRateAgreementSpecification, InterestRateSwapSpecification], 

444 ref_date: _Union[date, datetime], 

445 ref_quote: float, 

446 curves: dict, # or should it be dictionary? 

447 interpolation_type: InterpolationType, 

448 extrapolation_type: ExtrapolationType, 

449 day_count_convention: DayCounterType = DayCounterType.ACT360, 

450 flag_irs_bootstrapped_as_fwd: bool = False, 

451 flag_multi_curve: bool = False, 

452): 

453 """Error function used for the bootstrapper using a brentq solver. 

454 Returns the differnce between an input target value and calculated 

455 model value. 

456 

457 Given a list of corresponding dates and discount factors, create a disount curve object 

458 and update the curve dictionary necessary. 

459 

460 Pass relevant instrument information in order to calculate the fair rate given the current 

461 curve data. 

462 

463 #TODO think about how to IMPROVE implementation in the case where forward curve is the same as discount curve 

464 

465 

466 Args: 

467 df_val (float): discount factor value used as guess for next value of the bootstrapped discount curve 

468 index (int): list index of where to insert df_val. usually -1 is passed to ensure it is the last entry 

469 dfs (_List): list of predetermined discount factors 

470 yc_dates (_List): corresponding datetime objects 

471 instrument_spec (): instrument specific data 

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

473 ref_quote (float): target quote to compare to 

474 curves (dict): dictionary of relevant curve data 

475 interpolation_type (InterpolationType): the interpolation method to be used by the curves 

476 extrapolation_type (ExtrapolationType): the extrapolation method to be used by the curves 

477 day_count_convention: day coutn convention to be used for the dummy curve built 

478 flag_irs_bootstrapped_as_fwd (bool): Flag to trigger if fixing curve is the same as discount curve 

479 flag_multi_curve (bool): Flag to trigger if multi-curve bootstrapping is there 

480 

481 Returns: 

482 float: difference between target quote and calculated quote 

483 """ 

484 df_tmp = copy.deepcopy(dfs) 

485 df_tmp[index] = df_val 

486 # here reference date is used as placeholder 

487 yc = DiscountCurve("bootstrappedYC", ref_date, yc_dates, df_tmp, interpolation_type, extrapolation_type, day_count_convention) 

488 curves_copy = copy.deepcopy(curves) 

489 bootstrap_context = {"flag_multi_curve": flag_multi_curve} 

490 # Multi-curve logic possible logic and ssingle curve 

491 if flag_multi_curve: 

492 # This is a forward curve — use Given discount curve for discounting 

493 curves_copy["fixing_curve"] = yc 

494 # Keep discount_curve unchanged 

495 # print("UPDATING ONLY FIXING CURVE IN MULTI CURVE BOOTSTRAP") 

496 else: 

497 # Single-curve: updating discount curve itself 

498 curves_copy["discount_curve"] = yc 

499 if flag_irs_bootstrapped_as_fwd: # if it is an irs instrument that needs the forward curve as well or TBS 

500 curves_copy["fixing_curve"] = yc 

501 

502 calc_quote = get_quote(ref_date, instrument_spec, curves_copy, bootstrap_context) 

503 

504 # # DEBUG statement 

505 # print("----------------") 

506 # print("Error function trial curve") 

507 # print(yc.get_df()) 

508 # print("----------------") 

509 # print(f"using {df_val} -> calc_quote: {calc_quote} - ref_quote: {ref_quote} = {calc_quote - ref_quote}") 

510 

511 # inside error_fn, after constructing yc and curves_copy and computing calc_quote 

512 # compute model_residual = calc_quote - ref_quote or whichever sign convention you use 

513 # print(f"[DEBUG error_fn] df_val={df_val:.12f}, calc_quote={calc_quote}, ref_quote={ref_quote}, residual={calc_quote - ref_quote}") 

514 # optionally print underlying leg PVs (you can return them from compute_basis_spread or log inside). 

515 

516 return calc_quote - ref_quote 

517 

518 

519def find_bracket(error_fn, guess, args, expand=2.0, max_tries=10, min_lower=1e-8, max_upper=10.0): 

520 """ 

521 Tries to find a [lower, upper] bracket where error_fn(lower) and error_fn(upper) 

522 have opposite signs, indicating a root lies between them. 

523 

524 Parameters 

525 ---------- 

526 error_fn : callable 

527 Your pricing error function (same as passed to brentq). 

528 guess : float 

529 A rough estimate for the root (e.g., previous DF). 

530 args : tuple 

531 Extra arguments passed to error_fn. 

532 expand : float 

533 Factor by which to widen the bracket each iteration. 

534 max_tries : int 

535 Maximum number of expansions before giving up. 

536 min_lower, max_upper : float 

537 Hard limits to keep brackets within safe numeric bounds. 

538 """ 

539 lower = max(guess * 0.8, min_lower) 

540 upper = min(guess * 1.2, max_upper) 

541 

542 f_lower = error_fn(lower, *args) 

543 f_upper = error_fn(upper, *args) 

544 

545 tries = 0 

546 while f_lower * f_upper > 0 and tries < max_tries: 

547 # Expand symmetrically outward 

548 lower = max(lower / expand, min_lower) 

549 upper = min(upper * expand, max_upper) 

550 f_lower = error_fn(lower, *args) 

551 f_upper = error_fn(upper, *args) 

552 tries += 1 

553 

554 if f_lower * f_upper > 0: 

555 raise RuntimeError("Could not find valid bracket for brentq") 

556 

557 return lower, upper 

558 

559 

560def get_quote( 

561 ref_date: _Union[date, datetime], 

562 instrument_spec: _Union[ 

563 DepositSpecification, ForwardRateAgreementSpecification, InterestRateSwapSpecification, InterestRateBasisSwapSpecification 

564 ], 

565 curve_dict: dict, 

566 bootstrap_context=None, 

567): 

568 """Get the instrument specific fair quote calculation result to be used in the bootstrapper. 

569 

570 Args: 

571 ref_date (_Union[date, datetime]): 

572 instrument_spec (_Union[DepositSpecification, ForwardRateAgreementSpecification, InterestRateSwapSpecification]): 

573 curve_dict (dict): Dictionary containing the market data curves needed for discounting or fwd rates. 

574 

575 Returns: 

576 float: calculated fair rate 

577 """ 

578 

579 quote = 0.0 

580 if instrument_spec.ins_type() == Instrument.DEPOSIT: 

581 

582 # old 

583 discount_curve = curve_dict["discount_curve"] 

584 # spread_curve=curve_dict["spread_curve"] 

585 quote = DepositPricer.get_implied_simply_compounded_rate(ref_date, instrument_spec, discount_curve) # TODO assumes no spread curve for now 

586 

587 elif instrument_spec.ins_type() == Instrument.FRA: 

588 if bootstrap_context is None or bootstrap_context.get("flag_multi_curve", False) == False: 

589 # single curve case 

590 curve_used = curve_dict["discount_curve"] 

591 else: 

592 # multicurve, where discount given, FRA builds forward 

593 curve_used = curve_dict["fixing_curve"] 

594 quote = ForwardRateAgreementPricer.compute_fair_rate(ref_date, instrument_spec, discount_curve=curve_used) 

595 

596 elif instrument_spec.ins_type() == Instrument.IRS: 

597 

598 yc_discount = curve_dict["discount_curve"] 

599 yc_forward = curve_dict["fixing_curve"] 

600 # according to pyvayon example, the fixing table is assumed to default to empty to allow the code to run... 

601 fixing_table = FixingTable() 

602 

603 float_leg = instrument_spec.get_float_leg() 

604 fixed_leg = instrument_spec.get_fixed_leg() 

605 fixing_grace_period = 0 # TODO take in as parameter? in pyvacon example, the extra swap parameters are assumed to be empty, only the curves were passed as arguments... 

606 

607 # parameters specific to ir swap bootstrapping, in regards to the fixed leg for calculating the fair swap rate 

608 # needed to pass these settings onto the InterestRateSwapPricer.price_leg 

609 pricing_params = {"fixing_grace_period": fixing_grace_period, "set_rate": True, "desired_rate": 1.0} 

610 

611 quote = InterestRateSwapPricer.compute_swap_rate(ref_date, yc_discount, yc_forward, float_leg, fixed_leg, fixing_table, pricing_params) 

612 

613 elif instrument_spec.ins_type() == Instrument.BS: # basis swap # E.g. tenor basis swap 

614 

615 yc_discount = curve_dict["discount_curve"] 

616 yc_forward = curve_dict["fixing_curve"] # THIS IS THE CURVE TO BE SOLVED 

617 yc_basis_curve = curve_dict.get("basis_curve", None) # THIS IS THE EXISTING KNOWN CURVE - assume is for SHORT 

618 # NOTE - the rerquirement is that error_fn has the flags to determine if discoutn curve is the same as fixing curve or not already 

619 

620 if yc_basis_curve is None: 

621 raise Exception("Missing basis curve for pricing TBS") 

622 

623 fixing_table = FixingTable() 

624 

625 pay_leg = instrument_spec.get_pay_leg() 

626 receive_leg = instrument_spec.get_receive_leg() 

627 spread_leg = instrument_spec.get_spread_leg() 

628 fixing_grace_period = 0 # TODO take in as parameter? in pyvacon example, the extra swap parameters are assumed to be empty, only the curves were passed as arguments... 

629 pricing_params = { 

630 "fixing_grace_period": fixing_grace_period, 

631 "set_rate": True, 

632 "desired_rate": 1.0, 

633 } # need annuity again for spread_leg(modeled as fixed leg) 

634 

635 quote = InterestRateSwapPricer.compute_basis_spread( 

636 ref_date, 

637 discount_curve=yc_discount, 

638 payLegFixingCurve=yc_basis_curve, 

639 receiveLegFixingCurve=yc_forward, 

640 pay_leg=pay_leg, 

641 receive_leg=receive_leg, 

642 spread_leg=spread_leg, 

643 fixing_map=fixing_table, 

644 pricing_params=pricing_params, 

645 ) 

646 

647 # # DEBUG 

648 # print(f"Calculated quote for {instrument_spec.ins_type()} is {quote}") 

649 return quote 

650 

651 

652def bootstrap_curve_from_quote_table(input_data): 

653 pass 

654 

655 

656# Main 

657 

658if __name__ == "__main__": 

659 pass