Hello, dear friend, you can consult us at any time if you have any questions, add WeChat: daixieit

Foundations of a Risk Management System in C++

FE5226 Project

2022

Abstract

This source code demonstrates the foundation of a risk management system. It can load trades from a database, compute the price of trades, discover dynamically market dependencies and construct necessary market objects, retrieve trade life-cycling information, compute price sensitivities with respect to risk factors, analyse the impact of hypothetical market scenarios.

A real risk management system entails of many more features, e.g. sophisticate market objects and pricing models; efficient algorithms to avoid unnecessary re-computations when calculating Greeks; support for a large number of trade types; extensive set of tools to orchestrate re-computations and post-process their results  (e.g.  P&L explain, VaR, PFE, XVA); tools to manage life-cycle of trades;  connections to external databases and systems of various nature; client-server pricing APIs; connections to cash ow management systems and payment systems; support for parallel computations (thread safety).

All these features can be implemented as modifications or extensions of the code provided.  You are supposed to carry out various improvements and extensions as specified below.

1    License

The license for the source code is in the main directory. Please read the license le.

2    Source code organization and build instructions

All source code les are in the subdirectory src. Source les with names matching the patterns Demo*. cpp or Test*. cpp contain the  main  routine for separate programs.   The other cpp les are library components used by the various programs.

The target platform is x86 64 (64 bits). The C++ language standard used is: C++17. In Visual Studio you must:

•  Select platform x64” (the default is x86”, i.e. 32 bits).

•  Go to the property page for the project (right click on the project from solution explorer), select all platform” and ”all configurations”, then navigate the configuration tree to C++/AllOptions and set:

–  C++ language standard: C++17

–  Disable language extensions: yes

–  Treat warnings as errors: yes

–  Warning level: W3

The src subdirectory also contains a Makefile for gcc compilation. If you use Visual Studio, you need to: create a new solution; in the solution add one console project for each cpp le with name Demo*. cpp or  Test*. cpp; in each project also add all other cpp les (except the ones with name Demo*. cpp or Test*. cpp), so that in each project you have one and only one cpp le containing the function main. You can work in Debug” mode, which allows you to execute code step by step and use breakpoints, but when you are nished with development and want to test the performance of your final program you should use Release” mode.  The language standard can be selected from the project configuration (right click on the project in the Solution Explorer pane, then select C++/Lanugage/C++LanguageStandards). Note that this must be repeated for all modes (debug and release).

A number of txt les are given.  portfolio 0.txt and risk factors 0.txt work with the program in its current state. portfolio i .txt and risk factors i .txt work after completion of the rst i tasks. If a le with appropriate enumeration is missing, you can use the last one available (e.g. in task 11, you can use risk factors 5.txt )

To run the program type from the command line:

DemoRisk -p portfolio .txt -f risk factors .txt

To run the program directly from inside Visual Studio, you can specify the working directory where your txt les are located and the command line arguments  (portfolio .txt risk factors .txt ) in the Debugging  section of the project configuration.

3    What to do

You  have to  complete  sequentially  all tasks  mentioned  in  section  6.   After  completing  each task  you  should test correctness of your work by succesfully running the command

DemoRisk portfolio i.txt risk factors i.txt

and comparing the output of your program with the output given in output i.txt. To compare les you can use the free Windows program WinMerge, or sdiff on Linux. Your goal is to generate an output le identical to the one provided.

4    Marking criteria

Marking is going to be based on the following criteria.

4.1    Correctly follow submission instructions (-2 marks)

By simply following correctly submission procedures described in section 5, you get full marks.  Otherwise, up to 2 marks are subtracted from your total mark. While this seems trivial, every year some student group fails to do this, so please be careful.

4.2    Coding style (3 marks)

Follow good practice

•  correct use of indentation (configure your editor to convert tab to spaces and to use tabs of 4 characters)

•  if called with no arguments or with incorrect arguments, the program must produce an explanation of the correct command line syntax required to invoke the program

•  proper choices of variable and function names

•  appropriate use of source code comments

•  avoid code duplication (do not copy and paste, introduce new functions as appropriate)

•  code robustness (e.g. assert function arguments are valid)

•  code conciseness (do not do in 10 lines what you could do in one line)

•  do not reinvent the wheel (use library functions when available)

