Skip to content

Commit 5886cf3

Browse files
committed
Add smileSection() to BlackVolTermStructure with default adapter
1 parent 6650464 commit 5886cf3

File tree

5 files changed

+175
-0
lines changed

5 files changed

+175
-0
lines changed

ql/termstructures/volatility/equityfx/blackvoltermstructure.cpp

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
/*
44
Copyright (C) 2002, 2003 Ferdinando Ametrano
5+
Copyright (C) 2026 Yassine Idyiahia
56
67
This file is part of QuantLib, a free-software/open-source library
78
for financial quantitative analysts and developers - http://quantlib.org/
@@ -18,9 +19,30 @@
1819
*/
1920

2021
#include <ql/termstructures/volatility/equityfx/blackvoltermstructure.hpp>
22+
#include <ql/termstructures/volatility/smilesection.hpp>
2123

2224
namespace QuantLib {
2325

26+
namespace {
27+
28+
class BlackVolSmileSectionAdapter : public SmileSection {
29+
public:
30+
BlackVolSmileSectionAdapter(const BlackVolTermStructure& vol,
31+
Time t)
32+
: SmileSection(t, vol.dayCounter()), vol_(vol) {}
33+
Real minStrike() const override { return vol_.minStrike(); }
34+
Real maxStrike() const override { return vol_.maxStrike(); }
35+
Real atmLevel() const override { return Null<Real>(); }
36+
protected:
37+
Volatility volatilityImpl(Rate strike) const override {
38+
return vol_.blackVol(exerciseTime(), strike, true);
39+
}
40+
private:
41+
const BlackVolTermStructure& vol_;
42+
};
43+
44+
}
45+
2446
BlackVolTermStructure::BlackVolTermStructure(BusinessDayConvention bdc,
2547
const DayCounter& dc)
2648
: VolatilityTermStructure(bdc, dc) {}
@@ -151,4 +173,9 @@ namespace QuantLib {
151173
const DayCounter& dc)
152174
: BlackVolTermStructure(settlementDays, cal, bdc, dc) {}
153175

176+
ext::shared_ptr<SmileSection>
177+
BlackVolTermStructure::smileSectionImpl(Time t) const {
178+
return ext::make_shared<BlackVolSmileSectionAdapter>(*this, t);
179+
}
180+
154181
}

ql/termstructures/volatility/equityfx/blackvoltermstructure.hpp

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
/*
44
Copyright (C) 2002, 2003 Ferdinando Ametrano
55
Copyright (C) 2003, 2004, 2005, 2006 StatPro Italia srl
6+
Copyright (C) 2026 Yassine Idyiahia
67
78
This file is part of QuantLib, a free-software/open-source library
89
for financial quantitative analysts and developers - http://quantlib.org/
@@ -30,6 +31,8 @@
3031

3132
namespace QuantLib {
3233

34+
class SmileSection;
35+
3336
//! Black-volatility term structure
3437
/*! This abstract class defines the interface of concrete
3538
Black-volatility term structures which will be derived from
@@ -102,6 +105,15 @@ namespace QuantLib {
102105
Real strike,
103106
bool extrapolate = false) const;
104107
//@}
108+
//! \name Smile
109+
//@{
110+
//! returns the smile for a given option date
111+
ext::shared_ptr<SmileSection> smileSection(const Date& maturity,
112+
bool extrapolate = false) const;
113+
//! returns the smile for a given option time
114+
ext::shared_ptr<SmileSection> smileSection(Time maturity,
115+
bool extrapolate = false) const;
116+
//@}
105117
//! \name Visitability
106118
//@{
107119
virtual void accept(AcyclicVisitor&);
@@ -119,6 +131,13 @@ namespace QuantLib {
119131
virtual Real blackVarianceImpl(Time t, Real strike) const = 0;
120132
//! Black volatility calculation
121133
virtual Volatility blackVolImpl(Time t, Real strike) const = 0;
134+
/*! Smile section calculation. The default implementation wraps
135+
the vol surface into a SmileSection adapter; the returned
136+
object holds a reference to \c *this and must not outlive it.
137+
Derived classes with a native smile representation can override
138+
to return self-contained objects.
139+
*/
140+
virtual ext::shared_ptr<SmileSection> smileSectionImpl(Time t) const;
122141
//@}
123142
};
124143

@@ -248,6 +267,20 @@ namespace QuantLib {
248267
return blackVarianceImpl(t, strike);
249268
}
250269

