diff --git a/.github/workflows/quantlib-risks.yml b/.github/workflows/quantlib-risks.yml index 5fd8471c..afd39f8d 100644 --- a/.github/workflows/quantlib-risks.yml +++ b/.github/workflows/quantlib-risks.yml @@ -100,11 +100,7 @@ jobs: submodules: recursive persist-credentials: false - name: Build sdist - # no isolation until xad 2.2 is on PyPI - run: | - python3 -m venv /tmp/sdist-env - /tmp/sdist-env/bin/pip install build "scikit-build-core>=0.11" - /tmp/sdist-env/bin/python -m build --sdist --no-isolation --skip-dependency-check + run: pipx run build --sdist - name: Check metadata run: pipx run twine check --strict dist/* - name: Upload sdist diff --git a/CHANGELOG.md b/CHANGELOG.md index 74ef2d63..8236140d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,16 +7,39 @@ the underlying QuantLib-SWIG changelog. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). -## [1.41.0] - Unreleased +## [1.44.0] - Unreleased + +First release as QuantLibAAD. - Renamed QuantLib-Risks -> QuantLibAAD: `pip install QuantLibAAD`, `import QuantLibAAD` -- `QuantLib-Risks` 1.41.0 is a deprecated shim (`shim/`) that requires `QuantLibAAD==1.41.*` and +- `QuantLib-Risks` 1.44.0 is a deprecated shim (`shim/`) that requires `QuantLibAAD==1.44.*` and re-exports it as `QuantLib_Risks` with a `DeprecationWarning` - Anyone pinned to `QuantLib-Risks==1.33.3` must also pin `xad<2` -- Matches QuantLib and QuantLib Python bindings version 1.41 -- Builds against QuantLibAAD 1.41 (formerly QuantLib-Risks-Cpp) and the shared libxad of the installed xad 2.2 package (`xad>=2.2,<2.3`), which is not bundled -- Tapes are shared with xad through libxad, thread-local per Python thread -- Builds the unmodified QuantLib 1.41 release, no QuantLib patches +- Matches QuantLib and QuantLib Python bindings version 1.44, with C++ QuantLibAAD (formerly + QuantLib-Risks-Cpp) +- Removed from the Python API with QuantLib-SWIG 1.42 to 1.44, as in the C++ library: + `SpreadOption` and `KirkSpreadOptionEngine`; `SubPeriodsCoupon`, `SubPeriodsLeg`, + `SubPeriodsPricer`, `CompoundingRatePricer` and `AveragingRatePricer`; `DatedOISRateHelper`; + the history functions of `IndexManager` (`setHistory`, `getHistory`, `hasHistory`, + `clearHistory`, `hasHistoricalFixing`); `AnalyticHestonEngine_Integration` (now + `FourierIntegration`); `InflationTermStructure.observationLag` and `hasExplicitBaseDate`; the + YoY inflation index and curve constructors taking an `interpolated` flag; + `DiscountingSwapEngine(curve, settlementDate, npvDate)` (pass `includeSettlementDateFlows` + first, or use keywords). The constructor of `GlobalLinearSimpleZeroCurve` no longer takes + keyword arguments. +- Builds QuantLib 1.44 with two temporary fixes, kept in `patches/quantlib/`, applied at build + time to a copy of the QuantLib source and dropped with QuantLib 1.45: + - QuantLib compiles with automatic-differentiation number types, a fix from QuantLib's main + branch ([QuantLib #2934](https://github.com/lballabio/QuantLib/pull/2934)) + - an implied rate keeps its derivative when the compound factor is 1, so for example the + dividend rho at a zero dividend yield is no longer lost, as proposed in + [QuantLib #3057](https://github.com/lballabio/QuantLib/pull/3057); with a compound factor + of 1 and a positive time, `SimpleThenCompounded` and `CompoundedThenSimple` now throw, as + they do on QuantLib's main branch +- Builds against the shared libxad of the installed xad 2.2 package (`xad>=2.2,<2.3`), which is + not bundled; tapes are shared with xad through libxad, thread-local per Python thread +- QuantLib and the module are built with no FMA contraction (`-ffp-contract=off` with GCC and + Clang), so AD values match a plain-double build with the same setting - One-step build with scikit-build-core; wheels for Python 3.10-3.14 on Linux, macOS and Windows - `tools/sync-swig.sh` syncs `SWIG/` with a QuantLib-SWIG release, re-applying the XAD changes in `SWIG/patches/` @@ -36,7 +59,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 Initial release, matching QuantLib Python bindings version 1.33 -[1.41.0]: https://github.com/auto-differentiation/QuantLibAAD-Py/compare/v1.33.3...v1.41.0 +[1.44.0]: https://github.com/auto-differentiation/QuantLibAAD-Py/compare/v1.33.3...v1.44.0 [1.33.3]: https://github.com/auto-differentiation/QuantLib-Risks-Py/compare/v1.33.2...v1.33.3 diff --git a/CMakeLists.txt b/CMakeLists.txt index aa570356..50bf6d3e 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -24,10 +24,10 @@ ############################################################################## cmake_minimum_required(VERSION 3.18...4.2) -project(QuantLibAAD_Py LANGUAGES CXX VERSION 1.41.0) +project(QuantLibAAD_Py LANGUAGES CXX VERSION 1.44.0) set(QLR_VERSION ${PROJECT_VERSION}) -set(QLR_HEX_VERSION "0x014100f0") +set(QLR_HEX_VERSION "0x014400f0") if(NOT DEFINED CMAKE_CXX_STANDARD) set(CMAKE_CXX_STANDARD 17) @@ -46,6 +46,7 @@ set(QLR_QUANTLIB_PREFIX "" CACHE PATH list(APPEND CMAKE_MODULE_PATH "${PROJECT_SOURCE_DIR}/cmake") +include(QLRBuildDefaults) include(QLRXad) if(QLR_QUANTLIB_PREFIX AND NOT QLR_QUANTLIB_ONLY) diff --git a/ChangeLog.txt b/ChangeLog.txt index 990d2709..4e4fd852 100644 --- a/ChangeLog.txt +++ b/ChangeLog.txt @@ -1,640 +1,1327 @@ -commit 47bc2a352c8dd74ba7cd2813fbf1086de7de5eea +commit 7fca5e0607cbdc815723eb9bf56b500e1fc8fac6 Author: lballabio[bot] <224797326+lballabio-bot@users.noreply.github.com> -Date: Mon, 12 Jan 2026 13:34:28 +0000 +Date: Mon, 12 Oct 2026 08:08:01 +0000 - Set version to 1.41 + Set version to 1.44 Python/setup.py | 2 +- configure.ac | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) -commit 951d93657dbb5d0032bc5de0e773bfa1c377c90b -Merge: 1e9b8754 255681df +commit 2cf6a9709f9fc7d2c3c30fcabdb1d8ed06e9c776 Author: Luigi Ballabio -Date: Thu, 8 Jan 2026 10:57:04 +0100 +Date: Sat, 3 Oct 2026 14:27:52 +0200 - Add missing constructor to a couple of pricers (#810) + Update description of free-threaded wheels -commit 255681df2fe1f3812a9560ad7ec7113538aa1320 + Python/setup.py | 21 ++++++++++++++------- + Python/setup.py.in | 21 ++++++++++++++------- + 2 files changed, 28 insertions(+), 14 deletions(-) + +commit 7e85d8232af6b152eef3d8827571438beea95b5b Author: Luigi Ballabio -Date: Thu, 8 Jan 2026 10:05:11 +0100 +Date: Sat, 3 Oct 2026 13:48:43 +0200 + + Updated News - Add missing constructor to a couple of pricers + News.md | 6 ++++++ + 1 file changed, 6 insertions(+) + +commit 04f124ab561c5b9831c603d8d0433d7b505303a1 +Author: Luigi Ballabio +Date: Sat, 3 Oct 2026 10:31:05 +0200 + + Disable wheels that can't be built by the CI workflow - SWIG won't generate a default constructor if the base class doesn't have one. + On i686 manylinux, building the pypy and no-gil wheels causes + the compilation to exceed the virtual memory limit for the + allocated runner machine. + + .github/workflows/wheels.yml | 5 +---- + 1 file changed, 1 insertion(+), 4 deletions(-) - SWIG/cashflows.i | 10 ++++++++-- - 1 file changed, 8 insertions(+), 2 deletions(-) +commit 51faac51fe1107dea12d35c35c3218f1ce0ddcdf +Author: Luigi Ballabio +Date: Fri, 2 Oct 2026 22:29:46 +0200 -commit 1e9b8754fd040f1691a0f3738dbf986b81c26e07 + Avoid obsoleted macos-14 in CI builds + + .github/workflows/nupkg.yml | 2 +- + .github/workflows/wheels.yml | 4 ++-- + 2 files changed, 3 insertions(+), 3 deletions(-) + +commit d19e495848777261bd5ccfc34ff0379c8df14d6b Author: lballabio[bot] <224797326+lballabio-bot@users.noreply.github.com> -Date: Mon, 5 Jan 2026 19:41:32 +0000 +Date: Fri, 2 Oct 2026 15:38:47 +0000 Update changelog - ChangeLog.txt | 978 +++++++++++++++++++++++----------------------------------- - 1 file changed, 392 insertions(+), 586 deletions(-) + ChangeLog.txt | 1407 +++++++++++++++++++++++++++++++++++++++++---------------- + 1 file changed, 1022 insertions(+), 385 deletions(-) -commit f820895a2c7d4e962df77c232116bd09e1cee5d0 +commit f9b869f72b638388910828fb8c34a6f20902932e Author: lballabio[bot] <224797326+lballabio-bot@users.noreply.github.com> -Date: Mon, 5 Jan 2026 19:41:32 +0000 +Date: Fri, 2 Oct 2026 15:38:47 +0000 - Set version to 1.41-rc + Set version to 1.44-rc Python/setup.py | 2 +- SWIG/ql.i | 4 ++-- configure.ac | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) -commit 99086d5271d307aa2b58ad677d3aa31d337a0c5b +commit 8302346390c82be8146f3b3ea7c67d274bf1b92f Author: Luigi Ballabio -Date: Mon, 5 Jan 2026 12:37:53 +0100 +Date: Fri, 2 Oct 2026 17:12:51 +0200 - Update news for 1.41 + Fix apt-install step for new base image - News.md | 23 ++++++++++++++--------- - 1 file changed, 14 insertions(+), 9 deletions(-) + dockerfiles/ci.csharp.Dockerfile | 2 +- + 1 file changed, 1 insertion(+), 1 deletion(-) -commit 9adba846e2d00c15239508cbb30197af3fee9f72 -Merge: 0e5f50cd eef65314 +commit 68ea84e474af350b2d3d72f679b24f6e68948be9 Author: Luigi Ballabio -Date: Mon, 5 Jan 2026 10:10:51 +0100 +Date: Fri, 2 Oct 2026 12:35:58 +0200 + + Update news - Fix typo in method mapping for `MakeSwaption` (#808) + News.md | 6 +++--- + 1 file changed, 3 insertions(+), 3 deletions(-) -commit eef65314846d8068173a6d920dd8753a5324081a +commit ffb683eaf0b05151baa98681ecfedc415f265fcf Author: Luigi Ballabio -Date: Mon, 5 Jan 2026 09:19:16 +0100 +Date: Thu, 1 Oct 2026 09:48:35 +0200 - Fix typo in method mapping + Re-enable keyword arguments in some exported methods - SWIG/swaption.i | 2 +- - 1 file changed, 1 insertion(+), 1 deletion(-) + SWIG/barrieroptions.i | 3 +++ + SWIG/fdm.i | 9 +++++++++ + SWIG/lmm.i | 7 +++++++ + SWIG/swap.i | 3 +++ + 4 files changed, 22 insertions(+) -commit 0e5f50cdff3bb0bceeb421a36daa483cafe4766e +commit e9b4909cee9e6903115b5693506b8e89ef3eb246 +Merge: 4bb179eb fce5dca9 Author: Luigi Ballabio -Date: Mon, 29 Dec 2025 16:44:51 +0100 +Date: Tue, 29 Sep 2026 15:40:06 +0200 - Restore kwargs for ultimate term-structure constructor + Export tanh-sinh Fourier integration (#935) - SWIG/termstructures.i | 3 +++ - 1 file changed, 3 insertions(+) +commit fce5dca9826a30f44e3f4603c21facf938398b6b +Author: Luigi Ballabio +Date: Tue, 29 Sep 2026 11:55:26 +0200 + + Export tanh-sinh Fourier integration -commit 85b621bc9296f62df7982ae33df2281c573c5be0 -Merge: b66e8eca 13f727a4 + SWIG/options.i | 11 ++++++----- + 1 file changed, 6 insertions(+), 5 deletions(-) + +commit 4bb179ebfd416f6e59d4f5f9359c8826f52514df +Merge: 4dbeea1f f4ed40fd Author: Luigi Ballabio -Date: Wed, 24 Dec 2025 12:19:04 +0100 +Date: Tue, 29 Sep 2026 14:17:34 +0200 - Export new features of overnight-indexed coupons (#806) + Export the underlying instrument from a few rate helpers (#934) -commit 13f727a477996308938bc77f4db7f0c0cd7df11d +commit 4dbeea1fe7497153b64920006aa4b2092f429e3e Author: Luigi Ballabio -Date: Wed, 24 Dec 2025 11:35:15 +0100 +Date: Tue, 29 Sep 2026 12:18:07 +0200 - Export new features of overnight-indexed coupons + Use .Net 10 in CI build - SWIG/cashflows.i | 133 ++++++++++++++++++++++++++++++++++++++++++++----------- - 1 file changed, 107 insertions(+), 26 deletions(-) + CSharp/Makefile.am | 20 ++++++++++---------- + CSharp/csharp/NQuantLib.csproj | 2 +- + .../BermudanSwaption/BermudanSwaption.csproj | 2 +- + CSharp/examples/EquityOption/EquityOption.csproj | 2 +- + .../FiniteDifferenceMethods.csproj | 2 +- + CSharp/examples/Times/Times.csproj | 2 +- + dockerfiles/ci.csharp.Dockerfile | 2 +- + 7 files changed, 16 insertions(+), 16 deletions(-) -commit b66e8ecaeca075ebb96c03f33f0b52593ba0bd56 -Merge: 2afb7e19 f335ac11 +commit f4ed40fd8ac5df8c611a37128c97b830ba8c4124 Author: Luigi Ballabio -Date: Tue, 23 Dec 2025 20:08:05 +0100 +Date: Tue, 29 Sep 2026 12:08:21 +0200 - Export new constructor for Asian option and engine (#805) + Export the underlying instrument from a few rate helpers -commit f335ac11662a86f9cda939b7163f88ae995a5edb + SWIG/ratehelpers.i | 5 +++++ + SWIG/swap.i | 32 ++++++++++++++++++++++++++++++++ + 2 files changed, 37 insertions(+) + +commit e17b1c2f484edf90fcc3a4b5e92e48b682ce0316 +Merge: 85209c47 82823c39 Author: Luigi Ballabio -Date: Tue, 23 Dec 2025 18:37:40 +0100 +Date: Mon, 28 Sep 2026 10:14:28 +0200 - Export new constructor for Asian option and engine + Move as_swaption_helper declaration after SwaptionHelper (#931) - SWIG/asianoptions.i | 7 +++++++ - 1 file changed, 7 insertions(+) +commit 85209c47003ae05432e36a61d5f9f73f2c91d880 +Author: lballabio[bot] <224797326+lballabio-bot@users.noreply.github.com> +Date: Mon, 28 Sep 2026 08:01:22 +0000 -commit 2afb7e19bfc453f81f63ed1f271d09becccf459e -Merge: 7b6a8dbb ae9dba82 -Author: Luigi Ballabio -Date: Tue, 23 Dec 2025 18:10:55 +0100 + Update copyright list in license - Export const-notional cross-currency swap helper (#804) + LICENSE.TXT | 1 + + 1 file changed, 1 insertion(+) -commit ae9dba82b1e007032bbd937c90d7409d766a2ae2 +commit ea7555e49aa0acf6e0c97d2c9357e2c827124b52 +Merge: 85e80e71 eb8f9eb6 Author: Luigi Ballabio -Date: Tue, 23 Dec 2025 16:53:37 +0100 - - Export const-notional cross-currency swap helper +Date: Mon, 28 Sep 2026 09:25:19 +0200 - SWIG/ratehelpers.i | 26 ++++++++++++++++++++++---- - 1 file changed, 22 insertions(+), 4 deletions(-) + Export VarianceSwap and its pricing engines (#928) -commit 7b6a8dbb71ed1d42e32a8ba3ba0bfd201694f1ac -Merge: 965f8b46 35925cd7 +commit 85e80e715f6954311da7a322db54bb1d17c50015 +Merge: a1af6021 a2d7ccb6 Author: Luigi Ballabio -Date: Tue, 23 Dec 2025 15:08:02 +0100 +Date: Mon, 28 Sep 2026 09:12:51 +0200 - Export `zeroRate` and `yoyRate` methods without lag (#803) + Expose fixedDayCount in OISRateHelper (#929) -commit 965f8b460a025fb7952122d2bf3a8eb8653d9de3 +commit a1af60213acfa5cd18da5c9be4b994c34fabda5a +Merge: 61c618a0 cc15064b Author: Luigi Ballabio -Date: Tue, 23 Dec 2025 12:37:28 +0100 +Date: Mon, 28 Sep 2026 08:40:50 +0200 + + Update basisPointValue test after convexity fix upstream (#932) + +commit cc15064b5cef639263e2b08687cdd6082ffc4df3 +Author: kp9991 +Date: Sun, 27 Sep 2026 12:50:43 +1000 + + Update basisPointValue test after convexity fix upstream + + Python/test/test_bondfunctions.py | 4 ++-- + 1 file changed, 2 insertions(+), 2 deletions(-) + +commit d31209830e5666ba8c6b80e2242ff430b9c5edaf +Author: kp9991 +Date: Sat, 26 Sep 2026 21:35:33 +1000 + + Move as_swaption_helper declaration after SwaptionHelper + + SWIG/calibrationhelpers.i | 9 ++++++--- + 1 file changed, 6 insertions(+), 3 deletions(-) + +commit ff7712c5ee36fe467f6f83ec7ec12839cfd0c243 +Author: Mahimn Patel +Date: Fri, 25 Sep 2026 12:32:40 -0400 + + Enable kwargs for ReplicatingVarianceSwapEngine + + SWIG/varianceswaps.i | 3 +++ + 1 file changed, 3 insertions(+) + +commit cf8239bd20f5ff73262ba3e93924c76520c0e9bd +Author: Eugene Toder +Date: Fri, 25 Sep 2026 10:18:11 -0400 + + Expose fixedDayCount in OISRateHelper + + SWIG/ratehelpers.i | 20 +++++++++++++------- + 1 file changed, 13 insertions(+), 7 deletions(-) + +commit d7bdab7089517691a624c5ab858758554618d6cd +Author: Mahimn Patel +Date: Thu, 24 Sep 2026 20:26:04 -0400 - No need for %extend + Export VarianceSwap and its pricing engines - SWIG/cashflows.i | 83 ++++++++++++++++++++++++-------------------------------- - 1 file changed, 36 insertions(+), 47 deletions(-) + Python/test/test_varianceswap.py | 88 ++++++++++++++++++++++++++++ + SWIG/ql.i | 1 + + SWIG/varianceswaps.i | 123 +++++++++++++++++++++++++++++++++++++++ + 3 files changed, 212 insertions(+) -commit 35925cd7bc1e050ca8ba45a87e2a380f94062c9c +commit 61c618a0cdffc24d83e7cff91cb8a5bf0fc1ff7f Author: Luigi Ballabio -Date: Tue, 23 Dec 2025 12:56:41 +0100 +Date: Wed, 23 Sep 2026 12:14:39 +0200 - Export zeroRate and yoyRate methods without lag + Remove obsolete workaround - SWIG/inflation.i | 8 ++++++-- - 1 file changed, 6 insertions(+), 2 deletions(-) + Scala/examples/CPIBond.scala | 2 -- + 1 file changed, 2 deletions(-) -commit 73402fff013652d83efd7eb30b04678964f54cbe -Merge: d795a2b0 e12869eb +commit 1c55b1d38649c4e7095bccf2db26587fee1dfe9b +Merge: 59f49754 ef095911 Author: Luigi Ballabio -Date: Tue, 23 Dec 2025 11:40:11 +0100 +Date: Wed, 23 Sep 2026 09:01:10 +0200 - Add `setInterpolation` method in `BlackVarianceCurve` (#802) + Funding helper (#916) -commit e12869eb5c21c0263ca92153d7b188ffbb241716 -Author: paolodelia99 -Date: Tue, 23 Dec 2025 10:29:06 +0100 +commit ef095911e4f2372687244112c8efbd636603da6a +Author: kp9991 +Date: Wed, 23 Sep 2026 07:10:43 +1000 + + Using forDates - Fix error for missing interpolators + SWIG/ratehelpers.i | 85 ++++++++++++++++++++++++++++++++++-------------------- + 1 file changed, 53 insertions(+), 32 deletions(-) - SWIG/volatilities.i | 4 ---- - 1 file changed, 4 deletions(-) +commit 59f49754e178ae89eb2e92414580e26a995b1e81 +Merge: dad91ad0 a9126f72 +Author: Luigi Ballabio +Date: Tue, 22 Sep 2026 14:50:01 +0200 -commit ed53ea0206131eb59e4b93478176a730fa025f4b -Author: paolodelia99 -Date: Tue, 23 Dec 2025 10:21:18 +0100 + Export new Philippines calendar (#927) - wrap new overidden methon in extend +commit a9126f72b5c58002ed70e20836b661f215b7d132 +Author: Luigi Ballabio +Date: Tue, 22 Sep 2026 13:08:07 +0200 - SWIG/volatilities.i | 29 +++++++++++++++-------------- - 1 file changed, 15 insertions(+), 14 deletions(-) + Export new Philippines calendar -commit e00cbf4860a9750df7ac0f999195e4a62f5756f8 -Author: paolodelia99 -Date: Tue, 23 Dec 2025 10:16:33 +0100 + SWIG/calendars.i | 6 ++++++ + 1 file changed, 6 insertions(+) - Add set interpolation method in BlackVarianceCurve class +commit dad91ad0950e0e7a797b1dcd52d22a3395b3c311 +Merge: dc84dd9f 1fd23fc7 +Author: Luigi Ballabio +Date: Mon, 21 Sep 2026 11:30:02 +0200 - SWIG/volatilities.i | 15 +++++++++++++++ - 1 file changed, 15 insertions(+) + Compile the C++ wrappers as UTF-8 instead of warning C4819 (#925) -commit d795a2b011fe6cdd5cdc729d358c449c0159f889 -Author: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> -Date: Mon, 15 Dec 2025 18:02:20 +0000 +commit 1fd23fc793c224a85377dd1b71782ab5680446de +Author: RalfKonrad +Date: Fri, 18 Sep 2026 19:09:59 +0200 - Bump peter-evans/create-pull-request from 7 to 8 + Apply the /utf-8 flag to setup.py.in as well - Bumps [peter-evans/create-pull-request](https://github.com/peter-evans/create-pull-request) from 7 to 8. - - [Release notes](https://github.com/peter-evans/create-pull-request/releases) - - [Commits](https://github.com/peter-evans/create-pull-request/compare/v7...v8) + Python/setup.py is generated from Python/setup.py.in (configure.ac lists it + in AC_CONFIG_FILES), so editing only the generated copy had no effect: the + release workflows run ./configure before make dist, which regenerated it and + dropped the flag from the tarball. Verified on a fork dispatch of the wheels + workflow, where quantlib_wrap.cpp compiled with - --- - updated-dependencies: - - dependency-name: peter-evans/create-pull-request - dependency-version: '8' - dependency-type: direct:production - update-type: version-update:semver-major - ... + /GR /FD /Zm250 /EHsc /bigobj /std:c++17 /MT - Signed-off-by: dependabot[bot] + and no /utf-8. Both files are tracked and now differ only by + @PACKAGE_VERSION@, as before. + + Co-Authored-By: Claude Opus 5 (1M context) + + Python/setup.py.in | 13 ++++++++++++- + 1 file changed, 12 insertions(+), 1 deletion(-) + +commit d3b9a3a8ca86052632adb90af4688f8050ebfdbc +Author: RalfKonrad +Date: Fri, 18 Sep 2026 17:26:46 +0200 + + Compile the C++ wrappers as UTF-8 instead of warning C4819 + + The QuantLib headers are UTF-8 without a BOM (author names in the licence + headers, mathematical symbols in the documentation), so MSVC warns C4819 + for every translation unit that includes them unless the system code page + happens to be UTF-8. QuantLib itself now passes /utf-8 rather than + suppressing the warning (lballabio/QuantLib#2803); do the same in the two + wrapper builds that use MSVC, the C# property sheet and Python's setup.py. + + The toolsets below v141 go away with it. The wrapper is built as C++17 + (LanguageStandard in cpp/QuantLibWrapper.vcxproj), which VS2015 and earlier + cannot do, so those conditions were already dead; /utf-8 needs VS2015 + Update 2 in any case. v145 is added to match QuantLib's own QuantLib.props. + + Co-Authored-By: Claude Opus 5 (1M context) - .github/workflows/copyrights.yml | 2 +- - .github/workflows/increase-version-number.yml | 2 +- - .github/workflows/license-url.yml | 2 +- - .github/workflows/misspell.yml | 2 +- - .github/workflows/namespaces.yml | 2 +- - .github/workflows/prepare-release-candidate.yml | 2 +- - .github/workflows/prepare-release.yml | 2 +- - 7 files changed, 7 insertions(+), 7 deletions(-) + CSharp/QuantLib.props | 34 +++++++++++++++++++++------------- + Python/setup.py | 13 ++++++++++++- + 2 files changed, 33 insertions(+), 14 deletions(-) -commit d41d093301825d1ab17c2b71fc75b654473e0b98 -Merge: 9b8a5500 784b3b11 +commit dc84dd9fe316f03223241107cfb98fd43cef8973 +Merge: c0f970b3 fadd3445 Author: Luigi Ballabio -Date: Tue, 9 Dec 2025 16:08:13 +0100 +Date: Thu, 17 Sep 2026 09:01:45 +0200 + + Swaption getters (#924) + +commit fadd3445c1246c48098b0be8391c0effe8265647 +Author: kp9991 +Date: Thu, 17 Sep 2026 08:30:07 +1000 + + Swaption getters - Re-enable Python 3.8 and 3.9 (#800) + SWIG/swaption.i | 16 ++++++++++++++++ + 1 file changed, 16 insertions(+) -commit 784b3b11d79c567585950d68a91ff1ecb7339dd9 +commit c0f970b38c84462b7eab8b90355e9decf8c504cd +Merge: 7fda2092 c25a6555 Author: Luigi Ballabio -Date: Tue, 9 Dec 2025 13:17:27 +0100 +Date: Fri, 11 Sep 2026 09:31:07 +0200 - Re-enable Python 3.8 and 3.9. - - SWIG 4.4.1 restored support for them. + Stub helpers (#913) + +commit 7fda209232dad97c9596a0a8cedcfcccb8e063dd +Merge: 438fb1af f9c2af83 +Author: Luigi Ballabio +Date: Fri, 11 Sep 2026 09:08:30 +0200 + + Update a few tests after changes in underlying C++ library (#923) + +commit f9c2af83eb4cfeae60741fae54fb6f54188fea33 +Author: Luigi Ballabio +Date: Thu, 10 Sep 2026 18:42:36 +0200 + + Update tests after changes in underlying C++ library + + Python/examples/global-bootstrap.py | 8 ++------ + Python/test/test_bondfunctions.py | 4 ++-- + R/demo/global-bootstrap.R | 4 ---- + 3 files changed, 4 insertions(+), 12 deletions(-) + +commit de5c0823cfb536de387a01526e91c8951693b4f7 +Author: kp9991 +Date: Thu, 10 Sep 2026 09:36:22 +1000 + + Copyright + retrigger GitHub actions + + SWIG/cashflows.i | 1 + + 1 file changed, 1 insertion(+) + +commit a770d144f8de607b74c2c137719d19c2dd213a26 +Author: kp9991 +Date: Mon, 7 Sep 2026 21:21:17 +1000 + + Updating following StubHelpers going into master in QuantLib + + SWIG/cashflows.i | 14 ++++++-------- + 1 file changed, 6 insertions(+), 8 deletions(-) + +commit 438fb1af3640d75355f5f1b9e8beb1b301e1c062 +Author: Luigi Ballabio +Date: Mon, 7 Sep 2026 13:13:23 +0200 + + Fix method corresponding to keyword in MakeCreditDefaultSwap + + SWIG/creditdefaultswap.i | 2 +- + 1 file changed, 1 insertion(+), 1 deletion(-) + +commit cf1f4b1b5afa4d554e7a1ee78b50ca5f3f368232 +Merge: 79dda86a 60c76660 +Author: Luigi Ballabio +Date: Mon, 7 Sep 2026 12:27:13 +0200 + + Export interpolated curves (#921) - Python/setup.py | 4 ++-- - Python/setup.py.in | 4 ++-- - 2 files changed, 4 insertions(+), 4 deletions(-) +commit b3d822522e0986c499529970af43d1cd5593498d +Author: kp9991 +Date: Sat, 5 Sep 2026 11:21:30 +1000 + + StubIndexConfig -> StubIndexSelection to be consistent with QuantLib refact + + SWIG/cashflows.i | 20 ++++++++++---------- + SWIG/ratehelpers.i | 16 ++++++++-------- + SWIG/swap.i | 18 +++++++++--------- + 3 files changed, 27 insertions(+), 27 deletions(-) + +commit 60c7666047b85a6f87df133c579492953b262d6f +Author: kp9991 +Date: Sat, 5 Sep 2026 10:05:15 +1000 + + Export interpolated curves + + SWIG/discountcurve.i | 7 +++++++ + SWIG/forwardcurve.i | 7 +++++++ + SWIG/zerocurve.i | 16 ++++++++++++++++ + 3 files changed, 30 insertions(+) -commit 4cc52a73c6bc9743c13293d67cd1e70fd1179218 +commit 79dda86adc949a62eec41ff479c45eb4e96f3623 +Merge: 92504411 1f44275f Author: Luigi Ballabio -Date: Tue, 9 Dec 2025 13:58:34 +0100 +Date: Fri, 4 Sep 2026 20:07:14 +0200 - Upgrade to SWIG 4.4.1 + Export recently added parameters for `OptionletStripper1` and `OvernightIborBasisSwapRateHelper` (#920) +commit 92504411046982162d83afee9aabd1e5d454631d +Author: Luigi Ballabio +Date: Fri, 4 Sep 2026 18:13:11 +0200 + + Upgrade to SWIG 4.5.1 + + .github/workflows/nupkg.yml | 2 +- .github/workflows/publish-release.yml | 2 +- + .github/workflows/wheels.yml | 2 +- dockerfiles/ci.base.Dockerfile | 2 +- - 2 files changed, 2 insertions(+), 2 deletions(-) + 4 files changed, 4 insertions(+), 4 deletions(-) -commit 9b8a5500bbd62d75fc83946d48fa5bb0f641945b -Merge: 1eef94d6 17908e4d +commit 1f44275f36d10aad863ec7b1576b4d7866a35479 Author: Luigi Ballabio -Date: Mon, 1 Dec 2025 10:33:17 +0100 +Date: Fri, 4 Sep 2026 18:05:22 +0200 + + Export recently added parameters for OptionletStripper1 and OvernightIborBasisSwapRateHelper + + SWIG/old_volatility.i | 5 +++-- + SWIG/ratehelpers.i | 5 ++++- + 2 files changed, 7 insertions(+), 3 deletions(-) + +commit a417adbd28040e32380093a73dbac7acf76f9a2b +Merge: 188bb49f cef8b854 +Author: Luigi Ballabio +Date: Tue, 1 Sep 2026 20:51:40 +0200 + + Export rough Heston model and engine (#919) + +commit cef8b8544ce91e525ba858c0971154965d3c208f +Author: Luigi Ballabio +Date: Tue, 1 Sep 2026 13:16:58 +0200 + + Export rough Heston model and engine + + Java/examples/EquityOptions.java | 4 +- + SWIG/options.i | 115 ++++++++++++++++++++++++++------------- + 2 files changed, 80 insertions(+), 39 deletions(-) + +commit 188bb49ff15fa7041a5c41f45ce862d65627948d +Author: Luigi Ballabio +Date: Tue, 1 Sep 2026 18:11:07 +0200 + + Sync enum with C++ + + Python/test/test_mtmcrosscurrencybasisswap.py | 4 ++-- + SWIG/swap.i | 2 +- + 2 files changed, 3 insertions(+), 3 deletions(-) + +commit 156515379c4a699a1ae663eabad49cba24d0c5b9 +Merge: 2c7779ab c60ae56c +Author: Luigi Ballabio +Date: Mon, 31 Aug 2026 12:15:12 +0200 + + Fixing missing or incorrect SWIG exports (#918) + +commit c60ae56c89e7c12c2bf059d667e47e7ee454dda2 +Author: kp9991 +Date: Mon, 31 Aug 2026 05:17:40 +1000 + + Fixing R errors + + SWIG/volatilities.i | 27 +++++++++++++++++---------- + 1 file changed, 17 insertions(+), 10 deletions(-) + +commit 4e1a673bf7282fa7d7181e7442991e1a6f608fcc +Author: kp9991 +Date: Mon, 31 Aug 2026 04:41:19 +1000 + + Fixing missing or incorrect SWIG exports + + SWIG/cashflows.i | 36 ++++++++++++++++++++++++++++++++---- + SWIG/indexes.i | 7 +++++++ + SWIG/swaption.i | 4 ++-- + SWIG/termstructures.i | 2 ++ + SWIG/volatilities.i | 29 +++++++++++++++++++++++++++++ + 5 files changed, 72 insertions(+), 6 deletions(-) + +commit 2c7779abef61a30fcdb7987f19e232b4bc285fc3 +Merge: 488e027c 6a67400c +Author: Luigi Ballabio +Date: Thu, 27 Aug 2026 11:53:29 +0200 + + Exposing GlobalBootstrapInitialGuessFn (#915) + +commit fee7a07cd756ead5467af6a3d30fb5b7242f46e9 +Author: kp9991 +Date: Thu, 27 Aug 2026 11:49:41 +1000 + + Remove unsupported funding-helper keyword arguments + + SWIG/ratehelpers.i | 3 --- + 1 file changed, 3 deletions(-) + +commit 6b354316c9c096a155c6f2c5d060d22a6e7f4a24 +Author: kp9991 +Date: Thu, 27 Aug 2026 11:39:57 +1000 + + OvernightIndexedFundingRateHelper + + SWIG/ratehelpers.i | 70 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ + 1 file changed, 70 insertions(+) + +commit 6a67400cbdc38b7e858d0f02375af3b770f69908 +Author: kp9991 +Date: Sat, 22 Aug 2026 18:52:41 +1000 + + Exposing GlobalBootstrapInitialGuessFn, simple zero rate interp + + SWIG/piecewiseyieldcurve.i | 44 ++++++++++++++++++++++++++++++++------------ + SWIG/zerocurve.i | 25 +++++++++++++++++++++++++ + 2 files changed, 57 insertions(+), 12 deletions(-) + +commit 619dbacdf017951aa691162aa761c9c9a8b8e267 +Author: kp9991 +Date: Sat, 22 Aug 2026 10:09:29 +1000 + + Update with basisOnIborLeg + + SWIG/cashflows.i | 1 - + SWIG/ratehelpers.i | 1 + + 2 files changed, 1 insertion(+), 1 deletion(-) + +commit 488e027c677eb53b11d363af3fae8b213df36bdf +Merge: 42b6a741 75ad1ec2 +Author: Luigi Ballabio +Date: Wed, 19 Aug 2026 12:34:03 +0200 + + Enable keyword arguments for a few rate helper classes (#914) + +commit 75ad1ec27226dad64c872a62628610043eb4db40 +Author: Luigi Ballabio +Date: Wed, 19 Aug 2026 11:45:24 +0200 + + Enable keyword arguments for a few rate helper classes + + SWIG/ratehelpers.i | 25 +++++++++++++++++++++++++ + 1 file changed, 25 insertions(+) + +commit 42b6a7410854dc75c082f90d2c564d49c5325cbe +Merge: 174e1408 17ad3f91 +Author: Luigi Ballabio +Date: Wed, 19 Aug 2026 11:27:07 +0200 + + Export `OvernightOvernightBasisSwapRateHelper` class (#912) + +commit 7264a9701a4ca18b734f95caf2fb00df59b827ad +Author: kp9991 +Date: Tue, 18 Aug 2026 09:33:57 +1000 + + Typo fix + + SWIG/ratehelpers.i | 4 ++-- + 1 file changed, 2 insertions(+), 2 deletions(-) + +commit 174e1408d5d2281cb4dc2f2653663b2a20c0aeed +Merge: 5319e6d6 135d301a +Author: Luigi Ballabio +Date: Mon, 17 Aug 2026 11:09:06 +0200 + + Export new arguments for basis-swap rate helpers (#898) + +commit 5319e6d6501b2a476eb22590d768eb4ca059c0ee +Merge: 276cc237 2e53d02a +Author: Luigi Ballabio +Date: Mon, 17 Aug 2026 11:07:31 +0200 + + ConstantNotional xccy helper swap (#911) + +commit a9a56d35bf440faad9519d91b86e23270ed37369 +Author: kp9991 +Date: Sun, 16 Aug 2026 10:53:31 +1000 + + Extending to all floating legs + + SWIG/ratehelpers.i | 16 ++++++++++++---- + SWIG/swap.i | 11 ++++++++--- + 2 files changed, 20 insertions(+), 7 deletions(-) + +commit 52602997f886ba5925457c9d0693bd47a70932ea +Author: kp9991 +Date: Sun, 16 Aug 2026 08:57:16 +1000 + + StubIborCoupon + + SWIG/cashflows.i | 49 +++++++++++++++++++++++++++++++++++++++++++++++-- + SWIG/indexes.i | 3 +++ + SWIG/ratehelpers.i | 13 ++++++++++--- + SWIG/swap.i | 9 +++++++-- + 4 files changed, 67 insertions(+), 7 deletions(-) + +commit 135d301aec08430ee1edd5499a1470d447aa6281 +Author: kp9991 +Date: Fri, 14 Aug 2026 20:27:25 +1000 + + Copyright + + SWIG/ratehelpers.i | 1 + + 1 file changed, 1 insertion(+) + +commit 2e53d02aaf812528915a80bf1d0cb3fa1d75be26 +Author: kp9991 +Date: Fri, 14 Aug 2026 20:26:31 +1000 + + Trigger GitHub actions + + SWIG/swap.i | 2 +- + 1 file changed, 1 insertion(+), 1 deletion(-) + +commit 40b1625fc979abf5d5bc0e0b231d9d96818ad406 +Author: kp9991 +Date: Thu, 13 Aug 2026 10:47:48 +1000 + + paymentLagOnNotionalExchanges - added semi-analytic European engine for a single asset with cash (#799) + SWIG/common.i | 1 + + SWIG/ratehelpers.i | 3 ++- + SWIG/swap.i | 3 ++- + 3 files changed, 5 insertions(+), 2 deletions(-) -commit 17908e4df0143b4432111c70bef185bd56d81c72 -Author: klaus spanderen -Date: Sun, 30 Nov 2025 22:00:44 +0100 +commit 1fd0b000cecd3ba0a87b12e86e6921e38b235cba +Author: kp9991 +Date: Wed, 12 Aug 2026 20:21:51 +1000 + + Adding floatPaymentFrequency - fixed potential leap year issue. + SWIG/ratehelpers.i | 3 ++- + 1 file changed, 2 insertions(+), 1 deletion(-) + +commit 2ced317dd25c549a516b894d8015e7578dc4e8bc +Author: kp9991 +Date: Tue, 11 Aug 2026 21:49:44 +1000 - Python/test/test_options.py | 14 +++++++------- - 1 file changed, 7 insertions(+), 7 deletions(-) + Remove iborPaymentFrequency -commit 1eef94d6a83f54cd4f53f04282b5530328086ca3 + SWIG/ratehelpers.i | 1 - + 1 file changed, 1 deletion(-) + +commit 276cc237a6e039fdeefad2bf550273416dbb21e3 Author: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> -Date: Mon, 24 Nov 2025 18:36:44 +0000 +Date: Mon, 10 Aug 2026 18:32:42 +0000 - Bump actions/checkout from 5 to 6 + Bump pypa/cibuildwheel from 4.1 to 4.2 - Bumps [actions/checkout](https://github.com/actions/checkout) from 5 to 6. - - [Release notes](https://github.com/actions/checkout/releases) - - [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md) - - [Commits](https://github.com/actions/checkout/compare/v5...v6) + Bumps [pypa/cibuildwheel](https://github.com/pypa/cibuildwheel) from 4.1 to 4.2. + - [Release notes](https://github.com/pypa/cibuildwheel/releases) + - [Changelog](https://github.com/pypa/cibuildwheel/blob/main/docs/changelog.md) + - [Commits](https://github.com/pypa/cibuildwheel/compare/v4.1...v4.2) --- updated-dependencies: - - dependency-name: actions/checkout - dependency-version: '6' + - dependency-name: pypa/cibuildwheel + dependency-version: '4.2' dependency-type: direct:production - update-type: version-update:semver-major + update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] - .github/workflows/copyrights.yml | 2 +- - .github/workflows/devenv-images.yml | 10 +++++----- - .github/workflows/increase-version-number.yml | 2 +- - .github/workflows/license-url.yml | 2 +- - .github/workflows/linux.yml | 2 +- - .github/workflows/misspell.yml | 2 +- - .github/workflows/namespaces.yml | 2 +- - .github/workflows/prepare-release-candidate.yml | 2 +- - .github/workflows/prepare-release.yml | 2 +- - .github/workflows/publish-release.yml | 2 +- - 10 files changed, 14 insertions(+), 14 deletions(-) + .github/workflows/wheels.yml | 12 ++++++------ + 1 file changed, 6 insertions(+), 6 deletions(-) -commit 2021282adf61d5a5f05d9d57c1ede2d9ad290704 -Merge: 847c5783 2481867d +commit 4f98e4b6f14d1f98ee6e0c403d89719bae213608 +Merge: 8e89475b 7d424226 Author: Luigi Ballabio -Date: Fri, 21 Nov 2025 10:33:54 +0100 +Date: Mon, 10 Aug 2026 13:10:49 +0200 - Expose data on piecewise inflation curves (#797) + Expose ObservableSettings (#909) -commit 2481867d84bbd6e7dd5e151496a35513acce5bb4 -Author: Eugene Toder -Date: Thu, 20 Nov 2025 13:41:54 -0500 +commit 8e89475b66b46d91b8d7288697a8a3bb31693b60 +Author: lballabio[bot] <224797326+lballabio-bot@users.noreply.github.com> +Date: Mon, 10 Aug 2026 09:44:16 +0000 - Expose data on piecewise inflation curves + Update copyright list in license - SWIG/inflation.i | 2 ++ - 1 file changed, 2 insertions(+) + LICENSE.TXT | 1 + + 1 file changed, 1 insertion(+) -commit 847c578397a275e994ce0a5149d35e1fbd067f4c -Merge: 0bf01839 69f63c7b +commit c1c4ea078b01eb4b55693ba86929d745b1070648 +Merge: 68a8e43b 5ad98faf Author: Luigi Ballabio -Date: Mon, 17 Nov 2025 09:22:12 +0100 +Date: Mon, 10 Aug 2026 11:28:32 +0200 - Expose argument for enableExtrapolation (#795) + vol type accessor in SwaptionVolatilityStructure (#906) -commit 69f63c7bcb8072fe9ae541930988e3af519a9016 -Author: Eugene Toder -Date: Fri, 14 Nov 2025 14:38:14 -0500 +commit 68a8e43b860c9397a76e9dc04b0594373f696892 +Merge: c58fb0df c94b6c1d +Author: Luigi Ballabio +Date: Mon, 10 Aug 2026 11:27:30 +0200 - Expose argument for enableExtrapolation + Add Malaysia Calendar (#905) - SWIG/termstructures.i | 5 ++--- - 1 file changed, 2 insertions(+), 3 deletions(-) +commit c58fb0df1589f2a970ec7f0e1d1e4d9b9bb02789 +Merge: 64213234 03e480d3 +Author: Luigi Ballabio +Date: Mon, 10 Aug 2026 11:24:45 +0200 -commit 0bf01839659042659273cd9b95ffc358bc7f8f93 + Enable sessions in free-threading wheels (#907) + +commit 03e480d377d0fecceab9caedff41eba0d2628ba9 Author: Luigi Ballabio -Date: Thu, 13 Nov 2025 10:12:08 +0100 +Date: Mon, 10 Aug 2026 09:03:17 +0200 - Fix a few typos + Enable sessions in free-threading wheels - Java/examples/Bonds.java | 4 ++-- - Python/README.md | 2 +- - 2 files changed, 3 insertions(+), 3 deletions(-) + .ci/wheels/before_all_linux_nogil.sh | 2 +- + .ci/wheels/userconfig_nogil.hpp | 2 +- + .github/workflows/wheels.yml | 2 +- + 3 files changed, 3 insertions(+), 3 deletions(-) -commit 37cf3e4ee5c88010d527e5b78dced94ba6027ced -Author: klaus spanderen -Date: Wed, 12 Nov 2025 20:35:35 +0100 +commit 7d42422688b624318f7666c0d9cfddad491ef95f +Author: kp9991 +Date: Sun, 9 Aug 2026 06:14:14 +1000 - added semi-analytic European engine for a single asset with cash - dividends + Expose ObservableSettings and LazyObject deepUpdate - Python/test/test_options.py | 46 +++++++++++++++++++++++++++++++++++++++++++-- - SWIG/options.i | 17 +++++++++++++++++ - 2 files changed, 61 insertions(+), 2 deletions(-) + SWIG/lazyobject.i | 1 + + SWIG/observer.i | 12 ++++++++++++ + 2 files changed, 13 insertions(+) -commit 04d62c5aeb214e2e58c6b6835244bec4359ac9a5 -Merge: a48b4208 23f732c1 -Author: Luigi Ballabio -Date: Tue, 11 Nov 2025 17:22:05 +0100 +commit 5ad98faf3e8eec042b9132abd2c8cf223ad257e7 +Author: kp9991 +Date: Sat, 8 Aug 2026 11:20:52 +1000 - Enable building wheels for free-threading Python; increase minimum supported Python version to 3.10 (#794) + Exposing vol type -commit 23f732c17939aa37f78421feec1d6b7a71978125 + SWIG/volatilities.i | 5 +++-- + 1 file changed, 3 insertions(+), 2 deletions(-) + +commit c94b6c1dee2d13d9df62fa2192de48cef385841a +Author: Lawrenz Law +Date: Sat, 8 Aug 2026 07:45:53 +0800 + + Add Malaysia Calendar + + Python/test/test_calendars.py | 15 +++++++++++++++ + SWIG/calendars.i | 7 +++++++ + 2 files changed, 22 insertions(+) + +commit 64213234a7a2f0cb2144261995e3b2b3073c9ded Author: Luigi Ballabio -Date: Tue, 11 Nov 2025 14:58:21 +0100 +Date: Fri, 7 Aug 2026 14:23:47 +0200 - Upgrade to SWIG 4.4.0 in workflows + Upgrade to SWIG 4.5.0 + .github/workflows/nupkg.yml | 2 +- .github/workflows/publish-release.yml | 2 +- + .github/workflows/wheels.yml | 2 +- dockerfiles/ci.base.Dockerfile | 2 +- - 2 files changed, 2 insertions(+), 2 deletions(-) + 4 files changed, 4 insertions(+), 4 deletions(-) -commit a48b4208777c931f3a13bea2efbf0f8a373fa82f -Merge: 70048a0f 39611e80 -Author: Luigi Ballabio -Date: Tue, 11 Nov 2025 13:52:57 +0100 +commit c49031c2936a27de6315ae70d0e5beff9b8ebc5c +Author: lballabio[bot] <224797326+lballabio-bot@users.noreply.github.com> +Date: Fri, 7 Aug 2026 11:13:40 +0000 + + Fix uses of boost and/or ext namespace - Export a few missing Bond inspectors (#793) + SWIG/swap.i | 2 +- + 1 file changed, 1 insertion(+), 1 deletion(-) -commit 39611e80abf8d58338fee8b682abdeb317248ba6 +commit 7d7fffa4a21aff60f02961705b52cdd4fa3650b5 +Merge: b3abd39d 6661017f Author: Luigi Ballabio -Date: Tue, 11 Nov 2025 12:51:22 +0100 +Date: Fri, 7 Aug 2026 13:13:29 +0200 - Export a few missing Bond inspectors + MtM Cross Currency Basis Swap (#880) - SWIG/bonds.i | 3 +++ - 1 file changed, 3 insertions(+) +commit 6661017fe7acfd823652322cf6ff1780b7530371 +Author: kp9991 +Date: Fri, 7 Aug 2026 19:26:47 +1000 + + Returning copies instead of refs (highlighted by unit test) + + SWIG/cashflows.i | 4 ++-- + 1 file changed, 2 insertions(+), 2 deletions(-) + +commit 62c3a187ab2095cf5e97be880028ea152498bc5f +Author: kp9991 +Date: Fri, 7 Aug 2026 06:25:29 +1000 + + Removing IborOISSpread + + SWIG/ratehelpers.i | 8 ++------ + 1 file changed, 2 insertions(+), 6 deletions(-) -commit 70048a0f8300f21c77c76bf38fb73b48eb8138ed -Merge: 5bc9d5ca c3d9ad29 +commit b3abd39ddfac6c89469b5137f94396a85e7b2ad3 +Author: lballabio[bot] <224797326+lballabio-bot@users.noreply.github.com> +Date: Thu, 6 Aug 2026 14:10:59 +0000 + + Fix uses of boost and/or ext namespace + + SWIG/swap.i | 2 +- + 1 file changed, 1 insertion(+), 1 deletion(-) + +commit d0a5a633d8a054c12b65e69ee0708c2dfc8f9c1e +Merge: f797f33d 17ef8a34 Author: Luigi Ballabio -Date: Wed, 5 Nov 2025 15:26:12 +0100 +Date: Thu, 6 Aug 2026 16:10:48 +0200 - Expose ZeroCouponInflationSwap::fixedLegBPS() (#791) + Make DiscountingSwapEngine arguments match C++ (#896) -commit 5bc9d5ca4197a76d52d0d3b354aca87896628565 -Merge: 0c92ed99 cdbcbe83 +commit f797f33d6b7876bc0f53dd2dd29aca93052a0b73 +Merge: b8f2a489 dd25bd83 Author: Luigi Ballabio -Date: Mon, 3 Nov 2025 09:45:36 +0100 +Date: Thu, 6 Aug 2026 16:09:14 +0200 - Enable rounding in UFR interpolation (#788) + SOFR in GSR (#897) -commit cdbcbe833b41691b739d61be59263b36474ae70b -Author: Marcin Rybacki -Date: Thu, 30 Oct 2025 16:48:47 +0100 +commit ddb100ef0b5dfde5c58a1fcbf34a7f36e7401e06 +Author: kp9991 +Date: Thu, 6 Aug 2026 21:02:49 +1000 - Implemented PR feedback. + Updating unit test (new API) - SWIG/common.i | 21 +++++++++++++++++++++ - SWIG/termstructures.i | 16 ---------------- - 2 files changed, 21 insertions(+), 16 deletions(-) + Python/test/test_mtmcrosscurrencybasisswap.py | 11 +++++------ + 1 file changed, 5 insertions(+), 6 deletions(-) -commit c3d9ad291cbc8704d5ace90f904003337b756466 -Author: Eugene Toder -Date: Tue, 28 Oct 2025 13:45:30 -0400 +commit 1e1b57d2fa6b04828a81f7b87ebe510c047db39f +Author: kp9991 +Date: Thu, 6 Aug 2026 20:54:10 +1000 - Expose ZeroCouponInflationSwap::fixedLegBPS() + Copyright cleanup - SWIG/inflation.i | 1 + - 1 file changed, 1 insertion(+) + Python/test/test_mtmcrosscurrencybasisswap.py | 1 - + 1 file changed, 1 deletion(-) -commit 0c92ed9941075cce0e3eb357d1cf409fb4b40a5c -Merge: d219d0c7 068d66dc +commit b8f2a4893134730d12dcc4504e21728f1ec34eec +Merge: 52ff4251 91dd1805 Author: Luigi Ballabio -Date: Mon, 27 Oct 2025 10:02:45 +0100 +Date: Thu, 6 Aug 2026 10:33:38 +0200 - Adding vanna and volga functions (#790) + Fix for QuantoForwardEuropeanEngine (#902) -commit 068d66dce19173c447c9acc0ba3f26ca6b16d5ac +commit 356dfac53c1f707ec8f87f7df8f582de5217ad50 Author: kp9991 -Date: Sat, 25 Oct 2025 16:05:31 +1100 +Date: Thu, 6 Aug 2026 09:11:11 +1000 - Adding vanna and volga functions + Replace FxResetConvention with fxResetFixingDays and fxResetFixingCalendar to align with QuantLib - SWIG/options.i | 4 ++++ - 1 file changed, 4 insertions(+) + SWIG/ratehelpers.i | 3 ++- + SWIG/swap.i | 6 ++++-- + 2 files changed, 6 insertions(+), 3 deletions(-) -commit 6f1d8684238c812933e05baf1f53529295cd0ee6 -Author: Luigi Ballabio -Date: Tue, 21 Oct 2025 16:06:50 +0200 +commit 91dd18052f443ffcaad581865c4fd9b284b4fa00 +Author: kp9991 +Date: Wed, 5 Aug 2026 22:41:03 +1000 + + The inner engine must understand ForwardVanillaOption::arguments: QuantoEngine::calculate casts its inner engine's arguments to Instr::arguments, so a plain AnalyticEuropeanEngine (VanillaOption::arguments) throws an error. - Upgrade minimum Python version to 3.10 + SWIG/options.i | 2 +- + 1 file changed, 1 insertion(+), 1 deletion(-) + +commit 52ff42514ccd51cb8168c15dc697172dbed3e91e +Author: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> +Date: Mon, 3 Aug 2026 18:32:25 +0000 + + Bump actions/stale from 10 to 11 + + Bumps [actions/stale](https://github.com/actions/stale) from 10 to 11. + - [Release notes](https://github.com/actions/stale/releases) + - [Changelog](https://github.com/actions/stale/blob/main/CHANGELOG.md) + - [Commits](https://github.com/actions/stale/compare/v10...v11) + + --- + updated-dependencies: + - dependency-name: actions/stale + dependency-version: '11' + dependency-type: direct:production + update-type: version-update:semver-major + ... + + Signed-off-by: dependabot[bot] + + .github/workflows/stale.yml | 2 +- + 1 file changed, 1 insertion(+), 1 deletion(-) + +commit 17ef8a34436c44d797c68a39693c0ad5b5481b8d +Author: Eugene Toder +Date: Fri, 31 Jul 2026 15:09:00 -0400 + + Make DiscountingSwapEngine arguments match C++ - This is requested for compatibility with SWIG 4.4, that - we'll use going forward in order to enable building wheels - for free-threading Python. + Also, enable kwargs. - Python/setup.py | 46 +++++++++++++++++++++++++--------------------- - Python/setup.py.in | 46 +++++++++++++++++++++++++--------------------- - 2 files changed, 50 insertions(+), 42 deletions(-) + SWIG/swap.i | 19 +++++++++---------- + 1 file changed, 9 insertions(+), 10 deletions(-) -commit d219d0c72cab206d9ddddf2156a87e40609563b8 +commit 0c8389ce5955bebbe60294e6261c077dbf89234c +Merge: 5d942806 8ca78ecb Author: Luigi Ballabio -Date: Tue, 21 Oct 2025 16:10:21 +0200 +Date: Mon, 3 Aug 2026 12:07:18 +0200 - Unpin base for devenv image + Export new trade date argument for CDS-helper constructors (#900) - .github/workflows/devenv-images.yml | 2 +- - dockerfiles/ci.csharp.Dockerfile | 5 +++++ - 2 files changed, 6 insertions(+), 1 deletion(-) +commit 5d94280683658fdde989e8a7f37e8b92abe4d48a +Author: lballabio[bot] <224797326+lballabio-bot@users.noreply.github.com> +Date: Mon, 3 Aug 2026 09:44:30 +0000 + + Update copyright list in license + + LICENSE.TXT | 1 + + 1 file changed, 1 insertion(+) -commit 4fef4aba7e7d34266160ddd78d5705e277ee2af8 -Merge: 85ed8efc 6a39261c +commit 538f7d941475708ebe466913e6db0b5a9243ea61 +Merge: e85e9f95 55d65d40 Author: Luigi Ballabio -Date: Fri, 24 Oct 2025 09:17:38 +0200 +Date: Mon, 3 Aug 2026 11:44:13 +0200 + + Multi curve (#889) + +commit 55d65d400def594a615047a9d8ee1d7d0538fa7d +Author: kp9991 +Date: Mon, 3 Aug 2026 18:27:38 +1000 + + Removing unecessary copyright, GlobalBootstrap now has jump dates - Replace passed NULL with NA in R examples for compatibility with R 3.5 (#789) + Python/test/test_multicurve.py | 8 +++++++- + SWIG/piecewiseyieldcurve.i | 16 ++++++++++++---- + SWIG/ratehelpers.i | 1 - + 3 files changed, 19 insertions(+), 6 deletions(-) -commit 6a39261c1b610e7cd453f9b1a19b7a179daeef16 +commit 8ca78ecb8d8631dd6fe0627b75a0cc02e11048f3 Author: Luigi Ballabio -Date: Thu, 23 Oct 2025 08:09:18 +0200 +Date: Mon, 3 Aug 2026 10:12:47 +0200 - Replace passed NULL with NA in R examples - - NULL doesn't work with R 3.5 and the current typemaps + Export new trade date argument for CDS-helper constructors - R/demo/european-option.R | 22 +++++++++++----------- - 1 file changed, 11 insertions(+), 11 deletions(-) + SWIG/defaultprobability.i | 18 ++++++++++++------ + 1 file changed, 12 insertions(+), 6 deletions(-) -commit 85ed8efc12645d0b361341941c88487cee871013 +commit e85e9f954b2f0914b27f8cc44e8dfda075fbd230 +Merge: 16dabdc4 ddb1c613 Author: Luigi Ballabio -Date: Wed, 22 Oct 2025 15:14:38 +0200 +Date: Mon, 3 Aug 2026 09:10:18 +0200 - Move CI build to .Net 9 + Expose pricingEngine and valuationDate on Instrument (#895) - CSharp/Makefile.am | 20 ++++++++++---------- - CSharp/csharp/NQuantLib.csproj | 2 +- - .../BermudanSwaption/BermudanSwaption.csproj | 2 +- - CSharp/examples/EquityOption/EquityOption.csproj | 2 +- - .../FiniteDifferenceMethods.csproj | 2 +- - CSharp/examples/Times/Times.csproj | 2 +- - dockerfiles/ci.csharp.Dockerfile | 2 +- - 7 files changed, 16 insertions(+), 16 deletions(-) +commit dd25bd83cd5ed05b89847306e77555d8af4b6639 +Author: kp9991 +Date: Mon, 3 Aug 2026 16:53:30 +1000 + + Align with QuantLib + + SWIG/gaussian1dmodel.i | 7 +++++++ + SWIG/swap.i | 9 ++++++++- + 2 files changed, 15 insertions(+), 1 deletion(-) + +commit ddb1c61362649216928b74ac6d49bdb4a2eea9f3 +Author: Eugene Toder +Date: Fri, 31 Jul 2026 14:01:31 -0400 + + Expose pricingEngine and valuationDate on Instrument + + SWIG/instruments.i | 2 ++ + 1 file changed, 2 insertions(+) + +commit b4a5943ab3c9c46c1442d82daeef27e8ce05a17a +Author: kp9991 +Date: Thu, 30 Jul 2026 21:08:10 +1000 + + Adding OvernightOvernightBasisSwapRateHelper + + SWIG/ratehelpers.i | 23 +++++++++++++++++++++++ + 1 file changed, 23 insertions(+) + +commit 839780d9e163869df59c3907f215539ddb767d3c +Author: kp9991 +Date: Tue, 28 Jul 2026 20:51:50 +1000 + + Alignment with QuantLib + + SWIG/ratehelpers.i | 6 ++++-- + 1 file changed, 4 insertions(+), 2 deletions(-) + +commit fb0d6443440a652564fca6cba1c1dd12a78736ff +Author: kp9991 +Date: Tue, 28 Jul 2026 20:09:13 +1000 + + Alignment with QuantLib + + Python/test/test_mtmcrosscurrencybasisswap.py | 9 +++++++-- + SWIG/cashflows.i | 4 ++-- + SWIG/ratehelpers.i | 3 ++- + SWIG/swap.i | 3 ++- + 4 files changed, 13 insertions(+), 6 deletions(-) -commit 486668c3e029144209728a31ada4c2c0ac7c5623 -Merge: 2e2fce59 4d9ddd92 +commit 16dabdc4d64dfa27d50b4c16cefc7241d089915e +Merge: 7bbcd0ec abcc8b37 Author: Luigi Ballabio -Date: Tue, 21 Oct 2025 13:02:08 +0200 +Date: Mon, 27 Jul 2026 16:13:58 +0200 - Expose forDates on inflation swap helpers (#787) + Export new optional parameters for `OvernightIborBasisSwapRateHelper` (#893) -commit 2e2fce59eef9476bfd2524ea77a2ef03dd36df0d -Merge: d636f144 77357411 +commit 7bbcd0ecdd3d0099487866005d0d9988ad9f108d +Merge: 0fdc68eb 55e7df65 Author: Luigi Ballabio -Date: Tue, 21 Oct 2025 11:18:34 +0200 +Date: Mon, 27 Jul 2026 15:50:56 +0200 - Rename piecewise spread curves classes to *SpreadDiscount (#786) + Export CMA-ES optimizer (#892) -commit 4d9ddd92f1cb49b43c864330f6353f7da394b36f -Author: Eugene Toder -Date: Mon, 20 Oct 2025 13:20:32 -0400 +commit abcc8b37ec6c3cac4ba277c909a2ce4bfa1309f3 +Author: Luigi Ballabio +Date: Mon, 27 Jul 2026 15:07:38 +0200 - Expose forDates on inflation swap helpers + Export new optional parameters for OvernightIborBasisSwapRateHelper - SWIG/inflation.i | 55 ++++++++++++++++++++++++++++++++++++++++++++++--------- - 1 file changed, 46 insertions(+), 9 deletions(-) + SWIG/ratehelpers.i | 4 +++- + 1 file changed, 3 insertions(+), 1 deletion(-) -commit 77357411385efd3d83c918279bd4fc469f26d234 -Author: Eugene Toder -Date: Sat, 18 Oct 2025 17:28:27 -0400 +commit 55e7df65dbac3ea0bd52e6c97be4c568c779dd8a +Author: Luigi Ballabio +Date: Mon, 27 Jul 2026 14:44:58 +0200 - Rename piecewise spread curves classes to *SpreadDiscount - - For consistency with InterpolatedSpreadDiscountCurve classes. + Export CMAES optimizer - SWIG/piecewiseyieldcurve.i | 11 ++++++++--- - 1 file changed, 8 insertions(+), 3 deletions(-) + SWIG/optimizers.i | 28 +++++++++++++++++++++++++++- + 1 file changed, 27 insertions(+), 1 deletion(-) -commit d636f14414e020078de9cbaa0761467106046043 -Merge: b9d03fca 9b904d87 +commit 0fdc68eb593d59f70e66fc61a77b1295a5989589 +Merge: 484d717b 614e4253 Author: Luigi Ballabio -Date: Thu, 16 Oct 2025 08:42:39 +0200 +Date: Mon, 27 Jul 2026 14:35:44 +0200 - Expose InterpolatedSpreadDiscountCurve (#785) + Export optional rounding precision for `OvernightIndexedSwap` and `MakeOIS` (#891) -commit 9b904d879dd1d1118eb814fc848e8983a349243d -Author: Eugene Toder -Date: Wed, 15 Oct 2025 12:06:43 -0400 +commit 95dcd83575fa18bcf6b3146030cb185eda3025da +Author: kp9991 +Date: Mon, 27 Jul 2026 22:17:06 +1000 - Expose InterpolatedSpreadDiscountCurve + Expose FX resets, leg NPVs - SWIG/ql.i | 1 + - SWIG/spreaddiscountcurve.i | 37 +++++++++++++++++++++++++++++++++++++ - 2 files changed, 38 insertions(+) + Python/test/test_mtmcrosscurrencybasisswap.py | 19 +++++++++++++++++++ + SWIG/swap.i | 5 +++++ + 2 files changed, 24 insertions(+) -commit b9d03fca2f8e6b251e62bbcf00cca9208aa33af9 -Merge: 7ad4fc40 b319a172 +commit 614e425362408966ba73af9c07d17ce8291c443b Author: Luigi Ballabio -Date: Wed, 15 Oct 2025 16:17:22 +0200 +Date: Mon, 27 Jul 2026 13:31:42 +0200 - Expose more spread curve types (#784) + Export new parameter for OIS and MakeOIS -commit b319a172a896e1b73c15822ac8507e3109f7e187 -Author: Eugene Toder -Date: Tue, 14 Oct 2025 14:09:31 -0400 + SWIG/swap.i | 11 ++++++++--- + 1 file changed, 8 insertions(+), 3 deletions(-) + +commit 78a0feee648150e6cf117811d087c106b56026b4 +Author: kp9991 +Date: Mon, 27 Jul 2026 19:53:23 +1000 - Expose more spread curve types + Returning fx reset value date in SWIG - SWIG/piecewiseyieldcurve.i | 11 +++++++++-- - 1 file changed, 9 insertions(+), 2 deletions(-) + Python/test/test_mtmcrosscurrencybasisswap.py | 1 + + SWIG/cashflows.i | 1 + + 2 files changed, 2 insertions(+) -commit 7ad4fc406fc93b45ee9a559f795de424bb2d3965 -Merge: 864b090f be0789ef +commit 484d717b454f48914d79ef68573907d13d5a0b95 +Merge: 9fb7633c 9e3e80d3 Author: Luigi Ballabio -Date: Tue, 14 Oct 2025 15:10:04 +0200 +Date: Mon, 27 Jul 2026 11:47:12 +0200 - Avoid features removed from underlying C++ library (#783) + Export new parameter for `SabrSwaptionVolatilityCube` (#890) -commit 864b090f0a0f78922288f8ff9c96828578a512da -Author: lballabio[bot] <224797326+lballabio-bot@users.noreply.github.com> -Date: Tue, 14 Oct 2025 09:34:11 +0000 +commit 0bf7aa0894740535eee2f91e3370a09a8da7d269 +Author: kp9991 +Date: Sun, 26 Jul 2026 20:04:30 +1000 - Set version to 1.41-dev + useIndexedCoupons in SWIG - Python/setup.py | 2 +- - R/DESCRIPTION | 2 +- - configure.ac | 2 +- - 3 files changed, 3 insertions(+), 3 deletions(-) + Python/test/test_mtmcrosscurrencybasisswap.py | 49 +++++++++++++++++++++++++++ + SWIG/ratehelpers.i | 9 +++-- + SWIG/swap.i | 9 +++-- + 3 files changed, 61 insertions(+), 6 deletions(-) + +commit 5ca22515e7bc58e4aa5e2b9a1a425a3a4213cd1e +Author: kp9991 +Date: Sun, 26 Jul 2026 09:54:38 +1000 + + Reset cash flow object + + Python/test/test_mtmcrosscurrencybasisswap.py | 101 +++++++++++++++++++++ + SWIG/cashflows.i | 123 ++++++++++++++++++++++++++ + SWIG/ratehelpers.i | 5 +- + SWIG/swap.i | 8 ++ + 4 files changed, 236 insertions(+), 1 deletion(-) + +commit 889302a09544cedfda7adbf254dc0d5a714c8e66 +Author: kp9991 +Date: Sun, 26 Jul 2026 05:07:21 +1000 + + Added useIndexedCoupons to IborIborBasisSwapRateHelper + + SWIG/ratehelpers.i | 3 ++- + 1 file changed, 2 insertions(+), 1 deletion(-) + +commit c6443ea61d8a3a796252dfa31ad6b14264b4034d +Author: kp9991 +Date: Sat, 25 Jul 2026 18:22:58 +1000 + + Integrating QuantLib's IborOISSpread branch + + SWIG/ratehelpers.i | 7 ++++++- + 1 file changed, 6 insertions(+), 1 deletion(-) + +commit 4ed09195bd7adc4178ddf3c57457f6cd8edd6e97 +Author: kp9991 +Date: Fri, 24 Jul 2026 14:33:46 +1000 + + Expose swap() + + SWIG/ratehelpers.i | 3 +++ + 1 file changed, 3 insertions(+) + +commit 72f31f5d8bc0e612b4377cca86c1ddd26b992afe +Author: kp9991 +Date: Thu, 23 Jul 2026 21:33:07 +1000 + + Exposing different interpolation types for single-curve global bootstrapping + + Python/test/test_multicurve.py | 2 +- + SWIG/piecewiseyieldcurve.i | 83 ++++++++++++++++++++++++++++++++---------- + 2 files changed, 64 insertions(+), 21 deletions(-) + +commit 902db093ef8a7a4de894b9a263bbbffa16610194 +Author: kp9991 +Date: Thu, 23 Jul 2026 16:14:57 +1000 + + Enable multicurve -commit be0789efdc4868d8c0df405ca9a54c1cb818aa22 + Python/test/test_multicurve.py | 81 ++++++++++++++++++++++++++++++++++++++++++ + SWIG/multicurve.i | 54 ++++++++++++++++++++++++++++ + SWIG/ql.i | 1 + + 3 files changed, 136 insertions(+) + +commit 9fb7633c89e16f533a8ec93534b6580d14929312 +Author: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> +Date: Mon, 20 Jul 2026 18:32:32 +0000 + + Bump actions/setup-dotnet from 5 to 6 + + Bumps [actions/setup-dotnet](https://github.com/actions/setup-dotnet) from 5 to 6. + - [Release notes](https://github.com/actions/setup-dotnet/releases) + - [Commits](https://github.com/actions/setup-dotnet/compare/v5...v6) + + --- + updated-dependencies: + - dependency-name: actions/setup-dotnet + dependency-version: '6' + dependency-type: direct:production + update-type: version-update:semver-major + ... + + Signed-off-by: dependabot[bot] + + .github/workflows/nupkg.yml | 10 +++++----- + 1 file changed, 5 insertions(+), 5 deletions(-) + +commit 8bee4c61975d1eb0dcc6ff0d7837bcd389d74320 +Author: kp9991 +Date: Mon, 20 Jul 2026 23:13:38 +1000 + + paymentLag + + SWIG/ratehelpers.i | 3 ++- + 1 file changed, 2 insertions(+), 1 deletion(-) + +commit 74549768ce9c9839fc93c61988c3d44ffc0be5ed +Author: kp9991 +Date: Mon, 20 Jul 2026 21:35:58 +1000 + + bootstrapBaseCurve flag + + SWIG/ratehelpers.i | 3 ++- + 1 file changed, 2 insertions(+), 1 deletion(-) + +commit 4270b338f5c5fe378ae1be3f6588d9d5e95f7116 +Author: kp9991 +Date: Mon, 20 Jul 2026 10:59:39 +1000 + + Removing FxIndex + + SWIG/indexes.i | 19 ------------------- + SWIG/swap.i | 2 -- + 2 files changed, 21 deletions(-) + +commit 3bc6e1f37b7638214defc143871d22913130dbac +Merge: 0bf541dd 7166a8aa +Author: Luigi Ballabio +Date: Fri, 17 Jul 2026 10:05:02 +0200 + + Fix uses of boost and/or ext namespace (#886) + +commit 7166a8aa63b44de26c1c839e576ce55c1e783997 Author: Luigi Ballabio -Date: Tue, 14 Oct 2025 11:21:04 +0200 +Date: Fri, 17 Jul 2026 09:27:07 +0200 - Update tests + Fix namespace definition - Python/test/test_equityindex.py | 2 +- - Python/test/test_swap.py | 1 + - 2 files changed, 2 insertions(+), 1 deletion(-) + SWIG/common.i | 2 +- + 1 file changed, 1 insertion(+), 1 deletion(-) -commit d89406cc88b7e126980cabe9cc72381c2a95c68c +commit 1c9720e38c5263c21e199a51c1fe99030f7584b6 +Author: lballabio[bot] <224797326+lballabio-bot@users.noreply.github.com> +Date: Thu, 16 Jul 2026 17:27:42 +0000 + + Fix uses of boost and/or ext namespace + + SWIG/calendars.i | 6 +++--- + SWIG/cashflows.i | 18 +++++++++--------- + SWIG/common.i | 24 ++++++++++++------------ + SWIG/creditdefaultswap.i | 2 +- + SWIG/date.i | 18 +++++++++--------- + SWIG/old_volatility.i | 2 +- + SWIG/options.i | 2 +- + SWIG/ratehelpers.i | 32 ++++++++++++++++---------------- + SWIG/scheduler.i | 14 +++++++------- + SWIG/settings.i | 8 ++++---- + SWIG/swap.i | 14 +++++++------- + SWIG/termstructures.i | 2 +- + SWIG/volatilities.i | 10 +++++----- + 13 files changed, 76 insertions(+), 76 deletions(-) + +commit 0bf541dd7edc9e7412118801e992a670ba158546 Author: Luigi Ballabio -Date: Tue, 14 Oct 2025 10:10:51 +0200 +Date: Thu, 16 Jul 2026 18:01:59 +0200 - Add workflow to increase version number after release + Update namespace to check for deprecated boost:: or ext:: namespaces - .github/workflows/increase-version-number.yml | 37 +++++++++++++++++++++++++++ - 1 file changed, 37 insertions(+) + .github/workflows/namespaces.yml | 19 ++++++++----------- + 1 file changed, 8 insertions(+), 11 deletions(-) -commit f54285262cdd40acc00e36e97928e1f979675b7c +commit 842a30f8341342d54cd3a14ccf495d02f75d6024 Author: Luigi Ballabio -Date: Tue, 14 Oct 2025 10:40:24 +0200 +Date: Tue, 14 Apr 2026 10:36:46 +0200 - Avoid features removed from underlying library in version 1.41 + Increase version number automatically after release - SWIG/indexes.i | 5 -- - SWIG/inflation.i | 137 +++++++++++++---------------------------------------- - SWIG/ratehelpers.i | 21 -------- - SWIG/swap.i | 51 -------------------- - 4 files changed, 34 insertions(+), 180 deletions(-) + .github/workflows/increase-version-number.yml | 10 +++++++--- + 1 file changed, 7 insertions(+), 3 deletions(-) -commit 1923c81a2809618c9e85f5408201060af4bafd68 +commit bb311f6d5a0cb6d4d69924926e1eb165d425d05c +Merge: 4719513a e8c8eb6a Author: Luigi Ballabio -Date: Tue, 14 Oct 2025 09:52:20 +0200 +Date: Tue, 14 Jul 2026 15:00:31 +0200 - Fix links in draft release notes + Remove features deprecated in version 1.39 and removed in the C++ library (#885) - .github/workflows/publish-release.yml | 4 ++-- - 1 file changed, 2 insertions(+), 2 deletions(-) +commit 4719513a1eeabbdb4709eea7d41306c000e0f5c2 +Author: lballabio[bot] <224797326+lballabio-bot@users.noreply.github.com> +Date: Tue, 14 Jul 2026 09:36:33 +0000 + + Set version to 1.44-dev + + Python/setup.py | 2 +- + R/DESCRIPTION | 2 +- + configure.ac | 2 +- + 3 files changed, 3 insertions(+), 3 deletions(-) -commit 2c903776a294111a39aa74a2b35be676b086a1a9 +commit c28fea0744a6b9a0d9a1844eeae4dc4bb710d021 Author: Luigi Ballabio -Date: Tue, 14 Oct 2025 09:50:56 +0200 +Date: Tue, 14 Jul 2026 11:29:16 +0200 - Don't run CI builds when pushing a tag + Revert "Increase version number automatically after release" + + This reverts commit f0a6ff3bc80b5cb2611184426e428c16b2803e0b. - .github/workflows/linux.yml | 2 ++ - 1 file changed, 2 insertions(+) + .github/workflows/increase-version-number.yml | 9 +++------ + 1 file changed, 3 insertions(+), 6 deletions(-) -commit 2e34601a490a6096d0a79d864f8701cfb2d7c905 -Author: Marcin Rybacki -Date: Thu, 9 Oct 2025 16:07:59 +0200 +commit 8df3d621c7045011d1386c2e0c41f2daab808137 +Author: kp9991 +Date: Sun, 12 Jul 2026 16:37:40 +1000 + + add constructor arg + + SWIG/volatilities.i | 4 +++- + 1 file changed, 3 insertions(+), 1 deletion(-) - Exposed changes related to UFR rounding. +commit d3e4052d89b64da17084f7f23a5f2faed1a008fb +Author: kp9991 +Date: Mon, 29 Jun 2026 21:16:08 +1000 + + Initial commit - Python/test/test_termstructures.py | 2 +- - SWIG/termstructures.i | 17 ++++++++++++++++- - 2 files changed, 17 insertions(+), 2 deletions(-) + SWIG/indexes.i | 19 +++++++++++++ + SWIG/swap.i | 84 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ + 2 files changed, 103 insertions(+) -commit ad3db45c4b1f668f11258c7f0a786a97cc394086 -Author: Marcin Rybacki -Date: Thu, 9 Oct 2025 09:55:10 +0200 +commit e8c8eb6a0cfd21220efc1a0023ece62ac709476f +Author: Luigi Ballabio +Date: Mon, 29 Jun 2026 11:39:19 +0200 - Updated `UltimateForwardTermStructure` signature + Remove features deprecated and removed in the C++ library - SWIG/termstructures.i | 5 ++++- - 1 file changed, 4 insertions(+), 1 deletion(-) + SWIG/inflation.i | 32 ++------------------------------ + Scala/examples/CPIBond.scala | 2 +- + 2 files changed, 3 insertions(+), 31 deletions(-) diff --git a/LICENSE.md b/LICENSE.md index d65553d8..36040002 100644 --- a/LICENSE.md +++ b/LICENSE.md @@ -680,17 +680,17 @@ QuantLib-SWIG is Copyright (C) 2003, 2004, 2005, 2006, 2007, 2008, 2009, 2010, 2011, 2014, 2020, 2022, 2023 StatPro Italia srl Copyright (C) 2005 Dominic Thuillier Copyright (C) 2005 Johan Witters - Copyright (C) 2006, 2007, 2010 Joseph Wang Copyright (C) 2007 Eric H. Jensen Copyright (C) 2007 Luis Cota Copyright (C) 2007 Richard Gomes Copyright (C) 2007, 2008 Tito Ingargiola + Copyright (C) 2007, 2010 Joseph Wang Copyright (C) 2008 Allen Kuo Copyright (C) 2008 Florent Grenier Copyright (C) 2009 Joseph Malicki Copyright (C) 2010 Andrea Odetti Copyright (C) 2010, 2011 Lluis Pujol Bajador - Copyright (C) 2010, 2011, 2012, 2013, 2014, 2015, 2018, 2019, 2020, 2021, 2022, 2023 Klaus Spanderen + Copyright (C) 2010, 2011, 2012, 2013, 2014, 2015, 2018, 2019, 2020, 2021, 2022, 2023, 2024 Klaus Spanderen Copyright (C) 2011, 2012 Tawanda Gwena Copyright (C) 2012 Francis Duffy Copyright (C) 2013 Simon Shakeshaft @@ -710,10 +710,15 @@ QuantLib-SWIG is Copyright (C) 2020 Gorazd Brumen Copyright (C) 2020, 2021, 2022 Jack Gillett Copyright (C) 2020, 2021, 2023 Marcin Rybacki - Copyright (C) 2021 Ralf Konrad Eckel + Copyright (C) 2021, 2024 Ralf Konrad Eckel Copyright (C) 2022 Ignacio Anguita - Copyright (C) 2022, 2023 Skandinaviska Enskilda Banken AB (publ) + Copyright (C) 2022, 2023, 2024 Skandinaviska Enskilda Banken AB (publ) Copyright (C) 2023 Francois Botha + Copyright (C) 2025 Hiroto Ogawa + Copyright (C) 2026 Arihant Lodha + Copyright (C) 2026 Kyrylo Protsenko + Copyright (C) 2026 Lawrenz Law + Copyright (C) 2026 Mahimn Patel Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: diff --git a/Python/README.md b/Python/README.md index 2f1f67df..0d2fecb9 100644 --- a/Python/README.md +++ b/Python/README.md @@ -12,7 +12,7 @@ Useful links: pip install QuantLibAAD ``` -This package was called QuantLib-Risks up to version 1.33.3. `QuantLib-Risks` 1.41 is a +This package was called QuantLib-Risks up to version 1.33.3. `QuantLib-Risks` 1.44 is a deprecated alias that installs `QuantLibAAD` and keeps `import QuantLib_Risks` working with a `DeprecationWarning`. Anyone pinned to `QuantLib-Risks==1.33.3` must also pin `xad<2`. diff --git a/Python/examples/global-bootstrap.py b/Python/examples/global-bootstrap.py index d9378a2a..d74bae06 100644 --- a/Python/examples/global-bootstrap.py +++ b/Python/examples/global-bootstrap.py @@ -166,12 +166,8 @@ for i, h in enumerate(helpers): pillar = h.pillarDate() - if i < 13: - day_counter = ql.Actual360() - compounding = ql.Simple - else: - day_counter = ql.Thirty360(ql.Thirty360.BondBasis) - compounding = ql.SimpleThenCompounded + day_counter = ql.Actual360() + compounding = ql.Simple r = curve.zeroRate(pillar, day_counter, compounding, ql.Annual).rate() data.append((pillar.to_date(), r * 100)) diff --git a/Python/test/test_bondfunctions.py b/Python/test/test_bondfunctions.py index f14f4545..9830e7b1 100644 --- a/Python/test/test_bondfunctions.py +++ b/Python/test/test_bondfunctions.py @@ -166,10 +166,10 @@ def testBps(self): 0.06527501) self.assertEqual(round(ql.BondFunctions.bps(self.bond, ql.InterestRate(0.03, self.day_counter, ql.Compounded, ql.Annual)), 8), - 0.07071951) + 0.07073835) self.assertEqual(round(ql.BondFunctions.bps(self.bond, 0.03, self.day_counter, ql.Compounded, ql.Annual), 8), - 0.07071951) + 0.07073835) def testCleanPrice(self): """ Testing BondFunctions cleanPrice. """ @@ -219,12 +219,12 @@ def testBasisPointValue(self): self.assertEqual(round(ql.BondFunctions.basisPointValue(self.bond, 0.03, self.day_counter, ql.Compounded, ql.Annual, self.settlement_date), 8), - -0.07527271) + -0.07524195) self.assertEqual(round(ql.BondFunctions.basisPointValue(self.bond, ql.InterestRate( 0.03, self.day_counter, ql.Compounded, ql.Annual), self.settlement_date), 8), - -0.07527271) + -0.07524195) def testYieldValueBasisPoint(self): """ Testing BondFunctions yieldValueBasisPoint. """ diff --git a/Python/test/test_calendars.py b/Python/test/test_calendars.py index 0a41cefc..c35b8bea 100644 --- a/Python/test/test_calendars.py +++ b/Python/test/test_calendars.py @@ -50,6 +50,21 @@ def test_joint_calendar_holidays(self): self.assertEqual(joint_holidays, base_holidays) +class MalaysiaCalendarTest(unittest.TestCase): + + def test_klse_holidays(self): + calendar = ql.Malaysia(ql.Malaysia.KLSE) + self.assertEqual(calendar.name(), "Kuala Lumpur stock exchange") + # Chinese New Year 2025 (Wed/Thu) and the National Day substitute (Mon) + self.assertTrue(calendar.isHoliday(ql.Date(29, ql.January, 2025))) + self.assertTrue(calendar.isHoliday(ql.Date(30, ql.January, 2025))) + self.assertTrue(calendar.isHoliday(ql.Date(1, ql.September, 2025))) + self.assertTrue(calendar.isBusinessDay(ql.Date(2, ql.January, 2025))) + + def test_default_market(self): + self.assertEqual(ql.Malaysia().name(), ql.Malaysia(ql.Malaysia.KLSE).name()) + + class BespokeCalendarTest(unittest.TestCase): def test_hash(self): @@ -70,12 +85,32 @@ def test_reset_added_holidays(self): self.assertFalse(calendar.isHoliday(test_date)) calendar.addHoliday(test_date) self.assertTrue(calendar.isHoliday(test_date)) - # TODO: Can extend test with this, if exposed: - # self.assertEqual(len(calendar.addedHolidays()), 1) + self.assertEqual(len(calendar.addedHolidays()), 1) calendar.resetAddedAndRemovedHolidays() self.assertFalse(calendar.isHoliday(test_date)) +class AddedRemovedHolidaysTest(unittest.TestCase): + + def test_added_and_removed_holidays(self): + calendar = ql.TARGET() + added = ql.Date(15, ql.March, 2023) # a Wednesday, normally a business day + removed = ql.Date(7, ql.April, 2023) # Good Friday 2023, a real TARGET holiday + + self.assertFalse(calendar.isHoliday(added)) + self.assertTrue(calendar.isHoliday(removed)) + + calendar.addHoliday(added) + calendar.removeHoliday(removed) + + self.assertEqual(list(calendar.addedHolidays()), [added]) + self.assertEqual(list(calendar.removedHolidays()), [removed]) + + calendar.resetAddedAndRemovedHolidays() + self.assertEqual(len(calendar.addedHolidays()), 0) + self.assertEqual(len(calendar.removedHolidays()), 0) + + if __name__ == "__main__": print("testing QuantLib", ql.__version__) unittest.main(verbosity=2) diff --git a/Python/test/test_coupons.py b/Python/test/test_coupons.py index 1f9038c5..1dfc4fae 100644 --- a/Python/test/test_coupons.py +++ b/Python/test/test_coupons.py @@ -1,92 +1,106 @@ -""" - Copyright (C) 2021 Marcin Rybacki +# Copyright (C) 2021 Marcin Rybacki - This file is part of QuantLib, a free-software/open-source library - for financial quantitative analysts and developers - http://quantlib.org/ +# 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 - . +# 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. - 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. -""" import unittest import QuantLibAAD as ql -EPSILON = 1.e-9 +EPSILON = 1.0e-9 CAL = ql.TARGET() VALUATION_DATE = CAL.adjust(ql.Date(15, ql.March, 2021)) -RATE_AVERAGING_MAP = {ql.RateAveraging.Compound: 'Compounded', - ql.RateAveraging.Simple: 'Simple'} +RATE_AVERAGING_MAP = { + ql.RateAveraging.Compound: "Compounded", + ql.RateAveraging.Simple: "Simple", +} def flat_rate(rate): - return ql.FlatForward( - 2, CAL, ql.makeQuoteHandle(rate), ql.Actual365Fixed()) + return ql.FlatForward(2, CAL, ql.makeQuoteHandle(rate), ql.Actual365Fixed()) def create_ibor_leg(ibor_idx, start, end, payment_lag=0): bdc = ibor_idx.businessDayConvention() - sch = ql.MakeSchedule(effectiveDate=start, - terminationDate=end, - tenor=ibor_idx.tenor(), - calendar=CAL, - convention=bdc, - backwards=True) - return ql.IborLeg([1.0], - sch, - ibor_idx, - paymentDayCounter=ibor_idx.dayCounter(), - paymentConvention=bdc, - paymentCalendar=CAL, - paymentLag=payment_lag) + sch = ql.MakeSchedule( + effectiveDate=start, + terminationDate=end, + tenor=ibor_idx.tenor(), + calendar=CAL, + convention=bdc, + backwards=True, + ) + return ql.IborLeg( + [1.0], + sch, + ibor_idx, + paymentDayCounter=ibor_idx.dayCounter(), + paymentConvention=bdc, + paymentCalendar=CAL, + paymentLag=payment_lag, + ) def create_overnight_leg(overnight_idx, start, end, payment_lag=0): - sch = ql.MakeSchedule(effectiveDate=start, - terminationDate=end, - tenor=ql.Period(1, ql.Years), - calendar=CAL, - convention=ql.Following, - backwards=True) - return ql.OvernightLeg([1.0], - sch, - overnight_idx, - paymentDayCounter=ql.Actual365Fixed(), - paymentConvention=ql.Following, - paymentCalendar=CAL, - paymentLag=payment_lag) + sch = ql.MakeSchedule( + effectiveDate=start, + terminationDate=end, + tenor=ql.Period(1, ql.Years), + calendar=CAL, + convention=ql.Following, + backwards=True, + ) + return ql.OvernightLeg( + [1.0], + sch, + overnight_idx, + paymentDayCounter=ql.Actual365Fixed(), + paymentConvention=ql.Following, + paymentCalendar=CAL, + paymentLag=payment_lag, + ) def create_fixed_rate_leg(start, end, payment_lag=0): - sch = ql.MakeSchedule(effectiveDate=start, - terminationDate=end, - tenor=ql.Period(1, ql.Years), - calendar=CAL, - convention=ql.Following, - backwards=True) - return ql.FixedRateLeg(sch, - ql.Actual365Fixed(), - [1.0], - [0.005], - paymentAdjustment=ql.Following, - paymentCalendar=CAL, - paymentLag=payment_lag) + sch = ql.MakeSchedule( + effectiveDate=start, + terminationDate=end, + tenor=ql.Period(1, ql.Years), + calendar=CAL, + convention=ql.Following, + backwards=True, + ) + return ql.FixedRateLeg( + sch, + ql.Actual365Fixed(), + [1.0], + [0.005], + paymentAdjustment=ql.Following, + paymentCalendar=CAL, + paymentLag=payment_lag, + ) class CashFlowsTest(unittest.TestCase): def setUp(self): - self.cash_flows = [ql.SimpleCashFlow(1.e6, ql.Date(22, 6, 2022)), - ql.SimpleCashFlow(5.e4, ql.Date(22, 6, 2022))] + self.cash_flows = [ + ql.SimpleCashFlow(1.0e6, ql.Date(22, 6, 2022)), + ql.SimpleCashFlow(5.0e4, ql.Date(22, 6, 2022)), + ] def test_previous_cash_flow_amount(self): """Testing previous cash flows amount""" @@ -94,12 +108,14 @@ def test_previous_cash_flow_amount(self): expected_amount = 1.05e6 include_settlement_date_flows = False actual_amount = ql.CashFlows.previousCashFlowAmount( - self.cash_flows, include_settlement_date_flows, reference_date) + self.cash_flows, include_settlement_date_flows, reference_date + ) fail_msg = """ Unable to replicate previous cash flow amount: calculated: {actual} expected: {expected} - """.format(actual=actual_amount, - expected=expected_amount) + """.format( + actual=actual_amount, expected=expected_amount + ) self.assertEqual(actual_amount, expected_amount, msg=fail_msg) def test_next_cash_flow_amount(self): @@ -108,12 +124,14 @@ def test_next_cash_flow_amount(self): expected_amount = 1.05e6 include_settlement_date_flows = False actual_amount = ql.CashFlows.nextCashFlowAmount( - self.cash_flows, include_settlement_date_flows, reference_date) + self.cash_flows, include_settlement_date_flows, reference_date + ) fail_msg = """ Unable to replicate next cash flow amount: calculated: {actual} expected: {expected} - """.format(actual=actual_amount, - expected=expected_amount) + """.format( + actual=actual_amount, expected=expected_amount + ) self.assertEqual(actual_amount, expected_amount, msg=fail_msg) @@ -136,16 +154,16 @@ def test_payment_lag(self): c_f_without_lag.date(), pay_lag, ql.Days, - self.ibor_idx.businessDayConvention()) + self.ibor_idx.businessDayConvention(), + ) fail_msg = """ Unable to replicate Ibor coupon payment date: calculated: {actual} expected: {expected} - """.format(actual=actual_payment_date, - expected=expected_payment_date) - self.assertEqual(actual_payment_date, - expected_payment_date, - msg=fail_msg) + """.format( + actual=actual_payment_date, expected=expected_payment_date + ) + self.assertEqual(actual_payment_date, expected_payment_date, msg=fail_msg) class OvernightCouponTest(unittest.TestCase): @@ -160,21 +178,20 @@ def test_payment_lag(self): end = ql.Date(17, ql.March, 2031) pay_lag = 2 leg_without_lag = create_overnight_leg(self.overnight_idx, start, end) - leg_with_lag = create_overnight_leg( - self.overnight_idx, start, end, pay_lag) + leg_with_lag = create_overnight_leg(self.overnight_idx, start, end, pay_lag) for c_f_without_lag, c_f_with_lag in zip(leg_without_lag, leg_with_lag): actual_payment_date = c_f_with_lag.date() expected_payment_date = CAL.advance( - c_f_without_lag.date(), pay_lag, ql.Days, ql.Following) + c_f_without_lag.date(), pay_lag, ql.Days, ql.Following + ) fail_msg = """ Unable to replicate overnight coupon payment date: calculated: {actual} expected: {expected} - """.format(actual=actual_payment_date, - expected=expected_payment_date) - self.assertEqual(actual_payment_date, - expected_payment_date, - msg=fail_msg) + """.format( + actual=actual_payment_date, expected=expected_payment_date + ) + self.assertEqual(actual_payment_date, expected_payment_date, msg=fail_msg) class FixedRateCouponTest(unittest.TestCase): @@ -192,47 +209,57 @@ def test_payment_lag(self): for c_f_without_lag, c_f_with_lag in zip(leg_without_lag, leg_with_lag): actual_payment_date = c_f_with_lag.date() expected_payment_date = CAL.advance( - c_f_without_lag.date(), pay_lag, ql.Days, ql.Following) + c_f_without_lag.date(), pay_lag, ql.Days, ql.Following + ) fail_msg = """ Unable to replicate fixed rate coupon payment date: calculated: {actual} expected: {expected} - """.format(actual=actual_payment_date, - expected=expected_payment_date) - self.assertEqual(actual_payment_date, - expected_payment_date, - msg=fail_msg) + """.format( + actual=actual_payment_date, expected=expected_payment_date + ) + self.assertEqual(actual_payment_date, expected_payment_date, msg=fail_msg) def create_sub_periods_coupon( - ibor_idx, start, end, averaging_method=ql.RateAveraging.Compound): - payment_calendar = ibor_idx.fixingCalendar() - payment_bdc = ibor_idx.businessDayConvention() - payment_date = payment_calendar.adjust(end, payment_bdc) - fixing_delay = ibor_idx.fixingDays() - cpn = ql.SubPeriodsCoupon( - payment_date, 1.0, start, end, fixing_delay, ibor_idx) - use_compounded_rate = (averaging_method == ql.RateAveraging.Compound) - if use_compounded_rate: - cpn.setPricer(ql.CompoundingRatePricer()) + ibor_idx, start, end, averaging_method=ql.RateAveraging.Compound +): + reset_schedule = ql.MakeSchedule( + effectiveDate=start, + terminationDate=end, + tenor=ibor_idx.tenor(), + calendar=ibor_idx.fixingCalendar(), + convention=ibor_idx.businessDayConvention(), + backwards=True, + ) + payment_date = reset_schedule[-1] + cpn = ql.MultipleResetsCoupon( + payment_date, 1.0, reset_schedule, ibor_idx.fixingDays(), ibor_idx + ) + if averaging_method == ql.RateAveraging.Compound: + cpn.setPricer(ql.CompoundingMultipleResetsPricer()) else: - cpn.setPricer(ql.AveragingRatePricer()) + cpn.setPricer(ql.AveragingMultipleResetsPricer()) return cpn -def create_sub_periods_leg( - ibor_idx, start, end, cpn_frequency, averaging_method): - sch = ql.MakeSchedule(effectiveDate=start, - terminationDate=end, - tenor=cpn_frequency, - calendar=ibor_idx.fixingCalendar(), - convention=ibor_idx.businessDayConvention(), - backwards=True) - return ql.SubPeriodsLeg( - [1.0], - sch, +def create_sub_periods_leg(ibor_idx, start, end, cpn_frequency, averaging_method): + reset_schedule = ql.MakeSchedule( + effectiveDate=start, + terminationDate=end, + tenor=ibor_idx.tenor(), + calendar=ibor_idx.fixingCalendar(), + convention=ibor_idx.businessDayConvention(), + backwards=True, + ) + resets_per_coupon = ibor_idx.tenor().frequency() // cpn_frequency + return ql.MultipleResetsLeg( + reset_schedule, ibor_idx, - averagingMethod=averaging_method) + resets_per_coupon, + [1.0], + averagingMethod=averaging_method, + ) def sum_leg_payments(leg): @@ -245,7 +272,7 @@ def compounded_leg_payment(leg): floating_cf = ql.as_floating_rate_coupon(cf) year_fraction = floating_cf.accrualPeriod() fixing = floating_cf.indexFixing() - compound *= (1.0 + year_fraction * fixing) + compound *= 1.0 + year_fraction * fixing return compound - 1.0 @@ -269,7 +296,8 @@ def setUp(self): def check_single_period_coupon_replication(self, start, end, averaging): ibor_leg = create_ibor_leg(self.ibor_idx, start, end) sub_periods_cpn = create_sub_periods_coupon( - self.ibor_idx, start, end, averaging) + self.ibor_idx, start, end, averaging + ) actual_payment = sub_periods_cpn.amount() expected_payment = sum_leg_payments(ibor_leg) @@ -279,19 +307,16 @@ def check_single_period_coupon_replication(self, start, end, averaging): expected: {expected} start: {start} end: {end} - """.format(actual=actual_payment, - expected=expected_payment, - start=start, - end=end) - self.assertTrue( - abs(actual_payment - expected_payment) < EPSILON, - msg=fail_msg) + """.format( + actual=actual_payment, expected=expected_payment, start=start, end=end + ) + self.assertTrue(abs(actual_payment - expected_payment) < EPSILON, msg=fail_msg) - def check_multiple_compounded_sub_periods_coupon_replication( - self, start, end): + def check_multiple_compounded_sub_periods_coupon_replication(self, start, end): ibor_leg = create_ibor_leg(self.ibor_idx, start, end) sub_periods_cpn = create_sub_periods_coupon( - self.ibor_idx, start, end, ql.RateAveraging.Compound) + self.ibor_idx, start, end, ql.RateAveraging.Compound + ) actual_payment = sub_periods_cpn.amount() expected_payment = compounded_leg_payment(ibor_leg) @@ -301,19 +326,16 @@ def check_multiple_compounded_sub_periods_coupon_replication( expected: {expected} start: {start} end: {end} - """.format(actual=actual_payment, - expected=expected_payment, - start=start, - end=end) - self.assertTrue( - abs(actual_payment - expected_payment) < EPSILON, - msg=fail_msg) + """.format( + actual=actual_payment, expected=expected_payment, start=start, end=end + ) + self.assertTrue(abs(actual_payment - expected_payment) < EPSILON, msg=fail_msg) - def check_multiple_averaged_sub_periods_coupon_replication( - self, start, end): + def check_multiple_averaged_sub_periods_coupon_replication(self, start, end): ibor_leg = create_ibor_leg(self.ibor_idx, start, end) sub_periods_cpn = create_sub_periods_coupon( - self.ibor_idx, start, end, ql.RateAveraging.Simple) + self.ibor_idx, start, end, ql.RateAveraging.Simple + ) actual_payment = sub_periods_cpn.amount() expected_payment = averaged_leg_payment(ibor_leg) @@ -323,22 +345,21 @@ def check_multiple_averaged_sub_periods_coupon_replication( expected: {expected} start: {start} end: {end} - """.format(actual=actual_payment, - expected=expected_payment, - start=start, - end=end) - self.assertTrue( - abs(actual_payment - expected_payment) < EPSILON, - msg=fail_msg) + """.format( + actual=actual_payment, expected=expected_payment, start=start, end=end + ) + self.assertTrue(abs(actual_payment - expected_payment) < EPSILON, msg=fail_msg) def check_sub_periods_leg_replication(self, averaging_method): start = ql.Date(18, ql.March, 2021) end = ql.Date(18, ql.March, 2022) sub_periods_cpn = create_sub_periods_coupon( - self.ibor_idx, start, end, averaging_method) + self.ibor_idx, start, end, averaging_method + ) sub_periods_leg = create_sub_periods_leg( - self.ibor_idx, start, end, ql.Period(1, ql.Years), averaging_method) + self.ibor_idx, start, end, ql.Annual, averaging_method + ) actual_payment = sum_leg_payments(sub_periods_leg) expected_payment = sub_periods_cpn.amount() @@ -347,50 +368,49 @@ def check_sub_periods_leg_replication(self, averaging_method): calculated: {actual} expected: {expected} averaging: {averaging} - """.format(actual=actual_payment, - expected=expected_payment, - averaging=RATE_AVERAGING_MAP[averaging_method]) - self.assertTrue( - abs(actual_payment - expected_payment) < EPSILON, - msg=fail_msg) + """.format( + actual=actual_payment, + expected=expected_payment, + averaging=RATE_AVERAGING_MAP[averaging_method], + ) + self.assertTrue(abs(actual_payment - expected_payment) < EPSILON, msg=fail_msg) def test_regular_single_period_forward_starting_coupon(self): """Testing regular single period forward starting coupon""" start = ql.Date(15, ql.April, 2021) end = ql.Date(15, ql.October, 2021) + self.check_single_period_coupon_replication(start, end, ql.RateAveraging.Simple) self.check_single_period_coupon_replication( - start, end, ql.RateAveraging.Simple) - self.check_single_period_coupon_replication( - start, end, ql.RateAveraging.Compound) + start, end, ql.RateAveraging.Compound + ) def test_regular_single_period_coupon_after_fixing(self): """Testing regular single period coupon after fixing""" start = ql.Date(12, ql.February, 2021) end = ql.Date(12, ql.August, 2021) + self.check_single_period_coupon_replication(start, end, ql.RateAveraging.Simple) self.check_single_period_coupon_replication( - start, end, ql.RateAveraging.Simple) - self.check_single_period_coupon_replication( - start, end, ql.RateAveraging.Compound) + start, end, ql.RateAveraging.Compound + ) def test_irregular_single_period_coupon_after_fixing(self): """Testing irregular single period coupon after fixing""" start = ql.Date(12, ql.February, 2021) end = ql.Date(12, ql.June, 2021) + self.check_single_period_coupon_replication(start, end, ql.RateAveraging.Simple) self.check_single_period_coupon_replication( - start, end, ql.RateAveraging.Simple) - self.check_single_period_coupon_replication( - start, end, ql.RateAveraging.Compound) + start, end, ql.RateAveraging.Compound + ) def test_regular_compounded_forward_starting_coupon_with_multiple_sub_periods(self): """Testing regular forward starting coupon with multiple compounded sub-periods""" start = ql.Date(15, ql.April, 2021) end = ql.Date(15, ql.April, 2022) - self.check_multiple_compounded_sub_periods_coupon_replication( - start, end) + self.check_multiple_compounded_sub_periods_coupon_replication(start, end) def test_regular_averaged_forward_starting_coupon_with_multiple_sub_periods(self): """Testing regular forward starting coupon with multiple averaged sub-periods""" @@ -409,63 +429,73 @@ def test_casting(self): start = ql.Date(18, ql.March, 2021) end = ql.Date(18, ql.March, 2022) sub_periods_leg = create_sub_periods_leg( - self.ibor_idx, start, end, ql.Period(1, ql.Years), ql.RateAveraging.Compound) + self.ibor_idx, start, end, ql.Annual, ql.RateAveraging.Compound + ) cf = sub_periods_leg[0] - self.assertTrue(not isinstance(cf, ql.SubPeriodsCoupon)) - self.assertTrue(isinstance( - ql.as_sub_periods_coupon(cf), ql.SubPeriodsCoupon)) + self.assertTrue(not isinstance(cf, ql.MultipleResetsCoupon)) + self.assertTrue( + isinstance(ql.as_multiple_resets_coupon(cf), ql.MultipleResetsCoupon) + ) def test_sub_period_coupon_fixing_dates(self): """Testing sub-period coupon fixing dates""" start = ql.Date(15, ql.April, 2021) end = ql.Date(15, ql.April, 2022) - cpn = ql.as_sub_periods_coupon( - create_sub_periods_coupon(self.ibor_idx, start, end)) + cpn = ql.as_multiple_resets_coupon( + create_sub_periods_coupon(self.ibor_idx, start, end) + ) actual_dates = cpn.fixingDates() expected_dates = (ql.Date(13, 4, 2021), ql.Date(13, 10, 2021)) fail_msg = """ Unable to replicate sub-period coupon fixing dates: calculated: {actual} expected: {expected} - """.format(actual=actual_dates, - expected=expected_dates) + """.format( + actual=actual_dates, expected=expected_dates + ) self.assertTupleEqual(actual_dates, expected_dates, msg=fail_msg) def test_sub_period_coupon_value_dates(self): """Testing sub-period coupon value dates""" start = ql.Date(15, ql.April, 2021) end = ql.Date(15, ql.April, 2022) - cpn = ql.as_sub_periods_coupon( - create_sub_periods_coupon(self.ibor_idx, start, end)) + cpn = ql.as_multiple_resets_coupon( + create_sub_periods_coupon(self.ibor_idx, start, end) + ) actual_dates = cpn.valueDates() - expected_dates = (ql.Date(15, 4, 2021), - ql.Date(15, 10, 2021), - ql.Date(19, 4, 2022)) + expected_dates = ( + ql.Date(15, 4, 2021), + ql.Date(15, 10, 2021), + ql.Date(19, 4, 2022), + ) fail_msg = """ Unable to replicate sub-period coupon value dates: calculated: {actual} expected: {expected} - """.format(actual=actual_dates, - expected=expected_dates) + """.format( + actual=actual_dates, expected=expected_dates + ) self.assertTupleEqual(actual_dates, expected_dates, msg=fail_msg) def test_sub_period_coupon_rate_spread(self): """Testing sub-period coupon rate spread""" start = ql.Date(15, ql.April, 2021) end = ql.Date(15, ql.April, 2022) - cpn = ql.as_sub_periods_coupon( - create_sub_periods_coupon(self.ibor_idx, start, end)) + cpn = ql.as_multiple_resets_coupon( + create_sub_periods_coupon(self.ibor_idx, start, end) + ) actual_spread = cpn.rateSpread() expected_spread = 0.0 fail_msg = """ Unable to replicate sub-period coupon rate spread: calculated: {actual} expected: {expected} - """.format(actual=actual_spread, - expected=expected_spread) + """.format( + actual=actual_spread, expected=expected_spread + ) self.assertEqual(actual_spread, expected_spread, msg=fail_msg) -if __name__ == '__main__': +if __name__ == "__main__": print("testing QuantLib", ql.__version__) unittest.main(verbosity=2) diff --git a/Python/test/test_ecb.py b/Python/test/test_ecb.py new file mode 100644 index 00000000..d0d21375 --- /dev/null +++ b/Python/test/test_ecb.py @@ -0,0 +1,80 @@ +""" + Copyright (C) 2026 Arihant Lodha + + 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. +""" + +import unittest +import QuantLibAAD as ql + + +class ECBTest(unittest.TestCase): + def test_is_ecb_date(self): + "Testing ECB.isECBdate" + # February 8, 2023 is a known ECB maintenance period start date + ecb_date = ql.Date(8, ql.February, 2023) + self.assertTrue(ql.ECB.isECBdate(ecb_date)) + # The day before is not + self.assertFalse(ql.ECB.isECBdate(ecb_date - 1)) + + def test_is_ecb_code(self): + "Testing ECB.isECBcode" + self.assertTrue(ql.ECB.isECBcode("FEB23")) + self.assertFalse(ql.ECB.isECBcode("NOTECB")) + + def test_code_and_date_roundtrip(self): + "Testing ECB code/date roundtrip" + ecb_date = ql.Date(8, ql.February, 2023) + code = ql.ECB.code(ecb_date) + self.assertEqual(code, "FEB23") + self.assertEqual(ql.ECB.date(code), ecb_date) + + def test_date_from_month_year(self): + "Testing ECB.date(month, year)" + # ECB.date(month, year) should return the same date as nextDate + ecb_date = ql.Date(8, ql.February, 2023) + self.assertEqual(ql.ECB.date(ql.February, 2023), ecb_date) + + def test_next_date(self): + "Testing ECB.nextDate" + before = ql.Date(1, ql.January, 2023) + # First ECB date on or after January 1, 2023 is February 8, 2023 + self.assertEqual(ql.ECB.nextDate(before), ql.Date(8, ql.February, 2023)) + # Next after February 8, 2023 is March 22, 2023 + ecb_feb = ql.Date(8, ql.February, 2023) + self.assertEqual(ql.ECB.nextDate(ecb_feb), ql.Date(22, ql.March, 2023)) + # nextDate also accepts an ECB code + self.assertEqual(ql.ECB.nextDate("FEB23"), ql.Date(22, ql.March, 2023)) + + def test_next_dates(self): + "Testing ECB.nextDates" + before = ql.Date(1, ql.January, 2023) + dates = ql.ECB.nextDates(before) + self.assertGreater(len(dates), 0) + self.assertEqual(dates[0], ql.Date(8, ql.February, 2023)) + # All returned dates must pass isECBdate + for d in dates: + self.assertTrue(ql.ECB.isECBdate(d)) + + def test_next_code(self): + "Testing ECB.nextCode" + before = ql.Date(1, ql.January, 2023) + self.assertEqual(ql.ECB.nextCode(before), "FEB23") + # nextCode also accepts an ECB code + self.assertEqual(ql.ECB.nextCode("FEB23"), "MAR23") + + +if __name__ == "__main__": + print("testing QuantLib", ql.__version__) + unittest.main(verbosity=2) diff --git a/Python/test/test_equityindex.py b/Python/test/test_equityindex.py index e855bff7..a253384a 100644 --- a/Python/test/test_equityindex.py +++ b/Python/test/test_equityindex.py @@ -1,25 +1,24 @@ -""" - Copyright (C) 2023 Marcin Rybacki +# Copyright (C) 2023 Marcin Rybacki - This file is part of QuantLib, a free-software/open-source library - for financial quantitative analysts and developers - http://quantlib.org/ +# 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 - . +# 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. - 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. -""" import QuantLibAAD as ql import unittest -EPSILON = 1.e-2 +EPSILON = 1.0e-2 CAL = ql.TARGET() DCT = ql.Actual365Fixed() @@ -27,8 +26,7 @@ def flat_rate(rate): - return ql.FlatForward( - 2, CAL, ql.makeQuoteHandle(rate), DCT) + return ql.FlatForward(2, CAL, ql.makeQuoteHandle(rate), DCT) class EquityIndexTest(unittest.TestCase): @@ -39,9 +37,15 @@ def setUp(self): self.dividend_handle = ql.YieldTermStructureHandle(flat_rate(0.01)) spot_handle = ql.makeQuoteHandle(8690.0) - ql.IndexManager.instance().clearHistory("eq_idx") + ql.IndexManager.instance().clearHistories() self.equity_idx = ql.EquityIndex( - "eq_idx", CAL, ql.Currency(), self.interest_handle, self.dividend_handle, spot_handle) + "eq_idx", + CAL, + ql.Currency(), + self.interest_handle, + self.dividend_handle, + spot_handle, + ) def test_equity_index_inspectors(self): """Testing equity index inspectors""" @@ -55,13 +59,15 @@ def test_equity_index_projections(self): fail_msg = "Failed to calculate the expected index projection." self.assertAlmostEqual( - self.equity_idx.fixing(VALUATION_DATE), 8690.0, delta=EPSILON, msg=fail_msg) + self.equity_idx.fixing(VALUATION_DATE), 8690.0, delta=EPSILON, msg=fail_msg + ) future_dt = ql.Date(20, ql.May, 2030) self.assertAlmostEqual( - self.equity_idx.fixing(future_dt), 10055.76, delta=EPSILON, msg=fail_msg) + self.equity_idx.fixing(future_dt), 10055.76, delta=EPSILON, msg=fail_msg + ) -if __name__ == '__main__': +if __name__ == "__main__": print("testing QuantLib", ql.__version__) unittest.main(verbosity=2) diff --git a/Python/test/test_fxforward.py b/Python/test/test_fxforward.py new file mode 100644 index 00000000..17c1a118 --- /dev/null +++ b/Python/test/test_fxforward.py @@ -0,0 +1,315 @@ +import math +import pytest +import QuantLibAAD as ql + +# PyTest fixture for common setup (analogous to the C++ CommonVars struct) +@pytest.fixture +def common_vars(): + # Set evaluation date + today = ql.Date(15, ql.March, 2024) + ql.Settings.instance().evaluationDate = today + + # Define maturity date 6 months from today + maturity_date = today + ql.Period(6, ql.Months) + + # Currencies + usd = ql.USDCurrency() + sgd = ql.SGDCurrency() + + # Flat yield term structures for USD and SGD discount curves + # (using flat forward rates of 5.0% for USD and 3.5% for SGD) + usd_curve = ql.FlatForward(today, 0.05, ql.Actual365Fixed()) + sgd_curve = ql.FlatForward(today, 0.035, ql.Actual365Fixed()) + usd_curve_handle = ql.RelinkableYieldTermStructureHandle(usd_curve) + sgd_curve_handle = ql.RelinkableYieldTermStructureHandle(sgd_curve) + + # Spot FX quote: 1 USD = 1.35 SGD + spot_quote = ql.SimpleQuote(1.35) + spot_handle = ql.RelinkableQuoteHandle(spot_quote) + + # Tolerance for floating-point comparisons + tolerance = 1.0e-6 + + # Return all common variables in a dict for use by tests + return { + "today": today, + "maturity_date": maturity_date, + "usd": usd, + "sgd": sgd, + "usd_curve_handle": usd_curve_handle, + "sgd_curve_handle": sgd_curve_handle, + "spot_quote": spot_quote, + "spot_handle": spot_handle, + "tolerance": tolerance + } + +def test_fx_forward_construction(common_vars): + """Testing FX forward construction with two nominal amounts.""" + vars = common_vars + usd_nominal = 1_000_000.0 + sgd_nominal = 1_350_000.0 + + # Create an FX forward (pay USD, receive SGD) with specified nominals + fwd = ql.FxForward(usd_nominal, vars["usd"], sgd_nominal, vars["sgd"], + vars["maturity_date"], True) # paySourceCurrency=True means paying source (USD) + + # Check that attributes match the inputs + assert float(fwd.sourceNominal()) == pytest.approx(float(usd_nominal), rel=vars["tolerance"]) + assert float(fwd.targetNominal()) == pytest.approx(float(sgd_nominal), rel=vars["tolerance"]) + assert fwd.sourceCurrency() == vars["usd"] + assert fwd.targetCurrency() == vars["sgd"] + assert fwd.maturityDate() == vars["maturity_date"] + assert fwd.paySourceCurrency() is True # paying the source currency (USD) + assert fwd.isExpired() is False # not expired since maturity is in the future + +def test_fx_forward_construction_with_rate(common_vars): + """Testing FX forward construction using a forward rate instead of explicit target nominal.""" + vars = common_vars + nominal = 1_000_000.0 + forward_rate = 1.36 # forward FX rate (SGD per USD) + + # Create an FX forward (sell USD) by specifying a forward rate instead of target nominal. + # paySourceCurrency=True -> paying USD, receiving SGD. + fwd = ql.FxForward(nominal, vars["usd"], vars["sgd"], forward_rate, + vars["maturity_date"], True) + + # The target nominal should be nominal * forward_rate (i.e., 1,000,000 * 1.36) + expected_target_nominal = nominal * forward_rate + assert float(fwd.sourceNominal()) == pytest.approx(float(nominal), rel=vars["tolerance"]) + assert float(fwd.targetNominal()) == pytest.approx(float(expected_target_nominal), rel=1e-6) + assert fwd.sourceCurrency() == vars["usd"] + assert fwd.targetCurrency() == vars["sgd"] + # fwd.forwardRate() should equal the input forward_rate + assert float(fwd.forwardRate()) == pytest.approx(float(forward_rate), rel=1e-10) + +def test_contracted_forward_rate(common_vars): + """Testing that the FX forward reports the contracted forward rate correctly.""" + vars = common_vars + usd_nominal = 1_000_000.0 + sgd_nominal = 1_350_000.0 + expected_rate = sgd_nominal / usd_nominal # expected contracted rate = 1.35 + + # FX forward constructed with explicit nominals + fwd1 = ql.FxForward(usd_nominal, vars["usd"], sgd_nominal, vars["sgd"], + vars["maturity_date"], True) + assert float(fwd1.forwardRate()) == pytest.approx(float(expected_rate), rel=1e-10) + + # FX forward constructed using a forward rate + input_rate = 1.36 + fwd2 = ql.FxForward(usd_nominal, vars["usd"], vars["sgd"], input_rate, + vars["maturity_date"], True) + assert float(fwd2.forwardRate()) == pytest.approx(float(input_rate), rel=1e-10) + + # Now attach a pricing engine and compare contracted vs fair forward rate + engine = ql.DiscountingFxForwardEngine(vars["usd_curve_handle"], vars["sgd_curve_handle"], vars["spot_handle"]) + fwd1.setPricingEngine(engine) + contracted_rate = fwd1.forwardRate() # contractual forward rate (still 1.35 in fwd1) + fair_rate = fwd1.fairForwardRate() # fair forward rate given curves and spot + # The contracted rate should generally differ from the fair forward rate (unless the deal was struck at fair value) + assert float(contracted_rate) != pytest.approx(float(fair_rate), rel=1e-12) + +def test_fx_forward_expiry(common_vars): + """Testing FX forward expiry flag for past maturity dates.""" + vars = common_vars + past_date = vars["today"] - ql.Period(1, ql.Days) + # Create a forward with maturity in the past (yesterday) + expired_fwd = ql.FxForward(1_000_000.0, vars["usd"], 1_350_000.0, vars["sgd"], + past_date, True) + assert expired_fwd.isExpired() is True + +def test_discounting_fx_forward_engine(common_vars): + """Testing that the DiscountingFxForwardEngine produces an NPV and fair forward rate.""" + vars = common_vars + usd_nominal = 1_000_000.0 + sgd_nominal = 1_350_000.0 + + fwd = ql.FxForward(usd_nominal, vars["usd"], sgd_nominal, vars["sgd"], + vars["maturity_date"], True) # pay USD, receive SGD + engine = ql.DiscountingFxForwardEngine(vars["usd_curve_handle"], vars["sgd_curve_handle"], vars["spot_handle"]) + fwd.setPricingEngine(engine) + + # Ensure NPV is calculated (should not be "null") + npv = fwd.NPV() + assert npv is not None and math.isfinite(npv) # NPV should be a finite number + # Ensure fair forward rate is positive + fair_rate = fwd.fairForwardRate() + assert fair_rate > 0.0 + +def test_fair_forward_rate(common_vars): + """Testing fair forward rate calculation vs. manual expected value.""" + vars = common_vars + usd_nominal = 1_000_000.0 + sgd_nominal = 1_350_000.0 + + fwd = ql.FxForward(usd_nominal, vars["usd"], sgd_nominal, vars["sgd"], + vars["maturity_date"], True) + engine = ql.DiscountingFxForwardEngine(vars["usd_curve_handle"], vars["sgd_curve_handle"], vars["spot_handle"]) + fwd.setPricingEngine(engine) + + # Calculate the expected fair forward rate: Spot * (DF_sgd / DF_usd), + # where DF_x = discount factor from settlement date to maturity for that currency. + settlement_date = fwd.settlementDate() + spot_fx = vars["spot_quote"].value() + df_usd = vars["usd_curve_handle"].discount(vars["maturity_date"]) / vars["usd_curve_handle"].discount(settlement_date) + df_sgd = vars["sgd_curve_handle"].discount(vars["maturity_date"]) / vars["sgd_curve_handle"].discount(settlement_date) + expected_fair_rate = spot_fx * (df_usd / df_sgd) + + calculated_fair_rate = fwd.fairForwardRate() + # The calculated fair rate should closely match the expected formula value + assert float(calculated_fair_rate) == pytest.approx(float(expected_fair_rate), rel=1e-4) + +def test_at_the_money(common_vars): + """Testing that an at-the-money FX forward (strike = fair forward rate) has zero NPV.""" + vars = common_vars + spot_fx = vars["spot_quote"].value() + usd_nominal = 1_000_000.0 + + # Determine settlement date (for a default 2-day spot settlement forward). + temp_fwd = ql.FxForward(usd_nominal, vars["usd"], usd_nominal, vars["sgd"], + vars["maturity_date"], True) # temporary to get settlementDate + settlement_date = temp_fwd.settlementDate() + + # Compute discount factors from settlement to maturity for USD and SGD + df_usd = vars["usd_curve_handle"].discount(vars["maturity_date"]) / vars["usd_curve_handle"].discount(settlement_date) + df_sgd = vars["sgd_curve_handle"].discount(vars["maturity_date"]) / vars["sgd_curve_handle"].discount(settlement_date) + fair_forward_rate = spot_fx * (df_sgd / df_usd) # fair forward FX rate for reference + + # Compute the SGD nominal that makes the forward at-the-money: target_nominal = source_nominal * df_usd * spot / df_sgd + sgd_nominal_atm = usd_nominal * df_usd * spot_fx / df_sgd + + # Create an at-the-money forward (pay USD, receive SGD at the fair forward rate) + fwd_atm = ql.FxForward(usd_nominal, vars["usd"], sgd_nominal_atm, vars["sgd"], + vars["maturity_date"], True) + fwd_atm.setPricingEngine(ql.DiscountingFxForwardEngine(vars["usd_curve_handle"], vars["sgd_curve_handle"], vars["spot_handle"])) + + # NPV should be approximately zero for an ATM forward + npv_atm = fwd_atm.NPV() + assert abs(npv_atm) < 1.0e-4 # allow a tiny tolerance around zero + +def test_position_direction(common_vars): + """Testing that long vs. short positions yield opposite NPVs.""" + vars = common_vars + usd_nominal = 1_000_000.0 + sgd_nominal = 1_350_000.0 + + # Long USD position: pay SGD, receive USD (paySourceCurrency=False means paying target currency) + fwd_long_usd = ql.FxForward(usd_nominal, vars["usd"], sgd_nominal, vars["sgd"], + vars["maturity_date"], False) + # Short USD position: pay USD, receive SGD (paySourceCurrency=True) + fwd_short_usd = ql.FxForward(usd_nominal, vars["usd"], sgd_nominal, vars["sgd"], + vars["maturity_date"], True) + + engine = ql.DiscountingFxForwardEngine(vars["usd_curve_handle"], vars["sgd_curve_handle"], vars["spot_handle"]) + fwd_long_usd.setPricingEngine(engine) + fwd_short_usd.setPricingEngine(engine) + + npv_long = fwd_long_usd.NPV() + npv_short = fwd_short_usd.NPV() + # The NPVs should be opposites (long vs short have equal and opposite value) + assert float(npv_long) == pytest.approx(float(-npv_short), rel=1e-4) + +def test_ir_curve_sensitivity(common_vars): + """Testing FX forward NPV sensitivity to interest rate curve shifts.""" + vars = common_vars + usd_nominal = 1_000_000.0 + sgd_nominal = 1_350_000.0 + + fwd = ql.FxForward(usd_nominal, vars["usd"], sgd_nominal, vars["sgd"], + vars["maturity_date"], True) # pay USD, receive SGD + engine = ql.DiscountingFxForwardEngine(vars["usd_curve_handle"], vars["sgd_curve_handle"], vars["spot_handle"]) + fwd.setPricingEngine(engine) + + npv_base = fwd.NPV() + # Bump USD interest rates up by 10 bps (0.05 -> 0.051) + vars["usd_curve_handle"].linkTo(ql.FlatForward(vars["today"], 0.051, ql.Actual365Fixed())) + npv_usd_up = fwd.NPV() + + # Reset USD curve and bump SGD rates up by 10 bps (0.035 -> 0.036) + vars["usd_curve_handle"].linkTo(ql.FlatForward(vars["today"], 0.05, ql.Actual365Fixed())) + vars["sgd_curve_handle"].linkTo(ql.FlatForward(vars["today"], 0.036, ql.Actual365Fixed())) + npv_sgd_up = fwd.NPV() + + # When paying USD (source) and receiving SGD (target): + # - Higher USD rates -> USD leg PV (payment) is discounted more -> less negative -> NPV increases. + # - Higher SGD rates -> SGD leg PV (receipt) is discounted more -> less positive -> NPV decreases. + assert npv_usd_up > npv_base + assert npv_sgd_up < npv_base + +def test_spot_fx_sensitivity(common_vars): + """Testing FX forward NPV sensitivity to spot FX changes.""" + vars = common_vars + usd_nominal = 1_000_000.0 + sgd_nominal = 1_350_000.0 + + fwd = ql.FxForward(usd_nominal, vars["usd"], sgd_nominal, vars["sgd"], + vars["maturity_date"], True) # pay USD, receive SGD + fwd.setPricingEngine(ql.DiscountingFxForwardEngine(vars["usd_curve_handle"], vars["sgd_curve_handle"], vars["spot_handle"])) + + npv_base = fwd.NPV() + + # Increase spot FX to 1.40 (USD strengthens against SGD) + vars["spot_handle"].linkTo(ql.SimpleQuote(1.40)) + npv_spot_up = fwd.NPV() + + # Decrease spot FX to 1.30 (USD weakens against SGD) + vars["spot_handle"].linkTo(ql.SimpleQuote(1.30)) + npv_spot_down = fwd.NPV() + + # Spot quote convention: spot = SGD per USD. + # If USD strengthens (spot up), the SGD payoff is worth less in USD terms -> NPV should drop. + # If USD weakens (spot down), the SGD payoff is worth more in USD terms -> NPV should rise. + assert npv_spot_up < npv_base + assert npv_spot_down > npv_base + +@pytest.mark.skip(reason="QuantLib-Python does not expose `additionalResults` for instruments[2](https://quant.stackexchange.com/questions/27837/calculating-the-greeks-for-quantlib-python-swaptions).") +def test_additional_results(common_vars): + """Testing FX forward additional results (internal engine outputs) - not directly available in Python.""" + vars = common_vars + # In C++, fwd.additionalResults() provides keys like "spotFx", "sourceCurrencyDiscountFactor", etc. + # Since this method isn't exposed in Python, this test is skipped. As a workaround, one could manually + # compute and verify those values if needed (see note below). + pass + +def test_settlement_days(common_vars): + """Testing FX forward settlementDays attribute without a specific calendar.""" + vars = common_vars + usd_nominal = 1_000_000.0 + sgd_nominal = 1_350_000.0 + + # Overnight (O/N) forward: 0 settlement days + fwd_on = ql.FxForward(usd_nominal, vars["usd"], sgd_nominal, vars["sgd"], + vars["maturity_date"], True, 0) + assert fwd_on.settlementDays() == 0 + assert fwd_on.settlementDate() == vars["today"] # 0 days -> settlement today + + # Tom/Next (T/N) forward: 1 settlement day + fwd_tn = ql.FxForward(usd_nominal, vars["usd"], sgd_nominal, vars["sgd"], + vars["maturity_date"], True, 1) + assert fwd_tn.settlementDays() == 1 + assert fwd_tn.settlementDate() == vars["today"] + ql.Period(1, ql.Days) + + # Spot forward (S/N): 2 settlement days (default) + fwd_sn = ql.FxForward(usd_nominal, vars["usd"], sgd_nominal, vars["sgd"], + vars["maturity_date"], True) # default settlementDays=2 + assert fwd_sn.settlementDays() == 2 + assert fwd_sn.settlementDate() == vars["today"] + ql.Period(2, ql.Days) + +def test_settlement_days_with_calendar(common_vars): + """Testing FX forward settlement with a calendar (business day adjustment).""" + vars = common_vars + usd_nominal = 1_000_000.0 + sgd_nominal = 1_350_000.0 + + cal = ql.TARGET() # calendar that observes weekends + # Choose an evaluation date that is a Friday (15 March 2024 is Friday) + friday = ql.Date(15, ql.March, 2024) + ql.Settings.instance().evaluationDate = friday + + # Forward with 2 settlement days using a calendar: settlement should skip the weekend + fwd = ql.FxForward(usd_nominal, vars["usd"], sgd_nominal, vars["sgd"], + vars["maturity_date"], True, 2, cal) + expected_settlement = cal.advance(friday, ql.Period(2, ql.Days)) + assert fwd.settlementDate() == expected_settlement + + # Restore evaluation date + ql.Settings.instance().evaluationDate = vars["today"] diff --git a/Python/test/test_iborindex.py b/Python/test/test_iborindex.py index bd0aa4c1..e5230e3c 100644 --- a/Python/test/test_iborindex.py +++ b/Python/test/test_iborindex.py @@ -84,6 +84,40 @@ def testTimeSeries(self): self.assertTrue(expected == actual) +class BMAIndexTest(unittest.TestCase): + def setUp(self): + self.index = ql.BMAIndex() + + def testInspectors(self): + """Testing BMA index inspectors""" + + self.assertEqual(self.index.familyName(), "BMA") + self.assertEqual(self.index.tenor(), ql.Period(1, ql.Weeks)) + self.assertEqual(self.index.fixingDays(), 1) + self.assertEqual(self.index.currency(), ql.USDCurrency()) + + def testFixingDates(self): + """Testing BMA fixing-date logic""" + + self.assertTrue(self.index.isValidFixingDate(ql.Date(4, 1, 2023))) + self.assertFalse(self.index.isValidFixingDate(ql.Date(5, 1, 2023))) + + def testMaturityDate(self): + """Testing BMA maturity-date calculation""" + + self.assertEqual( + self.index.maturityDate(ql.Date(5, 1, 2023)), + ql.Date(12, 1, 2023)) + + def testFixingSchedule(self): + """Testing BMA fixing schedule""" + + schedule = self.index.fixingSchedule( + ql.Date(5, 1, 2023), ql.Date(20, 1, 2023)) + self.assertEqual(schedule[0], ql.Date(4, 1, 2023)) + self.assertEqual(schedule[-1], ql.Date(25, 1, 2023)) + + if __name__ == "__main__": print("testing QuantLib", ql.__version__) unittest.main(verbosity=2) diff --git a/Python/test/test_inflation.py b/Python/test/test_inflation.py index 7c217bb8..e5719f67 100644 --- a/Python/test/test_inflation.py +++ b/Python/test/test_inflation.py @@ -34,7 +34,7 @@ import QuantLibAAD as ql -EPSILON = 1.e-9 +EPSILON = 1.e-8 # Hypothetical market data EUR_ZERO_RATES = [(ql.Period(1, ql.Days), 0.0048), diff --git a/Python/test/test_marketelements.py b/Python/test/test_marketelements.py index c86132ae..94acbada 100644 --- a/Python/test/test_marketelements.py +++ b/Python/test/test_marketelements.py @@ -30,6 +30,8 @@ FOR A PARTICULAR PURPOSE. See the license for more details. """ +import math +import operator import QuantLibAAD as ql import unittest @@ -99,6 +101,42 @@ def test_SimpleQuote(self): for value in (100, 2.71, 10**100): self.assertAlmostEqual(ql.SimpleQuote(value).value(), value) + def test_DerivedQuote(self): + sq = ql.SimpleQuote() + for fn in (math.sqrt, math.exp, math.sin): + dq = ql.DerivedQuote(ql.QuoteHandle(sq), fn) + for value in (12, 23, 34): + sq.setValue(value) + self.assertAlmostEqual(dq.value(), fn(value)) + + def test_CompositeQuote(self): + sq1 = ql.SimpleQuote() + sq2 = ql.SimpleQuote() + for fn in (operator.add, operator.mul, math.hypot): + cq = ql.CompositeQuote(ql.QuoteHandle(sq1), ql.QuoteHandle(sq2), fn) + for value in (12, 23, 34): + sq1.setValue(value) + sq2.setValue(value + 1) + self.assertAlmostEqual(cq.value(), fn(value, value + 1)) + self.assertEqual(cq.value1(), value) + self.assertEqual(cq.value2(), value + 1) + + def test_MultiCompositeQuote(self): + for fn in (math.fsum, math.prod, lambda xs: sum(x*x for x in xs)**0.5): + quotes = [] + handles = [] + for i in range(3): + quotes.append(ql.SimpleQuote(i + 1)) + handles.append(ql.QuoteHandle(quotes[-1])) + cq = ql.MultiCompositeQuote(ql.QuoteHandleVector(handles), fn) + for j in range(i + 1): + quotes[j].setValue(j * 10 + 1) + self.assertAlmostEqual(cq.value(), fn([q.value() for q in quotes])) + self.assertEqual( + list(map(cq.inputValue, range(len(quotes)))), + [q.value() for q in quotes], + ) + if __name__ == "__main__": print("testing QuantLib", ql.__version__) diff --git a/Python/test/test_mtmcrosscurrencybasisswap.py b/Python/test/test_mtmcrosscurrencybasisswap.py new file mode 100644 index 00000000..94f1bce0 --- /dev/null +++ b/Python/test/test_mtmcrosscurrencybasisswap.py @@ -0,0 +1,168 @@ +# Copyright (C) 2026 Kyrylo Protsenko + +# This file is part of QuantLib, a free-software/open-source library +# for financial quantitative analysts and developers - http://quantlib.org/ + +import unittest + +import QuantLibAAD as ql + + +class MtMCrossCurrencyBasisSwapTest(unittest.TestCase): + + def setUp(self): + self.today = ql.Date(15, ql.January, 2025) + ql.Settings.instance().evaluationDate = self.today + self.calendar = ql.TARGET() + self.curve = ql.YieldTermStructureHandle( + ql.FlatForward(self.today, 0.02, ql.Actual365Fixed())) + self.eur_index = ql.Euribor3M(self.curve) + self.usd_index = ql.USDLibor(ql.Period(3, ql.Months), self.curve) + + def tearDown(self): + ql.Settings.instance().evaluationDate = ql.Date() + + def testFxResetCashFlowsAndPricer(self): + """Testing FX-reset values, cash flows and their discounting pricer...""" + convention = ql.FxResetConvention(2, self.calendar) + value_date = ql.Date(17, ql.January, 2025) + reset = convention.reset(value_date) + + self.assertEqual(reset.fixingDate(), self.today) + self.assertEqual(reset.valueDate(), value_date) + self.assertEqual(convention.valueDate(reset.fixingDate()), value_date) + + pricer = ql.DiscountingFxResetPricer( + ql.EURCurrency(), ql.USDCurrency(), self.curve, self.curve, + ql.makeQuoteHandle(1.10), True) + self.assertAlmostEqual(pricer.fxRate(reset), 1.10) + + exchange = ql.FxResetNotionalExchange( + value_date, 100.0, None, reset) + self.assertFalse(exchange.hasPreviousReset()) + self.assertTrue(exchange.hasCurrentReset()) + self.assertEqual(exchange.currentReset().fixingDate(), self.today) + + exchange.setFxResetPricer(pricer) + self.assertAlmostEqual(exchange.amount(), -110.0) + + def testSwapAndRateHelperExposeFxResetConventions(self): + """Testing MtM swap and rate-helper FX-reset conventions...""" + start = ql.Date(17, ql.January, 2025) + end = ql.Date(17, ql.January, 2026) + schedule = ql.Schedule( + start, end, ql.Period(3, ql.Months), self.calendar, + ql.Following, ql.Following, ql.DateGeneration.Forward, False) + swap = ql.MtMCrossCurrencyBasisSwap( + ql.MtMCrossCurrencyBasisSwap.PayFxBaseCurrency, + 100.0, ql.EURCurrency(), schedule, self.eur_index, 0.0, 1.0, + 110.0, ql.USDCurrency(), schedule, self.usd_index, 0.0, 1.0, + True, 2, self.calendar, 2, 3, + ql.ModifiedFollowing, ql.Preceding) + + self.assertEqual(swap.fxResetFixingDays(), 2) + self.assertEqual(swap.fxBasePaymentConvention(), ql.ModifiedFollowing) + self.assertEqual(swap.fxQuotePaymentConvention(), ql.Preceding) + self.assertEqual(swap.resettingLegIndex(), 0) + self.assertEqual( + len(swap.resettingLeg()), 2 * (len(schedule) - 1) + 1) + + coupons = [ql.as_fx_reset_coupon(cf) for cf in swap.resettingLeg()] + coupons = [coupon for coupon in coupons if coupon] + self.assertTrue(coupons) + exchanges = [ + ql.as_fx_reset_notional_exchange(cf) + for cf in swap.resettingLeg()] + self.assertTrue(any(exchange for exchange in exchanges)) + first_coupon = coupons[0] + self.assertEqual( + first_coupon.fxResetValueDate(), first_coupon.accrualStartDate()) + self.assertEqual( + first_coupon.fxResetDate(), + self.calendar.advance( + first_coupon.fxResetValueDate(), -2, ql.Days)) + + swap.setPricingEngine(ql.DiscountingMtMCrossCurrencyBasisSwapEngine( + ql.USDCurrency(), self.curve, ql.EURCurrency(), self.curve, + ql.makeQuoteHandle(1.10))) + reset_rates = swap.fxResetRates() + reset_notionals = swap.fxResetNotionals() + self.assertEqual(len(reset_rates), len(coupons)) + self.assertEqual(len(reset_notionals), len(coupons)) + self.assertEqual(swap.legCurrency(0), ql.EURCurrency()) + self.assertEqual(swap.legCurrency(1), ql.USDCurrency()) + self.assertAlmostEqual(swap.legNPV(0), 1.10 * swap.inCcyLegNPV(0)) + self.assertAlmostEqual(swap.legNPV(1), swap.inCcyLegNPV(1)) + for rate, notional in zip(reset_rates, reset_notionals): + self.assertAlmostEqual(rate, 1.0 / 1.10) + self.assertAlmostEqual(notional, 100.0) + + pricer = ql.DiscountingFxResetPricer( + ql.USDCurrency(), ql.EURCurrency(), self.curve, self.curve, + ql.makeQuoteHandle(1.0), True) + ql.setFxResetPricer(swap.resettingLeg(), pricer) + self.assertTrue(first_coupon.fxResetPricer()) + + helper = ql.MtMCrossCurrencyBasisSwapRateHelper( + ql.makeQuoteHandle(-0.001), ql.Period(1, ql.Years), 2, + self.calendar, ql.Following, False, self.eur_index, + self.usd_index, self.curve, False, True, True, + ql.NoFrequency, 0, ql.NoFrequency, 2, self.calendar) + self.assertEqual(helper.fxResetFixingDays(), 2) + self.assertEqual(helper.swap().fxResetFixingDays(), 2) + self.assertEqual(helper.swap().legCurrency(0), ql.EURCurrency()) + self.assertEqual(helper.swap().legCurrency(1), ql.USDCurrency()) + self.assertTrue(hasattr(helper.swap(), 'inCcyLegNPV')) + + def testIndexedCouponArguments(self): + """Testing indexed-coupon arguments for cross-currency swaps...""" + start = ql.Date(17, ql.January, 2025) + end = ql.Date(17, ql.January, 2026) + schedule = ql.Schedule( + start, end, ql.Period(3, ql.Months), self.calendar, + ql.Following, ql.Following, ql.DateGeneration.Forward, False) + + fixed_vs_floating_swap = ( + ql.ConstNotionalCrossCurrencyFixedVsFloatingSwap( + ql.Swap.Payer, + 100.0, ql.EURCurrency(), schedule, 0.02, + ql.Actual365Fixed(), ql.Following, 0, self.calendar, + 110.0, ql.USDCurrency(), schedule, self.usd_index, 0.0, + ql.Following, 0, self.calendar, + useIndexedCoupons=True)) + basis_swap = ql.ConstNotionalCrossCurrencyBasisSwap( + 100.0, ql.EURCurrency(), schedule, self.eur_index, 0.0, 1.0, + 110.0, ql.USDCurrency(), schedule, self.usd_index, 0.0, 1.0, + useIndexedCoupons=False) + mtm_swap = ql.MtMCrossCurrencyBasisSwap( + ql.MtMCrossCurrencyBasisSwap.PayFxBaseCurrency, + 100.0, ql.EURCurrency(), schedule, self.eur_index, 0.0, 1.0, + 110.0, ql.USDCurrency(), schedule, self.usd_index, 0.0, 1.0, + False, useIndexedCoupons=True) + + quote = ql.makeQuoteHandle(-0.001) + tenor = ql.Period(1, ql.Years) + fixed_vs_floating_helper = ( + ql.ConstNotionalCrossCurrencySwapRateHelper( + ql.makeQuoteHandle(0.02), tenor, 2, self.calendar, + ql.Following, False, ql.Annual, ql.Actual365Fixed(), + self.usd_index, self.curve, False, 0, True)) + basis_helper = ql.ConstNotionalCrossCurrencyBasisSwapRateHelper( + quote, tenor, 2, self.calendar, ql.Following, False, + self.eur_index, self.usd_index, self.curve, False, True, + ql.Semiannual, 0, ql.Semiannual, False) + mtm_helper = ql.MtMCrossCurrencyBasisSwapRateHelper( + quote, tenor, 2, self.calendar, ql.Following, False, + self.eur_index, self.usd_index, self.curve, False, True, False, + ql.Semiannual, 0, ql.Semiannual, 0, self.calendar, True) + + self.assertTrue(fixed_vs_floating_swap) + self.assertTrue(basis_swap) + self.assertTrue(mtm_swap) + self.assertTrue(fixed_vs_floating_helper.swap()) + self.assertTrue(basis_helper.swap()) + self.assertTrue(mtm_helper.swap()) + + +if __name__ == '__main__': + unittest.main() diff --git a/Python/test/test_multicurve.py b/Python/test/test_multicurve.py new file mode 100644 index 00000000..ecb06f79 --- /dev/null +++ b/Python/test/test_multicurve.py @@ -0,0 +1,87 @@ +""" + Copyright (C) 2026 Kyrylo Protsenko + + 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. +""" + +import unittest + +import QuantLibAAD as ql + + +class MultiCurveTest(unittest.TestCase): + + def test_piecewise_and_spreaded_curve(self): + """Globally bootstrap a dependency cycle of two yield curves.""" + today = ql.Date(23, ql.October, 2025) + ql.Settings.instance().evaluationDate = today + + internal_ois = ql.RelinkableYieldTermStructureHandle() + internal_3m = ql.RelinkableYieldTermStructureHandle() + euribor_3m = ql.Euribor3M(internal_3m) + + quote = ql.SimpleQuote(0.03) + spread = ql.SimpleQuote(-0.01) + helpers = [] + for maturity in range(1, 11): + helpers.append( + ql.SwapRateHelper( + ql.QuoteHandle(quote), + ql.Period(maturity, ql.Years), + euribor_3m.fixingCalendar(), + ql.Annual, + ql.Following, + ql.Thirty360(ql.Thirty360.BondBasis), + euribor_3m, + ql.QuoteHandle(), + ql.Period(0, ql.Days), + internal_ois, + ) + ) + + accuracy = 1.0e-10 + multi_curve = ql.MultiCurve(accuracy) + jump = ql.SimpleQuote(0.99) + curve_3m = ql.GlobalPiecewiseLogLinearDiscount( + today, + helpers, + ql.Actual360(), + ql.GlobalBootstrap(accuracy), + [ql.QuoteHandle(jump)], + [ql.Date(31, ql.December, 2027)], + ) + external_3m = multi_curve.addBootstrappedCurve(internal_3m, curve_3m) + + curve_ois = ql.ZeroSpreadedTermStructure( + internal_3m, ql.QuoteHandle(spread) + ) + external_ois = multi_curve.addNonBootstrappedCurve(internal_ois, curve_ois) + + # The external handles keep the MultiCurve and all its members alive. + del multi_curve, curve_3m, curve_ois + + forecast = external_3m.currentLink() + discount = external_ois.currentLink() + self.assertAlmostEqual( + discount.zeroRate(1.0, ql.Continuous).rate() + - forecast.zeroRate(1.0, ql.Continuous).rate(), + spread.value(), + delta=accuracy, + ) + for helper in helpers: + self.assertAlmostEqual(helper.quoteError(), 0.0, delta=accuracy) + + +if __name__ == "__main__": + unittest.main() diff --git a/Python/test/test_ratehelpers.py b/Python/test/test_ratehelpers.py index 7eeb137d..1d5c8c7b 100644 --- a/Python/test/test_ratehelpers.py +++ b/Python/test/test_ratehelpers.py @@ -83,6 +83,37 @@ def tearDown(self): ql.Settings.instance().evaluationDate = ql.Date() +class BMASwapRateHelperTest(unittest.TestCase): + def setUp(self): + self.reference_date = ql.Date(4, 1, 2023) + ql.Settings.instance().evaluationDate = self.reference_date + + def testConstruction(self): + """Testing BMA swap rate-helper construction""" + + forecast_curve = ql.YieldTermStructureHandle( + ql.FlatForward( + self.reference_date, + ql.makeQuoteHandle(0.03), + ql.Actual360())) + helper = ql.BMASwapRateHelper( + ql.makeQuoteHandle(0.75), + ql.Period(5, ql.Years), + 2, + ql.UnitedStates(ql.UnitedStates.GovernmentBond), + ql.Period(1, ql.Weeks), + ql.Following, + ql.ActualActual(ql.ActualActual.ISDA), + ql.BMAIndex(), + ql.USDLibor(ql.Period(1, ql.Months), forecast_curve)) + + self.assertTrue(helper.earliestDate() < helper.latestDate()) + self.assertEqual(helper.quote().value(), 0.75) + + def tearDown(self): + ql.Settings.instance().evaluationDate = ql.Date() + + class OISRateHelperTest(unittest.TestCase): def setUp(self): diff --git a/Python/test/test_swap.py b/Python/test/test_swap.py index 42459eb2..abdaaa8c 100644 --- a/Python/test/test_swap.py +++ b/Python/test/test_swap.py @@ -1,51 +1,50 @@ -""" - Copyright (C) 2021 Marcin Rybacki - Copyright (C) 2023 Marcin Rybacki +# Copyright (C) 2021, 2023 Marcin Rybacki - This file is part of QuantLib, a free-software/open-source library - for financial quantitative analysts and developers - http://quantlib.org/ +# 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 - . +# 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. - 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. -""" import unittest import QuantLibAAD as ql -EPSILON = 1.e-8 +EPSILON = 1.0e-8 CAL = ql.TARGET() DCT = ql.Actual365Fixed() -IR_FIXINGS = [(ql.Date(3, ql.January, 2023), 0.033), - (ql.Date(4, ql.January, 2023), 0.033), - (ql.Date(5, ql.January, 2023), 0.033), - (ql.Date(6, ql.January, 2023), 0.033), - (ql.Date(9, ql.January, 2023), 0.03), - (ql.Date(10, ql.January, 2023), 0.03), - (ql.Date(11, ql.January, 2023), 0.03), - (ql.Date(12, ql.January, 2023), 0.03), - (ql.Date(13, ql.January, 2023), 0.03), - (ql.Date(17, ql.January, 2023), 0.03), - (ql.Date(20, ql.January, 2023), 0.03), - (ql.Date(23, ql.January, 2023), 0.03), - (ql.Date(24, ql.January, 2023), 0.03), - (ql.Date(25, ql.January, 2023), 0.03), - (ql.Date(26, ql.January, 2023), 0.03)] +IR_FIXINGS = [ + (ql.Date(3, ql.January, 2023), 0.033), + (ql.Date(4, ql.January, 2023), 0.033), + (ql.Date(5, ql.January, 2023), 0.033), + (ql.Date(6, ql.January, 2023), 0.033), + (ql.Date(9, ql.January, 2023), 0.03), + (ql.Date(10, ql.January, 2023), 0.03), + (ql.Date(11, ql.January, 2023), 0.03), + (ql.Date(12, ql.January, 2023), 0.03), + (ql.Date(13, ql.January, 2023), 0.03), + (ql.Date(17, ql.January, 2023), 0.03), + (ql.Date(20, ql.January, 2023), 0.03), + (ql.Date(23, ql.January, 2023), 0.03), + (ql.Date(24, ql.January, 2023), 0.03), + (ql.Date(25, ql.January, 2023), 0.03), + (ql.Date(26, ql.January, 2023), 0.03), +] def flat_rate(rate): - return ql.FlatForward( - 2, CAL, ql.makeQuoteHandle(rate), ql.Actual365Fixed()) + return ql.FlatForward(2, CAL, ql.makeQuoteHandle(rate), ql.Actual365Fixed()) class ZeroCouponSwapTest(unittest.TestCase): @@ -57,39 +56,38 @@ def setUp(self): self.engine = ql.DiscountingSwapEngine(self.nominal_ts_handle) def build_zcs_from_fixed_payment(self, amount): - return ql.ZeroCouponSwap(ql.Swap.Receiver, - 1.0e6, - ql.Date(3, ql.June, 2021), - ql.Date(3, ql.June, 2051), - amount, - self.ibor_idx, - CAL) + return ql.ZeroCouponSwap( + ql.Swap.Receiver, + 1.0e6, + ql.Date(3, ql.June, 2021), + ql.Date(3, ql.June, 2051), + amount, + self.ibor_idx, + CAL, + ) def build_zcs_from_rate(self, rate): - return ql.ZeroCouponSwap(ql.Swap.Receiver, - 1.0e6, - ql.Date(3, ql.June, 2021), - ql.Date(3, ql.June, 2051), - rate, - DCT, - self.ibor_idx, - CAL) + return ql.ZeroCouponSwap( + ql.Swap.Receiver, + 1.0e6, + ql.Date(3, ql.June, 2021), + ql.Date(3, ql.June, 2051), + rate, + DCT, + self.ibor_idx, + CAL, + ) def test_zero_coupon_swap_inspectors(self): """Testing zero coupon swap inspectors""" swap = self.build_zcs_from_fixed_payment(1.5e6) fail_msg = "Unable to replicate the properties of a ZC swap." - self.assertEqual(swap.type(), ql.Swap.Receiver, - msg=fail_msg) - self.assertEqual(swap.startDate(), ql.Date(3, ql.June, 2021), - msg=fail_msg) - self.assertEqual(swap.maturityDate(), ql.Date(3, ql.June, 2051), - msg=fail_msg) - self.assertAlmostEqual(swap.baseNominal(), 1.0e6, - delta=EPSILON, msg=fail_msg) - self.assertAlmostEqual(swap.fixedPayment(), 1.5e6, - delta=EPSILON, msg=fail_msg) + self.assertEqual(swap.type(), ql.Swap.Receiver, msg=fail_msg) + self.assertEqual(swap.startDate(), ql.Date(3, ql.June, 2021), msg=fail_msg) + self.assertEqual(swap.maturityDate(), ql.Date(3, ql.June, 2051), msg=fail_msg) + self.assertAlmostEqual(swap.baseNominal(), 1.0e6, delta=EPSILON, msg=fail_msg) + self.assertAlmostEqual(swap.fixedPayment(), 1.5e6, delta=EPSILON, msg=fail_msg) def test_npvs_of_par_zero_coupon_swap_with_fixed_payment(self): """Testing NPVs of a zero coupon swap with fixed payment""" @@ -102,8 +100,9 @@ def test_npvs_of_par_zero_coupon_swap_with_fixed_payment(self): fail_npv_msg = """ Unable to replicate par zero coupon swap NPV: calculated: {actual} expected: {expected} - """.format(actual=npv, - expected=0.0) + """.format( + actual=npv, expected=0.0 + ) self.assertAlmostEqual(npv, 0.0, delta=EPSILON, msg=fail_npv_msg) fxd_leg_npv = par_swap.fixedLegNPV() @@ -111,11 +110,12 @@ def test_npvs_of_par_zero_coupon_swap_with_fixed_payment(self): fail_legs_npv_msg = """ Unable to replicate the NPVs of a par zero coupon swap legs: fixed leg NPV: {fxd_leg} floating leg NPV: {flt_leg} - """.format(fxd_leg=fxd_leg_npv, - flt_leg=flt_leg_npv) - self.assertAlmostEqual(abs(fxd_leg_npv), abs(flt_leg_npv), - delta=EPSILON, - msg=fail_legs_npv_msg) + """.format( + fxd_leg=fxd_leg_npv, flt_leg=flt_leg_npv + ) + self.assertAlmostEqual( + abs(fxd_leg_npv), abs(flt_leg_npv), delta=EPSILON, msg=fail_legs_npv_msg + ) def test_npvs_of_par_zero_coupon_swap_with_fixed_rate(self): """Testing NPVs of a zero coupon swap with fixed rate""" @@ -128,8 +128,9 @@ def test_npvs_of_par_zero_coupon_swap_with_fixed_rate(self): fail_msg = """ Unable to replicate par zero coupon swap NPV: calculated: {actual} expected: {expected} - """.format(actual=npv, - expected=0.0) + """.format( + actual=npv, expected=0.0 + ) self.assertAlmostEqual(npv, 0.0, delta=EPSILON, msg=fail_msg) def test_zero_coupon_swap_legs(self): @@ -139,17 +140,19 @@ def test_zero_coupon_swap_legs(self): fxd_cf = ql.as_fixed_rate_coupon(fxd_leg[0]) fail_msg_fxd = """Fixed leg cash flow type should be FixedRateCoupon but was {actual}. - """.format(actual=type(fxd_cf)) - self.assertTrue(isinstance(fxd_cf, ql.FixedRateCoupon), - msg=fail_msg_fxd) + """.format( + actual=type(fxd_cf) + ) + self.assertTrue(isinstance(fxd_cf, ql.FixedRateCoupon), msg=fail_msg_fxd) flt_leg = swap.floatingLeg() - flt_cf = ql.as_sub_periods_coupon(flt_leg[0]) - fail_msg_flt = """Floating leg cash flow type should be SubPeriodsCoupon + flt_cf = ql.as_multiple_resets_coupon(flt_leg[0]) + fail_msg_flt = """Floating leg cash flow type should be MultipleResetsCoupon but was {actual}. - """.format(actual=type(flt_cf)) - self.assertTrue(isinstance( - flt_cf, ql.SubPeriodsCoupon), msg=fail_msg_flt) + """.format( + actual=type(flt_cf) + ) + self.assertTrue(isinstance(flt_cf, ql.MultipleResetsCoupon), msg=fail_msg_flt) class EquityTotalReturnSwapTest(unittest.TestCase): @@ -167,15 +170,15 @@ def setUp(self): ql.Currency(), self.interest_handle, self.dividend_handle, - equity_spot) - ql.IndexManager.instance().clearHistory(self.equity_idx.name()) + equity_spot, + ) + self.equity_idx.clearFixings() self.equity_idx.addFixing(ql.Date(5, ql.January, 2023), 9010.0) - self.ibor_idx = ql.USDLibor( - ql.Period(3, ql.Months), self.interest_handle) - ql.IndexManager.instance().clearHistory(self.ibor_idx.name()) + self.ibor_idx = ql.USDLibor(ql.Period(3, ql.Months), self.interest_handle) + self.ibor_idx.clearFixings() self.sofr_idx = ql.Sofr(self.interest_handle) - ql.IndexManager.instance().clearHistory(self.sofr_idx.name()) + self.sofr_idx.clearFixings() for f_dt, f_val in IR_FIXINGS: self.ibor_idx.addFixing(f_dt, f_val) @@ -190,14 +193,17 @@ def build_trs(self, interest_idx, start, end, margin=0.025): interest_idx.businessDayConvention(), interest_idx.businessDayConvention(), ql.DateGeneration.Backward, - False) - return ql.EquityTotalReturnSwap(ql.Swap.Receiver, - 1.0e6, - schedule, - self.equity_idx, - interest_idx, - DCT, - margin) + False, + ) + return ql.EquityTotalReturnSwap( + ql.Swap.Receiver, + 1.0e6, + schedule, + self.equity_idx, + interest_idx, + DCT, + margin, + ) def test_trs_interest_rate_index(self): """Testing equity total return swap interest rate index""" @@ -209,12 +215,14 @@ def test_trs_interest_rate_index(self): fail_msg = "Incorrect interest rate index set to TRS." - self.assertEqual(trs_vs_ibor.interestRateIndex().name(), - "USDLibor3M Actual/360", - msg=fail_msg) - self.assertEqual(trs_vs_sofr.interestRateIndex().name(), - "SOFRON Actual/360", - msg=fail_msg) + self.assertEqual( + trs_vs_ibor.interestRateIndex().name(), + "USDLibor3M Actual/360", + msg=fail_msg, + ) + self.assertEqual( + trs_vs_sofr.interestRateIndex().name(), "SOFRON Actual/360", msg=fail_msg + ) def test_trs_npv(self): """Testing equity total return swap NPV""" @@ -230,20 +238,20 @@ def test_trs_npv(self): trs_vs_sofr.setPricingEngine(pricer) par_trs_vs_ibor = self.build_trs( - self.ibor_idx, start, end, trs_vs_ibor.fairMargin()) + self.ibor_idx, start, end, trs_vs_ibor.fairMargin() + ) par_trs_vs_ibor.setPricingEngine(pricer) par_trs_vs_sofr = self.build_trs( - self.sofr_idx, start, end, trs_vs_sofr.fairMargin()) + self.sofr_idx, start, end, trs_vs_sofr.fairMargin() + ) par_trs_vs_sofr.setPricingEngine(pricer) fail_msg = "Par TRS expected to have NPV equal to zero." - self.assertAlmostEqual( - par_trs_vs_ibor.NPV(), 0.0, delta=EPSILON, msg=fail_msg) - self.assertAlmostEqual( - par_trs_vs_sofr.NPV(), 0.0, delta=EPSILON, msg=fail_msg) + self.assertAlmostEqual(par_trs_vs_ibor.NPV(), 0.0, delta=EPSILON, msg=fail_msg) + self.assertAlmostEqual(par_trs_vs_sofr.NPV(), 0.0, delta=EPSILON, msg=fail_msg) -if __name__ == '__main__': +if __name__ == "__main__": print("testing QuantLib", ql.__version__) unittest.main(verbosity=2) diff --git a/Python/test/test_termstructures.py b/Python/test/test_termstructures.py index e9bde5f9..709b32d6 100644 --- a/Python/test/test_termstructures.py +++ b/Python/test/test_termstructures.py @@ -261,6 +261,24 @@ def testTermStructureInterpolationSchemes(self): delta=1.0e-12, msg=failMsg) + def testTermStructureInterpolationCompatibility(self): + """Testing different interpolation schemes compatibility""" + args = (self.settlement, self.instruments, self.dayCounter, ql.IterativeBootstrap()) + types = { + ql.PiecewiseLogCubicDiscount: [ + ql.LogCubic, ql.MonotonicLogCubic, ql.SplineLogCubic, ql.KrugerLog, + ql.LogParabolicCubic, ql.MonotonicLogParabolicCubic + ], + ql.PiecewiseCubicZero: [ + ql.Cubic, ql.MonotonicCubic, ql.SplineCubic, ql.Kruger, + ql.ParabolicCubic, ql.MonotonicParabolicCubic + ], + } + for curve_type, inter_types in types.items(): + for interp_type in inter_types: + curve = curve_type(*args, interp_type()) + self.assertEqual(len(curve.nodes()), len(self.instruments) + 1) + def testInterpolatedPiecewiseZeroSpreadedTermStructure(self): """Testing different interpolation schemes for zero spreaded term structure""" h = ql.RelinkableYieldTermStructureHandle() diff --git a/Python/test/test_varianceswap.py b/Python/test/test_varianceswap.py new file mode 100644 index 00000000..3e9703ab --- /dev/null +++ b/Python/test/test_varianceswap.py @@ -0,0 +1,88 @@ +""" + Copyright (C) 2026 Mahimn Patel + + 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. +""" + +import unittest + +import QuantLibAAD as ql + + +class VarianceSwapTest(unittest.TestCase): + def setUp(self): + # data from "A Guide to Volatility and Variance Swaps", Derman, Kamal & Zou, 1999, + # with maturity corrected from 0.25 to 0.246575 (Jan 1, 1999 to Apr 1, 1999) + self.today = ql.Date(1, ql.January, 1999) + ql.Settings.instance().evaluationDate = self.today + self.maturity = self.today + 90 + self.day_counter = ql.Actual365Fixed() + + def tearDown(self): + ql.Settings.instance().evaluationDate = ql.Date() + + def make_process(self, vol_ts): + return ql.BlackScholesMertonProcess( + ql.QuoteHandle(ql.SimpleQuote(100.0)), + ql.YieldTermStructureHandle(ql.FlatForward(self.today, 0.0, self.day_counter)), + ql.YieldTermStructureHandle(ql.FlatForward(self.today, 0.05, self.day_counter)), + ql.BlackVolTermStructureHandle(vol_ts), + ) + + def testReplicatingVarianceSwap(self): + """Testing variance swap with replicating cost engine""" + put_strikes = [50.0, 55.0, 60.0, 65.0, 70.0, 75.0, 80.0, 85.0, 90.0, 95.0, 100.0] + put_vols = [0.30, 0.29, 0.28, 0.27, 0.26, 0.25, 0.24, 0.23, 0.22, 0.21, 0.20] + call_strikes = [100.0, 105.0, 110.0, 115.0, 120.0, 125.0, 130.0, 135.0] + call_vols = [0.20, 0.19, 0.18, 0.17, 0.16, 0.15, 0.14, 0.13] + + # the put and call strikes share the at-the-money point + vol_ts = ql.BlackVarianceSurface( + self.today, + ql.NullCalendar(), + [self.maturity], + put_strikes + call_strikes[1:], + ql.Matrix([[v] for v in put_vols + call_vols[1:]]), + self.day_counter, + ) + engine = ql.ReplicatingVarianceSwapEngine( + self.make_process(vol_ts), 5.0, call_strikes, put_strikes + ) + swap = ql.VarianceSwap(ql.Position.Long, 0.04, 50000.0, self.today, self.maturity) + swap.setPricingEngine(engine) + + self.assertAlmostEqual(swap.variance(), 0.04189, delta=1.0e-4) + + def testMCVarianceSwap(self): + """Testing variance swap with Monte Carlo engine""" + # with a variance curve, the fair variance is v*v for any intermediate + # (t1, v1) with 0 <= t1 < t and 0 <= v1 < v + vol_ts = ql.BlackVarianceCurve( + self.today, [self.today + 37, self.maturity], [0.1, 0.2], self.day_counter, True + ) + engine = ql.MCVarianceSwapEngine( + self.make_process(vol_ts), + "pseudorandom", + timeStepsPerYear=250, + requiredSamples=1023, + seed=42, + ) + swap = ql.VarianceSwap(ql.Position.Long, 0.04, 50000.0, self.today, self.maturity) + swap.setPricingEngine(engine) + + self.assertAlmostEqual(swap.variance(), 0.04, delta=3.0e-4) + + +if __name__ == "__main__": + unittest.main() diff --git a/README.md b/README.md index 445ddf6d..38096a65 100644 --- a/README.md +++ b/README.md @@ -34,7 +34,7 @@ calculations as well as your own. ### Formerly QuantLib-Risks -This package was called QuantLib-Risks up to version 1.33.3. From 1.41, `QuantLib-Risks` +This package was called QuantLib-Risks up to version 1.33.3. From 1.44, `QuantLib-Risks` is a deprecated alias that installs `QuantLibAAD` and keeps `import QuantLib_Risks` working with a `DeprecationWarning`. Please switch to `pip install QuantLibAAD` and `import QuantLibAAD`. Anyone pinned to `QuantLib-Risks==1.33.3` must also pin `xad<2`. diff --git a/SWIG/barrieroptions.i b/SWIG/barrieroptions.i index 30644b5b..b81bbd74 100644 --- a/SWIG/barrieroptions.i +++ b/SWIG/barrieroptions.i @@ -333,6 +333,9 @@ If max_steps is specified, it would limit binomial steps to this value. template class BinomialBarrierEngine : public PricingEngine { + #if !defined(SWIGJAVA) && !defined(SWIGCSHARP) + %feature("kwargs") BinomialBarrierEngine; + #endif public: BinomialBarrierEngine(const ext::shared_ptr&, Size steps, diff --git a/SWIG/basketoptions.i b/SWIG/basketoptions.i index d3139ec5..7e47481d 100644 --- a/SWIG/basketoptions.i +++ b/SWIG/basketoptions.i @@ -4,6 +4,7 @@ Copyright (C) 2005 Dominic Thuillier Copyright (C) 2007 Joseph Wang Copyright (C) 2018, 2019 Matthias Lungwitz + Copyright (C) 2020 Gorazd Brumen Copyright (C) 2024 Klaus Spanderen This file is part of QuantLib, a free-software/open-source library @@ -236,6 +237,8 @@ using QuantLib::KirkEngine; using QuantLib::BjerksundStenslandSpreadEngine; using QuantLib::OperatorSplittingSpreadEngine; using QuantLib::Fd2dBlackScholesVanillaEngine; +using QuantLib::PearsonSpreadEngine; +using QuantLib::GaussianCopulaSpreadEngine; %} %shared_ptr(StulzEngine) @@ -291,6 +294,28 @@ class Fd2dBlackScholesVanillaEngine : public PricingEngine { Real illegalLocalVolOverwrite = -Null()); }; +%shared_ptr(PearsonSpreadEngine) +class PearsonSpreadEngine : public PricingEngine { + public: + PearsonSpreadEngine( + ext::shared_ptr process1, + ext::shared_ptr process2, + Real correlation, + Real integrationTolerance = 1e-10, + Size maxIntegrationIterations = 10000, + Real nStd = 8.0); +}; + +%shared_ptr(GaussianCopulaSpreadEngine) +class GaussianCopulaSpreadEngine : public PricingEngine { + public: + GaussianCopulaSpreadEngine( + ext::shared_ptr process1, + ext::shared_ptr process2, + Real correlation, + Size nPoints = 64); +}; + %{ using QuantLib::ChoiBasketEngine; using QuantLib::DengLiZhouBasketEngine; diff --git a/SWIG/bondfunctions.i b/SWIG/bondfunctions.i index b24191d4..70f1c6a2 100644 --- a/SWIG/bondfunctions.i +++ b/SWIG/bondfunctions.i @@ -147,6 +147,12 @@ class BondFunctions { Frequency frequency, Date settlementDate = Date()); + static Real cleanPrice(const Bond& bond, + const ext::shared_ptr& discount, + Spread zSpread, + Compounding compounding, + Frequency frequency, + Date settlementDate = Date()); static Real cleanPrice(const Bond& bond, const ext::shared_ptr& discount, Spread zSpread, @@ -154,6 +160,12 @@ class BondFunctions { Compounding compounding, Frequency frequency, Date settlementDate = Date()); + static Real dirtyPrice(const Bond& bond, + const ext::shared_ptr& discount, + Spread zSpread, + Compounding compounding, + Frequency frequency, + Date settlementDate = Date()); static Real dirtyPrice(const Bond& bond, const ext::shared_ptr& discount, Spread zSpread, @@ -161,6 +173,15 @@ class BondFunctions { Compounding compounding, Frequency frequency, Date settlementDate = Date()); + static Spread zSpread(const Bond& bond, + BondPrice price, + const ext::shared_ptr& discountCurve, + Compounding compounding, + Frequency frequency, + Date settlementDate = Date(), + Real accuracy = 1.0e-10, + Size maxIterations = 100, + Rate guess = 0.0); static Spread zSpread(const Bond& bond, BondPrice price, const ext::shared_ptr& discountCurve, diff --git a/SWIG/bonds.i b/SWIG/bonds.i index 0da00bb6..c8aa53d3 100644 --- a/SWIG/bonds.i +++ b/SWIG/bonds.i @@ -31,6 +31,7 @@ %include cashflows.i %include interestrate.i %include indexes.i +%shared_ptr(Bond) %include inflation.i %include shortratemodels.i @@ -55,7 +56,6 @@ class BondPrice { bool isValid() const; }; -%shared_ptr(Bond) class Bond : public Instrument { #if defined(SWIGPYTHON) %rename(bondYield) yield; @@ -274,7 +274,8 @@ class FloatingRateBond : public Bond { const Period& exCouponPeriod = Period(), const Calendar& exCouponCalendar = Calendar(), BusinessDayConvention exCouponConvention = Unadjusted, - bool exCouponEndOfMonth = false); + bool exCouponEndOfMonth = false, + BusinessDayConvention fixingConvention = Preceding); }; diff --git a/SWIG/calendars.i b/SWIG/calendars.i index 56fb28d7..0fabb64d 100644 --- a/SWIG/calendars.i +++ b/SWIG/calendars.i @@ -5,6 +5,7 @@ Copyright (C) 2005 Johan Witters Copyright (C) 2018 Matthias Groncki Copyright (C) 2023 Skandinaviska Enskilda Banken AB (publ) + Copyright (C) 2026 Lawrenz Law This file is part of QuantLib, a free-software/open-source library for financial quantitative analysts and developers - http://quantlib.org/ @@ -63,15 +64,15 @@ using QuantLib::JoinBusinessDays; enum JointCalendarRule { JoinHolidays, JoinBusinessDays }; #if defined(SWIGPYTHON) -%typemap(in) ext::optional %{ +%typemap(in) std::optional %{ if ($input == Py_None) - $1 = ext::nullopt; + $1 = std::nullopt; else if (PyLong_Check($input)) $1 = (BusinessDayConvention)PyLong_AsLong($input); else SWIG_exception(SWIG_TypeError, "int expected"); %} -%typecheck (QL_TYPECHECK_BUSINESSDAYCONVENTION) ext::optional %{ +%typecheck (QL_TYPECHECK_BUSINESSDAYCONVENTION) std::optional %{ $1 = (PyLong_Check($input) || $input == Py_None) ? 1 : 0; %} #endif @@ -111,6 +112,14 @@ class Calendar { std::string name(); bool empty(); %extend { + std::vector addedHolidays() { + return std::vector(self->addedHolidays().begin(), + self->addedHolidays().end()); + } + std::vector removedHolidays() { + return std::vector(self->removedHolidays().begin(), + self->removedHolidays().end()); + } std::string __str__() { return self->name()+" calendar"; } @@ -172,6 +181,12 @@ namespace QuantLib { Chile(Market m = SSE); }; + class Croatia : public Calendar { + public: + enum Market { ZSE }; + Croatia(Market m = ZSE); + }; + class China : public Calendar { public: enum Market { SSE, IB }; @@ -227,7 +242,7 @@ namespace QuantLib { class Israel : public Calendar { public: - enum Market { Settlement, TASE, SHIR }; + enum Market { Settlement = 0, TASE = 1, SHIR = 2, Telbor = 3 }; Israel(Market m = Settlement); }; @@ -239,20 +254,50 @@ namespace QuantLib { class Japan : public Calendar {}; + class Malaysia : public Calendar { + public: + enum Market { KLSE }; + Malaysia(Market m = KLSE); + }; + + class Malta : public Calendar { + public: + enum Market { MSE }; + Malta(Market m = MSE); + }; + class Mexico : public Calendar { public: enum Market { BMV }; Mexico(Market m = BMV); }; + class Montenegro : public Calendar { + public: + enum Market { MNSE }; + Montenegro(Market m = MNSE); + }; + class NewZealand : public Calendar { public: enum Market { Wellington, Auckland }; NewZealand(Market m = Wellington); }; + class NorthMacedonia : public Calendar { + public: + enum Market { MSE }; + NorthMacedonia(Market m = MSE); + }; + class Norway : public Calendar {}; + class Philippines : public Calendar { + public: + enum Market { PSE }; + Philippines(Market m = PSE); + }; + class Poland : public Calendar { public: enum Market { Settlement, WSE }; @@ -277,6 +322,12 @@ namespace QuantLib { SaudiArabia(Market m = Tadawul); }; + class Serbia : public Calendar { + public: + enum Market { BSE }; + Serbia(Market m = BSE); + }; + class Singapore : public Calendar { public: enum Market { SGX }; @@ -289,6 +340,12 @@ namespace QuantLib { Slovakia(Market m = BSSE); }; + class Slovenia : public Calendar { + public: + enum Market { LSE }; + Slovenia(Market m = LSE); + }; + class SouthAfrica : public Calendar {}; class SouthKorea : public Calendar { @@ -329,6 +386,12 @@ namespace QuantLib { UnitedStates(Market m); }; + class Uzbekistan : public Calendar { + public: + enum Market { UZSE }; + Uzbekistan(Market m = UZSE); + }; + // others class NullCalendar : public Calendar {}; diff --git a/SWIG/calibrationhelpers.i b/SWIG/calibrationhelpers.i index a408efa4..2afee2e9 100644 --- a/SWIG/calibrationhelpers.i +++ b/SWIG/calibrationhelpers.i @@ -68,9 +68,6 @@ class BlackCalibrationHelper : public CalibrationHelper { ext::shared_ptr as_black_helper(const ext::shared_ptr& h) { return ext::dynamic_pointer_cast(h); } - ext::shared_ptr as_swaption_helper(const ext::shared_ptr& h) { - return ext::dynamic_pointer_cast(h); - } %} %shared_ptr(SwaptionHelper) @@ -147,6 +144,12 @@ class SwaptionHelper : public BlackCalibrationHelper { } }; +%inline %{ + ext::shared_ptr as_swaption_helper(const ext::shared_ptr& h) { + return ext::dynamic_pointer_cast(h); + } +%} + %shared_ptr(CapHelper) class CapHelper : public BlackCalibrationHelper { public: diff --git a/SWIG/cashflows.i b/SWIG/cashflows.i index 773da414..6a41c8e6 100644 --- a/SWIG/cashflows.i +++ b/SWIG/cashflows.i @@ -5,6 +5,7 @@ Copyright (C) 2010, 2011 Lluis Pujol Bajador Copyright (C) 2017, 2018, 2019, 2020 Matthias Lungwitz Copyright (C) 2021 Marcin Rybacki + Copyright (C) 2026 Kyrylo Protsenko This file is part of QuantLib, a free-software/open-source library for financial quantitative analysts and developers - http://quantlib.org/ @@ -191,6 +192,7 @@ class FloatingRateCoupon : public Coupon { public: Date fixingDate() const; Integer fixingDays() const; + BusinessDayConvention fixingConvention() const; bool isInArrears() const; Real gearing() const; Rate spread() const; @@ -200,6 +202,7 @@ class FloatingRateCoupon : public Coupon { Real price(const Handle& discountCurve) const; ext::shared_ptr index() const; void setPricer(const ext::shared_ptr& p); + ext::shared_ptr pricer() const; }; %inline %{ @@ -210,6 +213,130 @@ class FloatingRateCoupon : public Coupon { %} +%{ +using QuantLib::FxReset; +using QuantLib::FxResetConvention; +using QuantLib::FxResetPricer; +using QuantLib::DiscountingFxResetPricer; +using QuantLib::FxResetCoupon; +using QuantLib::FxResetNotionalExchange; +using QuantLib::setFxResetPricer; +%} + +class FxReset { + public: + FxReset(const Date& fixingDate, const Date& valueDate); + Date fixingDate() const; + Date valueDate() const; +}; + +class FxResetConvention { + #if !defined(SWIGJAVA) && !defined(SWIGCSHARP) + %feature("kwargs") FxResetConvention; + #endif + public: + FxResetConvention(Natural fixingDays = 0, + const Calendar& fixingCalendar = Calendar()); + FxReset reset(const Date& valueDate) const; + Date valueDate(const Date& fixingDate) const; + Natural fixingDays() const; + const Calendar& fixingCalendar() const; +}; + +%shared_ptr(FxResetPricer) +class FxResetPricer { + private: + FxResetPricer(); + public: + virtual Real fxRate(const FxReset& reset) const; +}; + +%shared_ptr(DiscountingFxResetPricer) +class DiscountingFxResetPricer : public FxResetPricer { + #if !defined(SWIGJAVA) && !defined(SWIGCSHARP) + %feature("kwargs") DiscountingFxResetPricer; + #endif + public: + DiscountingFxResetPricer( + const Currency& constantLegCurrency, + const Currency& resettableLegCurrency, + const Handle& constantLegCurve, + const Handle& resettableLegCurve, + const Handle& spotFx, + bool spotIsResettablePerConstant, + const Date& spotFxSettleDate = Date()); + Real fxRate(const FxReset& reset) const; + const Currency& constantLegCurrency() const; + const Currency& resettableLegCurrency() const; +}; + +%shared_ptr(FxResetCoupon) +class FxResetCoupon : public FloatingRateCoupon { + public: + FxResetCoupon(const ext::shared_ptr& underlying, + Real constantLegNotional, + const FxReset& fxReset); + const ext::shared_ptr& underlying() const; + Real constantLegNotional() const; + const FxReset& fxReset() const; + Date fxResetDate() const; + Date fxResetValueDate() const; + const ext::shared_ptr& fxResetPricer() const; + void setFxResetPricer(const ext::shared_ptr& pricer); +}; + +%inline %{ + ext::shared_ptr as_fx_reset_coupon( + const ext::shared_ptr& cf) { + return ext::dynamic_pointer_cast(cf); + } +%} + +%shared_ptr(FxResetNotionalExchange) +class FxResetNotionalExchange : public CashFlow { + public: + Real constantLegNotional() const; + const ext::shared_ptr& fxResetPricer() const; + void setFxResetPricer(const ext::shared_ptr& pricer); +}; + +%extend FxResetNotionalExchange { + FxResetNotionalExchange(const Date& paymentDate, + Real constantLegNotional, + const FxReset* previousReset, + const FxReset* currentReset) { + return new FxResetNotionalExchange( + paymentDate, constantLegNotional, + previousReset ? std::optional(*previousReset) : std::nullopt, + currentReset ? std::optional(*currentReset) : std::nullopt); + } + bool hasPreviousReset() const { + return self->previousReset().has_value(); + } + FxReset previousReset() const { + QL_REQUIRE(self->previousReset(), "the notional exchange has no previous FX reset"); + return *self->previousReset(); + } + bool hasCurrentReset() const { + return self->currentReset().has_value(); + } + FxReset currentReset() const { + QL_REQUIRE(self->currentReset(), "the notional exchange has no current FX reset"); + return *self->currentReset(); + } +}; + +%inline %{ + ext::shared_ptr as_fx_reset_notional_exchange( + const ext::shared_ptr& cf) { + return ext::dynamic_pointer_cast(cf); + } +%} + +void setFxResetPricer(const Leg&, + const ext::shared_ptr&); + + %{ using QuantLib::CappedFlooredCoupon; %} @@ -274,7 +401,9 @@ class OvernightIndexedCoupon : public FloatingRateCoupon { bool applyObservationShift = false, bool compoundSpread = false, const Date& rateComputationStartDate = Date(), - const Date& rateComputationEndDate = Date()); + const Date& rateComputationEndDate = Date(), + const Date& exCouponDate = Date(), + std::optional roundingPrecision = std::nullopt); const std::vector& fixingDates() const; const std::vector& interestDates() const; const std::vector