•  minimal changes to original les: only add or modify the lines of code strictly necessary, do not insert unnecessary white-spaces (you can use  WinMerge to view the difference between your les and the original ones, or use git, which is excellent for checking differences). I will use WinMerge to check differences.

4.3    Date class correctness (1 mark)

I will look at you date class implementation and the test, focusing on correctness.  I will run your date class against my test.

4.4    Basic correctness (5 marks)

If your code compiles I will run your DemoRisk with the given trade portfolio and market data and compare your output with mine using WinMerge.  If there is a no difference, you get full marks.  Note that if your code does not compile, you will get zero marks. The command line help message should also be correct (include all accepted command line arguments).

4.5    Performance (4 marks)

I create a very large test portfolio (millions of trades).  As simple way to do this is to simply copy the given test set portfolio many times in a bigger le. If your DemoRisk runs in a time comparable or better than my own implementation, you get full marks. If section 4.4 fails, here you get zero marks. Common mistakes that can make your program slow are: poor choice of data structures and redundant calculations (in particular, for market objects, do as much calculations as possible when you construct the objects, and reuse this information when pricers query the market data objects).

4.6    Advanced correctness (3 marks)

I create an extended set of test test les, with more currencies and more special cases than what covered by the sample test files.  This may include expired trades (i.e.  trades where the last payment date is before the pricing date, which should generate an error), missing market data (e.g.  a missing FX spot), missing xings and other corner cases.  If your DemoRisk runs correctly reproducing the output le provided you get full marks. I will compare your output with mine using WinMerge. If section 4.4 fails, here you get zero marks.

4.7    GCC build (3 marks)

Before submitting, verify that your program compiles and run also with the g++ compiler.  To do that on Windows 64 bits you can:

•  download setup-x86 64.exe from https://www.cygwin.com/install.html

•  Run the installer. When you install, select at least the packages gcc-g++” and make” .

•  launch cygwin and type the command cygpath -w $(pwd).

•  Copy your cpp and h les and the Makefile I provided to the directory revealed by the previous command

•  Type make”

•  If all is well, you should see all your les compiling without errors and an executable le created.

•  Test the executable le from the command line.

5    Submission instructions

•  After completing all tasks you need to zip and submit all les with extension *.h and *.cpp.

•  Do not submit any other le (e.g. txt les, visual studio project les, object les, executable les, output les).

•  Files  should be zipped without  directory path.   I.e.   your zip le  should  contain le1.cpp,  file2.cpp,  ...,  not dirname/file1.cpp, dirname/file2.cpp, ....

•  You need to include in the zip le also a le with name GROUP.TXT containing team members.  The le must contains the student IDs separated by a newline, i.e.

studentID1

studentID2

...

•  Submit only a zip le, other formats are not allowed (e.g. arj, 7z)

•  Submit only only once per group.

•  Only one member of the team must submit, not all members.

6    Tasks

6.1    Improve the Date class

The most common operations performed with the  Date  class when pricing  are comparison  (e.g.   <,  ==,  >)  and computation of distance between two dates.  The current internal representation of the date class is not optimal for these operations.

Refactor the Date class by modifying the current internal representation, which entails of the 4 data members day, month, year and is leap, to a single data member of type unsigned with name serial, representing the number of days elapsed since 1-Jan-1900. This allows to speed up the operations mentioned above and reduce memory footprint.

Change the serialization format to be based directly on serial, so that no extra work is necessary when saving or loading from a le.

When the Date class is constructed from the arguments day, month and year, you need to generate the equivalent serial number.  When converting to a string in calendar format for display to the screen, you need to convert serial on-the-fly into day, month and year.

6.2    Write a test for the Date class

Complete the program in TestDate. cpp, so that it tests the correctness of the Date class. It should perform the following tests:

1.  Construction of an invalid date should generate an error:  generate intentionally 1000 random invalid dates (e.g. 31-Apr-2010) and verify that the Date class constructor throws an error (use try. . . catch).

2.  Verify that converting a date in calendar format (day, month, year) to serial format and then converting back to calendar format yields the original date. Repeat for all dates in the valid range (1-Jan-1900, 31-Dec-2199).

3.  Verify that the serial number generated for 2 contiguous dates are contiguous.   For instance 31-Jan-2012 and 1-Feb-2012 are contiguous dates, hence the serial numbers they generate should only differ by 1.  Repeat for all pairs of contiguous dates in the valid range (1-Jan-1900, 31-Dec-2199).

