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
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions