Logo
Fully annotated reference manual - version 1.8.12
Loading...
Searching...
No Matches
historicalsensipnlcalculator.cpp
Go to the documentation of this file.
1/*
2 Copyright (C) 2023 Quaternion Risk Management Ltd
3 All rights reserved.
4
5 This file is part of ORE, a free-software/open-source library
6 for transparent pricing and risk analysis - http://opensourcerisk.org
7
8 ORE is free software: you can redistribute it and/or modify it
9 under the terms of the Modified BSD License. You should have received a
10 copy of the license along with this program.
11 The license is also available online at <http://opensourcerisk.org>
12
13 This program is distributed on the basis that it will form a useful
14 contribution to risk analytics and model standardisation, but WITHOUT
15 ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
16 FITNESS FOR A PARTICULAR PURPOSE. See the license for more details.
17*/
18
23
24#include <boost/accumulators/statistics/tail_quantile.hpp>
25#include <boost/range/adaptor/indexed.hpp>
26
27using namespace std;
28using namespace QuantLib;
29using namespace boost::accumulators;
30
31namespace {
32
33using TradeSensiCache = map<Size, map<Size, pair<Real, Real>>>;
35
36void cacheTradeSensitivities(TradeSensiCache& cache, ore::analytics::SensitivityStream& ss,
37 const set<SensitivityRecord>& srs,
38 const vector<string>& tradeIds) {
39
40 // Reset the stream to ensure at start.
41 ss.reset();
42
43 // One pass of sensitivity records to populate the trade level cache.
44 while (SensitivityRecord sr = ss.next()) {
45
46 // Sensitivity record is only relevant if it is in our set of trade IDs
47 auto itTrade = find(tradeIds.begin(), tradeIds.end(), sr.tradeId);
48 if (itTrade == tradeIds.end())
49 continue;
50
51 // This sensitivity record should be in the set of passed in sensitivity records.
52 auto itSr = find_if(srs.begin(), srs.end(), [&sr](const SensitivityRecord& other) {
53 return sr.key_1 == other.key_1 && sr.key_2 == other.key_2;
54 });
55
56 // Positions for the cache keys.
57 auto posTrade = distance(tradeIds.begin(), itTrade);
58 auto posSr = distance(srs.begin(), itSr);
59
60 // Add the sensitivity record values to the cache.
61 auto itCacheSr = cache.find(posSr);
62 if (itCacheSr == cache.end()) {
63 cache[posSr][posTrade] = make_pair(sr.delta, sr.gamma);
64 } else {
65 auto itCacheTrade = itCacheSr->second.find(posTrade);
66 if (itCacheTrade == itCacheSr->second.end()) {
67 itCacheSr->second[posTrade] = make_pair(sr.delta, sr.gamma);
68 } else {
69 auto& p = itCacheTrade->second;
70 p.first += sr.delta;
71 p.second += sr.gamma;
72 }
73 }
74 }
75
76 // Reset the stream to ensure at start.
77 ss.reset();
78}
79
80}
81
82namespace ore {
83namespace analytics {
84
85void CovarianceCalculator::initialise(const set<pair<RiskFactorKey, Size>>& keys) {
86 // Set up the boost accumulators that will calculate the covariance between the time series of historical shifts for
87 // each relevant risk factor key i.e. the risk factor keys in the set keys over the benchmark period
88
89 for (auto ito = keys.begin(); ito != keys.end(); ito++) {
90 accCov_[make_pair(ito->second, ito->second)] = accumulator();
91 for (auto iti = keys.begin(); iti != ito; iti++) {
92 accCov_[make_pair(iti->second, ito->second)] = accumulator();
93 }
94 }
95}
96
97void CovarianceCalculator::updateAccumulators(const ext::shared_ptr<NPVCube>& shiftCube, Date startDate, Date endDate, Size index) {
98 TLOG("Updating Covariance accumlators for sensitivity record " << index);
99 if (covariancePeriod_.contains(startDate) &&
100 covariancePeriod_.contains(endDate)) {
101 // Update the covariance accumulators if in benchmark period
102 for (auto it = accCov_.begin(); it != accCov_.end(); it++) {
103 Real oShift = shiftCube->get(it->first.first, 0, index);
104 if (it->first.first == it->first.second) {
105 it->second(oShift, covariate1 = oShift);
106 } else {
107 Real iShift = shiftCube->get(it->first.second, 0, index);
108 it->second(oShift, covariate1 = iShift);
109 }
110 }
111 }
112}
113
114void CovarianceCalculator::populateCovariance(const std::set<std::pair<RiskFactorKey, QuantLib::Size>>& keys) {
115 LOG("Populate the covariance matrix with the calculated covariances");
116 covariance_ = Matrix(keys.size(), keys.size());
117 Size i = 0;
118 for (auto ito = keys.begin(); ito != keys.end(); ito++) {
119 covariance_[i][i] = boost::accumulators::covariance(accCov_.at(make_pair(ito->second, ito->second)));
120 Size j = 0;
121 for (auto iti = keys.begin(); iti != ito; iti++) {
122 covariance_[i][j] = covariance_[j][i] =
123 boost::accumulators::covariance(accCov_.at(make_pair(iti->second, ito->second)));
124 j++;
125 }
126 i++;
127 }
128}
129
130void PNLCalculator::populatePNLs(const std::vector<Real>& allPnls,
131 const std::vector<Real>& allFoPnls,
132 const std::vector<Date>& startDates,
133 const std::vector<Date>& endDates) {
134 QL_REQUIRE(allPnls.size() == allFoPnls.size(), "PNLs and first order PNLs must be the same size");
135
136 pnls_.reserve(allPnls.size());
137 foPnls_.reserve(allPnls.size());
138
139 for (Size i = 0; i < allPnls.size(); i++) {
140 // Backtesting P&L vectors
141 if (pnlPeriod_.contains(startDates[i]) && pnlPeriod_.contains(endDates[i])) {
142 pnls_.push_back(allPnls[i]);
143 foPnls_.push_back(allFoPnls[i]);
144 }
145 }
146 pnls_.shrink_to_fit();
147 foPnls_.shrink_to_fit();
148}
149
151 tradePnls_ = allPnls;
153}
154
155 const bool PNLCalculator::isInTimePeriod(Date startDate, Date endDate) {
156 return pnlPeriod_.contains(startDate) && pnlPeriod_.contains(endDate);
157}
158
159void HistoricalSensiPnlCalculator::populateSensiShifts(QuantLib::ext::shared_ptr<NPVCube>& cube, const vector<RiskFactorKey>& keys,
160 ext::shared_ptr<ScenarioShiftCalculator> shiftCalculator) {
161
162 hisScenGen_->reset();
163 QuantLib::ext::shared_ptr<Scenario> baseScenario = hisScenGen_->baseScenario();
164
165 set<string> keyNames;
166 std::map<std::string, RiskFactorKey> keyNameMapping;
167 for (auto k : keys) {
168 keyNames.insert(ore::data::to_string(k));
169 keyNameMapping.insert({ore::data::to_string(k), k});
170 }
171
172 cube = QuantLib::ext::make_shared<DoublePrecisionInMemoryCube>(
173 baseScenario->asof(), keyNames, vector<Date>(1, baseScenario->asof()), hisScenGen_->numScenarios());
174
175 // Loop over each historical scenario which represents the market move from t_i to
176 // t_i + mpor applied to the base scenario for all i in historical period of scenario generator
177 for (Size i = 0; i < hisScenGen_->numScenarios(); i++) {
178 QuantLib::ext::shared_ptr<Scenario> scenario = hisScenGen_->next(baseScenario->asof());
179
180 Size j = 0;
181 for (const auto& [_, key] : keyNameMapping) {
182 try {
183 Real shift = shiftCalculator->shift(key, *baseScenario, *scenario);
184 cube->set(shift, j, 0, i);
185 } catch (const std::exception& e) {
187 "HistocialSensiPnlCalculator",
188 "Shift calcuation failed. Check consistency of simulation and sensi config.",
189 "Error retrieving sensi key '" + ore::data::to_string(key) + "' from ssm scenario: '" + e.what())
190 .log();
191 }
192 j++;
193 }
194 }
195}
196
198 const set<SensitivityRecord>& srs, const vector<RiskFactorKey>& rfKeys, ext::shared_ptr<NPVCube>& shiftCube,
199 const vector<ext::shared_ptr<PNLCalculator>>& pnlCalculators,
200 const ext::shared_ptr<CovarianceCalculator>& covarianceCalculator,
201 const vector<string>& tradeIds, const bool includeGammaMargin,
202 const bool includeDeltaMargin, const bool tradeLevel) {
203
204 // Set of relevant keys from sensitivity records, needed for covariance matrix
205 // Add the index of the key location in sensi shift cube
206 set<pair<RiskFactorKey, Size>> keys;
207 for (auto it = rfKeys.begin(); it != rfKeys.end(); it++) {
208 auto it1 = shiftCube->idsAndIndexes().find(ore::data::to_string(*it));
209 QL_REQUIRE(it1 != shiftCube->idsAndIndexes().end(),
210 "Could not find key " << *it << " in sensi shift cube keys");
211 keys.insert(make_pair(*it, it1->second));
212 }
213 // Create an index pair for each sensitivity record. The first element holds the index position
214 // in the sensi shift cube of key_1. The second element holds the index of key_2 for cross
215 // gamma record.
216 vector<pair<Size, Size>> srsIndex;
217 for (auto it = srs.begin(); it != srs.end(); it++) {
218 auto it1 = shiftCube->idsAndIndexes().find(ore::data::to_string(it->key_1));
219 QL_REQUIRE(it1 != shiftCube->idsAndIndexes().end(),
220 "Could not find key " << it->key_1 << " in sensi shift cube keys");
221 Size ind_1 = it1->second;
222 Size ind_2 = Size();
223 if (it->isCrossGamma()) {
224 auto it2 = shiftCube->idsAndIndexes().find(ore::data::to_string(it->key_2));
225 QL_REQUIRE(it2 != shiftCube->idsAndIndexes().end(),
226 "Could not find key " << it->key_2 << " in sensi shift cube keys");
227 ind_2 = it2->second;
228 }
229 srsIndex.push_back(make_pair(ind_1, ind_2));
230 }
231
232 if (covarianceCalculator)
233 covarianceCalculator->initialise(keys);
234
235 // we require a sensitivity stream to run at trade level
236 bool runTradeLevel = tradeLevel && sensitivityStream_;
237
238 // Local P&L vectors to hold _all_ historical P&Ls
239 Size nScenarios = hisScenGen_->numScenarios();
240 Size nCalculators = pnlCalculators.size();
241 vector<Real> allPnls(hisScenGen_->numScenarios(), 0.0);
242 vector<Real> allFoPnls(hisScenGen_->numScenarios(), 0.0);
243
244 // calculators,scenarios, trades
245 using TradePnLStore = std::vector<std::vector<QuantLib::Real>>;
246 std::vector<TradePnLStore> tradePnls, foTradePnls;
247
248 // We may need to store trade level P&Ls.
249 if (runTradeLevel) {
250 tradePnls.clear();
251 tradePnls.reserve(nCalculators);
252 foTradePnls.clear();
253 foTradePnls.reserve(nCalculators);
254 for (Size i = 0; i < nCalculators; i++) {
255 tradePnls.push_back(std::vector<std::vector<QuantLib::Real>>());
256 tradePnls.at(i).reserve(nScenarios);
257 foTradePnls.push_back(std::vector<std::vector<QuantLib::Real>>());
258 foTradePnls.at(i).reserve(nScenarios);
259 }
260 }
261
262 hisScenGen_->reset();
263 QuantLib::ext::shared_ptr<Scenario> baseScenario = hisScenGen_->baseScenario();
264
265 // If we have been asked for a trade level P&L contribution report or detail report, store the trade level
266 // sensitivities. We store them in a container here that is easily looked up in the loop below.
267 TradeSensiCache tradeSensiCache;
268 if (runTradeLevel) {
269 cacheTradeSensitivities(tradeSensiCache, *sensitivityStream_, srs, tradeIds);
270 }
271
272 // Loop over each historical scenario
273 for (Size i = 0; i < hisScenGen_->numScenarios(); i++) {
274
275 // Add trade level P&L vector if needed.
276 if (runTradeLevel) {
277 for (Size j = 0; j < pnlCalculators.size(); j++) {
278 bool inPeriod =
279 pnlCalculators.at(j)->isInTimePeriod(hisScenGen_->startDates()[i], hisScenGen_->endDates()[i]);
280 if (inPeriod) {
281 tradePnls.at(j).push_back(vector<Real>(tradeIds.size(), 0.0));
282 foTradePnls.at(j).push_back(vector<Real>(tradeIds.size(), 0.0));
283 }
284 }
285 }
286
287 for (const auto elem : srs | boost::adaptors::indexed(0)) {
288
289 const auto& sr = elem.value();
290 auto j = elem.index();
291
292 if (!sr.isCrossGamma()) {
293
294 Real shift = shiftCube->get(srsIndex[j].first, 0, i);
295 Real deltaPnl = shift * sr.delta;
296 Real gammaPnl = 0.5 * shift * shift * sr.gamma;
297 // Update the first order P&L
298 allFoPnls[i] += deltaPnl;
299 // If backtesting curvature margin, we exclude deltas i.e. 1st order effects from the sensi P&L
300 if (includeDeltaMargin)
301 allPnls[i] += deltaPnl;
302 // If backtesting delta margin, we exclude gammas i.e. second order effects from the sensi P&L
303 if (includeGammaMargin)
304 allPnls[i] += gammaPnl;
305
306 for (Size k = 0; k < pnlCalculators.size(); k++) {
307 if (pnlCalculators.at(k)->isInTimePeriod(hisScenGen_->startDates()[i],
308 hisScenGen_->endDates()[i])) {
309 pnlCalculators.at(k)->writePNL(i, true, sr.key_1, shift, sr.delta, sr.gamma, deltaPnl,
310 gammaPnl);
311 if (!tradeSensiCache.empty()) {
312 auto itSr = tradeSensiCache.find(j);
313 if (itSr != tradeSensiCache.end()) {
314 for (const auto& kv : itSr->second) {
315 const auto& tradeId = tradeIds[kv.first];
316 Real tradeDelta = kv.second.first;
317 Real tradeDeltaPnl = shift * tradeDelta;
318 Real tradeGamma = kv.second.second;
319 Real tradeGammaPnl = 0.5 * shift * shift * tradeGamma;
320 // Attempt to write trade level P&L contribution row.
321 pnlCalculators.at(k)->writePNL(i, true, sr.key_1, shift, tradeDelta, tradeGamma,
322 tradeDeltaPnl, tradeGammaPnl, RiskFactorKey(), 0.0,
323 tradeId);
324 // Update the sensitivity based trade level P&Ls
325 if (runTradeLevel) {
326 foTradePnls.at(k).back()[kv.first] += tradeDeltaPnl;
327 if (includeDeltaMargin)
328 tradePnls.at(k).back()[kv.first] += tradeDeltaPnl;
329 if (includeGammaMargin)
330 tradePnls.at(k).back()[kv.first] += tradeGammaPnl;
331 }
332 }
333 }
334 }
335 }
336 }
337 } else {
338 Real shift_1 = shiftCube->get(srsIndex[j].first, 0, i);
339 Real shift_2 = shiftCube->get(srsIndex[j].second, 0, i);
340 Real gammaPnl = shift_1 * shift_2 * sr.gamma;
341
342 // If backtesting delta margin, we exclude gammas i.e. second order effects from the sensi P&L
343 if (includeGammaMargin)
344 allPnls[i] += gammaPnl;
345
346 for (Size j = 0; j < pnlCalculators.size(); j++) {
347 const auto& c = pnlCalculators[j];
348 if (c->isInTimePeriod(hisScenGen_->startDates()[i], hisScenGen_->endDates()[i])) {
349 c->writePNL(i, true, sr.key_1, shift_1, sr.delta, sr.gamma, 0.0, gammaPnl, sr.key_2, shift_2);
350 if (!tradeSensiCache.empty()) {
351 auto itSr = tradeSensiCache.find(j);
352 if (itSr != tradeSensiCache.end()) {
353 for (const auto& kv : itSr->second) {
354 const auto& tradeId = tradeIds[kv.first];
355 Real tradeGamma = kv.second.second;
356 Real tradeGammaPnl = shift_1 * shift_2 * tradeGamma;
357 // Attempt to write trade level P&L contribution row.
358 c->writePNL(i, true, sr.key_1, shift_1, 0.0, tradeGamma, 0.0, tradeGammaPnl,
359 sr.key_2, shift_2, tradeId);
360 // Update the sensitivity based trade level P&Ls
361 if (runTradeLevel && includeGammaMargin) {
362 tradePnls.at(j).back()[kv.first] += tradeGammaPnl;
363 }
364 }
365 }
366 }
367 }
368
369 }
370 }
371 }
372 if (covarianceCalculator)
373 covarianceCalculator->updateAccumulators(shiftCube, hisScenGen_->startDates()[i], hisScenGen_->endDates()[i], i);
374 }
375 if (covarianceCalculator)
376 covarianceCalculator->populateCovariance(keys);
377
378 LOG("Populate the sensitivity backtesting P&L vectors");
379 for (Size j = 0; j < pnlCalculators.size(); j++) {
380 pnlCalculators.at(j)->populatePNLs(allPnls, allFoPnls, hisScenGen_->startDates(), hisScenGen_->endDates());
381 if (runTradeLevel)
382 pnlCalculators.at(j)->populateTradePNLs(tradePnls.at(j), foTradePnls.at(j));
383 }
384}
385
386} // namespace analytics
387} // namespace ore
boost::accumulators::accumulator_set< QuantLib::Real, boost::accumulators::stats< boost::accumulators::tag::covariance< QuantLib::Real, boost::accumulators::tag::covariate1 > > > accumulator
void initialise(const std::set< std::pair< RiskFactorKey, QuantLib::Size > > &keys)
void updateAccumulators(const QuantLib::ext::shared_ptr< NPVCube > &shiftCube, QuantLib::Date startDate, QuantLib::Date endDate, QuantLib::Size index)
void populateCovariance(const std::set< std::pair< RiskFactorKey, QuantLib::Size > > &keys)
std::map< std::pair< QuantLib::Size, QuantLib::Size >, accumulator > accCov_
QuantLib::ext::shared_ptr< HistoricalScenarioGenerator > hisScenGen_
QuantLib::ext::shared_ptr< SensitivityStream > sensitivityStream_
Stream of sensitivity records used for the sensitivity based backtest.
void populateSensiShifts(QuantLib::ext::shared_ptr< NPVCube > &cube, const vector< RiskFactorKey > &keys, QuantLib::ext::shared_ptr< ScenarioShiftCalculator > shiftCalculator)
void calculateSensiPnl(const std::set< SensitivityRecord > &srs, const std::vector< RiskFactorKey > &rfKeys, QuantLib::ext::shared_ptr< NPVCube > &shiftCube, const std::vector< QuantLib::ext::shared_ptr< PNLCalculator > > &pnlCalculators, const QuantLib::ext::shared_ptr< CovarianceCalculator > &covarianceCalculator, const std::vector< std::string > &tradeIds={}, const bool includeGammaMargin=true, const bool includeDeltaMargin=true, const bool tradeLevel=false)
const std::vector< QuantLib::Real > & foPnls()
void populateTradePNLs(const TradePnLStore &allPnls, const TradePnLStore &foPnls)
const bool isInTimePeriod(QuantLib::Date startDate, QuantLib::Date endDate)
void populatePNLs(const std::vector< QuantLib::Real > &allPnls, const std::vector< QuantLib::Real > &foPnls, const std::vector< QuantLib::Date > &startDates, const std::vector< QuantLib::Date > &endDates)
std::vector< QuantLib::Real > foPnls_
std::vector< std::vector< QuantLib::Real > > TradePnLStore
Data types stored in the scenario class.
Definition: scenario.hpp:48
Base Class for streaming SensitivityRecords.
virtual void reset()=0
Resets the stream so that SensitivityRecord objects can be streamed again.
virtual SensitivityRecord next()=0
Returns the next SensitivityRecord in the stream.
bool contains(const Date &d) const
Class for generating sensi pnl.
A cube implementation that stores the cube in memory.
#define LOG(text)
#define TLOG(text)
std::vector< std::vector< QuantLib::Real > > TradePnLStore
std::string to_string(const LocationInfo &l)
Structured analytics error.