At the begin of the test, randomize the random number generator.   If the test fails, the program should throw an exception, if it succeeds it should just print the message SUCCESS” .

Print out the random seed you used for the random number generator, so that, if the test fails, a programmer can re-use the same seed and reproduce the error, which is essential to be able to debug it.

6.3    Perfect streaming for type double

Currently when a oating point number in double precision is saved to a le it is transformed from IEEE-754 format to a decimal representation with a nite number of decimals in scientific notation.  The conversion from IEEE-754 to decimal (when saving) and then back to IEEE-754 (when reading) involves rounding and can cause accuracy loss.  In other words, if we start from a value of type double, we save it as a string in decimal format, then we read back the string and convert it to double, we may not get back exactly the same value we started with.

If we worked with binary les we would not have this problem, however here we are working text les, hence we need to represent our numbers using a set of characters which can be rendered with a text editor.

There are many ways to do this. For the purpose of this project we will solve the problem by changing the streaming representation of a double to the integer interpretation of its binary representation in hexadecimal format with letters in lowercase.

If x is a variable of type double, it binary representation can be re-interpreted as a 64 bits integer, which can be represented exactly. To do that, you can either get the memory address to the variable of type double and reinterpret cast it to a 64-bit int pointer or use a union. To make the textual representation more compact, we use base 16 (hexadecimal). For example, the double number -0.15625 should be saved to le as the sequence of 16 characters bfc4000000000000 (see example in DemoHex. cpp, which use a  union).   When reading, you need to read the integer number saved in hexadecimal format and re-interpret it as a double.

You need to modify the implementation of the operator<< for double and implement the overload of the operator>> (you may draw inspiration from the ones implemented for the Date class).

6.4    Improve the  CurveDiscount class

The current CurveDiscount curve assumes that the interest rate curve is constant.

Modify it so that it takes a set of rates related to different tenors and modify it so that when querying for a discount factor, the value returned is correctly interpolated.

For example, given the data points IR.1W.USD, IR.2W.USD, IR.1M.USD, IR.2M.USD, IR.3M.USD, IR.6M.USD, IR.9M.USD, IR.1Y.USD, IR.18M.USD, IR.2Y.USD, ..., you need to resolve each of them to an actual date with respect to the anchor date, construct a stepwise constant interpolation scheme, modify the function which returns discount factor.   For sake of clarity,  D, W,  M  and Y mean respectively days,  weeks,  months  and years.   Although this is not consistent with market conventions, for simplicity assumes that 1M=30 days and 1Y=365 days, and ignore the distinction between weekdays and weekends when computing tenor dates.

Since you do not know in advance which points are available in the market data server, if you want all the available points for the currency EUR, you need to modify the market data server to be able to return the array of risk factors with  pattern  IR.*.EUR.  Use the  <regex>  STL  library to  search  for  a  string  pattern  and  implement the  method MarketDataServer::matching, whose header is already defined in MarketDataServer.h.  Note that IR.*.EUR” is not the correct regular expression string to be used here, you need to read about regular expressions and gure out what is the most appropriate string to use, i.e. it should match just” what needed. For instance, ”IR.*EUR” would match ”IR.1W.EUR”, but also IRwhateverEUR”, which is undesirable.

The interpolation scheme requires computation of the local rates. If ri  and ri+1  are the absolute rates for tenor Ti and Ti+1, as returned from the market data server, the local rate ri,i+1  is the one that solves the following equation:

riTi ri,i+1(Ti+1 Ti)  = e ri+1Ti+1

Then the discount factor for any date t ∈ [Ti , Ti+1] is

df (t) = e riTi ri,i+1(t Ti)

In an efficient implementation as much calculation as possible should be done in the constructor, so that the function df , which will be invoked millions of times, is as fast as possible. For instance, the values of the products ri Ti  could be precomputed for all i and cached in a data member of the class.

In the df function use binary search (see lower bound) to determine the interval i such that Ti  ≤ t < Ti+1 .

If the function df is called with a date beyond the last tenor or before the anchor date (today), it should generate an error.

6.4.1    PV01 with tenors

