Skip to content

Allow non-zero spreads in Gaussian1dCapFloorEngine - #2999

Open
leafyoung wants to merge 2 commits into
lballabio:masterfrom
leafyoung:fix-gaussian1dcapfloorengine-nonzero-spread
Open

leafyoung wants to merge 2 commits into
lballabio:masterfrom
leafyoung:fix-gaussian1dcapfloorengine-nonzero-spread

Conversation

@leafyoung

Copy link
Copy Markdown

Summary

CapFloor::setupArguments() pre-adjusts capRates/floorRates to (strike-spread)/gearing before any pricing engine sees them — the analytic, Black and Bachelier cap/floor engines all rely on this pre-adjustment and place no restriction on the spread. Gaussian1dCapFloorEngine alone rejected every non-zero spread outright:

for (Real spread : arguments_.spreads)
    QL_REQUIRE(spread == 0.0, "Non zero spreads (" << spread << ") are not allowed.");

even though its own calculation (max(forward - capRate, 0) * gearing * nominal * accrual, in both the deterministic and stochastic branches) uses the exact same pre-adjusted-strike pattern as the other engines and is already algebraically correct for any spread: substituting the pre-adjusted strike gives max(gearing*forward + spread - K, 0) * nominal * accrual, the correct capped-coupon payoff. The check was simply unnecessary and blocked valid inputs that every sibling engine already supports.

Fix

Removed the restriction; left a comment explaining why it's safe to remove (the pre-adjustment already generalizes the payoff).

Test

Added testCapFloorNonZeroSpreadConsistency to test-suite/gsr.cpp: prices a capped floating leg with a non-zero spread via Gaussian1dCapFloorEngine and checks it agrees with the mathematically equivalent zero-spread cap at a shifted strike. Confirmed it fails with the old check (throws Non zero spreads (0.001) are not allowed.) and passes with the fix. Full GsrTests and MarkovFunctionalTests suites (the latter being the only other test consumer of this engine) pass with no regressions.

🤖 Co-worked with Claude Code

CapFloor::setupArguments() already pre-adjusts capRates/floorRates to
(strike-spread)/gearing before the engine sees them, exactly as the
analytic, Black and Bachelier cap/floor engines rely on without any
restriction on spread. Gaussian1dCapFloorEngine alone rejected every
non-zero spread outright (QL_REQUIRE(spread == 0.0, ...)) even though
its own calculation uses the same pre-adjusted-strike pattern and is
already correct for any spread -- the check was simply unnecessary.

Added a regression test (gsr.cpp) that prices a capped floating leg
with a non-zero spread and checks it agrees with the mathematically
equivalent zero-spread cap at a shifted strike; confirmed it fails
with the old check and passes with the fix.

Co-worked with Claude Code
Copilot AI balanced review requested due to automatic review settings November 1, 2026 13:53

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

The cap-pricing test uses mean reversion of 50.0, making both NPVs zero so equality cannot detect an ignored spread adjustment; it needs lower mean reversion and a positive reference NPV.

1 open issue
What changed in this PR

This PR enables non-zero floating-leg spreads in Gaussian1dCapFloorEngine, relying on the strike adjustment already performed by CapFloor::setupArguments().

Changes:

  • Removes the zero-spread restriction and documents why existing payoff calculations support spreads.
  • Adds a regression test comparing a spread-bearing cap with an equivalent zero-spread cap at a shifted strike.
File Description
test-suite/​gsr.cpp Adds the spread-consistency regression test.
ql/​pricingengines/​capfloor/​gaussian1dcapfloorengine.cpp Removes spread rejection and explains the existing strike adjustment.

Review effort: Balanced

Comment thread test-suite/gsr.cpp Resolved
testCapFloorNonZeroSpreadConsistency used a mean reversion of 50.0 and
an out-of-the-money strike, collapsing both legs' NPVs to (near) zero
on this integration grid -- the equality check passed trivially
regardless of whether the spread adjustment was actually applied.
Lowered mean reversion to 0.01 and the strike to 0.01 (comfortably
in-the-money against the 2% flat forward), and added an explicit
BOOST_REQUIRE that the reference NPV is non-trivially positive before
comparing. Also switched to ext::make_shared throughout per the
repository's own convention.

Co-worked with Claude Code
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants