|
Constructing yield curves is a foundational task in quantitative finance. In this article, we assume that you are already familiar with the general principles and best practices of curve construction. Our focus will be on the QuantLib implementation, some useful engineering practices, and building automated tests. In particular, we’ll zoom in on the creation of OIS swap curves. For those new to QuantLib, I recommend visiting Luigi’s Implementing QuantLib website and browsing his YouTube playlist. While these sources provide a comprehensive introduction, our discussion here will remain narrowly focused on the practical aspects of curve construction. | |
QuantLib at a High LevelThe typical workflow for pricing a security using curves in QuantLib involves four main steps:
import QuantLib as ql today = ql.Date(12, ql.February, 2025) ql.Settings.instance().evaluationDate = today yts = ql.RelinkableYieldTermStructureHandle() swap = ql.MakeOIS(ql.Period("5Y"), ql.Sofr(yts), 0.04) curve = ql.FlatForward(today, 0.05, ql.Actual360()) yts.linkTo(curve) swap.setPricingEngine(ql.DiscountingSwapEngine(yts)) print( swap.NPV() ) Building the curveYield curves in QuantLib are modeled using the YieldTermStructure interface. There are several concrete implementations of this interface—such as FlatForward, which was demonstrated in the earlier example. When building swap curves, the PiecewiseYieldCurve class (a templated C++ class) is the tool of choice. This class supports multiple interpolation methods and dimensions, and its various configurations are exposed in Python through classes such as:
The choice of interpolation method significantly impacts the shape of the curve and, by extension, the valuation and risk profile of swaps priced using that curve. By default, the Kupala-Nich application uses PiecewiseLogCubicDiscount. For more details on interpolation methods and dimensions, please refer to the QuantLib documentation. These curve-building classes offer several overloaded constructors that generally fall into one of two categories:
In the Kupala-Nich application, we rely on the constructor that takes five parameters: ql.PiecewiseLogCubicDiscount(settlement_days, calendar, helpers, day_count, BOOTSTRAP_PARAMS) Here:
The next section will explore how to build these helpers from swap market data Constructing OIS Swap CurvesTo construct an OIS swap curve, you first need reliable market data. In QuantLib, this data is represented as a series of RateHelper objects, which are then passed into the interpolation function. When building an OIS curve from GTR data, the process typically involves:
A minimal setup for a ql.OISRateHelper requires four parameters:
When your market quotes come from standardized sources like Bloomberg, these parameters are typically sufficient. However, bootstrapping from trade data introduces additional considerations:
Engineering Practices and Automated Testing for Curve ConstructionWhen building code that interfaces with QuantLib, it's common to tightly couple your business logic with QuantLib-specific implementations. While this can be effective for rapid prototyping, it often results in code that is difficult to test and maintain over the long term. A recommended approach is to use dependency injection. By abstracting your business logic away from QuantLib via well-defined interfaces, you can easily swap out QuantLib components for stubs during unit testing. This separation allows you to test your business logic in isolation and then integrate the actual QuantLib implementations for integration and production scenarios. Since much of QuantLib adheres to strict interface contracts, this design pattern also makes it simple to experiment with different interpolation methods or curve models without modifying your core business logic. With this design in place, your tests naturally fall into several categories:
Additional Considerations for Integration and Smoke Testing
Sample CodeBelow is a simplified code snippet that brings together the concepts discussed: import QuantLib as ql BOOTSTRAP_PARAMS = ql.IterativeBootstrap(accuracy=1e-13, minValue=None, maxValue=None, maxAttempts=20, maxFactor=1.5, minFactor=1.5, dontThrow=False, dontThrowSteps=20, maxEvaluations=200) def init_helpers(curve_data, index, calendar, day_count, settlement_days): helpers = ql.RateHelperVector() for p in curve_data: if p.product == 'SWAP': helpers.append(ql.OISRateHelper(settlement_days, ql.Period(p.tenor), ql.QuoteHandle(ql.SimpleQuote(p.rate)), index, paymentFrequency=p.frequency, forwardStart=p.forward_start) ) elif p.product == 'RATE': helpers.append(ql.DepositRateHelper( ql.QuoteHandle(ql.SimpleQuote(p.rate )), ql.Period(p.tenor), settlement_days, calendar, ql.ModifiedFollowing, False, day_count)) def build_curve( valuation_date: ql.Date, curve_data, interpolation_function = ql.PiecewiseLinearZero, calendar = ql.TARGET(), day_count = ql.Actual360(), index_class = ql.Sofr, settlement_days = 2): ql.Settings.instance().evaluationDate = valuation_date yts = ql.RelinkableYieldTermStructureHandle() index = index_class(yts) helpers = init_helpers(curve_data, index, calendar, day_count, settlement_days) params = [settlement_days, calendar, helpers, day_count, BOOTSTRAP_PARAMS] curve = interpolation_function(*params) curve.enableExtrapolation() yts.linkTo(curve) engine = ql.DiscountingSwapEngine(yts) return curve, engine A Few Closing WordsThis article only scratches the surface of the complexities involved in implementing effective solutions with QuantLib. One significant challenge is that QuantLib was not designed with serverless computing in mind—it’s optimized for long-lived processes that leverage large in-memory caches. In future articles, I plan to take a deeper dive into this topic and expand the product coverage. As always, your thoughts and feedback are welcome. | |
No comments:
Post a Comment