The PV01  risk function needs to be modified into 2 different risk functions:  PV01Parallel that computes risk with respect to parallel shift of the yield curve (all risk factor move simultaneously); and PV01Bucketed that computes risk with respect to individual yield curves (the yield curve for each tenor Ti  change, with all the rest remaining constant). In both cases use central differences, with the same bump size as currently defined in Demo . cpp. The 2 new functions must have the same arguments and return type as the existing one.

6.5    Recover from pricing failures

The function IPricer::price fails and throws an exception when an error occurs. For instance, if the settlement date of a Payment is in the past, pricing of the entire portfolio fails.  We would like instead that only problematic trades fail to price while all remaining trades price succesfully.

Modify the typedef portfolio values t to vector<pair<double,string>>. In the compute prices function use try{}catch{} to catch exceptions.  If there is no error set the double to the price and the string to an empty string.  If there is an    error set the double to NaN (see header < limits >) and the string to the error message embedded in the exception.

When computing thet total for the book (function portfolio total), you should return a pair containing the total for trades which price succesfully and an array of pairs with the index of the trades which price to fail and the associated error message.

pair < double, vector < pair < size t, string >>>

To identify trades with errors, check if the value is a NaN, not if the error message string is empty, because a trade could potentially fail and still have an empty error message.

When computing nite differences, if either the up or down bump are NaN, then set the result to NaN and set the error message to the correspnding error message (if both up and down states have error, you can ignore one of the two error messages).

Modify DemoRisk to display both the total for the book and the number of trades which fail to price. Compare the output of your program with the one given.

6.6    Add a new market object  CurveFXSpot

The new market object should implement the interface ICurveFXSpot.

Note that the risk factors contained in the market data server only include spot currencies in the format CCYUSD, i.e. the prices of CCY in USD. The class must support direct, inverse and cross currency pairs (e.g.  EURUSD, USD- JPY, EURGBP). It is up to you to make the correct queries to the market data server and construct the CurveFXSpot appropriately.

You need to modity the Market class as appropriate.

Note that according with market conventions the spot price observed at time T is the forward price for an exchange of money at T + 2. Ignore this difference and assume that the spot price observed at time T is defined as the exchange rate for an instantaneous exchange of money at time T .   Thanks to this simplification we can simply use the spot directly to convert the present value of a trade price from one currency to another.  A real risk management system however, should take this difference into account.

Modify the PricerPayment class to use the new class CurveFXSpot.

6.7    Add pricer conguration

Modify DemoRisk to read another optional argument form the command line, the base currency (argument name - b CCY ). If not explicitly passed, this argument takes the value USD. Pass this to the run function. Modify the function ITrade::Pricer adding an argument configuration of type string. Remember to update the command line help string.

In a real risk management system the configuration argument could be very complex and allow for instance to select a pricing model (e.g.  Black Scholes or Heston) or a numerical framework (e.g.  PDE or Monte Carlo).  In this project the configuration string is simply the base currency in which all risk is to be computed.  At the moment, by default everything is computed in USD. After your changes, pricer(”EUR”) should return a pricer that computes the price in EUR.

You need to modify all pricers accordingly. It may be convenient adding a base class Pricer which all pricers inherit from and take care of this conversion.

6.8    Add a new market object  CurveFXForward

The new market object should implement the interface ICurveFXForward.

The forward price observed at time T0  of currency ccy1  expressed in currency ccy2  for delivery at time T can be computed as

B1 (T0 , T)

F (T0 , T) = E[S(T)|FT0] = S(T0 )

where T0  is the pricing date, S(T0 ) is the current spot price of currency ccy1  expressed in currency ccy2 , Bi (T0 , T) is the discount factor in currency ccyi  for time T observed at time T0 .

This class should use the CurveFXSpot class to get S(T0 ) and the CurveDiscount classes to get the discount factors.

Modify the Market class as appropriate.

6.9    Add xing data server

To compute the price of some trades we need information in the past as well as in the future. For example, if today is the 15th of November and we want to compute the price of an Asian option that pays the average of the daily closing price in the month of November, some of these closing prices are in the past and need to be accounted for.

Past observations are called xings. Introduce a FixingDataServer class, which implements the following methods:

// return the xing if available, otherwise trigger an error

double get(const string& name, const Date& t) const;

// return the xing if available, NaN otherwise, and set the ag if found

pair < double, bool >  lookup(const string& name, const Date& t) const;

Populate the xing data server with the data provided in the le xings .txt.

