Skip to content

Zero-rate sensitivity leaks to adjacent pillar when parSensitivity=Y #344

@DmitriGoloubentsev

Description

@DmitriGoloubentsev

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 ScenarioSimMarketParSensitivityInstrumentBuilder
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

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions