|
From: Rui W. <Rui...@gm...> - 2025-02-13 20:00:55
|
Hi, I am trying to use QuantLib to price CDX options but encountered some challenges. It seems that the QuantLib: CdsOption Class Reference<https://www.quantlib.org/reference/class_quant_lib_1_1_cds_option.html> CDS options class is incomplete and does not provide common metrics such as price, delta or gamma. I also noticed that a similar issue was raised before but remains unresolved. Matching Bloomberg's CDSO using quantlib CdsOption * Issue #1341 * lballabio/QuantLib<https://github.com/lballabio/QuantLib/issues/1341> To work around this, I looked into MATLAB's implementation, which constructs a forward CDS and then adds the FEP back to account for the additional protection between t and te. (te is the option expiry date) Pricing a CDS Index Option<https://www.mathworks.com/help/fininst/pricing-a-cds-index-option.html> I wrote the following code to replicate this approach, either by constructing a forward CDS directly or by creating a CDS for the period (t,te). In theory, either method should work if the underlying calculations aligned with the intended model. However, I've found that both will have some discrepancies in the Adjusted Forward Spread and FwdRPV01 comparing with BLG or broker's data. My questions are: * Is there a better way to price CDS options from scratch using QuantLib? * If not, following the current method, where might the discrepancies arise? I suspect the issue may be related to the FwdRPV01 calculation. def calculate_fwd_spread_and_bpv( current_date: ql.Date, cds_maturity_date: ql.Date, option_expiry_date: ql.Date, spot_spread: float, running_spread: float, discount_curve: ql.YieldTermStructureHandle, notional: float = 1000000.0, recovery: float = 0.4, using_forward_cds: bool = False, ) -> dict: """Calculate forward spread, forward BPV, FEP, and adjusted forward spread. Args: current_date (ql.Date): Current date. cds_maturity_date (ql.Date): CDS maturity date. option_expiry_date (ql.Date): Option expiry date. spot_spread (float): Spot spread. running_spread (float): Running spread. discount_curve (ql.YieldTermStructureHandle): Discount curve. notional (float, optional): Notional amount. Default is 1000000.0. recovery (float, optional): Recovery rate. Default is 0.4. using_forward_cds (bool, optional): Whether to use forward CDS. Default is False. Returns: dict: A dictionary containing forward spread, forward BPV, FEP, and adjusted forward spread. """ ql.Settings.instance().evaluationDate = current_date cds_maturity_schedule = create_cds_schedule(current_date, cds_maturity_date) options_maturity_schedule = create_cds_schedule(current_date, option_expiry_date) forward_cds_schedule = create_cds_schedule(option_expiry_date, cds_maturity_date) # Quoted trade to infer the hazard rate quoted_trade = ql.CreditDefaultSwap( ql.Protection.Seller, notional, 0, spot_spread, cds_maturity_schedule, ql.Following, ql.Actual360(), True ) hazard_rate = quoted_trade.impliedHazardRate( 0, discount_curve, ql.Actual365Fixed(), recovery, 1e-8, ql.CreditDefaultSwap.ISDA ) # Create the CDS engine with the inferred hazard rate probability_curve = ql.FlatHazardRate( 0, ql.WeekendsOnly(), ql.QuoteHandle(ql.SimpleQuote(hazard_rate)), ql.Actual365Fixed() ) engine = ql.IsdaCdsEngine( ql.RelinkableDefaultProbabilityTermStructureHandle(probability_curve), recovery, discount_curve ) if using_forward_cds: # determine which method to use forward_cds_trade = ql.CreditDefaultSwap( ql.Protection.Seller, notional, running_spread, forward_cds_schedule, ql.Following, ql.Actual360(), True ) forward_cds_trade.setPricingEngine(engine) fwd_spread = forward_cds_trade.fairSpread() # Calculate Fwd BPV (present value of 1 bps spread) fwd_bpv = forward_cds_trade.couponLegBPS() / 1e2 else: conventional_trade = ql.CreditDefaultSwap( ql.Protection.Seller, notional, running_spread, options_maturity_schedule, ql.Following, ql.Actual360(), True, True, current_date, ql.FaceValueClaim(), ql.Actual360(True), True ) conventional_trade.setPricingEngine(engine) quoted_trade.setPricingEngine(engine) spread_t_te = conventional_trade.fairSpread() spread_t_tm = quoted_trade.fairSpread() # Calculate Fwd BPV (present value of 1 bps spread) rpv01_t_te = conventional_trade.couponLegBPS() / 1e2 rpv01_t_tm = quoted_trade.couponLegBPS() / 1e2 # calculate fwd spread fwd_bpv = rpv01_t_tm-rpv01_t_te fwd_spread = (spread_t_tm*rpv01_t_tm-spread_t_te*rpv01_t_te)/fwd_bpv # Calculate adjusted fwd spread discount_factor_te = discount_curve.discount(option_expiry_date) credit_curve_handle = ql.RelinkableDefaultProbabilityTermStructureHandle(probability_curve) survival_probability = credit_curve_handle.survivalProbability(option_expiry_date) fep = (1 - recovery) * discount_factor_te * (1 - survival_probability) fwd_spread_adj = fwd_spread + fep / fwd_bpv return { "fwd_spread": fwd_spread, "fwd_bpv": fwd_bpv, "fep": fep, "fwd_spread_adj": fwd_spread_adj } Any guidance or insights would be greatly appreciated. Rui |