Add an extra command line argument (-x ) to DemoRisk. cpp and use it to communicate to the program the name of the data le containing the xings. Remember to update the command line help string.

Create a FixingDataServer object in DemoRisk. cpp and pass it as an extra argument to the IPricer::price function. Only the pricers of trades requiring xings will do something with it, other pricers will ignore it (note that TradePayment does not have any xing, hence it will not use it). In particular: fixings occurring in the past should be obtained from this argument and if not available an exception should be thrown; fixings occurring exactly on the pricing date should be taken from this argument if available, resolved through the Market otherwise; fixings occurring in the future should be resolved through the Market (e.g. forward prices).

Do not change the format of dates to serial and of double to hexadecimal in the data le.

6.10    Add FX Spot Greek function

Add another function for Greeks calculation, named fx delta, which computes the risk of the book with respect to each of the FX spot rates quoted against USD in the market data server.  It should use central difference with a relative bump of 0.1%.

It should have the same return value and argument types as the PV01 functions.

Add calculation of FXDelta at the end of DemoRisk. cpp and display the cumulative book value for each fx spot rate.

6.11    Add new trade type  TradeFXForward

Given two dates T1  and T2 , at time T2  it pays the following amount of ccy2

payoff = N [S(T1 ) − K]

where S(T1 ) is the spot price of currency ccy1  expressed in currency ccy2  observed at the xing date T1 , K is a pre- defined strike price, N is the notional amount and T2  is the settlement date (it must be T1  < T2 for the trade to be valid).

Assign to the trade ID=3 (arbitrarily chosen) and the following serialized representation:

ID;NOTIONAL;CCY1;CCY2;STRIKE;FIXINGDATE;SETTLEDATE;

Example:

3;EUR;USD;notional in hex;strike in hex;date in serial;date in serial;

The currency pair ccy1 ccy2  can be any direct, inverse or cross pair.

Implement the corresponding pricer object. The price in ccy2  can be computed as:

price = B2 (T0 , T2 )[F (T0 , T1 ) − K]

where B2 (T0 , T2 ) is the discount factor in currency 2 for settlement date T2  and F (T0 , T1 ) is the currency forward price for date T1 . Do not forget to convert this price into USD (or any other specified base currency).

6.11.1    Historical xings

Note that when the pricing date T0  is between T1  and T2 , you need to know the value of ST1 , which is in the past, i.e. you need to know its xed value.

Note that there can be various situations with respect to the pricing date T0 :

.  T0  < T1 : both xing and settlement are in the future

•  T0  = T1  and xing for T1  not available: there is still full delta risk and PV01 risk

•  T1  ≤ T0  < T2  and xing for T1  available: there is no more delta risk with respect to the underlying, but we still have PV01 risk and there may be delta risk for conversion to base currency

•  T0  = T2 : no delta with respect to the underlying or PV01 risk, we assume the payment has not been settled yet, hence we can still have a non-zero price, there may be still delta risk with respect to conversion to base currency.

•  T0  > T2 : trade is expired, an error should be generated

7    Q&A

7.1    How do I save the output of my program to a le?

You can redirect the output of your program to a le adding at the end of the command line:  > filename.txt.

7.2    How do I measure the execution time of my program?

1.  Compile your program for x64 platform in Release mode.

2.  Redirect the console to a le, as explained above (this is because the console is very slow, and your time measure- ment would not be reflective of the real computation time)

3.  Run the command echo  %time%  before and after calling your command.  You might consider creating a batch file (.bat) and run it from the windows shell (cmd).

7.3    How can I know if the performance of my program is reasonable?

I will post my implementation as an executable for Win64.  You can compare your implementation with mine.  I will not post an equivalent binary le for MacOS.

7.4    What is the best way to divide the work among group members?

You are not supposed to divide the work among group members. I allow to work in groups so that strongest programmers can help weaker ones to improve, not so that you can split the work and work less. For each task repeat the following steps:

1.  get together and discuss the task

2.  each group member carry out the task individually

3.  get together and discuss individual solutions

4.  choose the best solution and add to the code

5.  move to the next task (where everybody starts from a common source code)

In this way, all Students carry out all tasks.

7.5    Do I need to submit my program output?

No, only your source code.

7.6    Do I need to provide intermediate source code, after each step?

No, just the nal source code.  Intermediate les are provided only to help you check after every step that your work is correct.