/src/quantlib/ql/experimental/fx/mtmcrosscurrencybasisswap.hpp
Line | Count | Source |
1 | | /* -*- mode: c++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ |
2 | | |
3 | | /* |
4 | | Copyright (C) 2026 Kyrylo Protsenko |
5 | | |
6 | | This file is part of QuantLib, a free-software/open-source library |
7 | | for financial quantitative analysts and developers - http://quantlib.org/ |
8 | | |
9 | | QuantLib is free software: you can redistribute it and/or modify it |
10 | | under the terms of the QuantLib license. You should have received a |
11 | | copy of the license along with this program; if not, please email |
12 | | <quantlib-dev@lists.sf.net>. The license is also available online at |
13 | | <https://www.quantlib.org/license.shtml>. |
14 | | |
15 | | This program is distributed in the hope that it will be useful, but WITHOUT |
16 | | ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS |
17 | | FOR A PARTICULAR PURPOSE. See the license for more details. |
18 | | */ |
19 | | |
20 | | /*! \file mtmcrosscurrencybasisswap.hpp |
21 | | \brief Mark-to-market (resetting-notional) cross-currency basis swap |
22 | | */ |
23 | | |
24 | | #ifndef quantlib_mtm_cross_currency_basis_swap_hpp |
25 | | #define quantlib_mtm_cross_currency_basis_swap_hpp |
26 | | |
27 | | #include <ql/cashflows/iborcoupon.hpp> |
28 | | #include <ql/cashflows/stubiborcoupon.hpp> |
29 | | #include <ql/cashflows/rateaveraging.hpp> |
30 | | #include <ql/experimental/fx/fxresetcashflows.hpp> |
31 | | #include <ql/indexes/iborindex.hpp> |
32 | | #include <ql/instruments/crosscurrencyswap.hpp> |
33 | | #include <ql/optional.hpp> |
34 | | #include <ql/pricingengine.hpp> |
35 | | #include <ql/time/schedule.hpp> |
36 | | |
37 | | namespace QuantLib { |
38 | | |
39 | | //! Mark-to-market cross-currency basis swap |
40 | | /*! A float-vs-float cross-currency basis swap in which the notional of one leg |
41 | | (the \e resettable leg) is reset for each coupon period to track the |
42 | | prevailing FX rate, while the other leg keeps a constant notional that is |
43 | | exchanged at inception and maturity. Each reset has a fixing date, on |
44 | | which the FX rate is observed, and a value date at the start of the accrual |
45 | | period. |
46 | | |
47 | | Following the convention of MtMCrossCurrencyBasisSwapRateHelper, the two legs |
48 | | are identified by their role in the FX pair: the \e base-currency leg and the |
49 | | \e quote-currency leg, and \c isFxBaseCurrencyLegResettable selects which of |
50 | | them carries the resetting notional. The trade \c Type controls whether the |
51 | | base-currency leg is paid or received. |
52 | | |
53 | | Resetting the notional removes most of the FX exposure of the principal and |
54 | | is the market-standard convention for many currency pairs. The resettable |
55 | | leg is built from FX-linked cash flows: FxResetCoupon instances, whose |
56 | | notional is the constant-leg notional converted at the period's FX-reset |
57 | | observation, and FxResetNotionalExchange instances paying the netted |
58 | | notional differences (the first notional at inception, the reset difference |
59 | | at each period boundary, and the last notional at maturity). |
60 | | |
61 | | During instrument pricing, the FX rates are supplied to local cash-flow |
62 | | wrappers by an engine that understands FX-resetting legs (see |
63 | | DiscountingMtMCrossCurrencyBasisSwapEngine). Independently querying the |
64 | | live reset cash flows requires assigning an FxResetPricer with |
65 | | setFxResetPricer. A future reset is projected as the forward FX implied by |
66 | | its pricer's discount curves. If a reset fixing date precedes the |
67 | | evaluation date, the realized exchange rate must be available from |
68 | | \c ExchangeRateManager on that fixing date. Direct, inverse, and derived |
69 | | exchange rates are supported. |
70 | | |
71 | | \ingroup instruments |
72 | | */ |
73 | | class MtMCrossCurrencyBasisSwap : public CrossCurrencySwap { |
74 | | public: |
75 | | class arguments; |
76 | | class results; |
77 | | class engine; |
78 | | |
79 | | enum Type { PayFxBaseCurrency, ReceiveFxBaseCurrency }; |
80 | | |
81 | | //! \name Constructors |
82 | | //@{ |
83 | | /*! Payment lags apply only to coupon payments. Initial and final notional |
84 | | exchanges remain on the effective and maturity dates, adjusted by their |
85 | | leg's payment convention. Interim reset exchanges occur on the current |
86 | | reset value date. |
87 | | |
88 | | \param type Pay or receive the FX base-currency leg. |
89 | | \param isFxBaseCurrencyLegResettable If true the base-currency (first) |
90 | | leg is the resettable leg; otherwise the |
91 | | quote-currency (second) leg resets. The other |
92 | | leg keeps a constant notional and exchanges it |
93 | | at inception and maturity. |
94 | | \param fxResetFixingDays Fixing days used to derive each FX fixing date |
95 | | from its accrual-start value date. The default |
96 | | preserves the legacy behavior of fixing on the |
97 | | accrual start date. |
98 | | \param fxResetFixingCalendar Calendar used to derive each FX fixing date. |
99 | | For a non-zero fixing lag, |
100 | | an empty fixing calendar is replaced by the |
101 | | resettable leg's schedule calendar. |
102 | | \param fxBasePaymentLag Coupon payment lag for the base-currency leg. |
103 | | \param fxQuotePaymentLag Coupon payment lag for the quote-currency leg. |
104 | | \param fxBasePaymentConvention Payment convention for the base-currency |
105 | | leg and its notional exchanges. |
106 | | \param fxQuotePaymentConvention Payment convention for the quote-currency |
107 | | leg and its notional exchanges. |
108 | | \param useIndexedCoupons If provided, overrides the global IborCoupon |
109 | | setting for both legs. |
110 | | \param fxBaseStubIndexSelection Index selection applied to irregular |
111 | | coupons of the base-currency leg when it is an |
112 | | Ibor leg (see StubIndexSelection). The default |
113 | | prices broken periods off the leg's own index. |
114 | | \param fxQuoteStubIndexSelection As fxBaseStubIndexSelection, for the |
115 | | quote-currency leg. |
116 | | */ |
117 | | MtMCrossCurrencyBasisSwap( |
118 | | Type type, |
119 | | Real fxBaseNominal, Currency fxBaseCurrency, Schedule fxBaseSchedule, |
120 | | const ext::shared_ptr<IborIndex>& fxBaseIndex, |
121 | | Spread fxBaseSpread, |
122 | | Real fxBaseGearing, |
123 | | Real fxQuoteNominal, Currency fxQuoteCurrency, Schedule fxQuoteSchedule, |
124 | | const ext::shared_ptr<IborIndex>& fxQuoteIndex, |
125 | | Spread fxQuoteSpread, |
126 | | Real fxQuoteGearing, |
127 | | bool isFxBaseCurrencyLegResettable, |
128 | | Natural fxResetFixingDays = 0, |
129 | | const Calendar& fxResetFixingCalendar = Calendar(), |
130 | | Integer fxBasePaymentLag = 0, Integer fxQuotePaymentLag = 0, |
131 | | BusinessDayConvention fxBasePaymentConvention = Following, |
132 | | BusinessDayConvention fxQuotePaymentConvention = Following, |
133 | | bool fxBaseCompoundSpread = false, |
134 | | Natural fxBaseLookbackDays = Null<Natural>(), |
135 | | bool fxBaseObservationShift = false, |
136 | | Natural fxBaseLockoutDays = 0, |
137 | | RateAveraging::Type fxBaseAveragingMethod = RateAveraging::Compound, |
138 | | bool fxQuoteCompoundSpread = false, |
139 | | Natural fxQuoteLookbackDays = Null<Natural>(), |
140 | | bool fxQuoteObservationShift = false, |
141 | | Natural fxQuoteLockoutDays = 0, |
142 | | RateAveraging::Type fxQuoteAveragingMethod = RateAveraging::Compound, |
143 | | bool telescopicValueDates = false, |
144 | | std::optional<bool> useIndexedCoupons = std::nullopt, |
145 | | StubIndexSelection fxBaseStubIndexSelection = {}, |
146 | | StubIndexSelection fxQuoteStubIndexSelection = {}); |
147 | | //@} |
148 | | |
149 | | //! \name Instrument interface |
150 | | //@{ |
151 | | void setupArguments(PricingEngine::arguments* args) const override; |
152 | | void fetchResults(const PricingEngine::results*) const override; |
153 | | //@} |
154 | | |
155 | | //! \name Inspectors |
156 | | //@{ |
157 | 0 | Type type() const { return type_; } |
158 | | |
159 | 0 | bool paysFxBaseCurrency() const { return type_ == PayFxBaseCurrency; } |
160 | | |
161 | 0 | Real fxBaseNominal() const { return fxBaseNominal_; } |
162 | 0 | const Currency& fxBaseCurrency() const { return fxBaseCurrency_; } |
163 | 0 | const Schedule& fxBaseSchedule() const { return fxBaseSchedule_; } |
164 | 0 | const ext::shared_ptr<IborIndex>& fxBaseIndex() const { return fxBaseIndex_; } |
165 | 0 | Spread fxBaseSpread() const { return fxBaseSpread_; } |
166 | 0 | Real fxBaseGearing() const { return fxBaseGearing_; } |
167 | 0 | const StubIndexSelection& fxBaseStubIndexSelection() const { |
168 | 0 | return fxBaseStubIndexSelection_; |
169 | 0 | } |
170 | 0 | const StubIndexSelection& fxQuoteStubIndexSelection() const { |
171 | 0 | return fxQuoteStubIndexSelection_; |
172 | 0 | } |
173 | | |
174 | 0 | Real fxQuoteNominal() const { return fxQuoteNominal_; } |
175 | 0 | const Currency& fxQuoteCurrency() const { return fxQuoteCurrency_; } |
176 | 0 | const Schedule& fxQuoteSchedule() const { return fxQuoteSchedule_; } |
177 | 0 | const ext::shared_ptr<IborIndex>& fxQuoteIndex() const { return fxQuoteIndex_; } |
178 | 0 | Spread fxQuoteSpread() const { return fxQuoteSpread_; } |
179 | 0 | Real fxQuoteGearing() const { return fxQuoteGearing_; } |
180 | | |
181 | 0 | Real payNominal() const { return paysFxBaseCurrency() ? fxBaseNominal_ : fxQuoteNominal_; } |
182 | 0 | const Currency& payCurrency() const { |
183 | 0 | return paysFxBaseCurrency() ? fxBaseCurrency_ : fxQuoteCurrency_; |
184 | 0 | } |
185 | 0 | const Schedule& paySchedule() const { |
186 | 0 | return paysFxBaseCurrency() ? fxBaseSchedule_ : fxQuoteSchedule_; |
187 | 0 | } |
188 | 0 | const ext::shared_ptr<IborIndex>& payIndex() const { |
189 | 0 | return paysFxBaseCurrency() ? fxBaseIndex_ : fxQuoteIndex_; |
190 | 0 | } |
191 | 0 | Spread paySpread() const { return paysFxBaseCurrency() ? fxBaseSpread_ : fxQuoteSpread_; } |
192 | 0 | Real payGearing() const { return paysFxBaseCurrency() ? fxBaseGearing_ : fxQuoteGearing_; } |
193 | | |
194 | 0 | Real recNominal() const { return paysFxBaseCurrency() ? fxQuoteNominal_ : fxBaseNominal_; } |
195 | 0 | const Currency& recCurrency() const { |
196 | 0 | return paysFxBaseCurrency() ? fxQuoteCurrency_ : fxBaseCurrency_; |
197 | 0 | } |
198 | 0 | const Schedule& recSchedule() const { |
199 | 0 | return paysFxBaseCurrency() ? fxQuoteSchedule_ : fxBaseSchedule_; |
200 | 0 | } |
201 | 0 | const ext::shared_ptr<IborIndex>& recIndex() const { |
202 | 0 | return paysFxBaseCurrency() ? fxQuoteIndex_ : fxBaseIndex_; |
203 | 0 | } |
204 | 0 | Spread recSpread() const { return paysFxBaseCurrency() ? fxQuoteSpread_ : fxBaseSpread_; } |
205 | 0 | Real recGearing() const { return paysFxBaseCurrency() ? fxQuoteGearing_ : fxBaseGearing_; } |
206 | | |
207 | | //! true if the base-currency (first) leg is the resettable leg |
208 | 0 | bool isFxBaseCurrencyLegResettable() const { return isFxBaseCurrencyLegResettable_; } |
209 | 0 | Size resettingLegIndex() const { return isFxBaseCurrencyLegResettable_ ? 0 : 1; } |
210 | 0 | Size constantLegIndex() const { return isFxBaseCurrencyLegResettable_ ? 1 : 0; } |
211 | 0 | const Leg& resettingLeg() const { return legs_[resettingLegIndex()]; } |
212 | 0 | const Leg& constantLeg() const { return legs_[constantLegIndex()]; } |
213 | | //! notional of the constant leg, in its own currency |
214 | 0 | Real constantLegNotional() const { |
215 | 0 | return isFxBaseCurrencyLegResettable_ ? fxQuoteNominal_ : fxBaseNominal_; |
216 | 0 | } |
217 | 0 | Natural fxResetFixingDays() const { return fxResetConvention_.fixingDays(); } |
218 | 0 | Calendar fxResetFixingCalendar() const { return fxResetConvention_.fixingCalendar(); } |
219 | 0 | BusinessDayConvention fxBasePaymentConvention() const { |
220 | 0 | return fxBasePaymentConvention_; |
221 | 0 | } |
222 | 0 | BusinessDayConvention fxQuotePaymentConvention() const { |
223 | 0 | return fxQuotePaymentConvention_; |
224 | 0 | } |
225 | | /*! FX rates used to determine the resettable coupon notionals. |
226 | | |
227 | | The values follow the order of the non-occurred FxResetCoupon |
228 | | instances in resettingLeg(). |
229 | | */ |
230 | 0 | const std::vector<Real>& fxResetRates() const { |
231 | 0 | calculate(); |
232 | 0 | return fxResetRates_; |
233 | 0 | } |
234 | | /*! Notionals of the resettable coupons, in resetting-leg currency. |
235 | | |
236 | | The values follow the order of the non-occurred FxResetCoupon |
237 | | instances in resettingLeg(). |
238 | | */ |
239 | 0 | const std::vector<Real>& fxResetNotionals() const { |
240 | 0 | calculate(); |
241 | 0 | return fxResetNotionals_; |
242 | 0 | } |
243 | | //@} |
244 | | |
245 | | //! \name Additional interface |
246 | | //@{ |
247 | 0 | Spread fairFxBaseSpread() const { |
248 | 0 | calculate(); |
249 | 0 | QL_REQUIRE(fairFxBaseSpread_ != Null<Real>(), "Fair FX-base spread is not available"); |
250 | 0 | return fairFxBaseSpread_; |
251 | 0 | } |
252 | 0 | Spread fairFxQuoteSpread() const { |
253 | 0 | calculate(); |
254 | 0 | QL_REQUIRE(fairFxQuoteSpread_ != Null<Real>(), "Fair FX-quote spread is not available"); |
255 | 0 | return fairFxQuoteSpread_; |
256 | 0 | } |
257 | 0 | Spread fairPaySpread() const { |
258 | 0 | return paysFxBaseCurrency() ? fairFxBaseSpread() : fairFxQuoteSpread(); |
259 | 0 | } |
260 | 0 | Spread fairRecSpread() const { |
261 | 0 | return paysFxBaseCurrency() ? fairFxQuoteSpread() : fairFxBaseSpread(); |
262 | 0 | } |
263 | | //@} |
264 | | |
265 | | protected: |
266 | | //! \name Instrument interface |
267 | | //@{ |
268 | | void setupExpired() const override; |
269 | | //@} |
270 | | |
271 | | private: |
272 | | void initialize(); |
273 | | |
274 | | Type type_; |
275 | | Real fxBaseNominal_; |
276 | | Currency fxBaseCurrency_; |
277 | | Schedule fxBaseSchedule_; |
278 | | ext::shared_ptr<IborIndex> fxBaseIndex_; |
279 | | Spread fxBaseSpread_; |
280 | | Real fxBaseGearing_; |
281 | | StubIndexSelection fxBaseStubIndexSelection_; |
282 | | |
283 | | Real fxQuoteNominal_; |
284 | | Currency fxQuoteCurrency_; |
285 | | Schedule fxQuoteSchedule_; |
286 | | ext::shared_ptr<IborIndex> fxQuoteIndex_; |
287 | | Spread fxQuoteSpread_; |
288 | | Real fxQuoteGearing_; |
289 | | StubIndexSelection fxQuoteStubIndexSelection_; |
290 | | |
291 | | bool isFxBaseCurrencyLegResettable_; |
292 | | FxResetConvention fxResetConvention_; |
293 | | |
294 | | Integer fxBasePaymentLag_; |
295 | | Integer fxQuotePaymentLag_; |
296 | | BusinessDayConvention fxBasePaymentConvention_; |
297 | | BusinessDayConvention fxQuotePaymentConvention_; |
298 | | std::optional<bool> useIndexedCoupons_; |
299 | | |
300 | | // OIS only |
301 | | bool fxBaseCompoundSpread_; |
302 | | Natural fxBaseLookbackDays_; |
303 | | bool fxBaseObservationShift_; |
304 | | Natural fxBaseLockoutDays_; |
305 | | RateAveraging::Type fxBaseAveragingMethod_; |
306 | | bool fxQuoteCompoundSpread_; |
307 | | Natural fxQuoteLookbackDays_; |
308 | | bool fxQuoteObservationShift_; |
309 | | Natural fxQuoteLockoutDays_; |
310 | | RateAveraging::Type fxQuoteAveragingMethod_; |
311 | | bool telescopicValueDates_; |
312 | | |
313 | | mutable Spread fairFxBaseSpread_; |
314 | | mutable Spread fairFxQuoteSpread_; |
315 | | mutable std::vector<Real> fxResetRates_; |
316 | | mutable std::vector<Real> fxResetNotionals_; |
317 | | }; |
318 | | |
319 | | |
320 | | class MtMCrossCurrencyBasisSwap::arguments : public CrossCurrencySwap::arguments { |
321 | | public: |
322 | | //! index of the resettable leg |
323 | | Size resettingLegIndex = Null<Size>(); |
324 | | //! index of the constant-notional leg whose notional is converted at reset |
325 | | Size constantLegIndex = Null<Size>(); |
326 | | //! convention for the FX fixing and its associated spot value date |
327 | | Natural fxResetFixingDays = 0; |
328 | | Calendar fxResetFixingCalendar; |
329 | | Spread fxBaseSpread = Null<Spread>(); |
330 | | Spread fxQuoteSpread = Null<Spread>(); |
331 | | void validate() const override; |
332 | | }; |
333 | | |
334 | | |
335 | | class MtMCrossCurrencyBasisSwap::results : public CrossCurrencySwap::results { |
336 | | public: |
337 | | Spread fairFxBaseSpread = Null<Spread>(); |
338 | | Spread fairFxQuoteSpread = Null<Spread>(); |
339 | | std::vector<Real> fxResetRates; |
340 | | std::vector<Real> fxResetNotionals; |
341 | | void reset() override; |
342 | | }; |
343 | | |
344 | | |
345 | | class MtMCrossCurrencyBasisSwap::engine |
346 | | : public GenericEngine<MtMCrossCurrencyBasisSwap::arguments, |
347 | | MtMCrossCurrencyBasisSwap::results> {}; |
348 | | |
349 | | } |
350 | | |
351 | | #endif |