Calls to CreditDefaultSwap.fair_upfront crash Python
tangent360 opened this issue · comments
Hey,
I attempted to modify pyql/examples/cds.py to get it working with the latest version of the library. I'm new to pyql and Quantlib so I may well have made an error but all the tests seem to execute fine. However, when I tried to call fair_upfront it crashes Python (The function is not called by any of the pyql examples but I saw it in the Quantlib docs and I see it listed as a property in instruments/credit_default_swap.pyx).
The problem line in the code below is the very last line, everything else runs fine:
print(" Upfront: ", cds_2y.fair_upfront)
I've tried this on two different machines and get the same result - Win 10 and Win 7 (64bit).
I've attached a debugger from pyCharm but it just crashes too whenever I try to inspect the cds_2y object.
I've tried ipython and python, I've tried launching from inside the IDE and from the command line ... always the same result.
Some info that may be useful:
platform : win-64
conda version : 4.2.13
conda-build version : 1.21.2
python version : 3.5.2.final.0
boost : 1.61.0
quantlib : 1.8
C++ IDE : VS 2015 Community
nm Version : MinGW-w64
I have also applied the singelton patch to line 37 of Settings.hpp and all the tests run fine but I'm experiencing the Heston numerical errors that others have noticed (Issues #64 and #161).
Appreciate any help. Thanks.
""" Example of CDS pricing with PyQL.
This example is based on the QuantLib CDS official example.
Copyright (C) 2012 Enthought Inc.
"""
from __future__ import print_function
from quantlib.instruments.credit_default_swap import CreditDefaultSwap, SELLER
from quantlib.pricingengines.credit import MidPointCdsEngine
from quantlib.settings import Settings
from quantlib.time.api import (
Date, May, Dec, Actual365Fixed, Following, TARGET, Period, Months,
Quarterly, TwentiethIMM, Years, Schedule, Unadjusted
)
from quantlib.time.calendar import Calendar
from quantlib.termstructures.credit.api import SpreadCdsHelper, PiecewiseDefaultCurve, ProbabilityTrait, Interpolator
from quantlib.termstructures.yields.api import FlatForward
if __name__ == '__main__':
#*********************
#*** MARKET DATA ***
#*********************
calendar = TARGET()
todays_date = Date(6, Dec, 2016)
# must be a business day
todays_date = calendar.adjust(todays_date)
Settings.instance().evaluation_date = todays_date
# dummy curve
ts_curve = FlatForward(
reference_date=todays_date, forward=0.01, daycounter=Actual365Fixed()
)
# In Lehmans Brothers "guide to exotic credit derivatives"
# p. 32 there's a simple case, zero flat curve with a flat CDS
# curve with constant market spreads of 150 bp and RR = 50%
# corresponds to a flat 3% hazard rate. The implied 1-year
# survival probability is 97.04% and the 2-years is 94.18%
# market
recovery_rate = 0.5
quoted_spreads = [0.0150, 0.0150, 0.0150, 0.0150 ]
tenors = [Period(i, Months) for i in [3, 6, 12, 24]]
maturities = [
calendar.adjust(todays_date + tenors[i], Following) for i in range(4)
]
instruments = []
for i in range(4):
helper = SpreadCdsHelper(
quoted_spreads[i], tenors[i], 0, calendar, Quarterly,
Following, TwentiethIMM, Actual365Fixed(), recovery_rate, ts_curve
)
instruments.append(helper)
hazard_rate_structure = PiecewiseDefaultCurve(ProbabilityTrait.HazardRate, # ProbabilityTrait trait
Interpolator.BackwardFlat, # Interpolator interpolator
0, # Natural settlement_days
calendar, # Calendar calendar
instruments, # list helpers
Actual365Fixed(), # DayCounter daycounter
1.0e-12) # Real accuracy=1e-12
#vector<pair<Date, Real> > hr_curve_data = hazardRateStructure->nodes();
#cout << "Calibrated hazard rate values: " << endl ;
#for (Size i=0; i<hr_curve_data.size(); i++) {
# cout << "hazard rate on " << hr_curve_data[i].first << " is "
# << hr_curve_data[i].second << endl;
#}
#cout << endl;
target = todays_date + Period(1, Years)
print('Target Date is: ', target)
print("Some survival probability values: ")
print("1Y survival probability: {:%}".format(
hazard_rate_structure.survival_probability(target)
))
print(" expected: {:%}".format(0.9704))
print("2Y survival probability: {:%}".format(
hazard_rate_structure.survival_probability(todays_date + Period(2, Years))
))
print(" expected: {:%}".format(0.9418))
# reprice instruments
nominal = 1000000.0;
#Handle<DefaultProbabilityTermStructure> probability(hazardRateStructure);
engine = MidPointCdsEngine(hazard_rate_structure, recovery_rate, ts_curve)
cds_schedule = Schedule(
todays_date, maturities[0], Period(Quarterly), calendar,
termination_date_convention=Unadjusted,
date_generation_rule=TwentiethIMM
)
cds_3m = CreditDefaultSwap(
SELLER, nominal, quoted_spreads[0], cds_schedule, Following,
Actual365Fixed()
)
cds_schedule = Schedule(
todays_date, maturities[1], Period(Quarterly), calendar,
termination_date_convention=Unadjusted,
date_generation_rule=TwentiethIMM
)
cds_6m = CreditDefaultSwap(
SELLER, nominal, quoted_spreads[1], cds_schedule, Following,
Actual365Fixed()
)
cds_schedule = Schedule(
todays_date, maturities[2], Period(Quarterly), calendar,
termination_date_convention=Unadjusted,
date_generation_rule=TwentiethIMM
)
cds_1y = CreditDefaultSwap(
SELLER, nominal, quoted_spreads[2], cds_schedule, Following,
Actual365Fixed()
)
cds_schedule = Schedule(
todays_date, maturities[3], Period(Quarterly), calendar,
termination_date_convention=Unadjusted,
date_generation_rule=TwentiethIMM
)
cds_2y = CreditDefaultSwap(
SELLER, nominal, quoted_spreads[3], cds_schedule, Following,
Actual365Fixed()
)
cds_3m.set_pricing_engine(engine);
cds_6m.set_pricing_engine(engine);
cds_1y.set_pricing_engine(engine);
cds_2y.set_pricing_engine(engine);
print("Repricing of quoted CDSs employed for calibration: ")
print("3M fair spread: {}".format(cds_3m.fair_spread))
print(" NPV: ", cds_3m.net_present_value)
print(" default leg: ", cds_3m.default_leg_npv)
print(" coupon leg: ", cds_3m.coupon_leg_npv)
print("6M fair spread: {}".format(cds_6m.fair_spread))
print(" NPV: ", cds_6m.net_present_value)
print(" default leg: ", cds_6m.default_leg_npv)
print(" coupon leg: ", cds_6m.coupon_leg_npv)
print("1Y fair spread: {}".format(cds_1y.fair_spread))
print(" NPV: ", cds_1y.net_present_value)
print(" default leg: ", cds_1y.default_leg_npv)
print(" coupon leg: ", cds_1y.coupon_leg_npv)
print("2Y fair spread: {}".format(cds_2y.fair_spread))
print(" NPV: ", cds_2y.net_present_value)
print(" default leg: ", cds_2y.default_leg_npv)
print(" coupon leg: ", cds_2y.coupon_leg_npv)
print(" Upfront: ", cds_2y.fair_upfront)
fair_upfront throws unless you build a cds with upfront which you can't do right now with pyql (See https://github.com/thrasibule/QuantLib/blob/master/ql/instruments/creditdefaultswap.cpp#L179). Now we should raise an exception in python instead of aborting.
You can check my branch upfront_lite which has this fix and also a class for building UpfrontCds.
I'll submit a pull request to make sure the example run as it is.
Thank you for this, it's very helpful. Apologies in advance because this would be much more straightforward if I had a good working knowledge of quantlib.
After looking through the quantlib code I did notice that fairSpread() "does not take any upfront into account, even if one was given", which rules out my intended use for specifying an upfront in the first place - i.e. as way to back out the fair spread given only the upfront.
I believe now the correct way to achieve this goal would be to use the UpfrontCdsHelper and not to try and back into the correct hazzards by changing attributes of the instrument and recomputing?
Is there a common industry equivalent use-case for specifying the upfront as implemented by quantlib?
If I'm thinking about this correctly, to get the market observed upfront on a SNAC CDS you can just set the coupon to 100 or 500 as appropriate on the CreditDefaultSwap instrument and compute the NPV - that should be the day one upfront payment? (this is all assuming you have built the correct underlying hazard rate curve)
Unless you have a very delayed upfront where the FV of the upfront amount is different than paying it today (or T+3) then I'm struggling to see where one would use it.
In any event, I was playing around with the C++ CDS example in quantlib and noticed that if you specify a protection startDate and an upfrontDate of today, then subsequent calls to fairUpfront() return the 'fair upfront not available' error that you referenced in your last post. If you change the upfrontDate = T+1 or later then fairUpfront() computes the correct upfront, which obviously ignores the provided upfront so I don't really understand the logic behind this behavior. If I move the accrual date, protection start date and upfront date all forward to T+1 and leave the evaluation date on T then it also works so I'm guessing this is associated with the includeTodaysCashFlows option in Settings but have not has a chance to investigate yet.
When I try the same from pyql using your branch, and creating the CDS with:
cds_with_upfront = CreditDefaultSwap.from_upfront( SELLER, nominal, upfront_rate, quoted_spreads, cds_schedule, Following, Actual365Fixed(), True, todays_date, todays_date )
and then call:
cds_with_upfront.fair_upfront
I get an error message which is much more informative than the prior crashes:
RuntimeError: fair upfront not available
this behaviour matches the C++ implementation, however when I advance the upfrontDate to todays_date+1, it crashes Python like before.
Similarly, adding 1 to both protectionStartDate and upfrontDate would give me an error that the "protection can not start after accrual" from C++ but from pyql it crashes.
The third option of advancing all 3 dates (accrual, protection start and upfront date) by one day seems to work fine - it matches the C++ numbers, which pretty much tie out with Bloomberg CDSW.
I'm not worried about a day or so of PV-ing difference ... this version will work perfectly fine for me, but I wanted to let you know the behaviour I was seeing in case you were ultimately planning to merge this back into the master branch. Many thanks for the help.
@tangent360 thanks for your detailed notes. The best for debugging such issue is to provide a minimal script that reproduces the bug. Is this something you could share?
I suspect we do have a c++ exception raised that we don't manage properly in pyql and that the fix should be quick and easy to implement (adding some except+
in the cython layer).
You're right, if you want to get the upfront equivalent, you can just look at the npv of the cds.
I've updated my branch upfront_lite to catch a few more exceptions , so it should bomb a lot less now, let me know if you can trigger other segfaults. It would be helpful if you have some example code written. We can turn it into tests, and I can propose it for merging if the code is helpful to you.
The QuantLib CDS code is pretty old, and doesn't handle current market conventions well at the moment. There is a pull request outstanding which improves the situation a lot (see lballabio/QuantLib#112), but not sure if or when it will get accepted. Maybe you could express interest on github or on the mailing list, that could get things going.
Thank you both for the responses.
I've pasted an example script below that should generate the errors I'm seeing.
I've also pasted my output when I run it so that you can see what I'm getting.
@thrasibule I'd be more than happy to try and get things going on the quantlib CDS updates but I fear the only value I can provide is motivation! What's the best way to see what's missing? Just read through all the proposed code changes or is there a doc somewhere outlining the shortcomings vs the current industry standards?
My output:
============================================================================
This attempt will behave the same as the C++ version.
It raises a runtime error because the accrual date, protection start date
and upfront date are the same as the evaluation date.
The error message should be: RuntimeError: fair upfront not available
============================================================================
Caught Runtime Error: fair upfront not available
============================================================================
This attempt will behave the same as the C++ version.
The accrual date, protection start date and upfront date have all been advanced
to T+1 where the evaluation date is T.
The computed upfront is 'practically correct', but we had to shift all the dates to make it work.
============================================================================
Upfront: 0.11184785949854062
============================================================================
This attempt will NOT behave the same as the C++ version.
The upfront date has been advanced to T+1 ... it crashes python while trying
to create the CreditDefaultSwap.
To match the C++ version it should be returning the 'correct' calculated
upfront amount, but it won't exactly match the upfront above because of the
other date changes
============================================================================
The Schedule has been correctly created ... now attempting to create the CreditDefaultSwap...
**** PYTHON CRASHES ***
My test script:
from __future__ import print_function
from quantlib.instruments.credit_default_swap import CreditDefaultSwap, SELLER
from quantlib.pricingengines.credit import MidPointCdsEngine
from quantlib.settings import Settings
from quantlib.time.api import (
Date, May, Sep, Dec, Actual360, Following, TARGET, Period, Months,
Quarterly, TwentiethIMM, Years, Schedule, Unadjusted
)
from quantlib.termstructures.credit.api import (
SpreadCdsHelper, PiecewiseDefaultCurve, ProbabilityTrait, Interpolator )
from quantlib.termstructures.yields.api import FlatForward
if __name__ == '__main__':
#*********************
#*** MARKET DATA ***
#*********************
calendar = TARGET()
todays_date = Date(20, Sep, 2016)
# must be a business day
todays_date = calendar.adjust(todays_date)
Settings.instance().evaluation_date = todays_date
# dummy curve
ts_curve = FlatForward(
reference_date=todays_date, forward=-0.005, daycounter=Actual360()
)
# In Lehmans Brothers "guide to exotic credit derivatives"
# p. 32 there's a simple case, zero flat curve with a flat CDS
# curve with constant market spreads of 150 bp and RR = 50%
# corresponds to a flat 3% hazard rate. The implied 1-year
# survival probability is 97.04% and the 2-years is 94.18%
# market
recovery_rate = 0.4
quoted_spreads = [0.08, 0.08, 0.08, 0.08 ]
tenors = [Period(i, Months) for i in [1, 4, 8, 5*12]]
maturities = [
calendar.adjust(todays_date + tenors[i], Following) for i in range(4)
]
instruments = []
for i in range(4):
helper = SpreadCdsHelper(
quoted_spreads[i], tenors[i], 0, calendar, Quarterly,
Following, TwentiethIMM, Actual360(), recovery_rate, ts_curve
)
instruments.append(helper)
# Bootstrap hazard rates
hazard_rate_structure = PiecewiseDefaultCurve.from_reference_date(
ProbabilityTrait.HazardRate, Interpolator.BackwardFlat, todays_date, instruments, Actual360()
)
#vector<pair<Date, Real> > hr_curve_data = hazardRateStructure->nodes();
#cout << "Calibrated hazard rate values: " << endl ;
#for (Size i=0; i<hr_curve_data.size(); i++) {
# cout << "hazard rate on " << hr_curve_data[i].first << " is "
# << hr_curve_data[i].second << endl;
#}
#cout << endl;
target = todays_date + Period(1, Years)
# reprice instruments
nominal = 1000000.0;
#Handle<DefaultProbabilityTermStructure> probability(hazardRateStructure);
engine = MidPointCdsEngine(hazard_rate_structure, recovery_rate, ts_curve)
print( "\n",
"============================================================================\n",
"This attempt will behave the same as the C++ version.\n",
"It raises a runtime error because the accrual date, protection start date \n",
"and upfront date are the same as the evaluation date.\n",
"The error message should be: RuntimeError: fair upfront not available\n",
"============================================================================\n\n"
)
cds_schedule = Schedule.from_effective_termination(
todays_date, maturities[3], Period(Quarterly), calendar,
termination_date_convention=Unadjusted,
date_generation_rule=TwentiethIMM
)
cds_upfront = CreditDefaultSwap.from_upfront(
SELLER, nominal, 0.0, 0.05, cds_schedule, Following,
Actual360(), True, todays_date, todays_date
)
cds_upfront.set_pricing_engine(engine);
try:
calculated_upfront = cds_upfront.fair_upfront
print(" Upfront: ", calculated_upfront )
except RuntimeError as err:
print("Caught Runtime Error:", err )
print("\n",
"============================================================================\n",
"This attempt will behave the same as the C++ version.\n",
"The accrual date, protection start date and upfront date have all been advanced\n",
"to T+1 where the evaluation date is T.\n",
"The computed upfront is 'practically correct', but we had to shift all the dates to make it work.\n",
"============================================================================\n\n"
)
cds_schedule = Schedule.from_effective_termination(
todays_date + 1, maturities[3], Period(Quarterly), calendar,
termination_date_convention=Unadjusted,
date_generation_rule=TwentiethIMM
)
cds_upfront = CreditDefaultSwap.from_upfront(
SELLER, nominal, 0.0, 0.05, cds_schedule, Following,
Actual360(), True, todays_date + 1, todays_date + 1
)
cds_upfront.set_pricing_engine(engine);
try:
calculated_upfront = cds_upfront.fair_upfront
print(" Upfront: ", calculated_upfront)
except RuntimeError as err:
print("Caught Runtime Error:", err)
print("\n",
"============================================================================\n",
"This attempt will NOT behave the same as the C++ version.\n",
"The upfront date has been advanced to T+1 ... it crashes python while trying\n",
"to create the CreditDefaultSwap.\n",
"To match the C++ version it should be returning the 'correct' calculated\n",
"upfront amount, but it won't exactly match the upfront above because of the\n",
"other date changes\n",
"============================================================================\n\n"
)
cds_schedule = Schedule.from_effective_termination(
todays_date, maturities[3], Period(Quarterly), calendar,
termination_date_convention=Unadjusted,
date_generation_rule=TwentiethIMM
)
print( "The Schedule has been correctly created ... now attempting to create the CreditDefaultSwap...")
try:
# This next line will cause Python to crash, the catch blocks don't get a chance to execute.
cds_upfront = CreditDefaultSwap.from_upfront(
SELLER, nominal, 0.0, 0.05, cds_schedule, Following,
Actual360(), True, todays_date, todays_date+1
)
except:
print("Unexpected error:", sys.exec_info()[0])
#This line will never run.
print("Now attempting to set Engine ... If you're seeing this output your experience is different to mine.")
cds_upfront.set_pricing_engine(engine);
try:
calculated_upfront = cds_upfront.fair_upfront
print(" Upfront: ", calculated_upfront )
except RuntimeError as err:
print("Caught Runtime Error:", err )
I've tried your script. It runs fine for me with my latest branch if I replace exec_info by exc_info (there is a typo there), and it catches the runtime exception. However it shows the same upfront as before, since obviously the cds is not updated. Are you positive you can build a cds with these parameters in C++? Can you share that C++ code as well if that's the case?
I was missing a second boolean value in the argument list to CreditDefaultSwap.from_upfront
so I was advancing the protection start date when I thought I was advancing the upfront date. The error coming back was 'protection cannot start before accrual date' but since it was crashing Python, before @thrasibule's latest update, I wasn't aware of the argument problem. Separately, I was reusing the calculated_upfront
variable for all test cases and that's why the tests appeared to be producing the same value ... it was the same because it was never being overwritten inside the failing try block. Now when I advance all dates vs just the upfront date, the computed upfront amounts have a small difference and match the C++ values. This is also the expected model behaviour. This particular test case, of advancing all dates, is not really relevant anymore since just advancing the upfront_date works from pyql. There are only two test cases worth considering:
- upfront_date = Settings.instance().evaluation_date which throws an error from quantlib and I don't understand the thinking but can speculate - in any event it's a quantlib isssue and not pyql.
- upfront_date = evaluation_date+1 we get the correct computed upfront.
Many thanks for all the help on this.
Here is my new output and test script:
Output:
============================================================================
This test case raises a runtime error because the accrual date, protection
start date and upfront date are the same as the evaluation date.
The error message should be: RuntimeError: fair upfront not available.
============================================================================
Caught Runtime Error: fair upfront not available
============================================================================
If we move the upfront date to T+1 and leave the evaluation date, accrual date
and protection start date on T then we can get the fair_upfront even though
the provided upfront has no bearing on the calculation of fair_upfront.
This is the behavior we see in the C++ implementation of quantlib.
============================================================================
Upfront: 0.11189181724989519
Script:
from __future__ import print_function
from quantlib.instruments.credit_default_swap import CreditDefaultSwap, SELLER
from quantlib.pricingengines.credit import MidPointCdsEngine
from quantlib.settings import Settings
from quantlib.time.api import (
Date, May, Sep, Dec, Actual360, Following, TARGET, Period, Months,
Quarterly, TwentiethIMM, Years, Schedule, Unadjusted
)
from quantlib.termstructures.credit.api import (
SpreadCdsHelper, PiecewiseDefaultCurve, ProbabilityTrait, Interpolator )
from quantlib.termstructures.yields.api import FlatForward
import sys
if __name__ == '__main__':
#*********************
#*** MARKET DATA ***
#*********************
calendar = TARGET()
todays_date = Date(20, Sep, 2016)
# must be a business day
todays_date = calendar.adjust(todays_date)
Settings.instance().evaluation_date = todays_date
# dummy curve
ts_curve = FlatForward(
reference_date=todays_date, forward=-0.005, daycounter=Actual360()
)
# In Lehmans Brothers "guide to exotic credit derivatives"
# p. 32 there's a simple case, zero flat curve with a flat CDS
# curve with constant market spreads of 150 bp and RR = 50%
# corresponds to a flat 3% hazard rate. The implied 1-year
# survival probability is 97.04% and the 2-years is 94.18%
# market
recovery_rate = 0.4
quoted_spreads = [0.08, 0.08, 0.08, 0.08 ]
tenors = [Period(i, Months) for i in [1, 4, 8, 5*12]]
maturities = [
calendar.adjust(todays_date + tenors[i], Following) for i in range(4)
]
instruments = []
for i in range(4):
helper = SpreadCdsHelper(
quoted_spreads[i], tenors[i], 0, calendar, Quarterly,
Following, TwentiethIMM, Actual360(), recovery_rate, ts_curve
)
instruments.append(helper)
# Bootstrap hazard rates
hazard_rate_structure = PiecewiseDefaultCurve.from_reference_date(
ProbabilityTrait.HazardRate, Interpolator.BackwardFlat, todays_date, instruments, Actual360()
)
#vector<pair<Date, Real> > hr_curve_data = hazardRateStructure->nodes();
#cout << "Calibrated hazard rate values: " << endl ;
#for (Size i=0; i<hr_curve_data.size(); i++) {
# cout << "hazard rate on " << hr_curve_data[i].first << " is "
# << hr_curve_data[i].second << endl;
#}
#cout << endl;
target = todays_date + Period(1, Years)
# reprice instruments
nominal = 1000000.0;
#Handle<DefaultProbabilityTermStructure> probability(hazardRateStructure);
engine = MidPointCdsEngine(hazard_rate_structure, recovery_rate, ts_curve)
print( "",
"============================================================================\n",
"This test case raises a runtime error because the accrual date, protection\n",
"start date and upfront date are the same as the evaluation date.\n",
"The error message should be: RuntimeError: fair upfront not available.\n",
"============================================================================\n\n"
)
cds_schedule = Schedule.from_effective_termination(
todays_date, maturities[3], Period(Quarterly), calendar,
termination_date_convention=Unadjusted,
date_generation_rule=TwentiethIMM
)
cds_upfront = CreditDefaultSwap.from_upfront(
SELLER, nominal, 0.0, 0.05, cds_schedule, Following,
Actual360(), True, True, todays_date, todays_date
)
cds_upfront.set_pricing_engine(engine);
try:
calculated_upfront = cds_upfront.fair_upfront
print(" Upfront: ", calculated_upfront )
except RuntimeError as err:
print("Caught Runtime Error:", err )
calculated_upfront = None
print("",
"============================================================================\n",
"If we move the upfront date to T+1 and leave the evaluation date, accrual date\n",
"and protection start date on T then we can get the fair_upfront even though\n",
"the provided upfront has no bearing on the calculation of fair_upfront.\n",
"This is the same behavior we see in the C++ implementation of quantlib.\n",
"============================================================================\n\n"
)
cds_schedule = Schedule.from_effective_termination(
todays_date, maturities[3], Period(Quarterly), calendar,
termination_date_convention=Unadjusted,
date_generation_rule=TwentiethIMM
)
try:
cds_upfront = CreditDefaultSwap.from_upfront(
SELLER, nominal, 0.0, 0.05, cds_schedule, Following,
Actual360(), True, True, todays_date, todays_date+1
)
except RuntimeError as err:
print("RuntimeError error:", err)
cds_upfront.set_pricing_engine(engine);
try:
calculated_upfront = cds_upfront.fair_upfront
print(" Upfront: ", calculated_upfront )
except RuntimeError as err:
print("Caught Runtime Error:", err )
calculated_upfront = None