Skip to content

Commit aee52b9

Browse files
authored
Add PiecewiseBlackVarianceSurface for ragged vol grids (#2443)
2 parents a385759 + 23678dd commit aee52b9

File tree

12 files changed

+1155
-2
lines changed

12 files changed

+1155
-2
lines changed

QuantLib.vcxproj

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1734,6 +1734,7 @@
17341734
<ClInclude Include="ql\termstructures\volatility\equityfx\localvolsurface.hpp" />
17351735
<ClInclude Include="ql\termstructures\volatility\equityfx\localvoltermstructure.hpp" />
17361736
<ClInclude Include="ql\termstructures\volatility\equityfx\noexceptlocalvolsurface.hpp" />
1737+
<ClInclude Include="ql\termstructures\volatility\equityfx\piecewiseblackvariancesurface.hpp" />
17371738
<ClInclude Include="ql\termstructures\volatility\flatsmilesection.hpp" />
17381739
<ClInclude Include="ql\termstructures\volatility\gaussian1dsmilesection.hpp" />
17391740
<ClInclude Include="ql\termstructures\volatility\inflation\all.hpp" />
@@ -2758,6 +2759,7 @@
27582759
<ClCompile Include="ql\termstructures\volatility\equityfx\hestonblackvolsurface.cpp" />
27592760
<ClCompile Include="ql\termstructures\volatility\equityfx\localvolsurface.cpp" />
27602761
<ClCompile Include="ql\termstructures\volatility\equityfx\localvoltermstructure.cpp" />
2762+
<ClCompile Include="ql\termstructures\volatility\equityfx\piecewiseblackvariancesurface.cpp" />
27612763
<ClCompile Include="ql\termstructures\volatility\flatsmilesection.cpp" />
27622764
<ClCompile Include="ql\termstructures\volatility\gaussian1dsmilesection.cpp" />
27632765
<ClCompile Include="ql\termstructures\volatility\inflation\constantcpivolatility.cpp" />

QuantLib.vcxproj.filters

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4254,6 +4254,9 @@
42544254
<ClInclude Include="ql\termstructures\volatility\equityfx\noexceptlocalvolsurface.hpp">
42554255
<Filter>termstructures\volatility\equityfx</Filter>
42564256
</ClInclude>
4257+
<ClInclude Include="ql\termstructures\volatility\equityfx\piecewiseblackvariancesurface.hpp">
4258+
<Filter>termstructures\volatility\equityfx</Filter>
4259+
</ClInclude>
42574260
<ClInclude Include="ql\math\polynomialmathfunction.hpp">
42584261
<Filter>math</Filter>
42594262
</ClInclude>
@@ -5624,6 +5627,9 @@
56245627
<ClCompile Include="ql\termstructures\volatility\equityfx\localvoltermstructure.cpp">
56255628
<Filter>termstructures\volatility\equityfx</Filter>
56265629
</ClCompile>
5630+
<ClCompile Include="ql\termstructures\volatility\equityfx\piecewiseblackvariancesurface.cpp">
5631+
<Filter>termstructures\volatility\equityfx</Filter>
5632+
</ClCompile>
56275633
<ClCompile Include="ql\termstructures\volatility\optionlet\constantoptionletvol.cpp">
56285634
<Filter>termstructures\volatility\optionlet</Filter>
56295635
</ClCompile>

ql/CMakeLists.txt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -839,6 +839,7 @@ set(QL_SOURCES
839839
termstructures/volatility/equityfx/hestonblackvolsurface.cpp
840840
termstructures/volatility/equityfx/localvolsurface.cpp
841841
termstructures/volatility/equityfx/localvoltermstructure.cpp
842+
termstructures/volatility/equityfx/piecewiseblackvariancesurface.cpp
842843
termstructures/volatility/flatsmilesection.cpp
843844
termstructures/volatility/gaussian1dsmilesection.cpp
844845
termstructures/volatility/inflation/constantcpivolatility.cpp
@@ -2109,6 +2110,7 @@ set(QL_HEADERS
21092110
termstructures/volatility/equityfx/localvolsurface.hpp
21102111
termstructures/volatility/equityfx/localvoltermstructure.hpp
21112112
termstructures/volatility/equityfx/noexceptlocalvolsurface.hpp
2113+
termstructures/volatility/equityfx/piecewiseblackvariancesurface.hpp
21122114
termstructures/volatility/flatsmilesection.hpp
21132115
termstructures/volatility/gaussian1dsmilesection.hpp
21142116
termstructures/volatility/inflation/constantcpivolatility.hpp

ql/termstructures/volatility/equityfx/Makefile.am

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,8 @@ this_include_HEADERS = \
1919
localvolcurve.hpp \
2020
localvolsurface.hpp \
2121
localvoltermstructure.hpp \
22-
noexceptlocalvolsurface.hpp
22+
noexceptlocalvolsurface.hpp \
23+
piecewiseblackvariancesurface.hpp
2324

2425
cpp_files = \
2526
andreasenhugelocalvoladapter.cpp \
@@ -32,7 +33,8 @@ cpp_files = \
3233
gridmodellocalvolsurface.cpp \
3334
hestonblackvolsurface.cpp \
3435
localvolsurface.cpp \
35-
localvoltermstructure.cpp
36+
localvoltermstructure.cpp \
37+
piecewiseblackvariancesurface.cpp
3638

3739
if UNITY_BUILD
3840

ql/termstructures/volatility/equityfx/all.hpp

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,4 +17,5 @@
1717
#include <ql/termstructures/volatility/equityfx/localvolsurface.hpp>
1818
#include <ql/termstructures/volatility/equityfx/localvoltermstructure.hpp>
1919
#include <ql/termstructures/volatility/equityfx/noexceptlocalvolsurface.hpp>
20+
#include <ql/termstructures/volatility/equityfx/piecewiseblackvariancesurface.hpp>
2021

Lines changed: 157 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,157 @@
1+
/* -*- mode: c++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */
2+
3+
/*
4+
Copyright (C) 2026 Rich Amaya
5+
6+
This file is part of QuantLib, a free-software/open-source library
7+
for financial quantitative analysts and developers - http://quantlib.org/
8+
9+
QuantLib is free software: you can redistribute it and/or modify it
10+
under the terms of the QuantLib license. You should have received a
11+
copy of the license along with this program; if not, please email
12+
<quantlib-dev@lists.sf.net>. The license is also available online at
13+
<https://www.quantlib.org/license.shtml>.
14+
15+
This program is distributed in the hope that it will be useful, but WITHOUT
16+
ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
17+
FOR A PARTICULAR PURPOSE. See the license for more details.
18+
*/
19+
20+
#include <ql/termstructures/volatility/equityfx/piecewiseblackvariancesurface.hpp>
21+
#include <ql/math/interpolations/linearinterpolation.hpp>
22+
#include <ql/termstructures/volatility/interpolatedsmilesection.hpp>
23+
#include <ql/utilities/null.hpp>
24+
#include <algorithm>
25+
#include <cmath>
26+
#include <utility>
27+
28+
namespace QuantLib {
29+
30+
PiecewiseBlackVarianceSurface::PiecewiseBlackVarianceSurface(
31+
const Date& referenceDate,
32+
const std::vector<Date>& dates,
33+
std::vector<ext::shared_ptr<SmileSection>> smileSections,
34+
DayCounter dayCounter)
35+
: BlackVarianceTermStructure(referenceDate),
36+
dayCounter_(std::move(dayCounter)),
37+
smileSections_(std::move(smileSections)) {
38+
39+
QL_REQUIRE(!dates.empty(),
40+
"at least one date is required");
41+
QL_REQUIRE(dates.size() == smileSections_.size(),
42+
"mismatch between " << dates.size() << " dates and "
43+
<< smileSections_.size() << " smile sections");
44+
45+
maxDate_ = dates.back();
46+
times_.resize(dates.size());
47+
48+
times_[0] = timeFromReference(dates[0]);
49+
QL_REQUIRE(times_[0] > 0.0,
50+
"first date (" << dates[0]
51+
<< ") must be after reference date ("
52+
<< referenceDate << ")");
53+
54+
for (Size i = 1; i < dates.size(); ++i) {
55+
times_[i] = timeFromReference(dates[i]);
56+
QL_REQUIRE(times_[i] > times_[i-1],
57+
"dates must be sorted and unique, but date "
58+
<< dates[i] << " (t=" << times_[i]
59+
<< ") is not after date " << dates[i-1]
60+
<< " (t=" << times_[i-1] << ")");
61+
}
62+
63+
for (Size i = 0; i < smileSections_.size(); ++i) {
64+
QL_REQUIRE(smileSections_[i],
65+
"null smile section at index " << i);
66+
registerWith(smileSections_[i]);
67+
}
68+
}
69+
70+
Real PiecewiseBlackVarianceSurface::sectionVariance(
71+
Size i, Real strike) const {
72+
const auto& s = smileSections_[i];
73+
QL_REQUIRE(allowsExtrapolation() ||
74+
(strike >= s->minStrike() && strike <= s->maxStrike()),
75+
"strike (" << strike
76+
<< ") is outside the range of smile section "
77+
<< i << " [" << s->minStrike() << ", "
78+
<< s->maxStrike() << "]");
79+
return s->variance(strike);
80+
}
81+
82+
Real PiecewiseBlackVarianceSurface::blackVarianceImpl(
83+
Time t, Real strike) const {
84+
85+
if (t == 0.0)
86+
return 0.0;
87+
88+
if (t <= times_.front()) {
89+
// linear interpolation from (0, 0) to first tenor
90+
Real var1 = sectionVariance(0, strike);
91+
return var1 * t / times_.front();
92+
}
93+
94+
if (t >= times_.back()) {
95+
// flat vol extrapolation beyond last tenor
96+
Real varN = sectionVariance(smileSections_.size() - 1, strike);
97+
return varN * t / times_.back();
98+
}
99+
100+
// find enclosing interval
101+
auto it = std::upper_bound(times_.begin(), times_.end(), t);
102+
Size hi = std::distance(times_.begin(), it);
103+
Size lo = hi - 1;
104+
105+
Real varLo = sectionVariance(lo, strike);
106+
Real varHi = sectionVariance(hi, strike);
107+
Real alpha = (t - times_[lo]) / (times_[hi] - times_[lo]);
108+
109+
return varLo + (varHi - varLo) * alpha;
110+
}
111+
112+
ext::shared_ptr<PiecewiseBlackVarianceSurface>
113+
PiecewiseBlackVarianceSurface::makeFromGrid(
114+
const Date& referenceDate,
115+
const std::vector<Date>& dates,
116+
const std::vector<Real>& strikes,
117+
const Matrix& blackVols,
118+
const DayCounter& dc) {
119+
120+
QL_REQUIRE(blackVols.rows() == strikes.size(),
121+
"mismatch between " << strikes.size() << " strikes and "
122+
<< blackVols.rows() << " matrix rows");
123+
QL_REQUIRE(blackVols.columns() == dates.size(),
124+
"mismatch between " << dates.size() << " dates and "
125+
<< blackVols.columns() << " matrix columns");
126+
127+
std::vector<ext::shared_ptr<SmileSection>> sections(dates.size());
128+
129+
for (Size j = 0; j < dates.size(); ++j) {
130+
std::vector<Real> stdDevs(strikes.size());
131+
Time t = dc.yearFraction(referenceDate, dates[j]);
132+
QL_REQUIRE(t > 0.0,
133+
"date " << dates[j]
134+
<< " must be after reference date "
135+
<< referenceDate);
136+
Real sqrtT = std::sqrt(t);
137+
for (Size i = 0; i < strikes.size(); ++i)
138+
stdDevs[i] = blackVols[i][j] * sqrtT;
139+
140+
sections[j] = ext::make_shared<InterpolatedSmileSection<Linear>>(
141+
dates[j], strikes, stdDevs, Null<Real>(),
142+
dc, Linear(), referenceDate);
143+
}
144+
145+
return ext::make_shared<PiecewiseBlackVarianceSurface>(
146+
referenceDate, dates, std::move(sections), dc);
147+
}
148+
149+
void PiecewiseBlackVarianceSurface::accept(AcyclicVisitor& v) {
150+
auto* v1 = dynamic_cast<Visitor<PiecewiseBlackVarianceSurface>*>(&v);
151+
if (v1 != nullptr)
152+
v1->visit(*this);
153+
else
154+
BlackVarianceTermStructure::accept(v);
155+
}
156+
157+
}
Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
/* -*- mode: c++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */
2+
3+
/*
4+
Copyright (C) 2026 Rich Amaya
5+
6+
This file is part of QuantLib, a free-software/open-source library
7+
for financial quantitative analysts and developers - http://quantlib.org/
8+
9+
QuantLib is free software: you can redistribute it and/or modify it
10+
under the terms of the QuantLib license. You should have received a
11+
copy of the license along with this program; if not, please email
12+
<quantlib-dev@lists.sf.net>. The license is also available online at
13+
<https://www.quantlib.org/license.shtml>.
14+
15+
This program is distributed in the hope that it will be useful, but WITHOUT
16+
ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
17+
FOR A PARTICULAR PURPOSE. See the license for more details.
18+
*/
19+
20+
/*! \file piecewiseblackvariancesurface.hpp
21+
\brief Black volatility surface built from smile sections
22+
*/
23+
24+
#ifndef quantlib_piecewise_black_variance_surface_hpp
25+
#define quantlib_piecewise_black_variance_surface_hpp
26+
27+
#include <ql/math/matrix.hpp>
28+
#include <ql/shared_ptr.hpp>
29+
#include <ql/termstructures/volatility/equityfx/blackvoltermstructure.hpp>
30+
#include <ql/termstructures/volatility/smilesection.hpp>
31+
#include <vector>
32+
33+
namespace QuantLib {
34+
35+
//! Black volatility surface built from smile sections
36+
/*! This class builds a Black volatility surface from a set of
37+
smile sections, one per tenor. It interpolates linearly in
38+
total variance between tenors for a given strike.
39+
40+
*/
41+
class PiecewiseBlackVarianceSurface
42+
: public BlackVarianceTermStructure {
43+
public:
44+
PiecewiseBlackVarianceSurface(
45+
const Date& referenceDate,
46+
const std::vector<Date>& dates,
47+
std::vector<ext::shared_ptr<SmileSection>> smileSections,
48+
DayCounter dayCounter = DayCounter());
49+
50+
DayCounter dayCounter() const override;
51+
Date maxDate() const override;
52+
Real minStrike() const override;
53+
Real maxStrike() const override;
54+
void accept(AcyclicVisitor&) override;
55+
56+
//! Build from a rectangular grid of Black vols.
57+
/*! This mirrors the BlackVarianceSurface constructor signature
58+
and provides a migration path. Each column of the matrix
59+
becomes an InterpolatedSmileSection with linear interpolation.
60+
61+
\param blackVols a matrix with rows indexed by strike and
62+
columns indexed by date
63+
*/
64+
static ext::shared_ptr<PiecewiseBlackVarianceSurface>
65+
makeFromGrid(const Date& referenceDate,
66+
const std::vector<Date>& dates,
67+
const std::vector<Real>& strikes,
68+
const Matrix& blackVols,
69+
const DayCounter& dc = DayCounter());
70+
71+
protected:
72+
Real blackVarianceImpl(Time t, Real strike) const override;
73+
74+
private:
75+
Real sectionVariance(Size i, Real strike) const;
76+
77+
DayCounter dayCounter_;
78+
Date maxDate_;
79+
std::vector<Time> times_;
80+
std::vector<ext::shared_ptr<SmileSection>> smileSections_;
81+
};
82+
83+
84+
// inline definitions
85+
86+
inline DayCounter
87+
PiecewiseBlackVarianceSurface::dayCounter() const {
88+
return dayCounter_;
89+
}
90+
91+
inline Date
92+
PiecewiseBlackVarianceSurface::maxDate() const {
93+
return maxDate_;
94+
}
95+
96+
inline Real
97+
PiecewiseBlackVarianceSurface::minStrike() const {
98+
return QL_MIN_REAL;
99+
}
100+
101+
inline Real
102+
PiecewiseBlackVarianceSurface::maxStrike() const {
103+
return QL_MAX_REAL;
104+
}
105+
106+
}
107+
108+
#endif

test-suite/CMakeLists.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -128,6 +128,7 @@ set(QL_TEST_SOURCES
128128
pathgenerator.cpp
129129
period.cpp
130130
perpetualfutures.cpp
131+
piecewiseblackvariancesurface.cpp
131132
piecewiseyieldcurve.cpp
132133
piecewisezerospreadedtermstructure.cpp
133134
preconditions.cpp

test-suite/Makefile.am

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -129,6 +129,7 @@ QL_TEST_SRCS = \
129129
pathgenerator.cpp \
130130
period.cpp \
131131
perpetualfutures.cpp \
132+
piecewiseblackvariancesurface.cpp \
132133
piecewiseyieldcurve.cpp \
133134
piecewisezerospreadedtermstructure.cpp \
134135
preconditions.cpp \

0 commit comments

Comments
 (0)