|
| 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 | +} |
0 commit comments