From 1107ee60833c43857352594cab40feb83d49b474 Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Fri, 5 Jun 2026 18:25:11 +0000 Subject: [PATCH 1/2] Add coverage reporting infrastructure and new test cases - Add QL_ENABLE_COVERAGE CMake option with gcov/lcov support - Add 'make coverage' custom target for HTML report generation - Add PayoffTests: NullPayoff, PlainVanilla, CashOrNothing, AssetOrNothing, Gap, SuperFund, SuperShare, PercentageStrike, FloatingType payoffs with boundary/edge cases - Add BondFunctionsTests: date inspectors, accrual, duration (Simple/Macaulay/Modified), convexity, bps, basisPointValue, yieldValueBasisPoint, clean/dirty price, yield, ATM rate, z-spread, zero-coupon bond, Duration::Type streaming - Add HestonProcessTests: accessors, size/factors, initial values, drift, drift with negative variance (PartialTruncation, FullTruncation, Reflection), diffusion matrix, apply, evolve with QE/PartialTruncation, multiple discretization schemes - Add CashFlowsMoreTests: NPV with InterestRate/YieldTermStructure, bps, duration types, convexity, basisPointValue, z-spread, ATM rate, yield round-trip, SimpleCashFlow leg, previous/next cash flow iteration, z-spread NPV consistency Local verification: 100% build, 1373 test cases passed, 0 failed Co-Authored-By: Toby Drinkall --- CMakeLists.txt | 37 ++++ test-suite/CMakeLists.txt | 4 + test-suite/bondfunctions.cpp | 363 +++++++++++++++++++++++++++++++++++ test-suite/cashflowsmore.cpp | 274 ++++++++++++++++++++++++++ test-suite/hestonprocess.cpp | 270 ++++++++++++++++++++++++++ test-suite/payoffs.cpp | 188 ++++++++++++++++++ 6 files changed, 1136 insertions(+) create mode 100644 test-suite/bondfunctions.cpp create mode 100644 test-suite/cashflowsmore.cpp create mode 100644 test-suite/hestonprocess.cpp create mode 100644 test-suite/payoffs.cpp diff --git a/CMakeLists.txt b/CMakeLists.txt index f346b7e665..8b5b4fcc86 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -42,6 +42,7 @@ set(QL_INSTALL_CMAKEDIR "lib/cmake/${PACKAGE_NAME}" CACHE STRING option(QL_BUILD_EXAMPLES "Build examples" ON) option(QL_BUILD_TEST_SUITE "Build test suite" ON) option(QL_BUILD_FUZZ_TEST_SUITE "Build fuzz test suite" OFF) +option(QL_ENABLE_COVERAGE "Enable code coverage reporting (gcov/lcov)" OFF) option(QL_ENABLE_OPENMP "Detect and use OpenMP" OFF) option(QL_ENABLE_PARALLEL_UNIT_TEST_RUNNER "Enable the parallel unit test runner" OFF) option(QL_ENABLE_SESSIONS "Singletons return different instances for different sessions" OFF) @@ -84,6 +85,42 @@ if (NOT DEFINED CMAKE_CXX_EXTENSIONS) set(CMAKE_CXX_EXTENSIONS FALSE) endif() +# Code coverage flags +if (QL_ENABLE_COVERAGE) + if (CMAKE_CXX_COMPILER_ID STREQUAL "GNU" OR CMAKE_CXX_COMPILER_ID MATCHES "Clang") + add_compile_options(-fprofile-arcs -ftest-coverage) + add_link_options(-fprofile-arcs -ftest-coverage) + else() + message(FATAL_ERROR "Code coverage is only supported with GCC or Clang") + endif() + + find_program(LCOV_PATH lcov REQUIRED) + find_program(GENHTML_PATH genhtml REQUIRED) + + add_custom_target(coverage + COMMENT "Running test suite and generating coverage report..." + # Reset counters + COMMAND ${LCOV_PATH} --directory ${CMAKE_BINARY_DIR} --zerocounters + # Run the test suite + COMMAND ${CMAKE_CTEST_COMMAND} --test-dir ${CMAKE_BINARY_DIR} --output-on-failure + # Capture coverage data + COMMAND ${LCOV_PATH} --directory ${CMAKE_BINARY_DIR} --capture + --output-file ${CMAKE_BINARY_DIR}/coverage.info + # Filter to only ql/ source files + COMMAND ${LCOV_PATH} + --extract ${CMAKE_BINARY_DIR}/coverage.info + "${CMAKE_SOURCE_DIR}/ql/*" + --output-file ${CMAKE_BINARY_DIR}/coverage_filtered.info + # Generate HTML report + COMMAND ${GENHTML_PATH} ${CMAKE_BINARY_DIR}/coverage_filtered.info + --output-directory ${CMAKE_BINARY_DIR}/coverage_report + --title "QuantLib Coverage Report" + --legend --show-details + WORKING_DIRECTORY ${CMAKE_SOURCE_DIR} + ) + message(STATUS "Coverage enabled: build, then run 'make coverage' to generate report") +endif() + # Convenience option to activate all STD options if (QL_USE_STD_CLASSES) set(QL_USE_STD_ANY ON) diff --git a/test-suite/CMakeLists.txt b/test-suite/CMakeLists.txt index 3717d23739..707670eb5b 100644 --- a/test-suite/CMakeLists.txt +++ b/test-suite/CMakeLists.txt @@ -19,6 +19,7 @@ set(QL_TEST_SOURCES blackformula.cpp blackvolsurfacedelta.cpp bondforward.cpp + bondfunctions.cpp bonds.cpp brownianbridge.cpp businessdayconventions.cpp @@ -27,6 +28,7 @@ set(QL_TEST_SOURCES capfloor.cpp capflooredcoupon.cpp cashflows.cpp + cashflowsmore.cpp catbonds.cpp cdo.cpp cdsoption.cpp @@ -82,6 +84,7 @@ set(QL_TEST_SOURCES gjrgarchmodel.cpp gsr.cpp hestonmodel.cpp + hestonprocess.cpp hestonslvmodel.cpp himalayaoption.cpp hybridhestonhullwhiteprocess.cpp @@ -134,6 +137,7 @@ set(QL_TEST_SOURCES pagodaoption.cpp partialtimebarrieroption.cpp pathgenerator.cpp + payoffs.cpp period.cpp perpetualfutures.cpp piecewiseblackvariancesurface.cpp diff --git a/test-suite/bondfunctions.cpp b/test-suite/bondfunctions.cpp new file mode 100644 index 0000000000..512ee137ea --- /dev/null +++ b/test-suite/bondfunctions.cpp @@ -0,0 +1,363 @@ +/* -*- mode: c++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ + +/* + Copyright (C) 2024 Cognition AI + + This file is part of QuantLib, a free-software/open-source library + for financial quantitative analysts and developers - http://quantlib.org/ + + QuantLib is free software: you can redistribute it and/or modify it + under the terms of the QuantLib license. You should have received a + copy of the license along with this program; if not, please email + . The license is also available online at + . + + This program is distributed in the hope that it will be useful, but WITHOUT + ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS + FOR A PARTICULAR PURPOSE. See the license for more details. +*/ + +#include "toplevelfixture.hpp" +#include "utilities.hpp" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +using namespace QuantLib; +using namespace boost::unit_test_framework; + +BOOST_FIXTURE_TEST_SUITE(QuantLibTests, TopLevelFixture) + +BOOST_AUTO_TEST_SUITE(BondFunctionsTests) + +namespace bondfunctions_test { + + struct CommonVars { + Calendar calendar; + Date today; + Date issueDate; + Date maturity; + Natural settlementDays; + Real faceAmount; + Rate couponRate; + ext::shared_ptr bond; + Handle discountCurve; + + CommonVars() { + calendar = TARGET(); + today = Date(15, March, 2024); + Settings::instance().evaluationDate() = today; + + settlementDays = 2; + faceAmount = 100.0; + couponRate = 0.05; + issueDate = Date(15, March, 2020); + maturity = Date(15, March, 2030); + + Schedule schedule(issueDate, maturity, Period(Annual), + calendar, Unadjusted, Unadjusted, + DateGeneration::Backward, false); + + bond = ext::make_shared( + settlementDays, faceAmount, + schedule, std::vector(1, couponRate), + Thirty360(Thirty360::BondBasis)); + + discountCurve = Handle( + ext::make_shared(today, 0.04, + Actual365Fixed())); + + bond->setPricingEngine( + ext::make_shared(discountCurve)); + } + }; + +} + +BOOST_AUTO_TEST_CASE(testDateInspectors) { + BOOST_TEST_MESSAGE("Testing BondFunctions date inspectors..."); + + bondfunctions_test::CommonVars vars; + + Date start = BondFunctions::startDate(*vars.bond); + BOOST_CHECK(start == vars.issueDate); + + Date mat = BondFunctions::maturityDate(*vars.bond); + BOOST_CHECK(mat == vars.maturity); + + BOOST_CHECK(BondFunctions::isTradable(*vars.bond)); +} + +BOOST_AUTO_TEST_CASE(testCashFlowInspectors) { + BOOST_TEST_MESSAGE("Testing BondFunctions cash flow inspectors..."); + + bondfunctions_test::CommonVars vars; + + Date settlement = vars.bond->settlementDate(); + + Date nextCfDate = BondFunctions::nextCashFlowDate(*vars.bond); + BOOST_CHECK(nextCfDate > settlement); + + Real nextCfAmount = BondFunctions::nextCashFlowAmount(*vars.bond); + BOOST_CHECK(nextCfAmount > 0.0); + + Date prevCfDate = BondFunctions::previousCashFlowDate(*vars.bond); + BOOST_CHECK(prevCfDate <= settlement); + + Real prevCfAmount = BondFunctions::previousCashFlowAmount(*vars.bond); + BOOST_CHECK(prevCfAmount > 0.0); + + Rate prevCoupon = BondFunctions::previousCouponRate(*vars.bond); + BOOST_CHECK_CLOSE(prevCoupon, vars.couponRate, 1e-10); + + Rate nextCoupon = BondFunctions::nextCouponRate(*vars.bond); + BOOST_CHECK_CLOSE(nextCoupon, vars.couponRate, 1e-10); +} + +BOOST_AUTO_TEST_CASE(testAccrualInspectors) { + BOOST_TEST_MESSAGE("Testing BondFunctions accrual inspectors..."); + + bondfunctions_test::CommonVars vars; + + Date accrualStart = BondFunctions::accrualStartDate(*vars.bond); + Date accrualEnd = BondFunctions::accrualEndDate(*vars.bond); + BOOST_CHECK(accrualStart < accrualEnd); + + Date refStart = BondFunctions::referencePeriodStart(*vars.bond); + Date refEnd = BondFunctions::referencePeriodEnd(*vars.bond); + BOOST_CHECK(refStart <= refEnd); + + Time accrualPeriod = BondFunctions::accrualPeriod(*vars.bond); + BOOST_CHECK(accrualPeriod > 0.0); + BOOST_CHECK(accrualPeriod <= 1.0); + + Date::serial_type accrualDays = BondFunctions::accrualDays(*vars.bond); + BOOST_CHECK(accrualDays > 0); + + Time accruedPeriod = BondFunctions::accruedPeriod(*vars.bond); + BOOST_CHECK(accruedPeriod >= 0.0); + BOOST_CHECK(accruedPeriod <= accrualPeriod); + + Date::serial_type accruedDays = BondFunctions::accruedDays(*vars.bond); + BOOST_CHECK(accruedDays >= 0); + BOOST_CHECK(accruedDays <= accrualDays); + + Real accruedAmount = BondFunctions::accruedAmount(*vars.bond); + BOOST_CHECK(accruedAmount >= 0.0); +} + +BOOST_AUTO_TEST_CASE(testDurationCalculations) { + BOOST_TEST_MESSAGE("Testing BondFunctions duration calculations..."); + + bondfunctions_test::CommonVars vars; + + Rate yield = 0.05; + DayCounter dc = Actual365Fixed(); + + Time simpleDur = BondFunctions::duration( + *vars.bond, yield, dc, Compounded, Annual, + Duration::Simple); + BOOST_CHECK(simpleDur > 0.0); + + Time macaulayDur = BondFunctions::duration( + *vars.bond, yield, dc, Compounded, Annual, + Duration::Macaulay); + BOOST_CHECK(macaulayDur > 0.0); + BOOST_CHECK(macaulayDur <= 10.0); + + Time modifiedDur = BondFunctions::duration( + *vars.bond, yield, dc, Compounded, Annual, + Duration::Modified); + BOOST_CHECK(modifiedDur > 0.0); + BOOST_CHECK(modifiedDur < macaulayDur); + + // modified = macaulay / (1 + y/freq) + Real expectedModified = macaulayDur / (1.0 + yield); + BOOST_CHECK_CLOSE(modifiedDur, expectedModified, 0.5); + + // also test InterestRate overload + InterestRate ir(yield, dc, Compounded, Annual); + Time dur2 = BondFunctions::duration(*vars.bond, ir, Duration::Modified); + BOOST_CHECK_CLOSE(dur2, modifiedDur, 1e-10); +} + +BOOST_AUTO_TEST_CASE(testConvexityCalculation) { + BOOST_TEST_MESSAGE("Testing BondFunctions convexity..."); + + bondfunctions_test::CommonVars vars; + + Rate yield = 0.05; + DayCounter dc = Actual365Fixed(); + + Real convex = BondFunctions::convexity( + *vars.bond, yield, dc, Compounded, Annual); + BOOST_CHECK(convex > 0.0); + + InterestRate ir(yield, dc, Compounded, Annual); + Real convex2 = BondFunctions::convexity(*vars.bond, ir); + BOOST_CHECK_CLOSE(convex, convex2, 1e-10); +} + +BOOST_AUTO_TEST_CASE(testBpsCalculation) { + BOOST_TEST_MESSAGE("Testing BondFunctions bps..."); + + bondfunctions_test::CommonVars vars; + + Rate yield = 0.05; + DayCounter dc = Actual365Fixed(); + + Real bps = BondFunctions::bps(*vars.bond, yield, dc, Compounded, Annual); + BOOST_CHECK(bps > 0.0); + + InterestRate ir(yield, dc, Compounded, Annual); + Real bps2 = BondFunctions::bps(*vars.bond, ir); + BOOST_CHECK_CLOSE(bps, bps2, 1e-10); +} + +BOOST_AUTO_TEST_CASE(testBasisPointValueAndYieldValue) { + BOOST_TEST_MESSAGE("Testing BondFunctions basisPointValue and yieldValueBasisPoint..."); + + bondfunctions_test::CommonVars vars; + + Rate yield = 0.05; + DayCounter dc = Actual365Fixed(); + + Real bpv = BondFunctions::basisPointValue( + *vars.bond, yield, dc, Compounded, Annual); + BOOST_CHECK(bpv != 0.0); + + Real yvbp = BondFunctions::yieldValueBasisPoint( + *vars.bond, yield, dc, Compounded, Annual); + BOOST_CHECK(yvbp != 0.0); + + // bpv * yvbp should approximately equal 0.01 (1bp * 1bp) + // They are roughly inverses scaled by notional + + InterestRate ir(yield, dc, Compounded, Annual); + Real bpv2 = BondFunctions::basisPointValue(*vars.bond, ir); + BOOST_CHECK_CLOSE(bpv, bpv2, 1e-10); + + Real yvbp2 = BondFunctions::yieldValueBasisPoint(*vars.bond, ir); + BOOST_CHECK_CLOSE(yvbp, yvbp2, 1e-10); +} + +BOOST_AUTO_TEST_CASE(testCleanAndDirtyPriceFromYield) { + BOOST_TEST_MESSAGE("Testing BondFunctions clean/dirty price from yield..."); + + bondfunctions_test::CommonVars vars; + + Rate yield = 0.05; + DayCounter dc = Thirty360(Thirty360::BondBasis); + + Real cleanPrice = BondFunctions::cleanPrice( + *vars.bond, yield, dc, Compounded, Annual); + Real dirtyPrice = BondFunctions::dirtyPrice( + *vars.bond, yield, dc, Compounded, Annual); + + BOOST_CHECK(cleanPrice > 0.0); + BOOST_CHECK(dirtyPrice > 0.0); + BOOST_CHECK(dirtyPrice >= cleanPrice); + + Real accrued = vars.bond->accruedAmount(); + BOOST_CHECK_CLOSE(dirtyPrice - cleanPrice, accrued, 0.01); +} + +BOOST_AUTO_TEST_CASE(testYieldFromPrice) { + BOOST_TEST_MESSAGE("Testing BondFunctions yield calculation..."); + + bondfunctions_test::CommonVars vars; + + DayCounter dc = Thirty360(Thirty360::BondBasis); + Rate targetYield = 0.05; + + Real price = BondFunctions::cleanPrice( + *vars.bond, targetYield, dc, Compounded, Annual); + + Rate computedYield = BondFunctions::yield( + *vars.bond, {price, Bond::Price::Clean}, + dc, Compounded, Annual); + + BOOST_CHECK_CLOSE(computedYield, targetYield, 0.001); +} + +BOOST_AUTO_TEST_CASE(testAtmRate) { + BOOST_TEST_MESSAGE("Testing BondFunctions ATM rate..."); + + bondfunctions_test::CommonVars vars; + + Rate atmRate = BondFunctions::atmRate( + *vars.bond, **vars.discountCurve); + BOOST_CHECK(atmRate > 0.0); +} + +BOOST_AUTO_TEST_CASE(testZSpread) { + BOOST_TEST_MESSAGE("Testing BondFunctions z-spread..."); + + bondfunctions_test::CommonVars vars; + + Real cleanPrice = vars.bond->cleanPrice(); + + Spread zSpread = BondFunctions::zSpread( + *vars.bond, {cleanPrice, Bond::Price::Clean}, + *vars.discountCurve, + Compounded, Annual); + + BOOST_CHECK_SMALL(std::abs(zSpread), 0.01); + + // price at a z-spread + Real priceAtSpread = BondFunctions::cleanPrice( + *vars.bond, *vars.discountCurve, + zSpread, Compounded, Annual); + BOOST_CHECK_CLOSE(priceAtSpread, cleanPrice, 0.01); +} + +BOOST_AUTO_TEST_CASE(testZeroCouponBondFunctions) { + BOOST_TEST_MESSAGE("Testing BondFunctions with zero-coupon bond..."); + + bondfunctions_test::CommonVars vars; + + Date zcMaturity = Date(15, March, 2034); + ZeroCouponBond zcBond(vars.settlementDays, vars.calendar, + 100.0, zcMaturity); + zcBond.setPricingEngine( + ext::make_shared(vars.discountCurve)); + + BOOST_CHECK(BondFunctions::isTradable(zcBond)); + + Date mat = BondFunctions::maturityDate(zcBond); + BOOST_CHECK(mat == zcMaturity); + + Real cleanPrice = zcBond.cleanPrice(); + BOOST_CHECK(cleanPrice > 0.0); + BOOST_CHECK(cleanPrice < 100.0); +} + +BOOST_AUTO_TEST_CASE(testDurationTypeStreaming) { + BOOST_TEST_MESSAGE("Testing Duration::Type streaming operator..."); + + std::ostringstream oss; + + oss << Duration::Simple; + BOOST_CHECK_EQUAL(oss.str(), "Simple"); + + oss.str(""); + oss << Duration::Macaulay; + BOOST_CHECK_EQUAL(oss.str(), "Macaulay"); + + oss.str(""); + oss << Duration::Modified; + BOOST_CHECK_EQUAL(oss.str(), "Modified"); +} + +BOOST_AUTO_TEST_SUITE_END() + +BOOST_AUTO_TEST_SUITE_END() diff --git a/test-suite/cashflowsmore.cpp b/test-suite/cashflowsmore.cpp new file mode 100644 index 0000000000..71c232d48a --- /dev/null +++ b/test-suite/cashflowsmore.cpp @@ -0,0 +1,274 @@ +/* -*- mode: c++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ + +/* + Copyright (C) 2024 Cognition AI + + This file is part of QuantLib, a free-software/open-source library + for financial quantitative analysts and developers - http://quantlib.org/ + + QuantLib is free software: you can redistribute it and/or modify it + under the terms of the QuantLib license. You should have received a + copy of the license along with this program; if not, please email + . The license is also available online at + . + + This program is distributed in the hope that it will be useful, but WITHOUT + ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS + FOR A PARTICULAR PURPOSE. See the license for more details. +*/ + +#include "toplevelfixture.hpp" +#include "utilities.hpp" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +using namespace QuantLib; +using namespace boost::unit_test_framework; + +BOOST_FIXTURE_TEST_SUITE(QuantLibTests, TopLevelFixture) + +BOOST_AUTO_TEST_SUITE(CashFlowsMoreTests) + +namespace cashflows_more_test { + + Leg makeFixedLeg(const Date& start, const Date& end, + Rate coupon, Real notional = 100.0) { + Schedule schedule(start, end, Period(Semiannual), + TARGET(), ModifiedFollowing, ModifiedFollowing, + DateGeneration::Backward, false); + return FixedRateLeg(schedule) + .withNotionals(notional) + .withCouponRates(coupon, Thirty360(Thirty360::BondBasis)); + } + +} + +BOOST_AUTO_TEST_CASE(testNpvWithInterestRate) { + BOOST_TEST_MESSAGE("Testing CashFlows::npv with InterestRate..."); + + Date today(15, March, 2024); + Settings::instance().evaluationDate() = today; + + Leg leg = cashflows_more_test::makeFixedLeg( + Date(15, March, 2024), Date(15, March, 2029), 0.05); + + InterestRate rate(0.04, Actual365Fixed(), Compounded, Semiannual); + Real npv = CashFlows::npv(leg, rate, ext::nullopt, today); + BOOST_CHECK(npv > 0.0); + + InterestRate rate2(0.06, Actual365Fixed(), Compounded, Semiannual); + Real npv2 = CashFlows::npv(leg, rate2, ext::nullopt, today); + BOOST_CHECK(npv2 < npv); +} + +BOOST_AUTO_TEST_CASE(testBpsWithInterestRate) { + BOOST_TEST_MESSAGE("Testing CashFlows::bps with InterestRate..."); + + Date today(15, March, 2024); + Settings::instance().evaluationDate() = today; + + Leg leg = cashflows_more_test::makeFixedLeg( + Date(15, March, 2024), Date(15, March, 2029), 0.05); + + InterestRate rate(0.04, Actual365Fixed(), Compounded, Semiannual); + Real bps = CashFlows::bps(leg, rate, ext::nullopt, today); + BOOST_CHECK(bps > 0.0); +} + +BOOST_AUTO_TEST_CASE(testDurationTypes) { + BOOST_TEST_MESSAGE("Testing CashFlows::duration with all Duration types..."); + + Date today(15, March, 2024); + Settings::instance().evaluationDate() = today; + + Leg leg = cashflows_more_test::makeFixedLeg( + Date(15, March, 2024), Date(15, March, 2034), 0.05); + + InterestRate rate(0.05, Actual365Fixed(), Compounded, Semiannual); + + Time simpleDur = CashFlows::duration(leg, rate, Duration::Simple, ext::nullopt, today); + BOOST_CHECK(simpleDur > 0.0); + + Time macaulayDur = CashFlows::duration(leg, rate, Duration::Macaulay, ext::nullopt, today); + BOOST_CHECK(macaulayDur > 0.0); + + Time modifiedDur = CashFlows::duration(leg, rate, Duration::Modified, ext::nullopt, today); + BOOST_CHECK(modifiedDur > 0.0); + BOOST_CHECK(modifiedDur < macaulayDur); +} + +BOOST_AUTO_TEST_CASE(testConvexity) { + BOOST_TEST_MESSAGE("Testing CashFlows::convexity..."); + + Date today(15, March, 2024); + Settings::instance().evaluationDate() = today; + + Leg leg = cashflows_more_test::makeFixedLeg( + Date(15, March, 2024), Date(15, March, 2034), 0.05); + + InterestRate rate(0.05, Actual365Fixed(), Compounded, Semiannual); + + Real convex = CashFlows::convexity(leg, rate, ext::nullopt, today); + BOOST_CHECK(convex > 0.0); +} + +BOOST_AUTO_TEST_CASE(testBasisPointValue) { + BOOST_TEST_MESSAGE("Testing CashFlows::basisPointValue and yieldValueBasisPoint..."); + + Date today(15, March, 2024); + Settings::instance().evaluationDate() = today; + + Leg leg = cashflows_more_test::makeFixedLeg( + Date(15, March, 2024), Date(15, March, 2029), 0.05, 100.0); + + InterestRate rate(0.05, Actual365Fixed(), Compounded, Semiannual); + + Real bpv = CashFlows::basisPointValue(leg, rate, ext::nullopt, today); + BOOST_CHECK(bpv != 0.0); + + Real yvbp = CashFlows::yieldValueBasisPoint(leg, rate, ext::nullopt, today); + BOOST_CHECK(yvbp != 0.0); +} + +BOOST_AUTO_TEST_CASE(testZSpread) { + BOOST_TEST_MESSAGE("Testing CashFlows::zSpread..."); + + Date today(15, March, 2024); + Settings::instance().evaluationDate() = today; + + Leg leg = cashflows_more_test::makeFixedLeg( + Date(15, March, 2024), Date(15, March, 2029), 0.05, 100.0); + + auto curve = ext::make_shared(today, 0.04, Actual365Fixed()); + + Real npv = CashFlows::npv(leg, *curve, ext::nullopt, today); + + Spread spread = CashFlows::zSpread( + leg, npv, curve, Compounded, Semiannual, ext::nullopt, today); + + BOOST_CHECK_SMALL(std::abs(spread), 0.0001); +} + +BOOST_AUTO_TEST_CASE(testAtmRate) { + BOOST_TEST_MESSAGE("Testing CashFlows::atmRate..."); + + Date today(15, March, 2024); + Settings::instance().evaluationDate() = today; + + Leg leg = cashflows_more_test::makeFixedLeg( + Date(15, March, 2024), Date(15, March, 2029), 0.05, 100.0); + + auto curve = ext::make_shared(today, 0.05, Actual365Fixed()); + + Rate atm = CashFlows::atmRate(leg, *curve, ext::nullopt, today); + BOOST_CHECK(atm > 0.0); +} + +BOOST_AUTO_TEST_CASE(testYieldFromNpv) { + BOOST_TEST_MESSAGE("Testing CashFlows yield from NPV round-trip..."); + + Date today(15, March, 2024); + Settings::instance().evaluationDate() = today; + + Leg leg = cashflows_more_test::makeFixedLeg( + Date(15, March, 2024), Date(15, March, 2029), 0.06, 100.0); + + InterestRate targetRate(0.05, Actual365Fixed(), Compounded, Semiannual); + Real npv = CashFlows::npv(leg, targetRate, ext::nullopt, today); + + Rate computedYield = CashFlows::yield( + leg, npv, Actual365Fixed(), Compounded, Semiannual, + ext::nullopt, today); + + BOOST_CHECK_CLOSE(computedYield, 0.05, 0.01); +} + +BOOST_AUTO_TEST_CASE(testSimpleCashFlowLeg) { + BOOST_TEST_MESSAGE("Testing CashFlows functions on SimpleCashFlow leg..."); + + Date today(15, March, 2024); + Settings::instance().evaluationDate() = today; + + Leg leg; + leg.push_back(ext::make_shared(50.0, today + 180)); + leg.push_back(ext::make_shared(1050.0, today + 365)); + + Date startDate = CashFlows::startDate(leg); + Date matDate = CashFlows::maturityDate(leg); + BOOST_CHECK(startDate < matDate); + + InterestRate rate(0.04, Actual365Fixed(), Compounded, Annual); + Real npv = CashFlows::npv(leg, rate, ext::nullopt, today); + BOOST_CHECK(npv > 0.0); +} + +BOOST_AUTO_TEST_CASE(testPreviousAndNextCashFlow) { + BOOST_TEST_MESSAGE("Testing CashFlows previous/next cash flow iteration..."); + + Date today(15, March, 2024); + Settings::instance().evaluationDate() = today; + + Date start = Date(15, March, 2023); + Date end = Date(15, March, 2025); + Leg leg = cashflows_more_test::makeFixedLeg(start, end, 0.05); + + auto next = CashFlows::nextCashFlow(leg, ext::nullopt, today); + BOOST_CHECK(next != leg.end()); + BOOST_CHECK((*next)->date() > today); + + auto prev = CashFlows::previousCashFlow(leg, ext::nullopt, today); + BOOST_CHECK(prev != leg.rend()); + + Date nextDate = CashFlows::nextCashFlowDate(leg, ext::nullopt, today); + BOOST_CHECK(nextDate > today); + + Date prevDate = CashFlows::previousCashFlowDate(leg, ext::nullopt, today); + BOOST_CHECK(prevDate <= today); + + Real nextAmt = CashFlows::nextCashFlowAmount(leg, ext::nullopt, today); + BOOST_CHECK(nextAmt > 0.0); + + Real prevAmt = CashFlows::previousCashFlowAmount(leg, ext::nullopt, today); + BOOST_CHECK(prevAmt > 0.0); +} + +BOOST_AUTO_TEST_CASE(testNpvWithYieldTermStructure) { + BOOST_TEST_MESSAGE("Testing CashFlows::npv with YieldTermStructure..."); + + Date today(15, March, 2024); + Settings::instance().evaluationDate() = today; + + Leg leg = cashflows_more_test::makeFixedLeg( + Date(15, March, 2024), Date(15, March, 2029), 0.05, 100.0); + + auto curve = ext::make_shared(today, 0.04, Actual365Fixed()); + + Real npv = CashFlows::npv(leg, *curve, ext::nullopt, today); + BOOST_CHECK(npv > 0.0); + + // NPV with z-spread of 0 should equal base NPV + Real npvZero = CashFlows::npv(leg, curve, 0.0, + Compounded, Semiannual, + ext::nullopt, today); + BOOST_CHECK_CLOSE(npv, npvZero, 0.01); + + // positive z-spread should reduce NPV + Real npvSpread = CashFlows::npv(leg, curve, 0.01, + Compounded, Semiannual, + ext::nullopt, today); + BOOST_CHECK(npvSpread < npv); +} + +BOOST_AUTO_TEST_SUITE_END() + +BOOST_AUTO_TEST_SUITE_END() diff --git a/test-suite/hestonprocess.cpp b/test-suite/hestonprocess.cpp new file mode 100644 index 0000000000..2296fe3560 --- /dev/null +++ b/test-suite/hestonprocess.cpp @@ -0,0 +1,270 @@ +/* -*- mode: c++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ + +/* + Copyright (C) 2024 Cognition AI + + This file is part of QuantLib, a free-software/open-source library + for financial quantitative analysts and developers - http://quantlib.org/ + + QuantLib is free software: you can redistribute it and/or modify it + under the terms of the QuantLib license. You should have received a + copy of the license along with this program; if not, please email + . The license is also available online at + . + + This program is distributed in the hope that it will be useful, but WITHOUT + ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS + FOR A PARTICULAR PURPOSE. See the license for more details. +*/ + +#include "toplevelfixture.hpp" +#include "utilities.hpp" +#include +#include +#include +#include + +using namespace QuantLib; +using namespace boost::unit_test_framework; + +BOOST_FIXTURE_TEST_SUITE(QuantLibTests, TopLevelFixture) + +BOOST_AUTO_TEST_SUITE(HestonProcessTests) + +namespace hestonprocess_test { + + struct CommonVars { + Handle riskFreeRate; + Handle dividendYield; + Handle s0; + Real v0, kappa, theta, sigma, rho; + + CommonVars() { + Date today(15, March, 2024); + Settings::instance().evaluationDate() = today; + + riskFreeRate = Handle( + ext::make_shared(today, 0.05, Actual365Fixed())); + dividendYield = Handle( + ext::make_shared(today, 0.02, Actual365Fixed())); + s0 = Handle(ext::make_shared(100.0)); + + v0 = 0.04; + kappa = 2.0; + theta = 0.04; + sigma = 0.5; + rho = -0.7; + } + + ext::shared_ptr makeProcess( + HestonProcess::Discretization d = + HestonProcess::QuadraticExponentialMartingale) const { + return ext::make_shared( + riskFreeRate, dividendYield, s0, + v0, kappa, theta, sigma, rho, d); + } + }; + +} + +BOOST_AUTO_TEST_CASE(testAccessors) { + BOOST_TEST_MESSAGE("Testing HestonProcess accessors..."); + + hestonprocess_test::CommonVars vars; + auto process = vars.makeProcess(); + + BOOST_CHECK_CLOSE(process->v0(), vars.v0, 1e-10); + BOOST_CHECK_CLOSE(process->kappa(), vars.kappa, 1e-10); + BOOST_CHECK_CLOSE(process->theta(), vars.theta, 1e-10); + BOOST_CHECK_CLOSE(process->sigma(), vars.sigma, 1e-10); + BOOST_CHECK_CLOSE(process->rho(), vars.rho, 1e-10); + BOOST_CHECK_CLOSE(process->s0()->value(), 100.0, 1e-10); +} + +BOOST_AUTO_TEST_CASE(testSizeAndFactors) { + BOOST_TEST_MESSAGE("Testing HestonProcess size and factors..."); + + hestonprocess_test::CommonVars vars; + + auto qe = vars.makeProcess(HestonProcess::QuadraticExponentialMartingale); + BOOST_CHECK_EQUAL(qe->size(), Size(2)); + BOOST_CHECK_EQUAL(qe->factors(), Size(2)); + + auto bk = vars.makeProcess(HestonProcess::BroadieKayaExactSchemeLobatto); + BOOST_CHECK_EQUAL(bk->size(), Size(2)); + BOOST_CHECK_EQUAL(bk->factors(), Size(3)); +} + +BOOST_AUTO_TEST_CASE(testInitialValues) { + BOOST_TEST_MESSAGE("Testing HestonProcess initial values..."); + + hestonprocess_test::CommonVars vars; + auto process = vars.makeProcess(); + + Array init = process->initialValues(); + BOOST_CHECK_EQUAL(init.size(), Size(2)); + BOOST_CHECK_CLOSE(init[0], 100.0, 1e-10); + BOOST_CHECK_CLOSE(init[1], vars.v0, 1e-10); +} + +BOOST_AUTO_TEST_CASE(testDrift) { + BOOST_TEST_MESSAGE("Testing HestonProcess drift..."); + + hestonprocess_test::CommonVars vars; + auto process = vars.makeProcess(HestonProcess::PartialTruncation); + + Array x(2); + x[0] = std::log(100.0); + x[1] = 0.04; + + Array d = process->drift(0.0, x); + BOOST_CHECK_EQUAL(d.size(), Size(2)); + + // drift[0] = r - q - 0.5*v + Real expectedDrift0 = 0.05 - 0.02 - 0.5 * 0.04; + BOOST_CHECK_CLOSE(d[0], expectedDrift0, 0.1); + + // drift[1] = kappa*(theta - v) for PartialTruncation + Real expectedDrift1 = vars.kappa * (vars.theta - x[1]); + BOOST_CHECK_CLOSE(d[1], expectedDrift1, 0.1); +} + +BOOST_AUTO_TEST_CASE(testDriftWithNegativeVariance) { + BOOST_TEST_MESSAGE("Testing HestonProcess drift with negative variance..."); + + hestonprocess_test::CommonVars vars; + + Array x(2); + x[0] = std::log(100.0); + x[1] = -0.01; + + // PartialTruncation: uses v directly in kappa*(theta-v), vol=0 + auto pt = vars.makeProcess(HestonProcess::PartialTruncation); + Array dpt = pt->drift(0.0, x); + Real expectedDrift1 = vars.kappa * (vars.theta - x[1]); + BOOST_CHECK_CLOSE(dpt[1], expectedDrift1, 0.1); + + // FullTruncation: vol=0, so drift1 = kappa*(theta - 0) + auto ft = vars.makeProcess(HestonProcess::FullTruncation); + Array dft = ft->drift(0.0, x); + Real expectedFtDrift1 = vars.kappa * vars.theta; + BOOST_CHECK_CLOSE(dft[1], expectedFtDrift1, 0.1); + + // Reflection: vol = -sqrt(-v) + auto ref = vars.makeProcess(HestonProcess::Reflection); + Array dref = ref->drift(0.0, x); + Real vol = -std::sqrt(0.01); + Real expectedRefDrift1 = vars.kappa * (vars.theta - vol*vol); + BOOST_CHECK_CLOSE(dref[1], expectedRefDrift1, 0.1); +} + +BOOST_AUTO_TEST_CASE(testDiffusion) { + BOOST_TEST_MESSAGE("Testing HestonProcess diffusion matrix..."); + + hestonprocess_test::CommonVars vars; + auto process = vars.makeProcess(); + + Array x(2); + x[0] = std::log(100.0); + x[1] = 0.04; + + Matrix diff = process->diffusion(0.0, x); + BOOST_CHECK_EQUAL(diff.rows(), Size(2)); + BOOST_CHECK_EQUAL(diff.columns(), Size(2)); + + Real vol = std::sqrt(0.04); + BOOST_CHECK_CLOSE(diff[0][0], vol, 1e-10); + BOOST_CHECK_CLOSE(diff[0][1], 0.0, 1e-10); + BOOST_CHECK_CLOSE(diff[1][0], vars.sigma * vol * vars.rho, 1e-10); + Real sqrtRhoTerm = vars.sigma * vol * std::sqrt(1.0 - vars.rho * vars.rho); + BOOST_CHECK_CLOSE(diff[1][1], sqrtRhoTerm, 1e-10); +} + +BOOST_AUTO_TEST_CASE(testApply) { + BOOST_TEST_MESSAGE("Testing HestonProcess apply..."); + + hestonprocess_test::CommonVars vars; + auto process = vars.makeProcess(); + + Array x0(2), dx(2); + x0[0] = std::log(100.0); + x0[1] = 0.04; + dx[0] = 0.01; + dx[1] = 0.001; + + Array result = process->apply(x0, dx); + BOOST_CHECK_EQUAL(result.size(), Size(2)); + // apply: spot component is x0[0]*exp(dx[0]), variance is x0[1]+dx[1] + BOOST_CHECK_CLOSE(result[0], x0[0] * std::exp(dx[0]), 1e-10); + BOOST_CHECK_CLOSE(result[1], x0[1] + dx[1], 1e-10); +} + +BOOST_AUTO_TEST_CASE(testEvolveQuadraticExponential) { + BOOST_TEST_MESSAGE("Testing HestonProcess evolve with QE scheme..."); + + hestonprocess_test::CommonVars vars; + auto process = vars.makeProcess(HestonProcess::QuadraticExponential); + + Array x0(2); + x0[0] = 100.0; + x0[1] = vars.v0; + + Array dw(2); + dw[0] = 0.1; + dw[1] = 0.2; + + Array result = process->evolve(0.0, x0, 0.01, dw); + BOOST_CHECK_EQUAL(result.size(), Size(2)); + BOOST_CHECK(result[0] > 0.0); + BOOST_CHECK(result[1] >= 0.0); +} + +BOOST_AUTO_TEST_CASE(testEvolvePartialTruncation) { + BOOST_TEST_MESSAGE("Testing HestonProcess evolve with PartialTruncation..."); + + hestonprocess_test::CommonVars vars; + auto process = vars.makeProcess(HestonProcess::PartialTruncation); + + Array x0(2); + x0[0] = 100.0; + x0[1] = vars.v0; + + Array dw(2); + dw[0] = 0.0; + dw[1] = 0.0; + + Array result = process->evolve(0.0, x0, 0.01, dw); + BOOST_CHECK(result[0] > 0.0); +} + +BOOST_AUTO_TEST_CASE(testDiscretizationSchemes) { + BOOST_TEST_MESSAGE("Testing HestonProcess various discretization schemes..."); + + hestonprocess_test::CommonVars vars; + + std::vector schemes = { + HestonProcess::PartialTruncation, + HestonProcess::FullTruncation, + HestonProcess::Reflection, + HestonProcess::QuadraticExponential, + HestonProcess::QuadraticExponentialMartingale + }; + + Array x0(2); + x0[0] = 100.0; + x0[1] = vars.v0; + + Array dw(2); + dw[0] = 0.1; + dw[1] = -0.05; + + for (auto scheme : schemes) { + auto process = vars.makeProcess(scheme); + Array result = process->evolve(0.0, x0, 0.01, dw); + BOOST_CHECK(result[0] > 0.0); + } +} + +BOOST_AUTO_TEST_SUITE_END() + +BOOST_AUTO_TEST_SUITE_END() diff --git a/test-suite/payoffs.cpp b/test-suite/payoffs.cpp new file mode 100644 index 0000000000..6800476816 --- /dev/null +++ b/test-suite/payoffs.cpp @@ -0,0 +1,188 @@ +/* -*- mode: c++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ + +/* + Copyright (C) 2024 Cognition AI + + This file is part of QuantLib, a free-software/open-source library + for financial quantitative analysts and developers - http://quantlib.org/ + + QuantLib is free software: you can redistribute it and/or modify it + under the terms of the QuantLib license. You should have received a + copy of the license along with this program; if not, please email + . The license is also available online at + . + + This program is distributed in the hope that it will be useful, but WITHOUT + ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS + FOR A PARTICULAR PURPOSE. See the license for more details. +*/ + +#include "toplevelfixture.hpp" +#include "utilities.hpp" +#include +#include + +using namespace QuantLib; +using namespace boost::unit_test_framework; + +BOOST_FIXTURE_TEST_SUITE(QuantLibTests, TopLevelFixture) + +BOOST_AUTO_TEST_SUITE(PayoffTests) + +BOOST_AUTO_TEST_CASE(testNullPayoff) { + BOOST_TEST_MESSAGE("Testing NullPayoff..."); + + NullPayoff payoff; + BOOST_CHECK_EQUAL(payoff.name(), "Null"); + BOOST_CHECK_EQUAL(payoff.description(), "Null"); + BOOST_CHECK_THROW(payoff(100.0), Error); +} + +BOOST_AUTO_TEST_CASE(testPlainVanillaPayoff) { + BOOST_TEST_MESSAGE("Testing PlainVanillaPayoff..."); + + Real strike = 100.0; + + PlainVanillaPayoff call(Option::Call, strike); + BOOST_CHECK_CLOSE(call(120.0), 20.0, 1e-10); + BOOST_CHECK_CLOSE(call(100.0), 0.0, 1e-10); + BOOST_CHECK_CLOSE(call(80.0), 0.0, 1e-10); + BOOST_CHECK_EQUAL(call.optionType(), Option::Call); + BOOST_CHECK_CLOSE(call.strike(), strike, 1e-10); + + PlainVanillaPayoff put(Option::Put, strike); + BOOST_CHECK_CLOSE(put(80.0), 20.0, 1e-10); + BOOST_CHECK_CLOSE(put(100.0), 0.0, 1e-10); + BOOST_CHECK_CLOSE(put(120.0), 0.0, 1e-10); + + // description + std::string desc = call.description(); + BOOST_CHECK(desc.find("Call") != std::string::npos); + BOOST_CHECK(desc.find("100") != std::string::npos); + + // boundary values + PlainVanillaPayoff zeroStrike(Option::Call, 0.0); + BOOST_CHECK_CLOSE(zeroStrike(50.0), 50.0, 1e-10); +} + +BOOST_AUTO_TEST_CASE(testCashOrNothingPayoff) { + BOOST_TEST_MESSAGE("Testing CashOrNothingPayoff..."); + + Real strike = 100.0; + Real cash = 10.0; + + CashOrNothingPayoff call(Option::Call, strike, cash); + BOOST_CHECK_CLOSE(call(120.0), cash, 1e-10); + BOOST_CHECK_CLOSE(call(80.0), 0.0, 1e-10); + BOOST_CHECK_CLOSE(call(100.0), 0.0, 1e-10); // at the money + BOOST_CHECK_CLOSE(call.cashPayoff(), cash, 1e-10); + + CashOrNothingPayoff put(Option::Put, strike, cash); + BOOST_CHECK_CLOSE(put(80.0), cash, 1e-10); + BOOST_CHECK_CLOSE(put(120.0), 0.0, 1e-10); + + std::string desc = call.description(); + BOOST_CHECK(desc.find("cash payoff") != std::string::npos); +} + +BOOST_AUTO_TEST_CASE(testAssetOrNothingPayoff) { + BOOST_TEST_MESSAGE("Testing AssetOrNothingPayoff..."); + + Real strike = 100.0; + + AssetOrNothingPayoff call(Option::Call, strike); + BOOST_CHECK_CLOSE(call(120.0), 120.0, 1e-10); + BOOST_CHECK_CLOSE(call(80.0), 0.0, 1e-10); + BOOST_CHECK_CLOSE(call(100.0), 0.0, 1e-10); + + AssetOrNothingPayoff put(Option::Put, strike); + BOOST_CHECK_CLOSE(put(80.0), 80.0, 1e-10); + BOOST_CHECK_CLOSE(put(120.0), 0.0, 1e-10); +} + +BOOST_AUTO_TEST_CASE(testGapPayoff) { + BOOST_TEST_MESSAGE("Testing GapPayoff..."); + + Real strike = 100.0; + Real secondStrike = 95.0; + + GapPayoff call(Option::Call, strike, secondStrike); + BOOST_CHECK_CLOSE(call(120.0), 120.0 - 95.0, 1e-10); + BOOST_CHECK_CLOSE(call(100.0), 100.0 - 95.0, 1e-10); // >= trigger + BOOST_CHECK_CLOSE(call(80.0), 0.0, 1e-10); + BOOST_CHECK_CLOSE(call.secondStrike(), secondStrike, 1e-10); + + GapPayoff put(Option::Put, strike, secondStrike); + BOOST_CHECK_CLOSE(put(80.0), 95.0 - 80.0, 1e-10); + BOOST_CHECK_CLOSE(put(100.0), 95.0 - 100.0, 1e-10); // = trigger + BOOST_CHECK_CLOSE(put(120.0), 0.0, 1e-10); + + std::string desc = call.description(); + BOOST_CHECK(desc.find("strike payoff") != std::string::npos); +} + +BOOST_AUTO_TEST_CASE(testSuperFundPayoff) { + BOOST_TEST_MESSAGE("Testing SuperFundPayoff..."); + + Real strike = 100.0; + Real secondStrike = 150.0; + + SuperFundPayoff payoff(strike, secondStrike); + BOOST_CHECK_CLOSE(payoff(120.0), 120.0 / 100.0, 1e-10); + BOOST_CHECK_CLOSE(payoff(100.0), 100.0 / 100.0, 1e-10); + BOOST_CHECK_CLOSE(payoff(80.0), 0.0, 1e-10); + BOOST_CHECK_CLOSE(payoff(150.0), 0.0, 1e-10); + BOOST_CHECK_CLOSE(payoff(149.99), 149.99 / 100.0, 1e-10); +} + +BOOST_AUTO_TEST_CASE(testSuperSharePayoff) { + BOOST_TEST_MESSAGE("Testing SuperSharePayoff..."); + + Real strike = 100.0; + Real secondStrike = 150.0; + Real cashPayoff = 1.0; + + SuperSharePayoff payoff(strike, secondStrike, cashPayoff); + BOOST_CHECK_CLOSE(payoff(120.0), cashPayoff, 1e-10); + BOOST_CHECK_CLOSE(payoff(100.0), cashPayoff, 1e-10); + BOOST_CHECK_CLOSE(payoff(80.0), 0.0, 1e-10); + BOOST_CHECK_CLOSE(payoff(150.0), 0.0, 1e-10); + BOOST_CHECK_CLOSE(payoff.cashPayoff(), cashPayoff, 1e-10); + BOOST_CHECK_CLOSE(payoff.secondStrike(), secondStrike, 1e-10); + + std::string desc = payoff.description(); + BOOST_CHECK(desc.find("second strike") != std::string::npos); + BOOST_CHECK(desc.find("amount") != std::string::npos); +} + +BOOST_AUTO_TEST_CASE(testPercentageStrikePayoff) { + BOOST_TEST_MESSAGE("Testing PercentageStrikePayoff..."); + + Real moneyness = 1.1; + + PercentageStrikePayoff call(Option::Call, moneyness); + BOOST_CHECK_CLOSE(call(100.0), 0.0, 1e-10); + + PercentageStrikePayoff put(Option::Put, moneyness); + BOOST_CHECK_CLOSE(put(100.0), 100.0 * (1.1 - 1.0), 1e-10); + + PercentageStrikePayoff call2(Option::Call, 0.9); + BOOST_CHECK_CLOSE(call2(100.0), 100.0 * 0.1, 1e-10); +} + +BOOST_AUTO_TEST_CASE(testFloatingTypePayoff) { + BOOST_TEST_MESSAGE("Testing FloatingTypePayoff..."); + + FloatingTypePayoff call(Option::Call); + BOOST_CHECK_CLOSE(call(120.0, 100.0), 20.0, 1e-10); + BOOST_CHECK_CLOSE(call(80.0, 100.0), 0.0, 1e-10); + BOOST_CHECK_THROW(call(100.0), Error); + + FloatingTypePayoff put(Option::Put); + BOOST_CHECK_CLOSE(put(80.0, 100.0), 20.0, 1e-10); + BOOST_CHECK_CLOSE(put(120.0, 100.0), 0.0, 1e-10); +} + +BOOST_AUTO_TEST_SUITE_END() + +BOOST_AUTO_TEST_SUITE_END() From 6b4bd0c2c30e482338d482d968364eb6afc5333b Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Fri, 5 Jun 2026 18:39:15 +0000 Subject: [PATCH 2/2] Fix BOOST_CHECK_CLOSE with 0.0 expected value Replace BOOST_CHECK_CLOSE(x, 0.0, tol) with BOOST_CHECK_EQUAL(x, 0.0) for exact-zero payoff/diffusion results. BOOST_CHECK_CLOSE uses relative comparison which is undefined against zero. Co-Authored-By: Toby Drinkall --- test-suite/hestonprocess.cpp | 2 +- test-suite/payoffs.cpp | 38 ++++++++++++++++++------------------ 2 files changed, 20 insertions(+), 20 deletions(-) diff --git a/test-suite/hestonprocess.cpp b/test-suite/hestonprocess.cpp index 2296fe3560..f76caf0db4 100644 --- a/test-suite/hestonprocess.cpp +++ b/test-suite/hestonprocess.cpp @@ -174,7 +174,7 @@ BOOST_AUTO_TEST_CASE(testDiffusion) { Real vol = std::sqrt(0.04); BOOST_CHECK_CLOSE(diff[0][0], vol, 1e-10); - BOOST_CHECK_CLOSE(diff[0][1], 0.0, 1e-10); + BOOST_CHECK_EQUAL(diff[0][1], 0.0); BOOST_CHECK_CLOSE(diff[1][0], vars.sigma * vol * vars.rho, 1e-10); Real sqrtRhoTerm = vars.sigma * vol * std::sqrt(1.0 - vars.rho * vars.rho); BOOST_CHECK_CLOSE(diff[1][1], sqrtRhoTerm, 1e-10); diff --git a/test-suite/payoffs.cpp b/test-suite/payoffs.cpp index 6800476816..a38a632cf3 100644 --- a/test-suite/payoffs.cpp +++ b/test-suite/payoffs.cpp @@ -45,15 +45,15 @@ BOOST_AUTO_TEST_CASE(testPlainVanillaPayoff) { PlainVanillaPayoff call(Option::Call, strike); BOOST_CHECK_CLOSE(call(120.0), 20.0, 1e-10); - BOOST_CHECK_CLOSE(call(100.0), 0.0, 1e-10); - BOOST_CHECK_CLOSE(call(80.0), 0.0, 1e-10); + BOOST_CHECK_EQUAL(call(100.0), 0.0); + BOOST_CHECK_EQUAL(call(80.0), 0.0); BOOST_CHECK_EQUAL(call.optionType(), Option::Call); BOOST_CHECK_CLOSE(call.strike(), strike, 1e-10); PlainVanillaPayoff put(Option::Put, strike); BOOST_CHECK_CLOSE(put(80.0), 20.0, 1e-10); - BOOST_CHECK_CLOSE(put(100.0), 0.0, 1e-10); - BOOST_CHECK_CLOSE(put(120.0), 0.0, 1e-10); + BOOST_CHECK_EQUAL(put(100.0), 0.0); + BOOST_CHECK_EQUAL(put(120.0), 0.0); // description std::string desc = call.description(); @@ -73,13 +73,13 @@ BOOST_AUTO_TEST_CASE(testCashOrNothingPayoff) { CashOrNothingPayoff call(Option::Call, strike, cash); BOOST_CHECK_CLOSE(call(120.0), cash, 1e-10); - BOOST_CHECK_CLOSE(call(80.0), 0.0, 1e-10); - BOOST_CHECK_CLOSE(call(100.0), 0.0, 1e-10); // at the money + BOOST_CHECK_EQUAL(call(80.0), 0.0); + BOOST_CHECK_EQUAL(call(100.0), 0.0); // at the money BOOST_CHECK_CLOSE(call.cashPayoff(), cash, 1e-10); CashOrNothingPayoff put(Option::Put, strike, cash); BOOST_CHECK_CLOSE(put(80.0), cash, 1e-10); - BOOST_CHECK_CLOSE(put(120.0), 0.0, 1e-10); + BOOST_CHECK_EQUAL(put(120.0), 0.0); std::string desc = call.description(); BOOST_CHECK(desc.find("cash payoff") != std::string::npos); @@ -92,12 +92,12 @@ BOOST_AUTO_TEST_CASE(testAssetOrNothingPayoff) { AssetOrNothingPayoff call(Option::Call, strike); BOOST_CHECK_CLOSE(call(120.0), 120.0, 1e-10); - BOOST_CHECK_CLOSE(call(80.0), 0.0, 1e-10); - BOOST_CHECK_CLOSE(call(100.0), 0.0, 1e-10); + BOOST_CHECK_EQUAL(call(80.0), 0.0); + BOOST_CHECK_EQUAL(call(100.0), 0.0); AssetOrNothingPayoff put(Option::Put, strike); BOOST_CHECK_CLOSE(put(80.0), 80.0, 1e-10); - BOOST_CHECK_CLOSE(put(120.0), 0.0, 1e-10); + BOOST_CHECK_EQUAL(put(120.0), 0.0); } BOOST_AUTO_TEST_CASE(testGapPayoff) { @@ -109,13 +109,13 @@ BOOST_AUTO_TEST_CASE(testGapPayoff) { GapPayoff call(Option::Call, strike, secondStrike); BOOST_CHECK_CLOSE(call(120.0), 120.0 - 95.0, 1e-10); BOOST_CHECK_CLOSE(call(100.0), 100.0 - 95.0, 1e-10); // >= trigger - BOOST_CHECK_CLOSE(call(80.0), 0.0, 1e-10); + BOOST_CHECK_EQUAL(call(80.0), 0.0); BOOST_CHECK_CLOSE(call.secondStrike(), secondStrike, 1e-10); GapPayoff put(Option::Put, strike, secondStrike); BOOST_CHECK_CLOSE(put(80.0), 95.0 - 80.0, 1e-10); BOOST_CHECK_CLOSE(put(100.0), 95.0 - 100.0, 1e-10); // = trigger - BOOST_CHECK_CLOSE(put(120.0), 0.0, 1e-10); + BOOST_CHECK_EQUAL(put(120.0), 0.0); std::string desc = call.description(); BOOST_CHECK(desc.find("strike payoff") != std::string::npos); @@ -130,8 +130,8 @@ BOOST_AUTO_TEST_CASE(testSuperFundPayoff) { SuperFundPayoff payoff(strike, secondStrike); BOOST_CHECK_CLOSE(payoff(120.0), 120.0 / 100.0, 1e-10); BOOST_CHECK_CLOSE(payoff(100.0), 100.0 / 100.0, 1e-10); - BOOST_CHECK_CLOSE(payoff(80.0), 0.0, 1e-10); - BOOST_CHECK_CLOSE(payoff(150.0), 0.0, 1e-10); + BOOST_CHECK_EQUAL(payoff(80.0), 0.0); + BOOST_CHECK_EQUAL(payoff(150.0), 0.0); BOOST_CHECK_CLOSE(payoff(149.99), 149.99 / 100.0, 1e-10); } @@ -145,8 +145,8 @@ BOOST_AUTO_TEST_CASE(testSuperSharePayoff) { SuperSharePayoff payoff(strike, secondStrike, cashPayoff); BOOST_CHECK_CLOSE(payoff(120.0), cashPayoff, 1e-10); BOOST_CHECK_CLOSE(payoff(100.0), cashPayoff, 1e-10); - BOOST_CHECK_CLOSE(payoff(80.0), 0.0, 1e-10); - BOOST_CHECK_CLOSE(payoff(150.0), 0.0, 1e-10); + BOOST_CHECK_EQUAL(payoff(80.0), 0.0); + BOOST_CHECK_EQUAL(payoff(150.0), 0.0); BOOST_CHECK_CLOSE(payoff.cashPayoff(), cashPayoff, 1e-10); BOOST_CHECK_CLOSE(payoff.secondStrike(), secondStrike, 1e-10); @@ -161,7 +161,7 @@ BOOST_AUTO_TEST_CASE(testPercentageStrikePayoff) { Real moneyness = 1.1; PercentageStrikePayoff call(Option::Call, moneyness); - BOOST_CHECK_CLOSE(call(100.0), 0.0, 1e-10); + BOOST_CHECK_EQUAL(call(100.0), 0.0); PercentageStrikePayoff put(Option::Put, moneyness); BOOST_CHECK_CLOSE(put(100.0), 100.0 * (1.1 - 1.0), 1e-10); @@ -175,12 +175,12 @@ BOOST_AUTO_TEST_CASE(testFloatingTypePayoff) { FloatingTypePayoff call(Option::Call); BOOST_CHECK_CLOSE(call(120.0, 100.0), 20.0, 1e-10); - BOOST_CHECK_CLOSE(call(80.0, 100.0), 0.0, 1e-10); + BOOST_CHECK_EQUAL(call(80.0, 100.0), 0.0); BOOST_CHECK_THROW(call(100.0), Error); FloatingTypePayoff put(Option::Put); BOOST_CHECK_CLOSE(put(80.0, 100.0), 20.0, 1e-10); - BOOST_CHECK_CLOSE(put(120.0, 100.0), 0.0, 1e-10); + BOOST_CHECK_EQUAL(put(120.0, 100.0), 0.0); } BOOST_AUTO_TEST_SUITE_END()