<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>VaR | Joey O'Brien</title><link>https://obrienjoey.github.io/tags/var/</link><atom:link href="https://obrienjoey.github.io/tags/var/index.xml" rel="self" type="application/rss+xml"/><description>VaR</description><generator>Source Themes Academic (https://sourcethemes.com/academic/)</generator><language>en-us</language><copyright>© 2026 Joey O'Brien</copyright><lastBuildDate>Wed, 07 Oct 2026 00:00:00 +0000</lastBuildDate><image><url>https://obrienjoey.github.io/img/og-banner.jpg</url><title>VaR</title><link>https://obrienjoey.github.io/tags/var/</link></image><item><title>Historical simulation VaR in ORE</title><link>https://obrienjoey.github.io/post/ore_var/</link><pubDate>Wed, 07 Oct 2026 00:00:00 +0000</pubDate><guid>https://obrienjoey.github.io/post/ore_var/</guid><description>&lt;p>Market risk analysis can be viewed from several angles. Sensitivities tell us how a portfolio responds to small, individual moves in its risk factors; Value at Risk asks a different question: how large a loss might the portfolio experience over a given horizon when those risk factors move together?&lt;/p>
&lt;p>In &lt;a href="https://obrienjoey.github.io/post/ore_sensitivity/">Part 3&lt;/a>, we looked at how ORE calculates zero and par Greeks on a simulation market grid. Here, we move from those local measures to historical simulation, using actual historical market moves to generate a distribution of portfolio P&amp;amp;Ls. This concept seems relatively straightforward, but there are several details that matters in practice, both computationally and theoretically. One has to generate consistent historical scenarios, reprice every trade across hundreds of windows (which can quickly lead to multiple million valuations), and decide how to handle holding periods, cash flows, and time decay.&lt;/p>
&lt;p>In this post, we walk through ORE&amp;rsquo;s historical simulation VaR (HistSim VaR) analytic end to end. Using a two-trade cross-currency book, we run full revaluation across 715 historical windows to compute a 99% 10-day VaR of EUR 874.5k. To see how ORE actually does the math, we re-implement its scenario interpolation and repricing logic by hand in Python, match ORE&amp;rsquo;s window P&amp;amp;L to the exact cent, verify the tail quantiles, and compare the empirical output with parametric benchmarks.&lt;/p>
&lt;p>Like earlier posts in the series, you do not need a C++ build environment. You can install ORE and follow along directly in Python:&lt;/p>
&lt;pre>&lt;code class="language-bash">pip install open-source-risk-engine matplotlib pandas numpy
&lt;/code>&lt;/pre>
&lt;p>This is Part 4 of the ORE Fundamentals series, building on the curve construction in &lt;a href="https://obrienjoey.github.io/post/ore_sofr_bootstrap/">Part 1&lt;/a>, trade valuation and validation in &lt;a href="https://obrienjoey.github.io/post/ore_model_validation/">Part 2&lt;/a>, and portfolio Greeks from &lt;a href="https://obrienjoey.github.io/post/ore_sensitivity/">Part 3&lt;/a>.&lt;/p>
&lt;hr>
&lt;h2 id="1-the-idea">1. The idea&lt;/h2>
&lt;p>VaR estimates the loss over a holding period exceeded with probability $p$, such as $1 - \alpha = 1%$ for a 99% confidence level. A 99% 10-day VaR of EUR 874.5k means that across our historical 10-day evaluation windows, only 1% produced a portfolio loss greater than that amount. Throughout this post, we quote VaR as a positive loss number, while keeping raw P&amp;amp;L vectors in standard accounting sign convention where losses are negative.&lt;/p>
&lt;p>The historical simulation workflow consists of four steps:&lt;/p>
&lt;ol>
&lt;li>&lt;strong>Collect scenarios.&lt;/strong> &lt;code>scenarios.csv&lt;/code> holds 817 daily market states from 2016-09-01 onwards. Windows ending between 2017-01-17 and 2019-12-30 enter the calculation. A 10-business-day horizon across that range gives 715 overlapping windows.&lt;/li>
&lt;li>&lt;strong>Shock today&amp;rsquo;s market.&lt;/strong> Each window defines a relative shift that ORE applies to the base market as of 2019-12-30.&lt;/li>
&lt;li>&lt;strong>Reprice everything.&lt;/strong> Every trade is repriced from scratch in the shocked market. This is a full revaluation, with no delta-gamma or Taylor series approximations.&lt;/li>
&lt;li>&lt;strong>Read the quantile.&lt;/strong> Sort the 715 simulated P&amp;amp;L outcomes. The 99% VaR is the 8th worst loss ($\lceil 0.01 \times 715 \rceil = 8$), the 95% VaR is the 36th worst ($\lceil 0.05 \times 715 \rceil = 36$), and Expected Shortfall (ES) is the average of those 8 worst losses.&lt;/li>
&lt;/ol>
&lt;p>&lt;img src="histsim_workflow.svg" alt="Historical simulation VaR pipeline">&lt;/p>
&lt;p>For window $i$, with base market $\mathcal{M}_0$, shocked market $\mathcal{M}_i$, and horizon $t_1 = t_0 + \Delta t$, ORE can report two P&amp;amp;L definitions:&lt;/p>
&lt;p>$$ \Delta V^{\text{clean}}(i) = V(t_0, \mathcal{M}_i) - V(t_0, \mathcal{M}_0) $$&lt;/p>
&lt;p>$$ \Delta V^{\text{dirty}}(i) = V(t_1, \mathcal{M}_i) - V(t_0, \mathcal{M}_0) + \text{CF} $$&lt;/p>
&lt;p>Clean P&amp;amp;L holds the valuation date fixed and moves only market risk factors. Dirty P&amp;amp;L ages trades forward over the holding period and accounts for cash flows paid or received in between. Section 9 compares the two. All preceding sections assume clean P&amp;amp;L.&lt;/p>
&lt;hr>
&lt;h2 id="2-the-book">2. The book&lt;/h2>
&lt;p>Our test book contains two trades, defined in &lt;a href="Input/HistSimVar/portfolio.xml">&lt;code>portfolio.xml&lt;/code>&lt;/a>:&lt;/p>
&lt;ul>
&lt;li>&lt;strong>&lt;code>XCCY_Swap_EUR_USD&lt;/code>.&lt;/strong> Pays EURIBOR 6M on EUR 30M and receives USD LIBOR 3M on USD 33.9M, from 2012-09-05 to 2025-09-05, with notional exchanged at both ends. ORE models this as a &lt;code>Swap&lt;/code> with two floating legs.&lt;/li>
&lt;li>&lt;strong>&lt;code>FXFWD_EURUSD_10Y&lt;/code>.&lt;/strong> Buys EUR 1M against USD 1.1M, cash settled on 2029-03-01.&lt;/li>
&lt;/ul>
&lt;p>Because both trades are linear, full revaluation and delta-gamma approximations give identical results here. We chose linear trades deliberately so we could audit every number by hand. Section 10 revisits options and nonlinear payoffs.&lt;/p>
&lt;p>Foreign exchange dominates the book. Both final notional exchanges involve fixed amounts of USD. Shocking USDEUR spot by 1% shifts portfolio value by about EUR 294k, far exceeding the impact of any single rate curve shift in our sample. That EUR 294k delta gives an intuitive baseline for our sanity check in Section 8.&lt;/p>
&lt;hr>
&lt;h2 id="3-what-ore-does-with-the-scenario-file">3. What ORE does with the scenario file&lt;/h2>
&lt;p>In &lt;a href="https://obrienjoey.github.io/post/ore_sensitivity/">Part 3&lt;/a>, we constructed a &lt;code>ScenarioSimMarket&lt;/code>, a discretized grid of yield curve pillars and FX spots. While sensitivity analysis bumped one grid point at a time to evaluate partial derivatives, historical simulation shifts all grid points simultaneously to replay joint historical moves.&lt;/p>
&lt;p>&lt;a href="Input/HistSimVar/simulation.xml">&lt;code>simulation.xml&lt;/code>&lt;/a> defines this simulation grid. It has EUR and USD discount curves at 12 pillars from 2W to 30Y, the EUR-EONIA, EURIBOR 3M and 6M, USD-FedFunds and USD-LIBOR-3M index curves, and USDEUR spot. Each row of &lt;a href="Input/HistSimVar/scenarios.csv">&lt;code>scenarios.csv&lt;/code>&lt;/a> gives the value of every grid point on one historical date:&lt;/p>
&lt;pre>&lt;code class="language-csv">Date,Scenario,Numeraire,DiscountCurve/EUR/0,...,DiscountCurve/USD/0,...,IndexCurve/EUR-EURIBOR-6M/1,...,FXSpot/USDEUR/0
01/09/2016,1,1,1.00013288,...,0.9996784,...,1.0000751,...,0.8929
&lt;/code>&lt;/pre>
&lt;p>The values are absolute discount factors from each date to the pillar date. They are not rates, and they are not changes. The trailing index counts pillars, so &lt;code>/0&lt;/code> is 2W and &lt;code>/11&lt;/code> is 30Y. The last row, 2019-12-30, equals the base market, which confirms the file aligns with the as-of date. Note, there is an example of the public ORE repository showing how this can be done for a single date &lt;a href="https://github.com/OpenSourceRisk/Engine/blob/master/Examples/MarketRisk/Input/ore_basescenario.xml">here&lt;/a>, which would be run for each date in your historical period.&lt;/p>
&lt;hr>
&lt;h2 id="4-replicating-historical-pl-by-hand-in-python">4. Replicating historical P&amp;amp;L by hand in Python&lt;/h2>
&lt;p>The pipeline sounds straightforward, but reconciling it to production precision requires care with conventions. To see how ORE converts scenario discount factors into portfolio P&amp;amp;L, we replicate the engine&amp;rsquo;s mechanics from scratch in Python using NumPy (&lt;a href="manual_bump.py">&lt;code>manual_bump.py&lt;/code>&lt;/a>). ORE provides only the static base market and initial swap schedule. All scenario shifts, curve reconstructions, and trade repricings run independently in Python.&lt;/p>
&lt;p>The recipe mirrors ORE&amp;rsquo;s internal execution:&lt;/p>
&lt;ol>
&lt;li>Take two rows from &lt;code>scenarios.csv&lt;/code>, window start date $d_1$ and end date $d_2$.&lt;/li>
&lt;li>Divide row $d_2$ by row $d_1$, pillar by pillar. That ratio forms the factor shock. FX spot follows the same logic.&lt;/li>
&lt;li>Multiply ORE&amp;rsquo;s base discount factor at each pillar by its corresponding ratio, and the base FX spot quote by the FX ratio.&lt;/li>
&lt;li>Rebuild each curve across pillars. Full curve re-bootstrapping under each scenario would be computationally intensive, so ORE takes the 12 simulation market pillars directly and applies log-linear interpolation on discount factors with $DF(0) = 1$.&lt;/li>
&lt;li>Reprice the portfolio. Each floating coupon projects forward rates from the shocked index curve using the index&amp;rsquo;s native value and maturity dates. Cash flows discount along the shocked discount curves, and foreign-currency flows convert to EUR at the curve-adjusted forward FX rate.&lt;/li>
&lt;li>Subtract the base NPV to obtain window P&amp;amp;L.&lt;/li>
&lt;/ol>
&lt;p>The core Python calculation logic reduces to:&lt;/p>
&lt;pre>&lt;code class="language-python">ratios = scen.loc[d2, cols].to_numpy() / scen.loc[d1, cols].to_numpy() # one curve, 12 pillars
shocked_df = base_df * ratios # pillar by pillar
spot = fx0 * fx_ratio # raw quote, shocked
fx = spot * df(shocked_eur, t_spot) / df(shocked_usd, t_spot) # what ORE converts USD at
fwd = (df(shocked_idx, v) / df(shocked_idx, e) - 1) * accrual / span # v, e, span from index conventions
&lt;/code>&lt;/pre>
&lt;p>&lt;code>scenarios.csv&lt;/code> stores discount factors, not zero rates. If you shock them additively by taking differences, you will not reconcile with ORE. Because zero-rate shifts compound exponentially, discount factors scale multiplicatively by their ratio, $DF_2 / DF_1$. Applying additive shifts leads to P&amp;amp;L discrepancies of up to EUR 7.7k in individual windows, averaging EUR 1.5k, with 387 of 715 windows off by more than EUR 1k. Multiplicative scaling aligns with ORE to the penny.&lt;/p>
&lt;p>The worst window is 2018-01-09 to 2018-01-24. Over those ten days USDEUR fell 3.8%, so USD assets lost value in euros, and long-dated rates moved too.&lt;/p>
&lt;p>&lt;img src="bump_mechanics.png" alt="Worst window manual repricing, showing pillar shocks and swap P&amp;L by risk factor">&lt;/p>
&lt;p>The left panel is the shock ORE applies. The right panel reprices the swap with one risk factor shocked at a time, then with all of them together. FX spot explains more than the whole loss, and the rate factors partly cancel.&lt;/p>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th align="left">2018-01-09 to 2018-01-24&lt;/th>
&lt;th align="right">Manual&lt;/th>
&lt;th align="right">ORE&lt;/th>
&lt;th align="right">Difference&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td align="left">&lt;code>XCCY_Swap_EUR_USD&lt;/code>&lt;/td>
&lt;td align="right">-1,099,887&lt;/td>
&lt;td align="right">-1,099,887&lt;/td>
&lt;td align="right">0&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td align="left">&lt;code>FXFWD_EURUSD_10Y&lt;/code>&lt;/td>
&lt;td align="right">+30,742&lt;/td>
&lt;td align="right">+30,742&lt;/td>
&lt;td align="right">0&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td align="left">Portfolio&lt;/td>
&lt;td align="right">-1,069,145&lt;/td>
&lt;td align="right">-1,069,145&lt;/td>
&lt;td align="right">0&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>Then run all 715 windows through the same code. The largest difference in any window is one cent.&lt;/p>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th align="left">&lt;/th>
&lt;th align="right">Manual&lt;/th>
&lt;th align="right">ORE&lt;/th>
&lt;th align="right">Difference&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td align="left">95% VaR&lt;/td>
&lt;td align="right">629,899&lt;/td>
&lt;td align="right">629,899&lt;/td>
&lt;td align="right">0&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td align="left">99% VaR&lt;/td>
&lt;td align="right">874,503&lt;/td>
&lt;td align="right">874,503&lt;/td>
&lt;td align="right">0&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td align="left">99% expected shortfall&lt;/td>
&lt;td align="right">949,846&lt;/td>
&lt;td align="right">949,846&lt;/td>
&lt;td align="right">0&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>Getting there took four alignments. The first two are visible in the config. The last two came from the per-pillar attribution in &lt;code>riskFactor_PnL.csv&lt;/code>, which showed exactly where the manual numbers parted from ORE&amp;rsquo;s:&lt;/p>
&lt;ol>
&lt;li>&lt;strong>Curve grid.&lt;/strong> In &lt;code>simulation.xml&lt;/code>, ORE defines pillar dates as unadjusted calendar additions (&lt;code>asof + tenor&lt;/code>), not business-day-rolled dates. A 10Y pillar sits at &lt;code>2029-12-30&lt;/code>, one day before the calendar-adjusted date in the pricing report.&lt;/li>
&lt;li>&lt;strong>Cashflow conventions.&lt;/strong> The stub coupon (March 2020) fixed before the as-of date, so its amount is deterministic (-EUR 65.4k and +USD 161.5k) and does not float with scenario curves. At maturity, the final notional exchange (-EUR 30M and +USD 33.9M) discounts to $t_0$.&lt;/li>
&lt;li>&lt;strong>FX conversion uses the shocked curves.&lt;/strong> ORE does not convert USD at the raw spot quote. It converts at $S \times DF_{EUR}(t_{spot}) / DF_{USD}(t_{spot})$ on the simulated curves, where spot is T+2 on the joint calendar, 2020-01-02 here. That is how &lt;code>flows.csv&lt;/code> gets 0.89310 from a quote of 1/1.1199 = 0.89294. It also means the 2W pillars of both discount curves move every USD flow. In the December 2017 windows, for example, the USD 2W discount factor moved by 26 bp; ORE attributes EUR 16.7k of P&amp;amp;L to that pillar, whereas holding the conversion rate at &lt;code>spot * ratio&lt;/code> attributes zero. That basis explains why naive spot conversions diverged by up to EUR 17k.&lt;/li>
&lt;li>&lt;strong>Floating coupons project over the index dates, not the accrual dates.&lt;/strong> With at-par coupons, QuantLib takes the forward from the index value date (fixing date plus the index&amp;rsquo;s two fixing days on its own calendar) to the accrual end shifted by the same amount. It divides by the index day-count spanning time and then multiplies by the accrual. The projected coupon is $N \cdot \alpha \cdot (DF(v)/DF(e) - 1) / \tau$. Using raw accrual dates costs up to about 0.5% of the index-curve sensitivity at the 5Y pillar, which shows up as a few hundred euros per window.&lt;/li>
&lt;/ol>
&lt;p>ORE&amp;rsquo;s simulation market base NPV differs from the valuation reported in &lt;code>npv.csv&lt;/code>. The simulation market interpolates across 12 discrete pillars, whereas today&amp;rsquo;s market pricing uses the full bootstrapped curve. As a result, the swap is valued at -EUR 4,177 in the simulation market versus -EUR 2,267 in the standard pricing report. Scenario P&amp;amp;L is measured relative to the simulation market baseline. Referencing the full pricing report PV instead introduces an artificial basis mismatch between base and shocked valuations.&lt;/p>
&lt;p>Once you account for multiplicative discount factor ratios, spot settlement adjustments, and index fixing calendars, historical simulation VaR is completely transparent. It is 715 repricings followed by an empirical sort.&lt;/p>
&lt;hr>
&lt;h2 id="5-running-it-in-ore">5. Running it in ORE&lt;/h2>
&lt;p>The calculation is configured in &lt;a href="Input/ore_histsimvar.xml">&lt;code>ore_histsimvar.xml&lt;/code>&lt;/a>. Key parameters controlling the run include:&lt;/p>
&lt;pre>&lt;code class="language-xml">&amp;lt;Analytic type=&amp;quot;historicalSimulationVar&amp;quot;&amp;gt;
&amp;lt;Parameter name=&amp;quot;historicalScenarioFile&amp;quot;&amp;gt;scenarios.csv&amp;lt;/Parameter&amp;gt;
&amp;lt;Parameter name=&amp;quot;simulationConfigFile&amp;quot;&amp;gt;simulation.xml&amp;lt;/Parameter&amp;gt;
&amp;lt;Parameter name=&amp;quot;historicalPeriod&amp;quot;&amp;gt;2017-01-17,2019-12-30&amp;lt;/Parameter&amp;gt;
&amp;lt;Parameter name=&amp;quot;mporDays&amp;quot;&amp;gt;10&amp;lt;/Parameter&amp;gt;
&amp;lt;Parameter name=&amp;quot;mporOverlappingPeriods&amp;quot;&amp;gt;true&amp;lt;/Parameter&amp;gt;
&amp;lt;Parameter name=&amp;quot;quantiles&amp;quot;&amp;gt;0.01,0.05,0.95,0.99&amp;lt;/Parameter&amp;gt;
&amp;lt;Parameter name=&amp;quot;includeExpectedShortfall&amp;quot;&amp;gt;Y&amp;lt;/Parameter&amp;gt;
&amp;lt;Parameter name=&amp;quot;includeTheta&amp;quot;&amp;gt;N&amp;lt;/Parameter&amp;gt;
&amp;lt;Parameter name=&amp;quot;includePeriodCashflow&amp;quot;&amp;gt;N&amp;lt;/Parameter&amp;gt;
&amp;lt;Parameter name=&amp;quot;tradePnl&amp;quot;&amp;gt;Y&amp;lt;/Parameter&amp;gt;
&amp;lt;Parameter name=&amp;quot;riskFactorBreakdown&amp;quot;&amp;gt;Y&amp;lt;/Parameter&amp;gt;
&amp;lt;/Analytic&amp;gt;
&lt;/code>&lt;/pre>
&lt;ul>
&lt;li>&lt;code>historicalPeriod&lt;/code>: Sets the range of window-end dates. The initial window spans 2016-12-30 to 2017-01-17, accounting for the 2017-01-16 USD holiday.&lt;/li>
&lt;li>&lt;code>mporDays&lt;/code>: Holding period in business days, counted on the designated &lt;code>mporCalendar&lt;/code> (USD).&lt;/li>
&lt;li>&lt;code>mporOverlappingPeriods&lt;/code>: Set to &lt;code>true&lt;/code> to start a new 10-day window on every business day. Setting this to &lt;code>false&lt;/code> creates contiguous, non-overlapping 10-day blocks. While non-overlapping windows provide independent observations, they reduce sample size tenfold, down to 71 observations, which is statistically inadequate for estimating a 99% quantile.&lt;/li>
&lt;li>&lt;code>includeTheta&lt;/code> and &lt;code>includePeriodCashflow&lt;/code>: Toggle between clean and dirty P&amp;amp;L.&lt;/li>
&lt;/ul>
&lt;p>&lt;a href="#appendix-a-parameter-reference">Appendix A&lt;/a> lists the remaining parameters. Run the calculation from the post folder:&lt;/p>
&lt;pre>&lt;code class="language-bash">python run_histsimvar.py # clean VaR
python run_histsimvar.py --dirty # theta and cashflows included
&lt;/code>&lt;/pre>
&lt;p>&lt;a href="run_histsimvar.py">&lt;code>run_histsimvar.py&lt;/code>&lt;/a> loads the XML, executes &lt;code>ORE.OREApp&lt;/code>, and prints the portfolio row of &lt;code>var.csv&lt;/code>. It also recomputes the same metrics directly from &lt;code>historical_PnL.csv&lt;/code> as an independent verification. The complete run takes approximately one minute.&lt;/p>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th align="left">&lt;/th>
&lt;th align="right">95% 10-day VaR&lt;/th>
&lt;th align="right">99% 10-day VaR&lt;/th>
&lt;th align="right">99% ES&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td align="left">Portfolio, full revaluation&lt;/td>
&lt;td align="right">EUR 629,899&lt;/td>
&lt;td align="right">EUR 874,503&lt;/td>
&lt;td align="right">EUR 949,846&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>&lt;code>var.csv&lt;/code> stores quantiles in raw P&amp;amp;L convention, where losses are negative. The 8th worst window defines the 99% VaR and the 36th worst defines the 95% VaR, reconciling with ORE&amp;rsquo;s reported values to the exact euro.&lt;/p>
&lt;p>In this ORE release, the standalone &lt;code>RiskClass&lt;/code> summary rows for &lt;code>InterestRate&lt;/code> and &lt;code>FX&lt;/code> in &lt;code>var.csv&lt;/code> and &lt;code>historical_PnL.csv&lt;/code> replicate total portfolio figures rather than isolating single-asset-class risk. To obtain a true risk attribution, we enable the granular risk factor breakdown discussed in Section 7.&lt;/p>
&lt;hr>
&lt;h2 id="6-reading-the-distribution">6. Reading the distribution&lt;/h2>
&lt;p>&lt;img src="pnl_distribution.png" alt="Empirical 10-day P&amp;L distribution">&lt;/p>
&lt;p>The top panel plots the 715 window P&amp;amp;Ls across 40 bins alongside an overlaid Gaussian KDE curve. Dashed lines mark 95% VaR (EUR 629.9k) and 99% VaR (EUR 874.5k), while the dotted purple line indicates 99% Expected Shortfall (EUR 949.8k, the average of the 8 worst losses). With a mean of -EUR 10.1k and a median of +EUR 11.2k, the empirical distribution is skewed to the left.&lt;/p>
&lt;p>The bottom panel plots the returns chronologically by window end date. This timeline shows what the histogram hides. The 36 windows breaching 95% VaR arrive in distinct clusters. The 8 windows driving 99% VaR trace back to four market episodes: May 2017 with 2 windows, July 2017 with 1, January 2018 with 3, and August 2018 with 2. Because overlapping windows reuse daily market moves, a single volatile week generates multiple tail observations. Our 99% VaR estimate reflects four macroeconomic events rather than eight independent shocks.&lt;/p>
&lt;hr>
&lt;h2 id="7-decomposing-tail-risk-by-factor">7. Decomposing tail risk by factor&lt;/h2>
&lt;p>Setting &lt;code>riskFactorBreakdown&lt;/code> to &lt;code>Y&lt;/code> writes &lt;code>riskFactor_PnL.csv&lt;/code>. ORE shocks one risk factor at a time, at each pillar, and reprices the trade. The resulting file contains 29,873 rows across 35 risk factor keys, such as &lt;code>DiscountCurve/USD/7&lt;/code>.&lt;/p>
&lt;p>Aggregated by curve for the swap in the worst window (2018-01-09 to 2018-01-24), these rows match our manual one-factor-at-a-time repricing from Section 4:&lt;/p>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th align="left">Swap, 2018-01-09 to 2018-01-24&lt;/th>
&lt;th align="right">Manual&lt;/th>
&lt;th align="right">ORE&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td align="left">FX spot&lt;/td>
&lt;td align="right">-1,147,956&lt;/td>
&lt;td align="right">-1,147,956&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td align="left">USD discount&lt;/td>
&lt;td align="right">-176,217&lt;/td>
&lt;td align="right">-176,217&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td align="left">EUR discount&lt;/td>
&lt;td align="right">+135,678&lt;/td>
&lt;td align="right">+135,678&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td align="left">USD LIBOR 3M index&lt;/td>
&lt;td align="right">+222,922&lt;/td>
&lt;td align="right">+222,922&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td align="left">EURIBOR 6M index&lt;/td>
&lt;td align="right">-132,092&lt;/td>
&lt;td align="right">-132,092&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td align="left">Sum of the factors&lt;/td>
&lt;td align="right">-1,097,664&lt;/td>
&lt;td align="right">-1,097,664&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td align="left">All factors shocked together&lt;/td>
&lt;td align="right">-1,099,887&lt;/td>
&lt;td align="right">-1,099,887&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>The EUR 2.2k gap between the sum of individual factor P&amp;amp;Ls and the joint shock represents cross-factor convexity, or cross-gamma, interaction, which remains negligible because the underlying book is linear.&lt;/p>
&lt;p>We can also run the 715 windows shocking one risk class at a time. Shocking only FX spot yields a 99% standalone VaR of EUR 884k. Shifting interest rate curves alone produces a 99% VaR of EUR 63k.&lt;/p>
&lt;p>The total portfolio VaR of EUR 875k is lower than the standalone FX VaR. During extreme dollar depreciation windows in this lookback, interest rate movements cushioned the FX losses. In the worst window, interest rate moves contributed +EUR 49k against the -EUR 1,117k FX loss.&lt;/p>
&lt;hr>
&lt;h2 id="8-does-the-number-make-sense">8. Does the number make sense?&lt;/h2>
&lt;p>Before trusting a simulation engine, check the number with a back-of-the-envelope calculation. Because FX spot dominates portfolio risk, an independent benchmark is straightforward:&lt;/p>
&lt;ol>
&lt;li>&lt;strong>Portfolio delta.&lt;/strong> The book has a net USD exposure of EUR 29.4M, translating to EUR 294k per 1% move in USDEUR spot, verified by a finite difference repricing at $\pm 1%$.&lt;/li>
&lt;li>&lt;strong>Historical volatility.&lt;/strong> Across the 715 historical windows, the sample standard deviation of 10-day USDEUR log-returns is 1.184%.&lt;/li>
&lt;li>&lt;strong>Parametric VaR.&lt;/strong> Under a normal distribution, the 99% one-tailed quantile corresponds to $z_{0.99} = 2.326$. Multiplying gives $2.326 \times 0.01184 \times \text{EUR } 29.4\text{M} \approx \text{EUR } 810\text{k}$.&lt;/li>
&lt;/ol>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th align="left">Method&lt;/th>
&lt;th align="right">95%&lt;/th>
&lt;th align="right">99%&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td align="left">Delta-normal, 10-day vol from the windows (1.184%)&lt;/td>
&lt;td align="right">EUR 573k&lt;/td>
&lt;td align="right">EUR 810k&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td align="left">Delta-normal, daily vol $\times \sqrt{10}$ (1.332%)&lt;/td>
&lt;td align="right">EUR 644k&lt;/td>
&lt;td align="right">EUR 911k&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td align="left">Historical, FX only&lt;/td>
&lt;td align="right">EUR 637k&lt;/td>
&lt;td align="right">EUR 884k&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td align="left">Historical, rates only&lt;/td>
&lt;td align="right">EUR 47k&lt;/td>
&lt;td align="right">EUR 63k&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td align="left">Historical, full book (ORE)&lt;/td>
&lt;td align="right">EUR 630k&lt;/td>
&lt;td align="right">EUR 875k&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>&lt;img src="tail_ranked.png" alt="Worst 40 windows ranked against delta-normal benchmarks">&lt;/p>
&lt;p>Both parametric benchmarks land within ~10% of ORE&amp;rsquo;s full historical revaluation. An order-of-magnitude error or a 2x gap would indicate an operational defect or missing risk factor. A 10% spread reflects expected distributional differences.&lt;/p>
&lt;p>Two differences stand out:&lt;/p>
&lt;ul>
&lt;li>&lt;strong>Fat tails.&lt;/strong> The parametric normal estimate using realized 10-day volatility is 7% lower at 99%, at EUR 810k compared to EUR 875k, because empirical FX returns have fatter tails than a Gaussian distribution.&lt;/li>
&lt;li>&lt;strong>Mean reversion vs. $\sqrt{t}$ scaling.&lt;/strong> Scaling 1-day volatility by $\sqrt{10}$ overshoots 99% VaR by 4%, at EUR 911k compared to EUR 875k. In this 3-year sample, realized 10-day moves were roughly 11% less volatile than an i.i.d. random walk implies. The conventional $\sqrt{t}$ scaling rule is an active modelling assumption rather than an identity. Many legacy risk systems scale 1-day VaR by $\sqrt{10}$ purely because multi-day simulation is computationally expensive. Historical simulation in ORE sidesteps this shortcut by measuring multi-day holding period returns directly.&lt;/li>
&lt;/ul>
&lt;hr>
&lt;h2 id="9-clean-and-dirty-var">9. Clean and dirty VaR&lt;/h2>
&lt;p>Switching &lt;code>includeTheta&lt;/code> and &lt;code>includePeriodCashflow&lt;/code> to &lt;code>Y&lt;/code> rolls the valuation date forward ten days and adds intermediate cash flows that fall within the window. &lt;a href="Input/ore_histsimvar_dirty.xml">&lt;code>ore_histsimvar_dirty.xml&lt;/code>&lt;/a> enables both flags.&lt;/p>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th align="left">&lt;/th>
&lt;th align="right">Clean&lt;/th>
&lt;th align="right">Dirty&lt;/th>
&lt;th align="right">Change&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td align="left">95% VaR&lt;/td>
&lt;td align="right">EUR 629,899&lt;/td>
&lt;td align="right">EUR 601,136&lt;/td>
&lt;td align="right">-28,763&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td align="left">99% VaR&lt;/td>
&lt;td align="right">EUR 874,503&lt;/td>
&lt;td align="right">EUR 846,346&lt;/td>
&lt;td align="right">-28,158&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td align="left">99% expected shortfall&lt;/td>
&lt;td align="right">EUR 949,846&lt;/td>
&lt;td align="right">EUR 921,659&lt;/td>
&lt;td align="right">-28,187&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>&lt;img src="clean_vs_dirty.png" alt="Clean against dirty P&amp;L distribution">&lt;/p>
&lt;p>Dirty P&amp;amp;L is higher by EUR 28.8k on average, which shifts the entire distribution to the right and reduces each tail risk measure by roughly EUR 28k. This shift comes mainly from positive net carry. Receiving ~1.9% on USD and paying ~-0.4% on EUR on comparable notionals yields approximately EUR 19k in net interest accrual over ten days. The remaining EUR 9.8k comes from discount-curve theta roll-down.&lt;/p>
&lt;p>Which definition you use depends on the objective. Clean VaR isolates market risk on a static portfolio, holding trade age and cash flows invariant. Dirty VaR incorporates theta decay and cash flows, matching what the desk&amp;rsquo;s P&amp;amp;L ledger experiences over the holding period. Regulatory backtesting frameworks such as Basel III and FRTB prescribe specific comparisons between hypothetical and actual P&amp;amp;L, making it practical to switch between both modes in ORE.&lt;/p>
&lt;hr>
&lt;h2 id="10-limits-of-this-example">10. Limits of this example&lt;/h2>
&lt;ul>
&lt;li>&lt;strong>Linear portfolio.&lt;/strong> Full revaluation is designed to capture nonlinear payoffs such as gamma and vega, which a swap and forward portfolio does not exercise. Adding FX options or swaptions and benchmarking against delta-gamma VaR is the natural next step.&lt;/li>
&lt;li>&lt;strong>Static curves.&lt;/strong> The simulation market in this setup shocks yield curves and FX spot. Volatility surfaces are not part of this simulation grid, so option portfolios would require extending &lt;code>simulation.xml&lt;/code> to model smile dynamics.&lt;/li>
&lt;li>&lt;strong>Single lookback window.&lt;/strong> We used three years of equally weighted daily observations with a single cutoff. In production, risk managers frequently calibrate exponentially weighted historical simulation or dedicated stressed VaR windows to address tail clustering.&lt;/li>
&lt;li>&lt;strong>FX precision.&lt;/strong> &lt;code>scenarios.csv&lt;/code> stores USDEUR spot quotes to four decimal places in this example dataset, so 10-day moves smaller than 0.5 pips round to zero. This affects only a handful of near-flat windows and does not impact tail quantiles.&lt;/li>
&lt;/ul>
&lt;p>Pricing, curve calibration, and sensitivities can be handled by many quantitative libraries. Historical simulation is where an engine gets tested as a portfolio system. Repricing a book across hundreds of historical market states, with clean handling of multi-day horizons, intermediate cash flows, and theta decay, is what turns pricing models into a functional risk framework.&lt;/p>
&lt;h3 id="need-help-with-ore-integration">Need help with ORE integration?&lt;/h3>
&lt;p>If you are evaluating ORE for your market risk workflow, setting up an initial installation, or benchmarking historical simulation and backtesting pipelines against your existing systems, feel free to &lt;strong>&lt;a href="https://obrienjoey.github.io/contact">reach out&lt;/a>&lt;/strong>. We specialise in ORE integrations, bespoke pricing pipelines, and model validation.&lt;/p>
&lt;hr>
&lt;h2 id="appendix-a-parameter-reference">Appendix A. Parameter reference&lt;/h2>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th align="left">Parameter&lt;/th>
&lt;th align="left">Value&lt;/th>
&lt;th align="left">What it does&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td align="left">&lt;code>historicalScenarioFile&lt;/code>&lt;/td>
&lt;td align="left">&lt;code>scenarios.csv&lt;/code>&lt;/td>
&lt;td align="left">Historical market states on the &lt;code>simulation.xml&lt;/code> grid&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td align="left">&lt;code>simulationConfigFile&lt;/code>&lt;/td>
&lt;td align="left">&lt;code>simulation.xml&lt;/code>&lt;/td>
&lt;td align="left">Grid of curves, tenors and FX pairs&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td align="left">&lt;code>historicalPeriod&lt;/code>&lt;/td>
&lt;td align="left">&lt;code>2017-01-17,2019-12-30&lt;/code>&lt;/td>
&lt;td align="left">First and last window-end dates&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td align="left">&lt;code>mporDays&lt;/code>&lt;/td>
&lt;td align="left">&lt;code>10&lt;/code>&lt;/td>
&lt;td align="left">Holding period in business days&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td align="left">&lt;code>mporCalendar&lt;/code>&lt;/td>
&lt;td align="left">&lt;code>USD&lt;/code>&lt;/td>
&lt;td align="left">Calendar used to count those days&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td align="left">&lt;code>mporOverlappingPeriods&lt;/code>&lt;/td>
&lt;td align="left">&lt;code>true&lt;/code>&lt;/td>
&lt;td align="left">New window every day, or back-to-back blocks&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td align="left">&lt;code>quantiles&lt;/code>&lt;/td>
&lt;td align="left">&lt;code>0.01,0.05,0.95,0.99&lt;/code>&lt;/td>
&lt;td align="left">Left tail gives losses, right tail gives gains&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td align="left">&lt;code>includeExpectedShortfall&lt;/code>&lt;/td>
&lt;td align="left">&lt;code>Y&lt;/code>&lt;/td>
&lt;td align="left">Writes $\mathbb{E}[L \mid L \ge \text{VaR}]$ next to each quantile&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td align="left">&lt;code>includeTheta&lt;/code>&lt;/td>
&lt;td align="left">&lt;code>N&lt;/code>&lt;/td>
&lt;td align="left">&lt;code>Y&lt;/code> moves the pricing date to $t_1$&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td align="left">&lt;code>includePeriodCashflow&lt;/code>&lt;/td>
&lt;td align="left">&lt;code>N&lt;/code>&lt;/td>
&lt;td align="left">&lt;code>Y&lt;/code> adds cash flows paid between $t_0$ and $t_1$&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td align="left">&lt;code>breakdown&lt;/code>&lt;/td>
&lt;td align="left">&lt;code>Y&lt;/code>&lt;/td>
&lt;td align="left">Adds &lt;code>RiskClass&lt;/code> and &lt;code>RiskType&lt;/code> rows (see note in Section 5)&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td align="left">&lt;code>tradePnl&lt;/code>&lt;/td>
&lt;td align="left">&lt;code>Y&lt;/code>&lt;/td>
&lt;td align="left">Per-trade P&amp;amp;L in &lt;code>historical_PnL.csv&lt;/code>&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td align="left">&lt;code>riskFactorBreakdown&lt;/code>&lt;/td>
&lt;td align="left">&lt;code>Y&lt;/code>&lt;/td>
&lt;td align="left">Per-factor P&amp;amp;L in &lt;code>riskFactor_PnL.csv&lt;/code>&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>&lt;code>RiskType&lt;/code> is &lt;code>All&lt;/code> for full revaluation and &lt;code>DeltaGamma&lt;/code> for the sensitivity approximation. The two are identical here because the book is linear.&lt;/p>
&lt;h2 id="appendix-b-files">Appendix B. Files&lt;/h2>
&lt;p>All configuration XMLs, scenario market files, and Python scripts used in this post are packaged for download:&lt;/p>
&lt;a href="ore_var_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;ul>
&lt;li>&lt;a href="run_histsimvar.py">&lt;code>run_histsimvar.py&lt;/code>&lt;/a> runs the analytic and prints VaR from &lt;code>var.csv&lt;/code> and from &lt;code>historical_PnL.csv&lt;/code>.&lt;/li>
&lt;li>&lt;a href="manual_bump.py">&lt;code>manual_bump.py&lt;/code>&lt;/a> reprices every window by hand and checks it against ORE.&lt;/li>
&lt;li>&lt;a href="plot_var_results.py">&lt;code>plot_var_results.py&lt;/code>&lt;/a> draws the figures.&lt;/li>
&lt;li>&lt;a href="var_common.py">&lt;code>var_common.py&lt;/code>&lt;/a> holds the shared quantile and report helpers.&lt;/li>
&lt;li>&lt;a href="Input/ore_histsimvar.xml">&lt;code>Input/ore_histsimvar.xml&lt;/code>&lt;/a>, &lt;a href="Input/ore_histsimvar_dirty.xml">&lt;code>Input/ore_histsimvar_dirty.xml&lt;/code>&lt;/a> and &lt;a href="Input/ore_base.xml">&lt;code>Input/ore_base.xml&lt;/code>&lt;/a> are the three ORE configs. The last one dumps the base NPVs, cashflows and pillar discount factors that &lt;code>manual_bump.py&lt;/code> reads.&lt;/li>
&lt;/ul></description></item></channel></rss>