<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>sensitivity | Joey O'Brien</title><link>https://obrienjoey.github.io/tags/sensitivity/</link><atom:link href="https://obrienjoey.github.io/tags/sensitivity/index.xml" rel="self" type="application/rss+xml"/><description>sensitivity</description><generator>Source Themes Academic (https://sourcethemes.com/academic/)</generator><language>en-us</language><copyright>© 2026 Joey O'Brien</copyright><lastBuildDate>Tue, 25 Aug 2026 00:00:00 +0000</lastBuildDate><image><url>https://obrienjoey.github.io/img/og-banner.png</url><title>sensitivity</title><link>https://obrienjoey.github.io/tags/sensitivity/</link></image><item><title>Sensitivity Analysis in ORE</title><link>https://obrienjoey.github.io/post/ore_sensitivity/</link><pubDate>Tue, 25 Aug 2026 00:00:00 +0000</pubDate><guid>https://obrienjoey.github.io/post/ore_sensitivity/</guid><description>&lt;p>Once you can price a portfolio in Open Source Risk Engine (ORE), the next step is measuring its risk. Sensitivity analysis tracks how portfolio value (NPV) shifts when market risk factors (rates, FX, volatilities) move by a small amount, and these numbers are the raw inputs for fundamental risk calculations. Having supported clients through vendor replacements, we have seen first-hand that sensitivity reconciliation is one of the harder steps of any migration: the figures are dense, easy to get subtly wrong, and every downstream risk metric inherits any error that slips in.&lt;/p>
&lt;p>Because ORE is entirely open-source, none of this needs to be taken on faith. Rather than treating the sensitivity engine as another &amp;ldquo;black box&amp;rdquo;, we can inspect every shift, conversion weight, and report it produces. In this post, we pull from ORE&amp;rsquo;s own &lt;a href="https://github.com/OpenSourceRisk/Engine/tree/master/Examples/MarketRisk">MarketRisk examples&lt;/a> to walk through the full pipeline from zero rates to par rates. We run the native zero-domain sensitivity analytic on a single-curve teaching trade, inspect &lt;code>sensitivity.csv&lt;/code>, and examine the output: a delta per zero-rate grid tenor split between discount and index factors. Next, we convert those zero-domain deltas into tradeable par-domain risk using ORE&amp;rsquo;s Jacobian par conversion: the configuration, the Jacobian matrix itself, and the resulting &lt;code>parsensitivity.csv&lt;/code> table. Finally, we close the loop with an independent check: bump the 15Y OIS market quote by 1 bp, re-value the trade from scratch, and compare the observed NPV change against the par table.&lt;/p>
&lt;blockquote>
&lt;h3 id="walkthrough-objectives">Walkthrough Objectives&lt;/h3>
&lt;p>In this guide, we will:&lt;/p>
&lt;ol>
&lt;li>Configure the simulation market (&lt;code>simulation.xml&lt;/code>) and sensitivity analytic (&lt;code>sensitivity.xml&lt;/code>) for zero-domain output on a single-curve EUR-ESTER OIS.&lt;/li>
&lt;li>Turn on ORE&amp;rsquo;s Jacobian par conversion to produce &lt;code>parsensitivity.csv&lt;/code>, &lt;code>jacobi.csv&lt;/code>, and &lt;code>jacobi_inverse.csv&lt;/code>.&lt;/li>
&lt;li>Verify the par table independently by bumping the 15Y OIS quote ±1 bp and repricing the trade from scratch.&lt;/li>
&lt;/ol>
&lt;p>All scripts and configuration files used in this guide are available for download in the resources section below.&lt;/p>
&lt;/blockquote>
&lt;hr>
&lt;h2 id="1-the-single-curve-teaching-trade">1. The single-curve teaching trade&lt;/h2>
&lt;p>In production markets, interest rate swaps frequently use dual-curve setups (such as EUR-ESTER discounting with EURIBOR forecasting). While standard for pricing, dual curves complicate sensitivity analysis because the curves are coupled during calibration: shifting one market rate moves discounting and forecasting simultaneously, making the split between the &lt;code>DiscountCurve&lt;/code> and &lt;code>IndexCurve&lt;/code> factors ORE reports harder to isolate.&lt;/p>
&lt;p>To keep the mechanics clear, we use a single-curve EUR-ESTER OIS where discounting and forecasting reference the same &lt;code>EUR-ESTER&lt;/code> curve (following the single-curve principles explored in our &lt;a href="https://obrienjoey.github.io/post/ore_sofr_bootstrap/">curve-bootstrapping post&lt;/a>). The trade started on 2023-02-21 and matures on 2043-02-21, a 20-year tenor valued as of 2025-02-10 on 10,000,000 EUR notional. We receive 2.1% fixed annually on an &lt;code>A360&lt;/code> day counter and pay the EUR-ESTER float annually, also &lt;code>A360&lt;/code>, with one fixing day.&lt;/p>
&lt;p>Because the trade is linear, the sensitivity computed here is delta: the first-order change in NPV for a 1 bp move in a rate. Vega (volatility sensitivity) applies to non-linear trades and will be covered in a later post.&lt;/p>
&lt;p>Keeping a single curve makes the discount-versus-index factor split straightforward to interpret. Even though both factors resolve to the same underlying market curve, ORE reports them separately.&lt;/p>
&lt;hr>
&lt;h2 id="2-config-pass-1-zero-domain-sensitivity-setup">2. Config pass 1: zero-domain sensitivity setup&lt;/h2>
&lt;p>Running sensitivities in ORE requires two configurations alongside the portfolio: the simulation market (the risk-factor grid) and the sensitivity analytic (which factors to shift and by how much). The complete files are included in the bundle (&lt;a href="#summary-and-code">download below&lt;/a>); here is pass 1 for zero-domain output.&lt;/p>
&lt;h3 id="simulation-market-simulationxml">Simulation market (&lt;code>simulation.xml&lt;/code>)&lt;/h3>
&lt;p>Before running risk calculations, ORE expresses the market as a simulation market snapshot. Building the full market repeatedly is computationally expensive (bootstrapping curves, building volatility surfaces, and linking handles). In a sensitivity run with dozens of factor shifts, ORE builds the market once and takes a static snapshot. Shifting a factor then nudges a single grid node while holding the rest of the snapshot constant.&lt;/p>
&lt;p>ORE evaluates discount factors at each grid tenor and rebuilds the curve with anchors at those points. Each anchor becomes a shiftable risk factor. That grid is configured in &lt;code>simulation.xml&lt;/code>:&lt;/p>
&lt;pre>&lt;code class="language-xml">&amp;lt;?xml version=&amp;quot;1.0&amp;quot; encoding=&amp;quot;utf-8&amp;quot;?&amp;gt;
&amp;lt;Simulation&amp;gt;
&amp;lt;Market&amp;gt;
&amp;lt;BaseCurrency&amp;gt;EUR&amp;lt;/BaseCurrency&amp;gt;
&amp;lt;Currencies&amp;gt;
&amp;lt;Currency&amp;gt;EUR&amp;lt;/Currency&amp;gt;
&amp;lt;/Currencies&amp;gt;
&amp;lt;YieldCurves&amp;gt;
&amp;lt;Configuration&amp;gt;
&amp;lt;Tenors&amp;gt;6M,1Y,2Y,3Y,5Y,7Y,10Y,15Y,20Y,30Y&amp;lt;/Tenors&amp;gt;
&amp;lt;Interpolation&amp;gt;LogLinear&amp;lt;/Interpolation&amp;gt;
&amp;lt;Extrapolation&amp;gt;Y&amp;lt;/Extrapolation&amp;gt;
&amp;lt;/Configuration&amp;gt;
&amp;lt;/YieldCurves&amp;gt;
&amp;lt;Indices&amp;gt;
&amp;lt;Index&amp;gt;EUR-ESTER&amp;lt;/Index&amp;gt;
&amp;lt;/Indices&amp;gt;
&amp;lt;/Market&amp;gt;
&amp;lt;/Simulation&amp;gt;
&lt;/code>&lt;/pre>
&lt;p>Here is what each block configures:&lt;/p>
&lt;ul>
&lt;li>&lt;strong>&lt;code>BaseCurrency&lt;/code>&lt;/strong>: The base currency of the snapshot (&lt;code>EUR&lt;/code>), anchoring discounting and any FX risk factors.&lt;/li>
&lt;li>&lt;strong>&lt;code>Currencies&lt;/code>&lt;/strong>: The currencies included in the simulation market. Only &lt;code>EUR&lt;/code> is needed here since the trade discounts and forecasts on &lt;code>EUR-ESTER&lt;/code>.&lt;/li>
&lt;li>&lt;strong>&lt;code>YieldCurves &amp;gt; Configuration&lt;/code>&lt;/strong>: How yield curves are represented on the grid:
&lt;ul>
&lt;li>&lt;strong>&lt;code>Tenors&lt;/code>&lt;/strong>: The zero-rate grid (&lt;code>6M,1Y,2Y,3Y,5Y,7Y,10Y,15Y,20Y,30Y&lt;/code>). Shifts apply only at these tenors.&lt;/li>
&lt;li>&lt;strong>&lt;code>Interpolation&lt;/code>&lt;/strong>: How curves interpolate between grid nodes. &lt;code>LogLinear&lt;/code> (piecewise-constant forward rates) is standard for OIS curves. ORE supports &lt;code>LogLinear&lt;/code> or &lt;code>LinearZero&lt;/code>.&lt;/li>
&lt;li>&lt;strong>&lt;code>Extrapolation&lt;/code>&lt;/strong>: Curve behaviour past the final tenor (&lt;code>FlatFwd&lt;/code> or &lt;code>FlatZero&lt;/code>). The legacy boolean &lt;code>Y&lt;/code> maps to &lt;code>FlatFwd&lt;/code> with a log warning. For this trade, whose final cash flow is at 20Y (inside the 30Y grid), extrapolation does not trigger.&lt;/li>
&lt;/ul>
&lt;/li>
&lt;li>&lt;strong>&lt;code>Indices&lt;/code>&lt;/strong>: The forecast indices tracked as risk factors (&lt;code>EUR-ESTER&lt;/code>). Each index becomes an independent risk factor (&lt;code>IndexCurve/EUR-ESTER&lt;/code>).&lt;/li>
&lt;/ul>
&lt;p>Three details about this snapshot matter here:&lt;/p>
&lt;ol>
&lt;li>It re-expresses curves on a grid rather than re-bootstrapping them. The curves originate from &lt;code>marketdata.csv&lt;/code> and &lt;code>curveconfig.xml&lt;/code>. The simulation market only changes where points are sampled and how they interpolate.&lt;/li>
&lt;li>The grid defines the universe of shifts. The sensitivity analytic shifts only the tenors defined here, so &lt;code>ShiftTenors&lt;/code> in &lt;code>sensitivity.xml&lt;/code> must match this grid.&lt;/li>
&lt;li>The file is shared across analytics. The same &lt;code>simulation.xml&lt;/code> feeds both sensitivity analysis (&lt;code>marketConfigFile&lt;/code>) and historical VaR (&lt;code>simulationConfigFile&lt;/code>).&lt;/li>
&lt;/ol>
&lt;h3 id="sensitivity-analytic-sensitivityxml">Sensitivity analytic (&lt;code>sensitivity.xml&lt;/code>)&lt;/h3>
&lt;p>&lt;code>sensitivity.xml&lt;/code> specifies which factors to shift and the shift size. For zero-domain output, we declare both the discount curve and the forecast index with an absolute 1 bp shift (&lt;code>ShiftSize&lt;/code> of &lt;code>0.0001&lt;/code>):&lt;/p>
&lt;pre>&lt;code class="language-xml">&amp;lt;?xml version=&amp;quot;1.0&amp;quot; encoding=&amp;quot;utf-8&amp;quot;?&amp;gt;
&amp;lt;SensitivityAnalysis&amp;gt;
&amp;lt;DiscountCurves&amp;gt;
&amp;lt;DiscountCurve ccy=&amp;quot;EUR&amp;quot;&amp;gt;
&amp;lt;ShiftType&amp;gt;Absolute&amp;lt;/ShiftType&amp;gt;
&amp;lt;ShiftSize&amp;gt;0.0001&amp;lt;/ShiftSize&amp;gt;
&amp;lt;ShiftTenors&amp;gt;6M,1Y,2Y,3Y,5Y,7Y,10Y,15Y,20Y,30Y&amp;lt;/ShiftTenors&amp;gt;
&amp;lt;/DiscountCurve&amp;gt;
&amp;lt;/DiscountCurves&amp;gt;
&amp;lt;IndexCurves&amp;gt;
&amp;lt;IndexCurve index=&amp;quot;EUR-ESTER&amp;quot;&amp;gt;
&amp;lt;ShiftType&amp;gt;Absolute&amp;lt;/ShiftType&amp;gt;
&amp;lt;ShiftSize&amp;gt;0.0001&amp;lt;/ShiftSize&amp;gt;
&amp;lt;ShiftTenors&amp;gt;6M,1Y,2Y,3Y,5Y,7Y,10Y,15Y,20Y,30Y&amp;lt;/ShiftTenors&amp;gt;
&amp;lt;/IndexCurve&amp;gt;
&amp;lt;/IndexCurves&amp;gt;
&amp;lt;/SensitivityAnalysis&amp;gt;
&lt;/code>&lt;/pre>
&lt;div class="callout callout-info">
&lt;h4>ParConversion in the shipped files&lt;/h4>
The downloadable &lt;code>sensitivity.xml&lt;/code> includes a &lt;code>&amp;lt;ParConversion&amp;gt;&lt;/code> block, and &lt;code>ore.xml&lt;/code> includes &lt;code>parSensitivity=Y&lt;/code>. This block does not affect &lt;code>sensitivity.csv&lt;/code>. We activate it in &lt;a href="#6-config-pass-2-turning-on-par-conversion">config pass 2&lt;/a> to generate &lt;code>parsensitivity.csv&lt;/code>.
&lt;/div>
&lt;h3 id="wiring-it-up-orexml">Wiring it up (&lt;code>ore.xml&lt;/code>)&lt;/h3>
&lt;p>In &lt;code>ore.xml&lt;/code>, we activate the sensitivity analytic and specify the input and output filenames:&lt;/p>
&lt;pre>&lt;code class="language-xml">&amp;lt;Analytic type=&amp;quot;sensitivity&amp;quot;&amp;gt;
&amp;lt;Parameter name=&amp;quot;active&amp;quot;&amp;gt;Y&amp;lt;/Parameter&amp;gt;
&amp;lt;Parameter name=&amp;quot;marketConfigFile&amp;quot;&amp;gt;simulation.xml&amp;lt;/Parameter&amp;gt;
&amp;lt;Parameter name=&amp;quot;sensitivityConfigFile&amp;quot;&amp;gt;sensitivity.xml&amp;lt;/Parameter&amp;gt;
&amp;lt;Parameter name=&amp;quot;sensitivityOutputFile&amp;quot;&amp;gt;sensitivity.csv&amp;lt;/Parameter&amp;gt;
&amp;lt;Parameter name=&amp;quot;outputSensitivityThreshold&amp;quot;&amp;gt;0.000001&amp;lt;/Parameter&amp;gt;
&amp;lt;/Analytic&amp;gt;
&lt;/code>&lt;/pre>
&lt;p>&lt;code>outputSensitivityThreshold&lt;/code> filters the output: any factor whose delta is smaller than &lt;code>0.000001&lt;/code> in absolute value is dropped from &lt;code>sensitivity.csv&lt;/code>. Every tenor on this trade is comfortably above that bar; on larger portfolios the same setting keeps the file down to the rows that matter.&lt;/p>
&lt;p>Running &lt;code>run_sensitivity.py&lt;/code> executes ORE and generates &lt;code>Output/sensitivity.csv&lt;/code>.&lt;/p>
&lt;hr>
&lt;h2 id="3-reading-the-output-risk-factors-and-units">3. Reading the output: risk factors and units&lt;/h2>
&lt;p>&lt;code>sensitivity.csv&lt;/code> contains one row per (factor, tenor) pair. Because we are using a single curve, both &lt;code>DiscountCurve/EUR&lt;/code> and &lt;code>IndexCurve/EUR-ESTER&lt;/code> reference the same underlying curve, but ORE tracks them separately:&lt;/p>
&lt;ul>
&lt;li>&lt;strong>&lt;code>DiscountCurve/EUR&lt;/code> delta&lt;/strong>: Sensitivity of NPV to a shift in the discounting zero rate, $\frac{\partial \text{NPV}}{\partial r_{\text{disc}}}$.&lt;/li>
&lt;li>&lt;strong>&lt;code>IndexCurve/EUR-ESTER&lt;/code> delta&lt;/strong>: Sensitivity of NPV to a shift in the forecasting zero rate, $\frac{\partial \text{NPV}}{\partial r_{\text{forecast}}}$ (used to project floating cash flows).&lt;/li>
&lt;/ul>
&lt;p>Factor names follow the pattern &lt;code>Handle/Name/Index/Tenor&lt;/code>, such as &lt;code>DiscountCurve/EUR/7/15Y&lt;/code> and &lt;code>IndexCurve/EUR-ESTER/7/15Y&lt;/code>. The middle &lt;code>Index&lt;/code> field is the zero-based position of that tenor in the simulation-market grid: &lt;code>7&lt;/code> means the 8th pillar, since the grid &lt;code>6M,1Y,2Y,3Y,5Y,7Y,10Y,15Y,20Y,30Y&lt;/code> is numbered 0 through 9 and 15Y falls at position 7.&lt;/p>
&lt;p>&lt;strong>Units&lt;/strong>: With &lt;code>&amp;lt;ShiftType&amp;gt;Absolute&amp;lt;/ShiftType&amp;gt;&lt;/code> and &lt;code>ShiftSize&lt;/code> set to &lt;code>0.0001&lt;/code> (1 bp), each delta value represents the EUR change in NPV per 1 bp absolute shift in that zero rate.&lt;/p>
&lt;p>One parameter you will not find in our &lt;code>sensitivity.xml&lt;/code> is &lt;code>&amp;lt;ShiftScheme&amp;gt;&lt;/code>, which selects the finite-difference method (&lt;code>Forward&lt;/code>, &lt;code>Backward&lt;/code>, or &lt;code>Central&lt;/code>) and defaults to &lt;code>Forward&lt;/code>. The default suits us for raw zero sensitivities. &lt;code>Central&lt;/code> averages up and down shifts to cancel second-order curvature, requiring two repricings per factor; we examine central differences in &lt;a href="#verification-bump-and-reval">section 9&lt;/a>.&lt;/p>
&lt;p>A quick orientation on the raw file before the cleaned tables: &lt;code>sensitivity.csv&lt;/code> carries ten columns (&lt;code>TradeId&lt;/code>, &lt;code>IsPar&lt;/code>, &lt;code>Factor_1&lt;/code>, &lt;code>ShiftSize_1&lt;/code>, &lt;code>Factor_2&lt;/code>, &lt;code>ShiftSize_2&lt;/code>, &lt;code>Currency&lt;/code>, &lt;code>Base NPV&lt;/code>, &lt;code>Delta&lt;/code>, &lt;code>Gamma&lt;/code>) with one row per trade and factor. &lt;code>Base NPV&lt;/code> repeats the unshifted trade value on every row, and the paired &lt;code>Factor_2&lt;/code>/&lt;code>ShiftSize_2&lt;/code> columns are reserved for cross-gamma terms, where ORE shifts two factors at once. With a single trade and one factor per row, those extra columns are constant or empty here, so the tables in this post show the &lt;code>Factor&lt;/code> and &lt;code>Delta&lt;/code> columns only. A sample, showing both factors at three tenors:&lt;/p>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>Factor&lt;/th>
&lt;th>Delta (EUR)&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>&lt;code>DiscountCurve/EUR/0/6M&lt;/code>&lt;/td>
&lt;td>0.29&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>IndexCurve/EUR-ESTER/0/6M&lt;/code>&lt;/td>
&lt;td>4.37&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>DiscountCurve/EUR/1/1Y&lt;/code>&lt;/td>
&lt;td>9.28&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>IndexCurve/EUR-ESTER/1/1Y&lt;/code>&lt;/td>
&lt;td>-12.99&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>DiscountCurve/EUR/7/15Y&lt;/code>&lt;/td>
&lt;td>198.33&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>IndexCurve/EUR-ESTER/7/15Y&lt;/code>&lt;/td>
&lt;td>-7030.45&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>This XML schema extends to other asset classes with minimal changes. ORE&amp;rsquo;s standard &lt;code>SensiSmile&lt;/code> example (&lt;a href="https://github.com/OpenSourceRisk/Engine/blob/master/Examples/MarketRisk/Input/SensiSmile/sensitivity.xml">&lt;code>Examples/MarketRisk/Input/SensiSmile/sensitivity.xml&lt;/code>&lt;/a>) applies the same structure to FX spots, swaption and cap/floor volatility surfaces, credit curves, and equity prices. Shocking an FX rate, for instance, needs only a single extra block declaring a 1% relative move on EURUSD:&lt;/p>
&lt;pre>&lt;code class="language-xml">&amp;lt;FxSpots&amp;gt;
&amp;lt;FxSpot ccypair=&amp;quot;EURUSD&amp;quot;&amp;gt;
&amp;lt;ShiftType&amp;gt;Relative&amp;lt;/ShiftType&amp;gt;
&amp;lt;ShiftSize&amp;gt;0.01&amp;lt;/ShiftSize&amp;gt;
&amp;lt;/FxSpot&amp;gt;
&amp;lt;/FxSpots&amp;gt;
&lt;/code>&lt;/pre>
&lt;p>No &lt;code>&amp;lt;ShiftTenors&amp;gt;&lt;/code> is required here because a spot is a single node rather than a curve, and the resulting delta is the base-currency NPV change per 1% move in the rate. One naming detail: ORE orders each pair with the simulation market&amp;rsquo;s base currency last, so in our EUR-based setup a USD shock is reported as the factor &lt;code>FXSpot/USDEUR/0/spot&lt;/code> in &lt;code>sensitivity.csv&lt;/code> (&lt;code>EURUSD&lt;/code> would appear with a USD base currency). The simulation market needs no matching block: FX spot factors are inferred from the &lt;code>&amp;lt;Currencies&amp;gt;&lt;/code> list, so adding &lt;code>USD&lt;/code> alongside &lt;code>EUR&lt;/code> in &lt;code>simulation.xml&lt;/code> brings the pair into play.&lt;/p>
&lt;hr>
&lt;h2 id="4-zero-domain-results-cleaned-table-and-risk-shape">4. Zero-domain results: cleaned table and risk shape&lt;/h2>
&lt;p>Because both factors share one underlying curve in reality, shifting the physical rate moves discounting and forecasting together. The combined delta per tenor is the algebraic sum of the discount and index deltas:&lt;/p>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>Tenor&lt;/th>
&lt;th>Discount delta (EUR)&lt;/th>
&lt;th>Index delta (EUR)&lt;/th>
&lt;th>Total (EUR)&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>6M&lt;/td>
&lt;td>0.29&lt;/td>
&lt;td>4.37&lt;/td>
&lt;td>4.66&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>1Y&lt;/td>
&lt;td>9.28&lt;/td>
&lt;td>-12.99&lt;/td>
&lt;td>-3.71&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>2Y&lt;/td>
&lt;td>-0.42&lt;/td>
&lt;td>-28.04&lt;/td>
&lt;td>-28.46&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>3Y&lt;/td>
&lt;td>-9.44&lt;/td>
&lt;td>-105.54&lt;/td>
&lt;td>-114.98&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>5Y&lt;/td>
&lt;td>-3.02&lt;/td>
&lt;td>-199.66&lt;/td>
&lt;td>-202.68&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>7Y&lt;/td>
&lt;td>21.04&lt;/td>
&lt;td>-343.29&lt;/td>
&lt;td>-322.25&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>10Y&lt;/td>
&lt;td>117.40&lt;/td>
&lt;td>-788.31&lt;/td>
&lt;td>-670.91&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>15Y&lt;/td>
&lt;td>198.33&lt;/td>
&lt;td>-7,030.45&lt;/td>
&lt;td>-6,832.12&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>20Y&lt;/td>
&lt;td>31.01&lt;/td>
&lt;td>-5,539.49&lt;/td>
&lt;td>-5,508.48&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;h3 id="risk-shape">Risk shape&lt;/h3>
&lt;p>Three observations stand out:&lt;/p>
&lt;ol>
&lt;li>Risk concentrates at the long end. The largest negative deltas are at 15Y and 20Y, near the swap maturity where cash-flow weights are greatest. Because the swap terminates at 20Y, ORE reports no 30Y sensitivity.&lt;/li>
&lt;li>The receiver loses value when rates rise. The trade receives fixed and pays floating; higher interest rates increase floating payments, reducing NPV.&lt;/li>
&lt;li>The index factor dominates, while discounting partially offsets. At 15Y, the index delta is &lt;code>-7,030.45&lt;/code> EUR compared to &lt;code>+198.33&lt;/code> EUR for discounting. Forecasting floating cash flows accounts for the majority of the risk on this receiver swap.&lt;/li>
&lt;/ol>
&lt;div class="callout callout-info">
&lt;h4>Discount vs Index on a single curve&lt;/h4>
The discount and index columns represent two components of the same &lt;code>EUR-ESTER&lt;/code> curve. Adding them together gives the total sensitivity to a single physical rate move. On a multi-curve portfolio, the same separation lets you isolate basis risk.
&lt;/div>
&lt;hr>
&lt;h2 id="5-converting-zero-deltas-into-hedge-instructions">5. Converting zero deltas into hedge instructions&lt;/h2>
&lt;p>A zero-domain table answers the modeller&amp;rsquo;s question, which grid node moved, but not the trader&amp;rsquo;s: what do I buy or sell? Zero rates are interpolated constructions, not instruments anyone can trade. To hedge risk in the market, desks trade liquid par instruments like OIS swaps, so the risk has to be re-expressed in their terms before it becomes an instruction.&lt;/p>
&lt;p>This zero-to-par step exists because a zero rate on the simulation grid is an interpolated construction, not a quote anyone can trade. It is a rates-curve problem, not a general one. For risk factors that are the traded quote themselves, such as an FX spot, an equity price, or a commodity price, shifting the handle moves exactly the instrument a desk would hedge, so the raw delta in &lt;code>sensitivity.csv&lt;/code> already reads as a par bump and no Jacobian is involved. The conversion below applies only to the factor types ORE can rebuild as par instruments: yield, credit, and inflation curves and cap/floor volatilities.&lt;/p>
&lt;p>Directly bumping every par quote in the bootstrap and repricing the portfolio would be slow on large books, requiring a full curve rebuild per quote per trade. Instead, ORE computes a linear change of basis using the Jacobian matrix ($J$). The Jacobian maps zero-rate shifts to par-rate shifts, allowing quick conversion of zero sensitivities into par sensitivities.&lt;/p>
&lt;hr>
&lt;h2 id="6-config-pass-2-turning-on-par-conversion">6. Config pass 2: turning on par conversion&lt;/h2>
&lt;p>Generating &lt;code>parsensitivity.csv&lt;/code>, &lt;code>jacobi.csv&lt;/code>, and &lt;code>jacobi_inverse.csv&lt;/code> requires adding a &lt;code>&amp;lt;ParConversion&amp;gt;&lt;/code> block to each factor in &lt;code>sensitivity.xml&lt;/code> and setting &lt;code>parSensitivity=Y&lt;/code> in &lt;code>ore.xml&lt;/code>.&lt;/p>
&lt;h3 id="extending-sensitivityxml">Extending &lt;code>sensitivity.xml&lt;/code>&lt;/h3>
&lt;p>We add a &lt;code>&amp;lt;ParConversion&amp;gt;&lt;/code> block to both &lt;code>DiscountCurve/EUR&lt;/code> and &lt;code>IndexCurve/EUR-ESTER&lt;/code>:&lt;/p>
&lt;pre>&lt;code class="language-xml">&amp;lt;ParConversion&amp;gt;
&amp;lt;Instruments&amp;gt;OIS,OIS,OIS,OIS,OIS,OIS,OIS,OIS,OIS,OIS&amp;lt;/Instruments&amp;gt;
&amp;lt;SingleCurve&amp;gt;true&amp;lt;/SingleCurve&amp;gt;
&amp;lt;Conventions&amp;gt;
&amp;lt;Convention id=&amp;quot;OIS&amp;quot;&amp;gt;EUR-ESTER-OIS&amp;lt;/Convention&amp;gt;
&amp;lt;/Conventions&amp;gt;
&amp;lt;/ParConversion&amp;gt;
&lt;/code>&lt;/pre>
&lt;ul>
&lt;li>&lt;strong>&lt;code>Instruments&lt;/code>&lt;/strong>: Ten &lt;code>OIS&lt;/code> tokens corresponding to the ten simulation grid tenors (&lt;code>6M&lt;/code> through &lt;code>30Y&lt;/code>).&lt;/li>
&lt;li>&lt;strong>&lt;code>SingleCurve&lt;/code>&lt;/strong>: Setting this to &lt;code>true&lt;/code> prices the par instruments off the same &lt;code>EUR-ESTER&lt;/code> curve used for discounting.&lt;/li>
&lt;li>&lt;strong>&lt;code>Conventions&lt;/code>&lt;/strong>: Maps the &lt;code>OIS&lt;/code> token to the &lt;code>EUR-ESTER-OIS&lt;/code> convention defined in &lt;code>conventions.xml&lt;/code> (annual fixed leg, &lt;code>A360&lt;/code>, modified following).&lt;/li>
&lt;/ul>
&lt;h3 id="available-instrument-types">Available instrument types&lt;/h3>
&lt;p>For interest rate curve factors, &lt;code>&amp;lt;Instruments&amp;gt;&lt;/code> supports these contract types:&lt;/p>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>Token&lt;/th>
&lt;th>Par instrument built by ORE&lt;/th>
&lt;th>Supported factors&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>&lt;code>OIS&lt;/code>&lt;/td>
&lt;td>Overnight-indexed swap on the specified index&lt;/td>
&lt;td>Discount, index, yield curves&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>IRS&lt;/code>&lt;/td>
&lt;td>Fixed-vs-floating swap on an Ibor index&lt;/td>
&lt;td>Discount, index, yield curves&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>DEP&lt;/code>&lt;/td>
&lt;td>Zero-coupon deposit&lt;/td>
&lt;td>Discount, index, yield curves&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>FRA&lt;/code>&lt;/td>
&lt;td>Forward rate agreement&lt;/td>
&lt;td>Discount, index, yield curves&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>TBS&lt;/code>&lt;/td>
&lt;td>Tenor basis swap (such as 6M vs 3M)&lt;/td>
&lt;td>Discount, index, yield curves&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>XBS&lt;/code>&lt;/td>
&lt;td>Cross-currency basis swap&lt;/td>
&lt;td>Discount, yield curves&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>FXF&lt;/code>&lt;/td>
&lt;td>FX forward&lt;/td>
&lt;td>Discount curves only&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>Other factor families carry their own par instruments: credit curve factors accept any code and interpret it as a CDS; zero inflation index curves build CPI swaps; year-on-year inflation curves and their cap/floor volatilities use &lt;code>ZIS&lt;/code> or &lt;code>YYS&lt;/code>; and cap/floor volatility factors use a flat &lt;code>CapFloor&lt;/code>, where an overnight-indexed underlying also needs a &lt;code>&amp;lt;RateComputationPeriod&amp;gt;&lt;/code>.&lt;/p>
&lt;p>Instrument codes can also carry an arbitrary suffix that binds them to different conventions of the same type: listing &lt;code>FRA1,FRA2&lt;/code> prices those tenors off conventions &lt;code>FRA1&lt;/code> and &lt;code>FRA2&lt;/code> respectively.&lt;/p>
&lt;p>Rules for configuring par instruments:&lt;/p>
&lt;ul>
&lt;li>Token and convention types must align. &lt;code>OIS&lt;/code> requires an &lt;code>&amp;lt;OIS&amp;gt;&lt;/code> convention, &lt;code>IRS&lt;/code> requires &lt;code>&amp;lt;Swap&amp;gt;&lt;/code>, &lt;code>DEP&lt;/code> requires &lt;code>&amp;lt;Deposit&amp;gt;&lt;/code>, &lt;code>FRA&lt;/code> requires &lt;code>&amp;lt;FRA&amp;gt;&lt;/code>, &lt;code>TBS&lt;/code> requires &lt;code>&amp;lt;TenorBasisSwap&amp;gt;&lt;/code>, &lt;code>XBS&lt;/code> requires &lt;code>&amp;lt;CrossCurrencyBasis&amp;gt;&lt;/code>, and &lt;code>FXF&lt;/code> requires &lt;code>&amp;lt;FX&amp;gt;&lt;/code>. Mismatches fail when ORE builds the par instruments.&lt;/li>
&lt;li>The index behind the token must match its type. &lt;code>OIS&lt;/code> requires an overnight index; pointing it at an Ibor index fails, and Ibor instruments must use &lt;code>IRS&lt;/code>, &lt;code>DEP&lt;/code>, or &lt;code>FRA&lt;/code>.&lt;/li>
&lt;/ul>
&lt;h3 id="conventions-to-jacobian">How conventions determine the Jacobian&lt;/h3>
&lt;p>The &lt;code>&amp;lt;Conventions&amp;gt;&lt;/code> mapping determines how ORE builds par instruments. &lt;code>ParSensitivityInstrumentBuilder&lt;/code> resolves each token to its template in &lt;code>conventions.xml&lt;/code> (such as &lt;code>MakeOIS&lt;/code> for &lt;code>OIS&lt;/code>), extracting payment frequencies, day counters, and settlement lags.&lt;/p>
&lt;p>This is where convention discipline pays off; every payment lag, day counter, and settlement detail in &lt;code>conventions.xml&lt;/code> feeds straight into the entries of $J$. Our &lt;a href="https://obrienjoey.github.io/post/ore_sofr_bootstrap/">curve bootstrapping post&lt;/a> covers these convention blocks in detail, down to how each parameter shapes the instruments ORE builds from your quotes.&lt;/p>
&lt;p>ORE determines the fair fixed rate $c_i$ that zeros each instrument&amp;rsquo;s NPV as a function of the zero rates on the grid. To compute the Jacobian matrix $J$, ORE bumps each zero rate $z_j$ by 1 bp, reprices each par instrument, and records the resulting par rate change $\partial c_i / \partial z_j$.&lt;/p>
&lt;p>Modifying a convention (such as switching from &lt;code>A360&lt;/code> to &lt;code>A365&lt;/code> or changing payment frequency) alters cash-flow timings and changes the resulting Jacobian entries.&lt;/p>
&lt;h3 id="wiring-it-up-orexml-1">Wiring it up (&lt;code>ore.xml&lt;/code>)&lt;/h3>
&lt;p>We add four parameters to the sensitivity analytic in &lt;code>ore.xml&lt;/code>:&lt;/p>
&lt;pre>&lt;code class="language-xml">&amp;lt;Analytic type=&amp;quot;sensitivity&amp;quot;&amp;gt;
&amp;lt;Parameter name=&amp;quot;active&amp;quot;&amp;gt;Y&amp;lt;/Parameter&amp;gt;
&amp;lt;Parameter name=&amp;quot;marketConfigFile&amp;quot;&amp;gt;simulation.xml&amp;lt;/Parameter&amp;gt;
&amp;lt;Parameter name=&amp;quot;sensitivityConfigFile&amp;quot;&amp;gt;sensitivity.xml&amp;lt;/Parameter&amp;gt;
&amp;lt;Parameter name=&amp;quot;sensitivityOutputFile&amp;quot;&amp;gt;sensitivity.csv&amp;lt;/Parameter&amp;gt;
&amp;lt;Parameter name=&amp;quot;outputSensitivityThreshold&amp;quot;&amp;gt;0.000001&amp;lt;/Parameter&amp;gt;
&amp;lt;Parameter name=&amp;quot;parSensitivity&amp;quot;&amp;gt;Y&amp;lt;/Parameter&amp;gt;
&amp;lt;Parameter name=&amp;quot;parSensitivityOutputFile&amp;quot;&amp;gt;parsensitivity.csv&amp;lt;/Parameter&amp;gt;
&amp;lt;Parameter name=&amp;quot;outputJacobi&amp;quot;&amp;gt;Y&amp;lt;/Parameter&amp;gt;
&amp;lt;Parameter name=&amp;quot;jacobiOutputFile&amp;quot;&amp;gt;jacobi.csv&amp;lt;/Parameter&amp;gt;
&amp;lt;Parameter name=&amp;quot;jacobiInverseOutputFile&amp;quot;&amp;gt;jacobi_inverse.csv&amp;lt;/Parameter&amp;gt;
&amp;lt;/Analytic&amp;gt;
&lt;/code>&lt;/pre>
&lt;p>Running &lt;code>run_sensitivity.py&lt;/code> now outputs:&lt;/p>
&lt;ul>
&lt;li>&lt;strong>&lt;code>parsensitivity.csv&lt;/code>&lt;/strong>: Sensitivities expressed in terms of par instrument rates.&lt;/li>
&lt;li>&lt;strong>&lt;code>jacobi.csv&lt;/code>&lt;/strong>: The Jacobian matrix ($\partial c / \partial z$).&lt;/li>
&lt;li>&lt;strong>&lt;code>jacobi_inverse.csv&lt;/code>&lt;/strong>: The inverse Jacobian ($\partial z / \partial c$).&lt;/li>
&lt;/ul>
&lt;hr>
&lt;h2 id="7-the-jacobian-changing-basis-from-zero-to-par">7. The Jacobian: changing basis from zero to par&lt;/h2>
&lt;p>The fair rate of a 15Y OIS ($c_{\text{15Y}}$) depends on the zero rates $z_j$ discounting its cash flows. Applying the chain rule to portfolio value $V$:&lt;/p>
&lt;p>$$
\frac{\partial V}{\partial z_j} = \sum_i \frac{\partial V}{\partial c_i}\cdot\frac{\partial c_i}{\partial z_j}
\quad\Longleftrightarrow\quad
\nabla_z V = J^{T} \nabla_c V
$$&lt;/p>
&lt;p>where the Jacobian is $J_{ji}=\frac{\partial c_i}{\partial z_j}$.&lt;/p>
&lt;p>Inverting the relationship yields the par sensitivities:&lt;/p>
&lt;p>$$ \nabla_c V = \left( J^{-1} \right)^{T} \nabla_z V. $$&lt;/p>
&lt;p>ORE constructs the par instruments, perturbs each grid zero rate by 1 bp, calculates $\partial c_i / \partial z_j$ numerically to populate $J$, and computes $J^{-1}$. The zero deltas of each risk factor are then converted through its corresponding inverse Jacobian: the &lt;code>DiscountCurve/EUR&lt;/code> deltas through the discount factor&amp;rsquo;s $J^{-1}$, the &lt;code>IndexCurve/EUR-ESTER&lt;/code> deltas through its own.&lt;/p>
&lt;p>As with ORE&amp;rsquo;s calibration reports, nothing here is hidden. The full matrices are written to &lt;code>jacobi.csv&lt;/code> and &lt;code>jacobi_inverse.csv&lt;/code>, so every weight in the conversion can be inspected, and verified by hand, rather than trusted blindly.&lt;/p>
&lt;h3 id="15y-column-example">15Y column example&lt;/h3>
&lt;p>In &lt;code>jacobi_inverse.csv&lt;/code>, the 15Y par factor contains three non-zero entries:&lt;/p>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>Raw zero factor&lt;/th>
&lt;th>∂z / ∂c (weight)&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>15Y&lt;/td>
&lt;td>1.128823&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>20Y&lt;/td>
&lt;td>-0.102531&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>30Y&lt;/td>
&lt;td>-0.060106&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>This upper triangular structure reflects ORE&amp;rsquo;s bootstrapping order. Shifting a par quote at tenor $t$ only affects zero rates at $t$ and longer maturities.&lt;/p>
&lt;p>Because the swap has no cash flows past 20Y, its 30Y zero delta is zero. Applying the weights to the discount factor gives:&lt;/p>
&lt;p>$$ 1.128823 \times (+198.33) + (-0.102531) \times (+31.01) = 220.70 $$&lt;/p>
&lt;p>For the index factor:&lt;/p>
&lt;p>$$ 1.128823 \times (-7030.45) + (-0.102531) \times (-5539.49) = -7368.16 $$&lt;/p>
&lt;p>These match the deltas in &lt;code>parsensitivity.csv&lt;/code> (&lt;code>DiscountCurve/EUR/7/15Y = 220.70&lt;/code> and &lt;code>IndexCurve/EUR-ESTER/7/15Y = -7368.16&lt;/code>).&lt;/p>
&lt;div class="callout callout-info">
&lt;h4>Conversion properties&lt;/h4>
&lt;ol>
&lt;li>Par conversion reallocates risk. It transforms existing zero-domain deltas into par space without repricing the underlying portfolio.&lt;/li>
&lt;li>The conversion runs per risk factor. The two factors from section 3, &lt;code>DiscountCurve/EUR&lt;/code> and &lt;code>IndexCurve/EUR-ESTER&lt;/code>, each carry their own zero deltas and are converted through their own $J^{-1}$. Here they happen to share the same &lt;code>EUR-ESTER&lt;/code> curve, so their conversion weights come out identical; on a multi-curve book the two matrices would differ, which is how basis risk would show up.&lt;/li>
&lt;/ol>
&lt;/div>
&lt;hr>
&lt;h2 id="8-par-domain-results-cleaned-table-and-risk-shape">8. Par-domain results: cleaned table and risk shape&lt;/h2>
&lt;p>Summing the discount and index deltas from &lt;code>parsensitivity.csv&lt;/code> gives the total par delta at each tenor:&lt;/p>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>Tenor&lt;/th>
&lt;th>Discount delta (EUR)&lt;/th>
&lt;th>Index delta (EUR)&lt;/th>
&lt;th>Total par delta (EUR)&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>6M&lt;/td>
&lt;td>0.48&lt;/td>
&lt;td>-0.97&lt;/td>
&lt;td>-0.49&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>1Y&lt;/td>
&lt;td>8.57&lt;/td>
&lt;td>9.34&lt;/td>
&lt;td>17.91&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>2Y&lt;/td>
&lt;td>-1.63&lt;/td>
&lt;td>16.25&lt;/td>
&lt;td>14.62&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>3Y&lt;/td>
&lt;td>-12.62&lt;/td>
&lt;td>-8.49&lt;/td>
&lt;td>-21.11&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>5Y&lt;/td>
&lt;td>-10.10&lt;/td>
&lt;td>7.91&lt;/td>
&lt;td>-2.19&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>7Y&lt;/td>
&lt;td>10.68&lt;/td>
&lt;td>1.30&lt;/td>
&lt;td>11.98&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>10Y&lt;/td>
&lt;td>111.25&lt;/td>
&lt;td>-73.45&lt;/td>
&lt;td>37.80&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>15Y&lt;/td>
&lt;td>220.70&lt;/td>
&lt;td>-7,368.16&lt;/td>
&lt;td>-7,147.46&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>20Y&lt;/td>
&lt;td>37.07&lt;/td>
&lt;td>-6,622.58&lt;/td>
&lt;td>-6,585.51&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;h3 id="reading-the-par-table">Reading the par table&lt;/h3>
&lt;ol>
&lt;li>Overall risk direction is preserved. The primary risk remains at 15Y and 20Y, with the receiver losing value as rates rise.&lt;/li>
&lt;li>The index factor remains dominant. At 15Y, the index delta is &lt;code>-7,368.16&lt;/code> EUR while discounting contributes &lt;code>+220.70&lt;/code> EUR.&lt;/li>
&lt;li>The 15Y delta represents tradeable risk. The total delta of &lt;code>-7,147.46&lt;/code> EUR indicates that a 1 bp increase in the 15Y OIS market quote decreases swap NPV by approximately 7,150 EUR.&lt;/li>
&lt;li>No 30Y sensitivity appears. With no cash flows past 20Y, the trade carries no 30Y risk.&lt;/li>
&lt;/ol>
&lt;hr>
&lt;h2 id="verification-bump-and-reval">9. Verification: the 15Y manual bump-and-reval check&lt;/h2>
&lt;p>To verify the &lt;code>-7,147.46&lt;/code> EUR par delta independently, we can bump the actual 15Y OIS market quote in &lt;code>marketdata.csv&lt;/code> (&lt;code>IR_SWAP/RATE/EUR/ESTER/2D/1D/15Y&lt;/code>) by ±1 bp, rebuild the curve, and reprice the trade.&lt;/p>
&lt;p>The calculation flow in &lt;code>validate_par_sensi.py&lt;/code>:&lt;/p>
&lt;pre>&lt;code class="language-python">v0 = npv_at(0) # base: -443,626.98
vp = npv_at(+1) # +1bp quote: -450,818.40
vm = npv_at(-1) # -1bp quote: -436,427.78
forward = vp - v0 # one-sided: -7,191.42
central = (vp - vm) / 2 # two-sided: -7,195.31
&lt;/code>&lt;/pre>
&lt;p>&lt;code>npv_at(shift_bp)&lt;/code> modifies &lt;code>marketdata.csv&lt;/code>, runs ORE&amp;rsquo;s pricing engine, and reads the resulting NPV from &lt;code>npv.csv&lt;/code>. Comparing against the par table:&lt;/p>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>Metric&lt;/th>
&lt;th>Measured NPV change (EUR)&lt;/th>
&lt;th>Difference vs par delta (EUR)&lt;/th>
&lt;th>Relative difference&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>Central difference (two-sided)&lt;/td>
&lt;td>-7,195.31&lt;/td>
&lt;td>-47.85&lt;/td>
&lt;td>0.67%&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Forward difference (one-sided)&lt;/td>
&lt;td>-7,191.42&lt;/td>
&lt;td>-43.96&lt;/td>
&lt;td>0.61%&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Total par delta (section 8)&lt;/td>
&lt;td>-7,147.46&lt;/td>
&lt;td>N/A&lt;/td>
&lt;td>N/A&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>The measured bump matches the par table to within 0.7%.&lt;/p>
&lt;h3 id="why-is-there-a-slight-difference">Why is there a slight difference?&lt;/h3>
&lt;p>The main reason for the small variance (~48 EUR or 0.67%) is the &lt;strong>linearisation&lt;/strong> inherent in the analytic Jacobian conversion versus a &lt;strong>full curve rebuild&lt;/strong>:&lt;/p>
&lt;ol>
&lt;li>&lt;strong>Linearised Jacobian conversion (ORE)&lt;/strong>: The par sensitivity table calculates risk via a first-order linear approximation (the Jacobian matrix $J = \partial \text{par} / \partial \text{zero}$). It converts zero-rate sensitivities into par sensitivities via the chain rule on a fixed simulation grid without recalibrating the yield curve from scratch.&lt;/li>
&lt;li>&lt;strong>Full curve re-bootstrapping (Bump &amp;amp; Reval)&lt;/strong>: In contrast, the manual bump-and-reval modifies the actual market quote in &lt;code>marketdata.csv&lt;/code> and re-runs the entire bootstrap process. This recalibrates all dependent discount factors through the curve&amp;rsquo;s interpolation routine before repricing the trade.&lt;/li>
&lt;/ol>
&lt;p>The 0.67% difference simply reflects this difference between a fast first-order linear approximation and a full non-linear curve recalibration.&lt;/p>
&lt;p>In summary, bumping the 15Y OIS quote by 1 bp moves this receiver by roughly -7,150 to -7,200 EUR, closely matching the -7,147.46 EUR calculated by the Jacobian. This confirms that ORE&amp;rsquo;s par conversion produces an accurate, tradeable risk figure without the overhead of re-bootstrapping curves for every risk factor.&lt;/p>
&lt;hr>
&lt;h2 id="summary-and-code">10. Summary and code&lt;/h2>
&lt;p>This post walked through the complete zero-to-par sensitivity process in ORE:&lt;/p>
&lt;ul>
&lt;li>&lt;strong>Config pass 1&lt;/strong>: Configured &lt;code>simulation.xml&lt;/code> and &lt;code>sensitivity.xml&lt;/code> for zero-domain deltas, separating discount and forecast factors.&lt;/li>
&lt;li>&lt;strong>Config pass 2&lt;/strong>: Added &lt;code>&amp;lt;ParConversion&amp;gt;&lt;/code> with OIS templates to generate &lt;code>parsensitivity.csv&lt;/code> and the Jacobian matrices (&lt;code>jacobi.csv&lt;/code>, &lt;code>jacobi_inverse.csv&lt;/code>).&lt;/li>
&lt;li>&lt;strong>Validation&lt;/strong>: Bumped the 15Y OIS market quote by ±1 bp in a manual bump-and-reval, verifying that the full re-bootstrap NPV change (-7,195.31 EUR) matches the Jacobian par delta (-7,147.46 EUR) within 0.7%.&lt;/li>
&lt;/ul>
&lt;p>With par-domain deltas proven against an independent repricing like this, the tables can feed scenario ladders, VaR, and stress testing directly: they quote risk against liquid instruments a desk can actually trade. Vega on non-linear trades comes in a later post.&lt;/p>
&lt;h3 id="source-code">Source Code&lt;/h3>
&lt;p>All configuration files and Python scripts demonstrated in this post are available for download:
&lt;a href="ore_sensitivity_files.zip" class="btn btn-outline-primary btn-sm mt-3 mb-3" download>
&lt;i class="fas fa-download mr-1">&lt;/i> Download Source Code
&lt;/a>
&lt;/p>
&lt;hr>
&lt;h3 id="need-help-with-ore-integration">Need Help with ORE Integration?&lt;/h3>
&lt;p>Dropping a risk engine into an established data landscape is rarely plug-and-play. Bespoke trade representations, legacy databases, and user-specific market conventions all get in the way of a clean feed.&lt;/p>
&lt;p>If you are evaluating ORE for your team, or need a custom trade translation layer, &lt;strong>&lt;a href="https://obrienjoey.github.io/contact">reach out&lt;/a>&lt;/strong>. We build robust ORE integration layers and validation pipelines tailored to your stack.&lt;/p></description></item></channel></rss>