@@ -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+
11081211BOOST_AUTO_TEST_SUITE_END ()
11091212
11101213BOOST_AUTO_TEST_SUITE_END()
0 commit comments