270+
inline ext::shared_ptr<SmileSection>
271+
BlackVolTermStructure::smileSection(const Date& d,
272+
bool extrapolate) const {
273+
checkRange(d, extrapolate);
274+
return smileSectionImpl(timeFromReference(d));
275+
}
276+
277+
inline ext::shared_ptr<SmileSection>
278+
BlackVolTermStructure::smileSection(Time t,
279+
bool extrapolate) const {
280+
checkRange(t, extrapolate);
281+
return smileSectionImpl(t);
282+
}
283+
251284
inline void BlackVolTermStructure::accept(AcyclicVisitor& v) {
252285
auto* v1 = dynamic_cast<Visitor<BlackVolTermStructure>*>(&v);
253286
if (v1 != nullptr)

ql/termstructures/volatility/equityfx/piecewiseblackvariancesurface.cpp

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
/*
44
Copyright (C) 2026 Rich Amaya
5+
Copyright (C) 2026 Yassine Idyiahia
56
67
This file is part of QuantLib, a free-software/open-source library
78
for financial quantitative analysts and developers - http://quantlib.org/
@@ -18,6 +19,7 @@
1819
*/
1920

2021
#include <ql/termstructures/volatility/equityfx/piecewiseblackvariancesurface.hpp>
22+
#include <ql/math/comparison.hpp>
2123
#include <ql/math/interpolations/linearinterpolation.hpp>
2224
#include <ql/termstructures/volatility/interpolatedsmilesection.hpp>
2325
#include <ql/utilities/null.hpp>
@@ -157,6 +159,14 @@ namespace QuantLib {
157159
referenceDate, dates, std::move(sections), dc);
158160
}
159161

