ORE Bug Report — Zero-rate sensitivity leaks to adjacent pillar when parSensitivity=Y
Summary
Enabling <Parameter name="parSensitivity">Y</Parameter> on a sensitivity
analytic corrupts the zero-rate sensitivity (sensitivity.csv) output.
A trade whose payoff depends on a discount factor at exactly a simmarket
pillar date receives spurious sensitivity at the adjacent pillar, even
though LogLinear DF interpolation makes that sensitivity mathematically
exactly zero.
The par-conversion computation should not affect the zero-rate bucket
output — and does not: running the same trade with parSensitivity=N
produces the correct zero-rate sensitivities. The issue is that turning
par sensitivity ON apparently modifies the scenario-sim-market yield-curve
build path in a way that no longer cleanly lines up pillar times with the
requested ShiftTenors, or equivalently introduces cross-pillar coupling
in the shift application.
Reproducer
Self-contained ORE config (from Examples/Legacy/Example_40
and Examples/Input).
/opt/.../bugs/ORE-upstream-risk-buckets/
├── README.md ← this file
├── ore.xml ← default: parSensitivity=Y → BUGGY
├── Input/
│ ├── portfolio.xml ← single FxForward, ValueDate = asof+2Y exactly
│ ├── sensitivity.xml ← EUR+USD discount curves, ParConversion=OIS
│ ├── simulation.xml ← simmarket tenors incl. 1Y and 2Y pillars
│ ├── todaysmarket.xml
│ ├── pricingengine.xml
│ ├── conventions.xml
│ ├── curveconfig.xml
│ ├── market_20160205.txt
│ └── fixings_20160205.txt
└── Output_WITH_parsens/ ← buggy output saved
Output_NO_parsens/ ← correct output saved
All inputs are taken from ORE's Examples/Legacy/Example_40/Input/
and Examples/Input/ (market_20160205.txt, fixings_20160205.txt,
conventions.xml, curveconfig.xml, pricingengine.xml). The only
trade is a stock FxForward pointing at the 2Y tenor:
<FxForwardData>
<ValueDate>2018-02-05</ValueDate> <!-- asof 2016-02-05 + 2Y exactly -->
<BoughtCurrency>EUR</BoughtCurrency>
<BoughtAmount>1000000</BoughtAmount>
<SoldCurrency>USD</SoldCurrency>
<SoldAmount>1100000</SoldAmount>
</FxForwardData>
Run
cd .../ORE-upstream-risk-buckets
/path/to/ore ore.xml
Default run uses parSensitivity=Y → reproduces the bug.
Toggle the parSensitivity flag in ore.xml to compare.
Expected
FxForward matures at the exact simmarket 2Y tenor date
(asof + 2Y = 2018-02-05). Its NPV is
NPV = BoughtAmount · DF_EUR(2Y) − SoldAmount · DF_USD(2Y) · FX(EURUSD)
depending on the discount curves only at t = T_2Y. Under the configured
<Interpolation>LogLinear</Interpolation>, a 1bp shift at the adjacent
1Y pillar contributes exactly zero at t = T_2Y (the LogLinear tent
has weight 0 at t=t2). See
OREAnalytics/orea/scenario/shiftscenariogenerator.cpp ~line 215:
else if (times[k] > t1 && times[k] <= t2)
w = (t2 - times[k]) / (t2 - t1); // = 0 at t2
Expected zero-rate sensitivities for FXFWD_2Y_EXACT:
| Factor |
Delta |
DiscountCurve/EUR/5/1Y |
0.00 |
DiscountCurve/EUR/6/2Y |
−201.66 |
DiscountCurve/USD/5/1Y |
0.00 |
DiscountCurve/USD/6/2Y |
+189.45 |
This is exactly what Output_NO_parsens/sensitivity.csv shows.
Actual (with parSensitivity=Y)
FXFWD_2Y_EXACT, DiscountCurve/EUR/5/1Y, ShiftSize 0.0001, Delta = -1.12 ← SPURIOUS
FXFWD_2Y_EXACT, DiscountCurve/EUR/6/2Y, ShiftSize 0.0001, Delta = -200.54 ← reduced
FXFWD_2Y_EXACT, DiscountCurve/USD/5/1Y, ShiftSize 0.0001, Delta = +1.05 ← SPURIOUS
FXFWD_2Y_EXACT, DiscountCurve/USD/6/2Y, ShiftSize 0.0001, Delta = +188.40 ← reduced
The EUR total is preserved (-1.12 + -200.54 = -201.66) and the USD total
is preserved (+1.05 + +188.40 = +189.45) — but the attribution is wrong.
Both effects scale with the magnitude of the 2Y sensitivity:
approximately 0.55% of the 2Y value leaks to 1Y.
Impact
- Per-bucket sensitivity reports are misleading when
parSensitivity=Y,
which is common in production setups.
- Aggregated totals across the full curve cancel the error, but per-pillar
attribution is wrong.
- Trades whose cashflows land on simmarket pillar dates (swaps at standard
tenors, zero-coupon bonds, FxForwards at IMM dates, etc.) all exhibit
the leak whenever par sensitivity is enabled.
Hypothesized root cause
Turning par sensitivity on switches the yield-curve construction path
inside ScenarioSimMarket — ParSensitivityInstrumentBuilder
constructs par instruments (OIS, IRS, etc.) to compute the Jacobian.
Building those instruments appears to either:
- re-place pillar times via a different day counter convention than the
shift-tenor day counter, producing T_bond ≠ T_pillar by a few
milliseconds of year-fraction, making the LogLinear interpolation no
longer collapse to a single pillar; OR
- inject a residual shift into the adjacent pillar while building the
Jacobi transposition.
Verifying: add DLOG of yieldCurveTimes[] in
OREAnalytics/orea/scenario/scenariosimmarket.cpp addYieldCurve
(around line 310) and compare the times between parSensitivity=Y and
parSensitivity=N runs. Alternatively, inspect
ParSensitivityInstrumentBuilder::createParInstruments() for any
side-effect that perturbs neighboring pillar quotes.
Environment
- ORE: https://github.com/OpenSourceRisk/Engine tag
v1.8.15.0
(commit 0b2fc17, QuantLib submodule 9094ab64c) — bug reproduces.
- Compiler: GCC 12.4, Linux x86-64
- Boost 1.82
- Default
<ObservationMode>None</ObservationMode> (from Example_40).
Build
git clone --recurse-submodules https://github.com/OpenSourceRisk/Engine.git
cd Engine && mkdir build && cd build
cmake .. -DORE_BUILD_APP=ON -DORE_BUILD_TESTS=OFF \
-DBOOST_ROOT=/path/to/boost -DBoost_NO_SYSTEM_PATHS=ON -G Ninja
ninja ore
ore-config.zip
ORE Bug Report — Zero-rate sensitivity leaks to adjacent pillar when
parSensitivity=YSummary
Enabling
<Parameter name="parSensitivity">Y</Parameter>on asensitivityanalytic corrupts the zero-rate sensitivity (
sensitivity.csv) output.A trade whose payoff depends on a discount factor at exactly a simmarket
pillar date receives spurious sensitivity at the adjacent pillar, even
though LogLinear DF interpolation makes that sensitivity mathematically
exactly zero.
The par-conversion computation should not affect the zero-rate bucket
output — and does not: running the same trade with
parSensitivity=Nproduces the correct zero-rate sensitivities. The issue is that turning
par sensitivity ON apparently modifies the scenario-sim-market yield-curve
build path in a way that no longer cleanly lines up pillar times with the
requested
ShiftTenors, or equivalently introduces cross-pillar couplingin the shift application.
Reproducer
Self-contained ORE config (from
Examples/Legacy/Example_40and
Examples/Input).All inputs are taken from ORE's
Examples/Legacy/Example_40/Input/and
Examples/Input/(market_20160205.txt,fixings_20160205.txt,conventions.xml,curveconfig.xml,pricingengine.xml). The onlytrade is a stock
FxForwardpointing at the2Ytenor:Run
cd .../ORE-upstream-risk-buckets /path/to/ore ore.xmlDefault run uses
parSensitivity=Y→ reproduces the bug.Toggle the
parSensitivityflag inore.xmlto compare.Expected
FxForward matures at the exact simmarket 2Y tenor date
(
asof + 2Y = 2018-02-05). Its NPV isdepending on the discount curves only at
t = T_2Y. Under the configured<Interpolation>LogLinear</Interpolation>, a 1bp shift at the adjacent1Ypillar contributes exactly zero att = T_2Y(the LogLinear tenthas weight 0 at t=t2). See
OREAnalytics/orea/scenario/shiftscenariogenerator.cpp~line 215:Expected zero-rate sensitivities for
FXFWD_2Y_EXACT:DiscountCurve/EUR/5/1YDiscountCurve/EUR/6/2YDiscountCurve/USD/5/1YDiscountCurve/USD/6/2YThis is exactly what
Output_NO_parsens/sensitivity.csvshows.Actual (with
parSensitivity=Y)The EUR total is preserved (-1.12 + -200.54 = -201.66) and the USD total
is preserved (+1.05 + +188.40 = +189.45) — but the attribution is wrong.
Both effects scale with the magnitude of the 2Y sensitivity:
approximately 0.55% of the 2Y value leaks to 1Y.
Impact
parSensitivity=Y,which is common in production setups.
attribution is wrong.
tenors, zero-coupon bonds, FxForwards at IMM dates, etc.) all exhibit
the leak whenever par sensitivity is enabled.
Hypothesized root cause
Turning par sensitivity on switches the yield-curve construction path
inside
ScenarioSimMarket—ParSensitivityInstrumentBuilderconstructs par instruments (OIS, IRS, etc.) to compute the Jacobian.
Building those instruments appears to either:
shift-tenor day counter, producing
T_bond ≠ T_pillarby a fewmilliseconds of year-fraction, making the LogLinear interpolation no
longer collapse to a single pillar; OR
Jacobi transposition.
Verifying: add
DLOGofyieldCurveTimes[]inOREAnalytics/orea/scenario/scenariosimmarket.cppaddYieldCurve(around line 310) and compare the times between
parSensitivity=YandparSensitivity=Nruns. Alternatively, inspectParSensitivityInstrumentBuilder::createParInstruments()for anyside-effect that perturbs neighboring pillar quotes.
Environment
v1.8.15.0(commit
0b2fc17, QuantLib submodule9094ab64c) — bug reproduces.<ObservationMode>None</ObservationMode>(from Example_40).Build
ore-config.zip