PROBLEM
So, what is then wrong with that program? There should be nothing wrong with the program, but experienced programmers should catch a lot of different issues to be improved. Program is actually not very flexible as it is at the moment. However, I will concentrate on just one specific problem: the object creation part of the program. When looking at its client (class Program), we can see how it is polluted with object creation procedures (DiscountCurve, CreditCurve, NAGCopula) and hard-coded data. What if I would like to create my objects and data from text file or from Excel workbook? The mess (main code size explosion) would be even greater. Needless to say, the program as it is now, is not leaving much of those options open.
SOLUTION
Solution for this problem is Builder design pattern. Gang of Four (GOF) definition for Builder is the following.
"The intent of the Builder design pattern is to separate the construction of a complex object from its representation. By doing so the same construction process can create different representations."
In the new program design, BasketCDS class needs three different objects: Copula, CreditMatrix and DiscountCurve. Ultimately, client will feed BasketCDS class with these three objects wrapped inside Tuple. Before this, client is using concrete ExcelBuilder (implementation of abstract ComponentBuilder) for creating all three objects. So, client is effectively outsourcing the creation part of this complex object (a tuple, consisting of three objects) for ExcelBuilder. Just for example, instead of ExcelBuilder we could have TextFileBuilder or ConsoleBuilder. So, the source from which we are creating these objects (Copula, CreditMatrix and DiscountCurve) can be changed. Moreover, our client has no part in the object creation process. As far as I see it, this scheme is satisfying that GOF definition for Builder pattern. With this small change in design, we are now having some important options open.
PROGRAM FLOW
In order to understand the program data flow a bit better, a simple class UML has been presented in the picture below. I would like to stress the fact, that in no way this presentation is formally correct. However, the point is just to open up how this program is working on a very general level.
Client is requesting for creation of several objects from ExcelBuilder object, which is an implementation of abstract ComponentBuilder class. ExcelBuilder is then creating three central objects (CreditMatrix, DiscountCurve and Copula) into generic Tuple.
Client is then feeding BasketCDS with Tuple, which is consisting all required objects. BasketCDS object is starting Monte Carlo process and sending calculated leg values to Statistics object by using delegate methods. Finally, client is requesting the result (basket CDS spread) from Statistics object.
MathTools and MatrixTools classes are static utility classes, used by several objects for different mathematical or matrix operations.
EXCEL BUILDER
For this example program, Excel is the most convenient platform for feeding parameters for our C# program. At this posting, I am only going to present the Excel interface and the results. For this kind of scheme, interfacing C# to Excel with Excel-DNA has been thoroughly covered in this blog posting.
My current Excel worksheet interface is the following.
The idea is simple. Client (Main) will delegate the creation part of three core objects (Copula, CreditMatrix and DiscountCurve) for ExcelBuilder class. ExcelBuilder class has been carefully configured to get specific data from specific ranges in Excel workbook. For example, it will read co-variance matrix from the named Excel range (_COVARIANCE). Personally, I am always using named Excel ranges instead of range addresses, in order to maintain flexibility when changing location of some range in the worksheet. By using this kind of a scheme, no further modifications will be needed in the program, since it communicates only with named Excel ranges.
After creating all those named ranges, a Button (Form Control) has been created into Excel worksheet, having program name Execute in its Assign Macro Box. I have been using Form Controls in order to get rid of any VBA code completely. Any public static void method in .NET code will be registered automatically by Excel-DNA as a macro in Excel. Thanks for this tip goes to Govert Van Drimmelen (inventor, developer and author of Excel-DNA).
PROGRAM RUN
After pressing Execute button, my C# program will write the following Basket CDS prices and program running time in seconds back to Excel worksheet (yellow areas).
THE PROGRAM
using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Windows.Forms; using System.Diagnostics; using ExcelDna.Integration; using NagLibrary; namespace ExcelBasketCDSPricer { // CLIENT public static class Main { private static Stopwatch timer; private static dynamic Excel; private static ComponentBuilder builder; private static Tuple<Copula, CreditMatrix, DiscountCurve> components; private static List<BasketCDS> engines; private static List<Statistics> statistics; // public static void Execute() { try { // create timer and Excel objects timer = new Stopwatch(); timer.Start(); Excel = ExcelDnaUtil.Application; // // create Excel builder objects and request for required components for all pricers builder = new ExcelBuilder(); components = builder.GetComponents(); int nSimulations = (int)Excel.Range["_N"].Value2; int nReferences = (int)Excel.Range["_REFERENCES"].Value2; // // create basket pricers and statistics gatherers into containers engines = new List<BasketCDS>(); statistics = new List<Statistics>(); // // order updating for statistics gatherers from basket pricers for (int kth = 1; kth <= nReferences; kth++) { statistics.Add(new Statistics(String.Concat(kth.ToString(), "-to-default"))); engines.Add(new BasketCDS(kth, nSimulations, nReferences, components)); engines[kth - 1].updateDefaultLeg += statistics[kth - 1].UpdateDefaultLeg; engines[kth - 1].updatePremiumLeg += statistics[kth - 1].UpdatePremiumLeg; } // // run basket pricers engines.ForEach(it => it.Process()); double[] prices = statistics.Select(it => Math.Round(it.Spread() * 10000, 3)).ToArray<double>(); timer.Stop(); // // print statistics back to Excel Excel.Range["_PRICES"] = Excel.WorksheetFunction.Transpose(prices); Excel.Range["_RUNTIME"] = Math.Round(timer.Elapsed.TotalSeconds, 3); } catch (Exception e) { MessageBox.Show(e.Message); } } } // // ******************************************************************************* // abstract base class for all component builders public abstract class ComponentBuilder { public abstract Copula CreateCopula(); public abstract CreditMatrix CreateCreditMatrix(); public abstract DiscountCurve CreateDiscountCurve(); public abstract Tuple<Copula, CreditMatrix, DiscountCurve> GetComponents(); } // // implementation for builder reading parameters from Excel public class ExcelBuilder : ComponentBuilder { dynamic Excel = null; private Copula copula = null; private CreditMatrix credit = null; private DiscountCurve curve = null; // public ExcelBuilder() { Excel = ExcelDnaUtil.Application; } public override Copula CreateCopula() { // create covariance matrix dynamic matrix = Excel.Range["_COVARIANCE"].Value2; int rows = matrix.GetUpperBound(0); int columns = matrix.GetUpperBound(1); double[,] covariance = new double[rows, columns]; // for (int i = 0; i < rows; i++) { for (int j = 0; j < columns; j++) { covariance[i, j] = (double)matrix.GetValue(i + 1, j + 1); } } // // create copula source information string source = (string)Excel.Range["_COPULA"].Value2; // // special parameters for NAG copula if (source == "NAG") { // generator id int genid = (int)Excel.Range["_GEN"].Value2; // // sub generator id int subgenid = (int)Excel.Range["_SUBGEN"].Value2; // // mode int mode = (int)Excel.Range["_MODE"].Value2; // // degrees of freedom int df = (int)Excel.Range["_DF"].Value2; // // create NAG copula copula = new NAGCopula(covariance, genid, subgenid, mode, df); } return copula; } public override CreditMatrix CreateCreditMatrix() { // create cds matrix dynamic matrix = Excel.Range["_CDS"].Value2; int rows = matrix.GetUpperBound(0); int columns = matrix.GetUpperBound(1); double[,] cds = new double[rows, columns]; // for (int i = 0; i < rows; i++) { for (int j = 0; j < columns; j++) { cds[i, j] = (double)matrix.GetValue(i + 1, j + 1); } } // // recovery estimate and discounting method as generic delegate double recovery = (double)Excel.Range["_RECOVERY"].Value2; Func<double, double> df = curve.GetDF; // CreditMatrix credit = new CreditMatrix(cds, df, recovery); return credit; } public override DiscountCurve CreateDiscountCurve() { // create zero-coupon curve dynamic matrix = Excel.Range["_ZERO"].Value2; int rows = matrix.GetUpperBound(0); int columns = matrix.GetUpperBound(1); double[,] zeroCouponCurve = new double[rows, columns]; // for (int i = 0; i < rows; i++) { for (int j = 0; j < columns; j++) { zeroCouponCurve[i, j] = (double)matrix.GetValue(i + 1, j + 1); } } // // create interpolation method int interpolationMethodID = (int)Excel.Range["_INTERPOLATION"].Value2; InterpolationAlgorithm interpolation = null; switch (interpolationMethodID) { case 1: interpolation = MathTools.LinearInterpolation; break; default: throw new Exception("interpolation algorithm not defined"); } // // create discounting method int discountingMethodID = (int)Excel.Range["_DISCOUNTING"].Value2; DiscountAlgorithm discounting = null; switch (discountingMethodID) { case 1: discounting = MathTools.ContinuousDiscountFactor; break; default: throw new Exception("discounting algorithm not defined"); } DiscountCurve curve = new DiscountCurve(zeroCouponCurve, interpolation, discounting); return curve; } public override Tuple<Copula, CreditMatrix, DiscountCurve> GetComponents() { copula = CreateCopula(); curve = CreateDiscountCurve(); credit = CreateCreditMatrix(); Tuple<Copula, CreditMatrix, DiscountCurve> components = new Tuple<Copula, CreditMatrix, DiscountCurve>(copula, credit, curve); return components; } } // // ******************************************************************************* public class BasketCDS { public PremiumLegUpdate updatePremiumLeg; // delegate for sending value public DefaultLegUpdate updateDefaultLeg; // delegate for sending value // private Copula copula; // NAG copula model private CreditMatrix cds; // credit matrix private DiscountCurve curve; // discount curve private int k; // kth-to-default private int m; // number of reference assets private int n; // number of simulations private int maturity; // basket cds maturity in years (integer) // public BasketCDS(int kth, int simulations, int maturity, Tuple<Copula, CreditMatrix, DiscountCurve> components) { this.k = kth; this.n = simulations; this.maturity = maturity; this.copula = components.Item1; this.cds = components.Item2; this.curve = components.Item3; } public void Process() { // request correlated random numbers from copula model double[,] randomArray = copula.Create(n, Math.Abs(Guid.NewGuid().GetHashCode())); m = randomArray.GetLength(1); // // process n sets of m correlated random numbers for (int i = 0; i < n; i++) { // create a set of m random numbers needed for one simulation round double[,] set = new double[1, m]; for (int j = 0; j < m; j++) { set[0, j] = randomArray[i, j]; } // // calculate default times for reference name set calculateDefaultTimes(set); } } private void calculateDefaultTimes(double[,] arr) { // convert uniform random numbers into default times int cols = arr.GetLength(1); double[,] defaultTimes = new double[1, cols]; // for (int j = 0; j < cols; j++) { // iteratively, find out the default tenor bucket double u = arr[0, j]; double t = Math.Abs(Math.Log(1 - u)); // for (int k = 0; k < cds.CumulativeHazardMatrix.GetLength(1); k++) { int tenor = 0; double dt = 0.0; double defaultTenor = 0.0; if (cds.CumulativeHazardMatrix[k, j] >= t) { tenor = k; if (tenor >= 1) { // calculate the exact default time for a given reference name dt = -(1 / cds.HazardMatrix[k, j]) * Math.Log((1 - u) / (Math.Exp(-cds.CumulativeHazardMatrix[k - 1, j]))); defaultTenor = tenor + dt; // // default time after basket maturity if (defaultTenor >= maturity) defaultTenor = 0.0; } else { // hard-coded assumption defaultTenor = 0.5; } defaultTimes[0, j] = defaultTenor; break; } } } // proceed to calculate leg values updateLegValues(defaultTimes); } private void updateLegValues(double[,] defaultTimes) { // check for defaulted reference names, calculate leg values // and send statistics updates for BasketCDSStatistics int nDefaults = getNumberOfDefaults(defaultTimes); if (nDefaults > 0) { // for calculation purposes, remove zeros and sort matrix MatrixTools.RowMatrix_removeZeroValues(ref defaultTimes); MatrixTools.RowMatrix_sort(ref defaultTimes); } // calculate and send values for statistics gatherer double dl = calculateDefaultLeg(defaultTimes, nDefaults); double pl = calculatePremiumLeg(defaultTimes, nDefaults); updateDefaultLeg(dl); updatePremiumLeg(pl); } private int getNumberOfDefaults(double[,] arr) { int nDefaults = 0; for (int i = 0; i < arr.GetLength(1); i++) { if (arr[0, i] > 0.0) nDefaults++; } return nDefaults; } private double calculatePremiumLeg(double[,] defaultTimes, int nDefaults) { double dt = 0.0; double t; double p = 0.0; double v = 0.0; if ((nDefaults > 0) && (nDefaults >= k)) { for (int i = 0; i < k; i++) { if (i == 0) { // premium components from 0 to t1 dt = defaultTimes[0, i] - 0.0; t = dt; p = 1.0; } else { // premium components from t1 to t2, etc. dt = defaultTimes[0, i] - defaultTimes[0, i - 1]; t = defaultTimes[0, i]; p = (m - i) / (double)m; } v += (curve.GetDF(t) * dt * p); } } else { for (int i = 0; i < maturity; i++) { v += curve.GetDF(i + 1); } } return v; } private double calculateDefaultLeg(double[,] defaultTimes, int nDefaults) { double v = 0.0; if ((nDefaults > 0) && (nDefaults >= k)) { v = (1 - cds.Recovery) * curve.GetDF(defaultTimes[0, k - 1]) * (1 / (double)m); } return v; } } // // ******************************************************************************* // abstract base class for all copula models public abstract class Copula { // request matrix of correlated uniform random numbers // number of rows are given argument n // Number of columns are inferred from the size of covariance matrix and public abstract double[,] Create(int n, int seed); } // // NAG G05 COPULAS WRAPPER public class NAGCopula : Copula { private double[,] covariance; private int genID; private int subGenID; private int mode; private int df; private int m; private int errorNumber; private double[] r; private double[,] result; // public NAGCopula(double[,] covariance, int genID, int subGenID, int mode, int df = 0) { // ctor : create correlated uniform random numbers // degrees-of-freedom parameter (df), being greater than zero, // will automatically trigger the use of student copula this.covariance = covariance; this.genID = genID; this.subGenID = subGenID; this.mode = mode; this.df = df; this.m = covariance.GetLength(1); r = new double[m * (m + 1) + (df > 0 ? 2 : 1)]; } public override double[,] Create(int n, int seed) { result = new double[n, m]; G05.G05State g05State = new G05.G05State(genID, subGenID, new int[1] { seed }, out errorNumber); if (errorNumber != 0) throw new Exception("G05 state failure"); // if (this.df != 0) { // student copula G05.g05rc(mode, n, df, m, covariance, r, g05State, result, out errorNumber); } else { // gaussian copula G05.g05rd(mode, n, m, covariance, r, g05State, result, out errorNumber); } // if (errorNumber != 0) throw new Exception("G05 method failure"); return result; } } // // ******************************************************************************* public class CreditMatrix { private Func<double, double> df; private double recovery; private double[,] CDSSpreads; private double[,] survivalMatrix; private double[,] hazardMatrix; private double[,] cumulativeHazardMatrix; // public CreditMatrix(double[,] CDSSpreads, Func<double, double> discountFactor, double recovery) { this.df = discountFactor; this.CDSSpreads = CDSSpreads; this.recovery = recovery; createSurvivalMatrix(); createHazardMatrices(); } // public read-only accessors to class data public double[,] HazardMatrix { get { return this.hazardMatrix; } } public double[,] CumulativeHazardMatrix { get { return this.cumulativeHazardMatrix; } } public double Recovery { get { return this.recovery; } } // private void createSurvivalMatrix() { // bootstrap matrix of survival probabilities from given CDS data int rows = CDSSpreads.GetUpperBound(0) + 2; int cols = CDSSpreads.GetUpperBound(1) + 1; survivalMatrix = new double[rows, cols]; // double term = 0.0; double firstTerm = 0.0; double lastTerm = 0.0; double terms = 0.0; double quotient = 0.0; int i = 0; int j = 0; int k = 0; // for (i = 0; i < rows; i++) { for (j = 0; j < cols; j++) { if (i == 0) survivalMatrix[i, j] = 1.0; if (i == 1) survivalMatrix[i, j] = (1 - recovery) / ((1 - recovery) + 1 * CDSSpreads[i - 1, j] / 10000); if (i > 1) { terms = 0.0; for (k = 0; k < (i - 1); k++) { term = df(k + 1) * ((1 - recovery) * survivalMatrix[k, j] - (1 - recovery + 1 * CDSSpreads[i - 1, j] / 10000) * survivalMatrix[k + 1, j]); terms += term; } quotient = (df(i) * ((1 - recovery) + 1 * CDSSpreads[i - 1, j] / 10000)); firstTerm = (terms / quotient); lastTerm = survivalMatrix[i - 1, j] * (1 - recovery) / (1 - recovery + 1 * CDSSpreads[i - 1, j] / 10000); survivalMatrix[i, j] = firstTerm + lastTerm; } } } } private void createHazardMatrices() { // convert matrix of survival probabilities into two hazard rate matrices int rows = survivalMatrix.GetUpperBound(0); int cols = survivalMatrix.GetUpperBound(1) + 1; hazardMatrix = new double[rows, cols]; cumulativeHazardMatrix = new double[rows, cols]; int i = 0; int j = 0; // for (i = 0; i < rows; i++) { for (j = 0; j < cols; j++) { cumulativeHazardMatrix[i, j] = -Math.Log(survivalMatrix[i + 1, j]); if (i == 0) hazardMatrix[i, j] = cumulativeHazardMatrix[i, j]; if (i > 0) hazardMatrix[i, j] = (cumulativeHazardMatrix[i, j] - cumulativeHazardMatrix[i - 1, j]); } } } } // // ******************************************************************************* // delegate methods for interpolation and discounting public delegate double InterpolationAlgorithm(double t, ref double[,] curve); public delegate double DiscountAlgorithm(double t, double r); // public class DiscountCurve { // specific algorithms for interpolation and discounting private InterpolationAlgorithm interpolationMethod; private DiscountAlgorithm discountMethod; private double[,] curve; // public DiscountCurve(double[,] curve, InterpolationAlgorithm interpolationMethod, DiscountAlgorithm discountMethod) { this.curve = curve; this.interpolationMethod = interpolationMethod; this.discountMethod = discountMethod; } public double GetDF(double t) { // get discount factor from discount curve return discountMethod(t, interpolation(t)); } private double interpolation(double t) { // get interpolation from discount curve return interpolationMethod(t, ref this.curve); } } // // ******************************************************************************* // collection of methods for different types of mathematical operations public static class MathTools { public static double LinearInterpolation(double t, ref double[,] curve) { int n = curve.GetUpperBound(0) + 1; double v = 0.0; // // boundary checkings if ((t < curve[0, 0]) || (t > curve[n - 1, 0])) { if (t < curve[0, 0]) v = curve[0, 1]; if (t > curve[n - 1, 0]) v = curve[n - 1, 1]; } else { // iteration through all given curve points for (int i = 0; i < n; i++) { if ((t >= curve[i, 0]) && (t <= curve[i + 1, 0])) { v = curve[i, 1] + (curve[i + 1, 1] - curve[i, 1]) * (t - (i + 1)); break; } } } return v; } public static double ContinuousDiscountFactor(double t, double r) { return Math.Exp(-r * t); } } // // ******************************************************************************* // collection of methods for different types of matrix operations public static class MatrixTools { public static double[,] CorrelationToCovariance(double[,] corr, double[] stdev) { // transform correlation matrix to co-variance matrix double[,] cov = new double[corr.GetLength(0), corr.GetLength(1)]; // for (int i = 0; i < cov.GetLength(0); i++) { for (int j = 0; j < cov.GetLength(1); j++) { cov[i, j] = corr[i, j] * stdev[i] * stdev[j]; } } // return cov; } public static void RowMatrix_sort(ref double[,] arr) { // sorting a given row matrix to ascending order // input must be 1 x M matrix // bubblesort algorithm implementation int cols = arr.GetUpperBound(1) + 1; double x = 0.0; // for (int i = 0; i < (cols - 1); i++) { for (int j = (i + 1); j < cols; j++) { if (arr[0, i] > arr[0, j]) { x = arr[0, i]; arr[0, i] = arr[0, j]; arr[0, j] = x; } } } } public static void RowMatrix_removeZeroValues(ref double[,] arr) { // removes zero values from a given row matrix // input must be 1 x M matrix List<double> temp = new List<double>(); int cols = arr.GetLength(1); int counter = 0; for (int i = 0; i < cols; i++) { if (arr[0, i] > 0) { counter++; temp.Add(arr[0, i]); } } if (counter > 0) { arr = new double[1, temp.Count]; for (int i = 0; i < temp.Count; i++) { arr[0, i] = temp[i]; } } else { arr = null; } } } // // ******************************************************************************* public delegate void PremiumLegUpdate(double v); public delegate void DefaultLegUpdate(double v); // public class Statistics { // data structures for storing leg values private List<double> premiumLeg; private List<double> defaultLeg; private string ID; // public Statistics(string ID) { this.ID = ID; premiumLeg = new List<double>(); defaultLeg = new List<double>(); } public void UpdatePremiumLeg(double v) { premiumLeg.Add(v); } public void UpdateDefaultLeg(double v) { defaultLeg.Add(v); } public void PrettyPrint() { // hard-coded 'report' output Console.WriteLine("{0} : {1} bps", ID, Math.Round(Spread() * 10000, 2)); } public double Spread() { return defaultLeg.Average() / premiumLeg.Average(); } } }
CONCLUSIONS
In this blog posting, Builder design pattern was applied to solve some of the problems observed in hard-coded program. By outsourcing the object creation for Builder (ExcelBuilder), client code size explosion has been avoided completely. Moreover, the source for objects creation is now open for new implementations (ConsoleBuilder, TextFileBuilder, etc). The program is still not as flexible as it could be, but that is another story.
Again, Thanks for Govert Van Drimmelen for his amazing Excel-DNA. For learning more things about Excel-DNA, check out its homepage. Getting more information and examples with your problems, the main source is Excel-DNA google group. Excel-DNA is an open-source project, and we (the happy users) can invest its future development by making a donation.
Last week, I spent three days in Datasim training course. During the course, we went systematically through most of the GOF design patterns in C#, using traditional object-oriented approach. Plus, the instructor was also presenting, how to use C# generics (delegates) for implementing some of the design patterns, using functional programming approach. The course was truly having a great balance between theory and programming. So, if anyone is looking for very practical hands-on training for this GOF stuff, this course is getting all five stars from me.
Thank You for reading my blog. I wish you have discovered some usage for Builder pattern in your programs. Mike.
No comments:
Post a Comment