162+
ext::shared_ptr<SmileSection>
163+
PiecewiseBlackVarianceSurface::smileSectionImpl(Time t) const {
164+
auto it = std::lower_bound(times_.begin(), times_.end(), t);
165+
if (it != times_.end() && close_enough(t, *it))
166+
return smileSections_[std::distance(times_.begin(), it)];
167+
return BlackVarianceTermStructure::smileSectionImpl(t);
168+
}
169+
160170
void PiecewiseBlackVarianceSurface::accept(AcyclicVisitor& v) {
161171
auto* v1 = dynamic_cast<Visitor<PiecewiseBlackVarianceSurface>*>(&v);
162172
if (v1 != nullptr)

ql/termstructures/volatility/equityfx/piecewiseblackvariancesurface.hpp

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
/*
44
Copyright (C) 2026 Rich Amaya
5+
Copyright (C) 2026 Yassine Idyiahia
56
67
This file is part of QuantLib, a free-software/open-source library
78
for financial quantitative analysts and developers - http://quantlib.org/
@@ -76,6 +77,7 @@ namespace QuantLib {
7677

7778
protected:
7879
Real blackVarianceImpl(Time t, Real strike) const override;
80+
ext::shared_ptr<SmileSection> smileSectionImpl(Time t) const override;
7981

8082
private:
8183
Real sectionVariance(Size i, Real strike) const;

test-suite/piecewiseblackvariancesurface.cpp

Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1105,6 +1105,109 @@ BOOST_AUTO_TEST_CASE(testLocalVolFdPricingFromSabrSmiles) {
11051105
}
11061106
}
11071107

1108+
BOOST_AUTO_TEST_CASE(testSmileSectionFromBlackVolSurface) {
1109+
BOOST_TEST_MESSAGE(
1110+
"Testing SmileSection extraction from BlackVolTermStructure...");
1111+
1112+
Date today(15, January, 2026);
1113+
Settings::instance().evaluationDate() = today;
1114+
DayCounter dc = Actual365Fixed();
1115+
1116+
// 1. Vol-native surface (default adapter path)
1117+
// BlackConstantVol has no native SmileSection — the base class
1118+
// adapter wraps blackVol() into a SmileSection on the fly.
1119+
Volatility flatVol = 0.20;
1120+
auto constVol = ext::make_shared<BlackConstantVol>(today, NullCalendar(),
1121+
flatVol, dc);
1122+
1123+
Date maturity = today + 1*Years;
1124+
auto smile = constVol->smileSection(maturity);
1125+
1126+
Real tolerance = 1.0e-12;
1127+
for (Real strike : {80.0, 100.0, 120.0}) {
1128+
Volatility v = smile->volatility(strike);
1129+
if (std::fabs(v - flatVol) > tolerance)
1130+
BOOST_FAIL("vol-native: failed to reproduce flat vol"
1131+
<< std::fixed << std::setprecision(12)
1132+
<< "\n strike: " << strike
1133+
<< "\n expected: " << flatVol
1134+
<< "\n calculated: " << v);
1135+
}
1136+
1137+
// 2. Smile-native surface, at a stored tenor (override path)
1138+
// PiecewiseBlackVarianceSurface stores SmileSection objects and
1139+
// overrides smileSectionImpl() to return them directly at stored tenors.
1140+
Date d1 = today + 6*Months;
1141+
Date d2 = today + 1*Years;
1142+
std::vector<Real> strikes = {80.0, 90.0, 100.0, 110.0, 120.0};
1143+
std::vector<Real> vols1 = {0.30, 0.25, 0.20, 0.22, 0.28};
1144+
std::vector<Real> vols2 = {0.28, 0.23, 0.19, 0.21, 0.26};
1145+
Time T1 = dc.yearFraction(today, d1);
1146+
Time T2 = dc.yearFraction(today, d2);
1147+
Real sqrtT1 = std::sqrt(T1);
1148+
Real sqrtT2 = std::sqrt(T2);
1149+
1150+
std::vector<Real> stdDevs1, stdDevs2;
1151+
for (auto v : vols1)
1152+
stdDevs1.push_back(v * sqrtT1);
1153+
for (auto v : vols2)
1154+
stdDevs2.push_back(v * sqrtT2);
1155+
1156+
auto section1 = ext::make_shared<InterpolatedSmileSection<Linear>>(
1157+
d1, strikes, stdDevs1, 100.0, dc, Linear(), today);
1158+
auto section2 = ext::make_shared<InterpolatedSmileSection<Linear>>(
1159+
d2, strikes, stdDevs2, 100.0, dc, Linear(), today);
1160+
auto surface = ext::make_shared<PiecewiseBlackVarianceSurface>(
1161+
today, std::vector<Date>{d1, d2},
1162+
std::vector<ext::shared_ptr<SmileSection>>{section1, section2}, dc);
1163+
1164+
// At a stored tenor: should return the same SmileSection object (pointer equality)
1165+
auto smile1 = surface->smileSection(d1);
1166+
if (smile1.get() != section1.get())
1167+
BOOST_FAIL("at stored tenor: smileSection(d1) did not return "
1168+
"the stored SmileSection (pointer mismatch)");
1169+
1170+
auto smile2 = surface->smileSection(d2);
1171+
if (smile2.get() != section2.get())
1172+
BOOST_FAIL("at stored tenor: smileSection(d2) did not return "
1173+
"the stored SmileSection (pointer mismatch)");
1174+
1175+
// At a stored tenor: volatilities should match exactly
1176+
for (Size i = 0; i < strikes.size(); ++i) {
1177+
Volatility surfaceVol = surface->blackVol(d1, strikes[i]);
1178+
Volatility sectionVol = smile1->volatility(strikes[i]);
1179+
if (std::fabs(surfaceVol - sectionVol) > tolerance)
1180+
BOOST_FAIL("at stored tenor: vol mismatch"
1181+
<< std::fixed << std::setprecision(12)
1182+
<< "\n strike: " << strikes[i]
1183+
<< "\n surface vol: " << surfaceVol
1184+
<< "\n section vol: " << sectionVol);
1185+
}
1186+
1187+
// 3. Smile-native surface, between tenors (adapter fallback path)
1188+
// Between stored tenors, smileSectionImpl() falls back to the
1189+
// default adapter which queries blackVol() per strike.
1190+
Date dMid = today + 9*Months;
1191+
auto smileMid = surface->smileSection(dMid);
1192+
1193+
// Between tenors: should NOT be either stored section (it's an adapter)
1194+
if (smileMid.get() == section1.get() || smileMid.get() == section2.get())
1195+
BOOST_FAIL("between tenors: smileSection(dMid) should not "
1196+
"return a stored SmileSection");
1197+
1198+
// Between tenors: adapter should reproduce blackVol() at each strike
1199+
for (Size i = 0; i < strikes.size(); ++i) {
1200+
Volatility surfaceVol = surface->blackVol(dMid, strikes[i]);
1201+
Volatility sectionVol = smileMid->volatility(strikes[i]);
1202+
if (std::fabs(surfaceVol - sectionVol) > tolerance)
1203+
BOOST_FAIL("between tenors: vol mismatch"
1204+
<< std::fixed << std::setprecision(12)
1205+
<< "\n strike: " << strikes[i]
1206+
<< "\n surface vol: " << surfaceVol
1207+
<< "\n section vol: " << sectionVol);
1208+
}
1209+
}
1210+
11081211
BOOST_AUTO_TEST_SUITE_END()
11091212

11101213
BOOST_AUTO_TEST_SUITE_END()

0 commit comments

Comments
 (0)