Book cover of Options and Derivatives Programming in C++23

Carlos Oliveira

Options and Derivatives Programming in C++23

Algorithms and Programming Techniques for the Financial Industry

3rd ed.

The Apress logo.

Carlos Oliveira

Seattle, WA, USA

ISBN 978-1-4842-9826-8e-ISBN 978-1-4842-9827-5

https://doi.org/10.1007/978-1-4842-9827-5

© Carlos Oliveira 2016, 2020, 2023

Apress Standard

The use of general descriptive names, registered names, trademarks, service marks, etc. in this publication does not imply, even in the absence of a specific statement, that such names are exempt from the relevant protective laws and regulations and therefore free for general use.

The publisher, the authors, and the editors are safe to assume that the advice and information in this book are believed to be true and accurate at the date of publication. Neither the publisher nor the authors or the editors give a warranty, expressed or implied, with respect to the material contained herein or for any errors or omissions that may have been made. The publisher remains neutral with regard to jurisdictional claims in published maps and institutional affiliations.

This Apress imprint is published by the registered company APress Media, LLC, part of Springer Nature.

The registered company address is: 1 New York Plaza, New York, NY 10004, U.S.A.

To my family, my real source of inspiration.

Introduction

On Wall Street, the use of algorithmic trading and other computational techniques has skyrocketed in the last few years, as can be seen from the public interest in automated trading as well as the profits generated by these strategies. This growing trend demonstrates the importance of using software to analyze and trade markets in diverse areas of finance. One particular area that has been growing in importance during the last decade is options and derivatives trading.

Initially considered only as a niche investment strategy, derivatives have become one of the most common instruments for investors in all areas. Likewise, the interest in automated trading and analysis of such instruments has also increased considerably.

Along with scientists and economists, software engineers have greatly contributed to the development of advanced computational techniques using financial derivatives. Such techniques have been used at banks, hedge funds, pension funds, and other financial institutions. In fact, every day new systems are developed to give a trading advantage to the players in this industry.

This book attempts to provide the basic programming knowledge needed by C++ programmers working with options and derivatives in the financial industry. This is a hands-on book for programmers who want to learn how C++ is used to develop solutions for options and derivatives trading. In the book’s chapters, you’ll explore the main algorithms and programming techniques used in the implementation of systems and solutions for trading options and other derivatives.

Because of stringent performance characteristics, most of these trading systems are developed using C++ as the main implementation language. This makes the topic of this book relevant to everyone interested in acquiring the programming skills necessary in the financial industry.

In Options and Derivatives Programming in C++23, I cover the features of the language that are more frequently used to write financial software for options and derivatives. These features include the STL, templates, functional programming, and support for numerical libraries. New features introduced in the latest updates of the C++ standard are also covered, including additional functional techniques such as lambda functions, automatic type detection, custom literals, and improved initialization strategies for C++ objects.

I also provide how-to examples that cover the major tools and concepts used to build working solutions for quantitative finance. The book teaches you how to employ advanced C++ concepts as well as the basic building libraries used by modern C++ developers, such as the STL, Boost, and QuantLib. I also discuss how to create correct and efficient applications, leveraging knowledge of object-oriented and template-based programming. I assume only a basic knowledge of C and C++. Throughout this book, a number of more advanced concepts, already mastered by experienced developers, will be introduced as needed.

In the process of writing this book, I was also concerned with providing value for readers who are trying to use their current programming knowledge in order to become proficient at the style of programming used in large banks, hedge funds, and other investment institutions. Therefore, the topics covered in the book are introduced in a logical and structured way. Even novice programmers will be able to absorb the most important topics and competencies necessary to develop in C++ for the problems occurring on the analysis of options and other financial derivatives.

In this book, we also discuss features introduced in the latest international standard, C++23. In this version of the standard, a number of simplifications and extensions of the core C++ language have been approved. You will learn about many of the features in the new standard, with examples to illustrate each major concept.

Audience

This book is intended for readers who already have a working knowledge of programing in C, C++, or another mainstream language. These are usually professionals or advanced students in computer science, engineering, and mathematics who have interest in learning about options and derivatives programming using the C++ language for personal or for professional reasons. The book is also directed at practitioners of C++ programming in financial institutions, who would use the book as a ready-to-use reference of software development algorithms and best practices for this important area of finance.

Many readers are interested in a book that would describe how modern C++ techniques are used to solve practical problems arising when considering options on financial instruments and other derivatives. Being a multiparadigm language, C++ usage may be slightly different in each area, so the skills that are useful for developing desktop applications, for example, are not necessarily the same ones used to write high-performance software.

A large part of high-performance financial applications are written in C++, which means that programmers who want to enter this lucrative market need to acquire a working knowledge of specific parts of the language. This book attempts to give developers who want to develop their knowledge effectively this choice, while learning one of the most sought-after and marketable skillsets for modern financial applications and high-performance software development.

This book is also targeted at students and new developers who have some experience with the C++ language and want to leverage that knowledge into financial software development. This book is written with the goal of reaching readers who need a concise, algorithms-based strategy, providing basic information through well-targeted examples and ready-to-use solutions. Readers will be able to directly apply the concepts and sample code to some of the most common problems faced regarding the analysis of options and derivative contracts.

What You Will Learn

Here is a sample of topics that are covered in the following chapters:

Fundamental problems in the options and derivatives market

Options market models

Derivative valuation problems

Trading strategies for options and derivatives

Pricing algorithms for derivatives

Binomial method

Differential equations method

Black-Scholes model

Quantitative finance algorithms for options and derivatives

Linear algebra techniques

Interpolation

Calculating roots

Numerical solution for partial differential equations (PDEs)

Important features of C++ language as used in quantitative financial programming, such as

Templates

STL containers

STL algorithms

Boost libraries

QuantLib

New features of C++23

Book Contents

Here is a quick overview of the major topics covered in each chapter:

Chapter 1—“Options Concepts”: An option is a standard financial contract that derives its value from an underlying asset such as a stock. Options can be used to pursue multiple economic objectives, such as hedging against variations on the underlying asset, or speculating on the future price of a stock. Chapter 1 presents the basic concepts of options, including their advantages and challenges. It also explains how options can be modeled using C++. The main topics covered in this chapter are as follows:

Basic definitions of options

An introduction to options strategies

Describing options with Greeks

Sample code for options handling

Chapter 2—“Financial Derivatives”: A derivative is a general term for a contract whose price is based on an underlying asset. In the previous decades, the financial industry created and popularized a large number of derivatives. Pricing and trading these derivatives are a large part of the work performed by trading desks throughout the world. Derivatives have been created based on diverse assets such as foreign currency, mortgage contracts, and credit default swaps. This chapter explores this type of financial instrument and presents a few C++ techniques to model specific derivatives. The main topics covered in this chapter are as follows:

Credit default swaps

Forex derivatives

Interest rate derivatives

Exotic derivatives

Chapter 3—“Basic C++ Algorithms”: To become a proficient C++ developer, it is essential to have good knowledge of the basic algorithms used in your application area. Some basic algorithms for tasks such as vector processing, date and time handling, and data access and storage are useful in almost all applications involving options and other financial derivatives. This chapter surveys such algorithms and their implementation in C++, including the following topics:

Date and time handling

Vector processing

Graphs and networks

Fast data processing

Chapter 4—“Object-Oriented Techniques”: For the last 30 years, object-oriented techniques have become the standard for software development. Since C++ fully supports OO programming, it is imperative that you have a good understanding of OO techniques in order to solve the problems presented by options and derivatives. I present a summary of what you need to become proficient in the relevant OO techniques used in the financial industry. Some of the topics covered in this chapter are as follows:

Problem partitioning

Designing solutions using OO strategies

OO implementation in C++

Reusing OO components

Chapter 5—“Design Patterns for Options Processing”: Design patterns are a set of common programming practices that can be used to simplify the solution of recurring problems. With the use of OO techniques, design patterns can be cleanly implemented as a set of classes that interact toward the solution of a common goal. In this chapter, you will learn about the most common design patterns employed when working with financial options and derivatives, with specific examples. It covers the following topics:

The importance of design patterns

Factory pattern

Visitor pattern

Singleton pattern

Less common patterns

Chapter 6—“Template-Based Techniques”: C++ templates allow programmers to write code that works without modification on different data types. Through the careful use of templates, C++ programmers can write code with high performance and low overhead, without the need to employ more computationally expensive object-oriented techniques. This chapter explores a few template-oriented practices used in the solution of options- and derivatives-based financial problems:

Motivating the use of templates

Compile-time algorithms

Containers and smart pointers

Template libraries

Chapter 7—“STL for Derivatives Programming”: Modern financial programming in C++ makes heavy use of template-based algorithms. Many of the basic template algorithms are implemented in the standard template library (STL). This chapter discusses the STL and how it can be used in quantitative finance projects, in particular to solve options and financial derivative problems. You will get a clear understanding of how the STL interacts with other parts of the system and how it imposes a certain structure on classes developed in C++. The following topics are covered:

STL-based algorithms

Functional techniques on STL

STL containers

Smart pointers

Chapter 8—“Functional Programming Techniques”: Functional programming is a technique that focuses on the direct use of functions as first-class objects. This means that you are allowed to create, store, and call functions as if they were just another variable of the system. Recently, functional techniques in C++ have been greatly improved with the adoption of the new standard (C++23), particularly with the introduction of lambda functions. The following topics are explored in this chapter:

Lambdas

Functional templates

Functions as first-class objects

Managing state in functional programming

Functional techniques for options processing

Chapter 9—“Linear Algebra Algorithms”: Linear algebra techniques are used throughout the area of financial engineering and, in particular, in the analysis of options and other financial derivatives. Therefore, it is important to understand how the traditional methods of linear algebra can be applied in C++. With this goal in mind, I present a few examples that illustrate how to use some of the most common linear algebra algorithms. In this chapter, you will also learn how to integrate existing LA libraries into your code. Here are the topics covered:

Implementing matrices

Matrix decomposition

Computing determinants

Solving linear systems of equations

Chapter 10—“Algorithms for Numerical Analysis”: Equations are some of the building blocks of financial algorithms for options and financial derivatives, and it is important to be able to efficiently calculate the solution for such mathematical models. In this chapter, you will see programming recipes for different methods of calculating equation roots and integrating functions, along with explanations of how they work and when they should be used. I also discuss numerical error and stability issues that present a challenge for developers in the area of quantitative financial programming. The following topics are covered:

Basic numerical algorithms

Root-finding algorithms

Integration algorithms

Reducing errors in numerical algorithms

Chapter 11—“Models Based on Differential Equations”: Differential equations are at the heart of many techniques used in the analysis of derivatives. There are several processes for solving and analyzing PDEs that can be implemented in C++. This chapter presents programming recipes that cover aspects of PDE-based option modeling and application in C++. Topics covered include the following:

Basic techniques for differential equations

Ordinary differential equations

Partial differential equations

Numerical algorithms for differential equations

Chapter 12—“Basic Models for Options Pricing”: Options pricing is the task of determining the fair value of a particular option, given a set of parameters that exactly determine the option type. This chapter discusses some of the most popular models for options pricing. They include tree-based methods, such as binomial and trinomial trees. This chapter also discusses the famous Black-Scholes model, which is frequently used as the basis for the analysis of most options and derivative contracts. We cover the following topics:

Binomial trees

Trinomial trees

Black-Scholes model

Implementation strategies

Chapter 13—“Monte Carlo Methods”: Among other programming techniques for equity markets, Monte Carlo simulation has a special place due to its wide applicability and easy implementation. These methods can be used to forecast prices or to validate options buying strategies, for example. This chapter provides programming recipes that can be used as part of simulation-based algorithms applied to options pricing. The following topics are covered:

Probability distributions

Random number generation

Stochastic models for options

Random walks

Improving performance

Chapter 14—“Backtesting Trading Strategies in C++”: This is a new chapter included in the third edition. Once a trading strategy is defined, backtesting is one of the steps necessary for proper validation of the algorithm. To create valid backtests, one needs to employ a well-organized and thorough strategy. We discuss in this chapter a few programming techniques that can simplify and improve the performance of backtesting procedures. We cover the following topics:

Creating a testing baseline

Clarifying the backtest goals

Implementing a backtesting infrastructure

Plotting data

Interpreting results

Chapter 15—“Using C++ Libraries for Finance”: Writing good financial code is not an individual task. You frequently have to use libraries created by other developers and integrate them into your own work. In the world of quantitative finance, a number of C++ libraries have been used with great success. This chapter reviews some of these libraries and explains how they can be integrated into your own derivatives-based applications. Some of the topics covered include the following:

Standard library tools

QuantLib

Boost math

Boost lambda

Chapter 16—“Credit Derivatives”: Credit derivatives are an increasingly popular type of financial derivative that aims at reducing credit risk, that is, the risk of default posed by contracts established with a counterparty. Credit derivatives can be modeled using some of the tools already discussed for options, although credit derivatives have their own peculiarities. We describe how to create the C++23 code needed to quantitatively analyze such financial contracts. Here are some of the topics discussed:

General concepts of credit derivatives

Modeling the problem

C++ algorithms for derivative pricing

Improving algorithm efficiency

Chapter 17—“Processing Financial Data”: This is a new chapter that has been included in the third edition. A good algorithm for quantitative finance needs to access and save data in several formats. In this chapter, we discuss the main formats used in financial applications, ranging from standard formats such as XML to more specific formats such as HDF5. We present the basic storage techniques along with C++ code to manipulate financial data stored in different formats. The following topics are covered:

Financial data APIs

Loading and storing data

XML-based data storage

HDF5 databases

Efficient strategies for data processing

Example Code

The examples given in this book have all been tested on macOS X using gcc. The code uses only standard C++ techniques, so you should be able to build the given examples using any standards-compliant C++ compiler that implements the C++23 standard. For example, gcc is available on most platforms, and Microsoft Visual Studio will also work on Windows. The clang compiler is another option that is available in multiple platforms, including Windows, macOS, and Linux.

If you use macOS X and don’t have Xcode installed in your computer yet, you can download it for free from the Apple store or from the Apple developer website at http://developer.apple.com.

If you instead prefer to use gcc on Windows, you can download the Cygwin distribution from the website www.cygwin.com.

Once Cygwin is installed, start the command prompt from the Cygwin program group in the Start menu. Then, you can type gcc to check that the compiler is properly installed.

To download the source code for all examples in this book, visit my web page at http://coliveira.net.

Any source code or other supplementary material referenced by the author in this book is available to readers on GitHub. For more detailed information, please visit https://www.apress.com/gp/services/source-code.

Table of Contents

Chapter 1:​ Options Concepts1

Basic Definitions2

Option Greeks6

Using C++23 to Analyze Options8

Availability8

Performance9

Standardization9

Expressiveness10

Modeling Options in C++11

Creating Well-Behaved Classes11

Computing the Option Value at Expiration12

Complete Listing14

Using the auto Keyword17

Initializing Variables in C++17

Printing Values in C++18

Building and Testing18

Further References19

Conclusion20

Chapter 2:​ Financial Derivatives21

Models for Derivative Pricing21

Credit Default Swaps23

Collateralized Debt Obligations23

FX Derivatives24

Equations for Derivative Modeling25

Numerical Models26

Binomial Trees26

Simulation Models27

Coding in C++ with the STL27

Generating a Random Walk in C++29

Complete Listing31

Building and Testing34

Further References35

Conclusion35

Chapter 3:​ Basic C++ Algorithms37

Date and Time Handling37

Date Operations38

Computing the Day of the Week39

Complete Listing42

A Compact Date Representation51

Complete Listings52

Building and Testing55

Using the Standard Chrono Header56

Working with Networks57

Creating a Dictionary Class57

Calculating a Shortest Path60

Complete Listings62

Building and Testing68

Conclusion69

Chapter 4:​ Object-Oriented Techniques71

OO Programming Concepts71

Encapsulation73

Inheritance75

Polymorphism76

Polymorphism and Virtual Tables79

Virtual Functions and Virtual Destructors80

Abstract Functions82

Building Class Hierarchies83

Object Composition85

When to Use Object-Oriented Features?​86

Objects and C++2386

Conclusion88

Chapter 5:​ Design Patterns for Options Processing89

Introduction to Design Patterns90

The Factory Method Design Pattern91

The Singleton Pattern94

Using the Singleton Design Pattern95

Clearing House Implementation in C++95

The Observer Design Pattern97

Complete Code100

The Visitor Pattern103

Conclusion105

Chapter 6:​ Template-Based Techniques107

Introduction to Templates107

Compilation-Time Polymorphism109

Template Functions110

Implementing Recursive Functions112

Recursive Functions and Template Classes114

Containers and Smart Pointers115

Avoiding Lengthy Template Instantiations117

Preinstantiating​ Templates117

Templates in C++23119

The constexpr Syntax in C++23120

Conclusion121

Chapter 7:​ STL for Derivatives Programming123

Introduction to Algorithms in the STL124

Sorting124

Presenting Frequency Data127

Copying Container Data129

Finding Elements131

Selecting Option Data133

STL Improvements in C++23134

Array Slices in C++23135

Conclusion136

Chapter 8:​ Functional Programming Techniques137

Functional Programming Concepts138

Function Objects138

Functional Predicates in the STL141

The Bind Function143

Lambda Functions in C++23145

Complete Code147

Changes to Lambda Functions in C++23150

Attributes on Lambdas151

Conclusion151

Chapter 9:​ Linear Algebra Algorithms153

Vector Operations153

Scalar-to-Vector Operations154

Vector-to-Vector Operations156

Matrix Implementation159

Using the uBLAS Library165

Complete Code166

Syntax Change in C++23:​ Multidimensional​ Subscripts170

Conclusion171

Chapter 10:​ Algorithms for Numerical Analysis173

Representing Mathematical Functions173

Using Horner’s Method175

Finding Roots of Equations177

Newton’s Method177

Integration183

Complete Code186

Conclusion192

Chapter 11:​ Models Based on Differential Equations193

General Differential Equations193

Ordinary Differential Equations194

Euler’s Method195

Implementing the Method196

The Runge-Kutta Method200

Runge-Kutta Implementation201

Complete Code204

Conclusion206

Chapter 12:​ Basic Models for Options Pricing207

Lattice Models207

Binomial Model208

Binomial Model Implementation210

Pricing American-Style Options214

Solving the Black-Scholes Model215

Numerical Solution of the Model216

Complete Code220

Conclusion223

Chapter 13:​ Monte Carlo Methods225

Introduction to Monte Carlo Methods225

Random Number Generation226

Probability Distributions228

Using Common Probability Distributions231

Using Random Walks235

Creating Random Walks236

Conclusion239

Chapter 14:​ Backtesting Trading Strategies in C++241

Obtaining Historical Market Data241

Data Cleaning and Preprocessing242

Adjustments for Corporate Actions242

How to Design Your Trading Strategy242

Backtest and Validation Phase243

Developing the Backtesting Engine243

Read Historical Market Data244

Create the Backtesting Engine244

Conclusion247

Chapter 15:​ Using C++ Libraries for Finance249

Boost Libraries249

Installing Boost251

Solving ODEs with Boost252

Solving a Simple ODE253

Creating Histograms with Boost255

The QuantLib Library256

Handling Dates257

Working with Calendars258

Computing Solutions for Black-Scholes Equations260

Creating a C++ Interface262

Complete Code264

Conclusion266

Chapter 16:​ Credit Derivatives267

Introduction to Credit Derivatives267

Modeling Credit Derivatives268

Using Barrier Options269

A Solver Class for Barrier Options270

Barrier Option Classes in QuantLib271

An Example Using QuantLib273

Complete Code275

Conclusion279

Chapter 17:​ Processing Financial Data281

Introduction to XML in Finance281

Using XML in Finance282

Understanding the XML Structure283

XML Tags and Elements283

Attributes283

Hierarchy and Nesting283

XML Schema (XSD) and Document Type Definition (DTD)284

XML Parsing in C++284

Installing the pugixml Library284

XML Parsing with pugixml284

Performing Data Processing and Analysis286

Error Handling287

Loading XML File and Checking for Errors288

Checking for Node Existence288

Handling Missing Attributes288

Other Techniques for Error Checking289

Using the HDF5 Format289

Using HDF5290

Compile and Run291

HDF5 for Trading Data291

Conclusion293

Index295

About the Author

Carlos Oliveira

A photograph of Carlos Oliveira.

is Principal Inventive Scientist a AT&T. He works in the area of data science and optimization, with more than 15 years of experience in creating scientific, quantitative, and financial models with C++. During his career, Carlos has also developed several large-scale applications for companies such as Amazon and Bloomberg LP. Carlos obtained a PhD in operations research and systems engineering from the University of Florida, an MSc in computer science from UFC (Brazil), and a BSc in computer science from UECE (Brazil). He has also performed academic research in the field of combinatorial optimization, with applications in diverse areas such as finance, telecommunications, computational biology, and logistics. Carlos has written more than 30 academic papers on optimization and authored several books, including Practical C++20 Financial Programming (Apress, 2020).

About the Technical Reviewer

Don Reamey

A photograph of Don Reamey.

is currently a Principal Software Engineering Manager at Microsoft Corporation. Previously, he worked at Apptio Software also as a Principal Software Engineering Manager. Don also managed engineering teams at TIBCO Software and Bank of America. He enjoys learning and creating new programming languages, creating web-based applications, playing guitar, and weightlifting.

© The Author(s), under exclusive license to APress Media, LLC, part of Springer Nature 2023

C. OliveiraOptions and Derivatives Programming in C++23https://doi.org/10.1007/978-1-4842-9827-5_1

1. Options Concepts

Carlos Oliveira1  

(1)

Seattle, WA, USA

In the last few decades, software development has become an integral part of the financial and investment industry. Advances in trading infrastructure, as well as the need for increased volume and liquidity, have prompted financial institutions to adopt computational techniques as part of their day-to-day operations. This means that there are many opportunities for computer scientists specializing in the design and development of automated strategies for trading and analyzing stocks, options, and other financial derivatives.

Options are among the several investment vehicles that are currently traded using automated methods, as you will learn in the following chapters. Given the mathematical structure and properties of options and related derivatives, it is possible to explore their features in a controlled way, which is ideal for the application of computational algorithms. In this book, I present many of the computational techniques currently used to develop strategies in order to trade options and other derivatives.

An option is a standard financial contract that derives its value from an underlying asset such as common stock, foreign currency, a basket of stocks, or a commodity. Options can be used to pursue multiple economic objectives, such as hedging against large variations on the underlying asset, or speculating on the future price of a stock. This chapter presents the basic concepts of options, along with supporting definitions. These concepts will be used in the next few chapters to describe algorithms and strategies with their implementation in C++20. In this chapter, I also give an overview of the use of C++ in the financial industry and how options can be modeled using this language.

The following concepts are explored in the next sections:

Basic definitions: You will learn fundamental definitions about option contracts and how they are used in the investment industry.

Fundamental option strategies: Due to their flexibility, options can be combined in a surprisingly large number of investment strategies. You will learn about some of the most common option strategies and how to model them using C++.

Option Greeks: One of the advantages of options investing is that it promotes a very analytical view of financial decisions. Each option is defined by a set of mathematical quantities called Greeks, which reflect the properties of an option contract at each moment in time.

Delta hedging: One of the ways to use options is to create a hedge for some other underlying asset positions. This is called delta hedging, and it is widely used in the financial industry. You will see how this investment technique works and how it can be modeled using C++.

Basic Definitions

Let’s start with an overview of concepts and programming problems presented by options in the financial industry. Options are specialized trading instruments and therefore require their users to be familiar with a number of details about their operation. In this section, I introduce some basic definitions about options and their associated concepts.

In the financial markets, a basic activity is to buy or sell a financial instrument. These instruments can be classified into several types, such as company stock, government bonds, currencies, commodities, or futures, among others. Options and derivatives are a class of financial instruments that derive their value from other assets. They can only exist in relation to a financial asset that is already traded, hence the name derivative.

Before starting, take a quick look at Table 1-1 for a summary of the most commonly used concepts. These concepts are defined in more detail in the remaining parts of this chapter.

Table 1-1

Basic Concepts in Options Trading

Concept

Definition

Call option

An option contract that gives its owner the right (but not the obligation) to buy the underlying asset for a predetermined price during certain time period.

Put option

An option contract that gives its owner the right (but not the obligation) to sell the underlying asset for a predetermined price during certain time period.

Underlying

Asset whose price is used as the base of the option contract.

Strike price

The price at which option owners can buy or sell the underlying asset under the duration of the option contract.

Expiration

The last date of validity for the option contract.

Settlement

The act of liquidating the option contract at the expiration date.

Intrinsic value

The part of the option value that is directly derived from the underlying price.

Time value

The part of the option value that is derived only from the time remaining in the option contract.

Break-even price

The price of the underlying at which an investor will start to make a profit in the option.

Exercise

The act of buying or selling the underlying asset under the price determined by the option contract.

American option

An option style where their owners can exercise the option contract at any moment between its purchase and expiration.

European option

An option style where option owners can exercise the option contract only at expiration time.

ATM

(At the money): Term that refers to options that have a strike price close to the current price for the underlying.

OTM

(Out of the money): Term that refers to options that have a strike price above (for calls) or below (for puts) the current price of the underlying asset. These options have no intrinsic value.

ITM

(In the money): Term that refers to options that have a strike price below (for calls) or above (for puts) the current price of the underlying asset. These options have some intrinsic value.

Options can be classified according to several criteria, some of which are listed in Table 1-1. The features of these options determine every aspect of how they can be used, such as the quantity of underlying assets, the strike price, and the expiration date, among others. There are two main types of option contracts: calls and puts. A call is a standard contract that gives its owner the right (but not the obligation) to buy an underlying instrument at a particular price. Similarly, a put is a standard contract that gives its owner the right (but not the obligation) to sell an underlying instrument at a predetermined price.

The strike price is the price at which the option can be exercised (i.e., the underlying can be bought or sold). For example, a call for IBM stock with strike $100 gives its owner the right to buy IBM stock at the price of $100. If the current price of IBM is greater than $100, the owner of such an option has the right to buy the stock at a price that is lower than the current price, which means that the call has a higher value as the value of IBM stock increases. This situation is exemplified in Figure 1-1. On the other hand, if the current price is lower than $100 at expiration, then the value of the option is zero, since there is no profit in exercising the contract. Clearly, the profit/loss situation will depend on the price originally paid for the option and the final price at expiration.

A line graph represents profit versus price. A line follows a flat trend from (90, minus 1) to (100, minus 1), then an ascending trend till (110, 9). Data are estimated.

Figure 1-1

Profit chart for a call option

As you have seen in this example, if you buy a call option, your possible gain is unlimited, while your losses are limited to the value originally paid. This is advantageous when you’re trying to limit losses in a particular investment scenario. As long as you are okay with losing a limited amount of money paid for the option, you can profit from the unlimited upside potential of a call (if the underlying grows in price). Put options don’t have unlimited profit potential because the maximum gain happens when the underlying price is zero. However, they still benefit from the well-defined, limited loss vs. the possible large gains that can be incurred.

Expiration: The expiration is the date when the option contract ends its validity and a final value exchange can be performed. Each option will have a particular, predefined expiration. For example, certain index-based options expire in the morning of the third Friday of the month. Most stock-based options expire in practice at the end of the third Friday of the month (although they will frequently list the Saturday as the formal expiration day). More recently, several weekly-based option contracts have been made available for some of the most traded stocks and indices. And finally, a few highly liquid trading instruments (such as S&P index funds) have expirations every day. Each option contract makes it clear when expiration is due, and investors need to plan accordingly on what to do before expiration date.

Settlement: The settlement is the agreed-on result of the option transaction at expiration, that is, at the specific date when the option contract expires. The particular details of how the settlement happens depend on the type of underlying asset. For example, options on common stock settle at expiration day, when the owner of the option needs to decide to sell (for puts) or buy (for calls) a certain quantity of stock. For index-based options, the settlement is normally performed directly in cash, determined as the cash equivalent for a certain number of units of the index. Some options on futures may require the settlement on the underlying commodity, such as grain, oil, or sugar. Investors need to be aware of the required settlement for different option contracts. Trading brokerages will typically let investors know about the steps required to settle the options they’re currently holding.

Selling options: An investor can buy or sell a call option. When doing so, it is important to understand the difference between these two scenarios. For option buyers, the goal is to profit from the possible increase (in the case of calls) or decrease (in the case of puts) in value for the underlying asset. For option sellers, on the other hand, the goal is to profit from the lack of movement (increase for calls or decrease for puts). So, for example, if you sell calls against a stock, the intent is to profit in the case that the stock decreases in price or stays at a price lower than the strike price. If you sell a put option, the goal is to profit when the stock increases in price or stays higher than the strike price until expiration.

Option exercise: An option contract can be used to buy or sell the underlying asset as dictated by the contract specification. This process of using the option to trade the underlying asset is called option exercising. If the option is a call, you can exercise it and buy the underlying asset at the specified price. If the option is a put, you can use the option to sell the underlying asset at the previously specified price. The price at which the option is exercised is defined by the contract. For example, a call option for AAPL stock with a $100 strike allows its owner to buy the stock for the strike price, independent of the current price of AAPL.

Exercise style: Option contracts can have different exercise styles based on when exercising is allowed. There are two main types:

American options: Can be exercised at any time until expiration. That is, the owner of the option can decide to exercise it at any moment, as long as the option has not expired.

European options: Can be exercised only upon expiration date. This style is more common for contracts that are settled directly on cash, such as index-based options.

An option is defined as a derivative of an underlying instrument. The underlying instrument is the asset whose price is used as the basic value for the option contract. There is no predefined restriction on the type of asset used as the underlying for an option contract, but in practice, options tend to be defined based on openly traded securities. Examples of securities that can be used as the underlying asset for commonly traded option contracts include the following:

Common stock: Probably the most common way to use options is to trade call or put options on common stock. In this way, you can profit largely from price changes in stocks of well-known public companies such as Apple, Alphabet, IBM, Walmart, and Ford.

Indices: An index, such as the Dow Industrials or the NASDAQ 100, can be used as the underlying for an option contract. Options based on indices are traditionally settled on cash (as previously explained), and each contract unit corresponds to multiples of the current index value.

Currencies: A currency, usually traded using Forex platforms, can also be used as the underlying for option contracts. Common currency pairs involving the US Dollar, Euro, Japanese Yen, and Swiss Franc are traded 24 hours a day. The related options are traded on a lot of currencies, which are defined according to the relative prices of the target currencies. Expiration varies similarly to stock options.

Commodities: Options can also be written on traded commodities. A commodity is a common product that can be traded in large quantities, including agricultural products such as corn, coffee, and sugar; fuels such as gasoline and crude oil; and even index-based underlying assets such as the S&P 500. Options can be used to transact such commodities, and trading exchanges now offer standard options for many of the commodity types.

Futures: These are contracts for the future delivery of a particular asset. Many of the commodity types discussed previously are traded using future contracts, including gasoline, crude oil, sugar, coffee, precious metals such as gold and silver, and other products. The structure of future contracts is created to simplify the trade of products that will only be available within a due period, such as next fall, for example.

ETFs (exchange-traded funds) and ETN (exchange-traded notes): More recently, an increasing number of funds have started to trade using the same rules applicable to common stocks in standard exchanges. Such funds are responsible for maintaining a basket of assets, and their shares are traded daily on exchanges. Examples of well-known ETFs include funds that hold components of the S&P 500, sectors of the economy, and even commodities and currency.

Cryptocurrencies or digital currencies: More recently, cryptocurrencies have become an important type of asset that is publicly traded around the world. Several types of crypto tokens have been established, with Bitcoin being the best known. Options can also be used to trade digital currencies in the same way as other assets.

Options trading has traditionally been done on market exchanges, just like other forms of stock and future trading. One of the most prominent options exchange is the Chicago Board Options Exchange (CBOE). Many other exchanges provide support and liquidity for the trading of options for many of the instruments listed previously.

The techniques described in this book are useful for options with any of these underlying instruments. Therefore, you don’t need to worry if the algorithms are applied to stock options or futures options, as long as you consider the peculiarities of these different contracts, such as their expiration and exercise.

Options can also be classified according to the relation between the strike price and the price of the underlying asset. There are three cases that are typically considered:

An option is said to be out of the money (OTM) when the strike price is above the current price of the underlying asset for call options, or when the strike price is below the current price of the underlying asset for put options.

An option is said to be at the money (ATM) when the strike price is close to the current price of the underlying asset.

An option is said to be in the money (ITM) when the strike price is below the current price of the underlying asset, for call options, or when the strike price is above the current price of the underlying asset, for put options.

Notice that OTM options are cheaper than a similar ATM option, since the OTM options (being away from the current price of the underlying) have a lower chance of profit than ATM options. Similarly, ATM options are cheaper than ITM options, because ATM options have less probability of making money than ITM options. Also notice that, when considering the relation between strike price and underlying price, the option price generally reflects the probability that the option will generate any profit.

A related concept is the intrinsic value of an option. The intrinsic value is the part of the value of an option that can be derived from the difference between strike price and the price of the underlying asset. For example, consider an ITM call option for a particular stock with a strike of $100. Assume that the current price for that stock is $102. Therefore, the price of the option must include the $2 difference between the strike and the price of the underlying, since the holder of a call option can exercise it and have an immediate value of $2. Similarly, ITM put options have intrinsic value when the current price of the underlying is below the strike price, using the same reasoning.

The break-even price is the price of the underlying on expiration at which the owner of an option will start to make a profit. The break-even price has to include not only the potential profit derived from an increase in intrinsic value but also the cost paid for the option. Therefore, for an investor to make a profit on a call option at expiration, the price of the underlying asset has to rise above the strike plus any cost paid for the option (and similarly it has to drop below the strike minus the option cost for put options). For example, if a $100 MSFT call option has a cost of $1, then the investor will have a net profit at expiration only when the price of MSFT rises above $101 (and this without considering transaction costs).

As part of the larger picture of investing, options have assumed an important role due to their flexibility and their profit potential. As a result, new programming problems introduced by the use of options and related derivatives have come to the forefront of the investment industry, including banks, hedge funds, and other financial institutions. As you will see in the next section, C++ is the ideal language to create efficient and elegant solutions to the programming problems occurring with options- and derivatives-based investing.

Option Greeks

One of the characteristics of financial derivatives is the use of derived quantitative measures that can be essential in the analysis and pricing of the product. In the case of options, a few important quantitative metrics are called Greeks, because most of these measures are referred to by Greek letters. These Greek quantities correspond to the variation of option price with respect to one or more variables, such as time, volatility, or underlying price.

The most well-known option Greek is delta. The delta of an option is defined as the amount of change in the price of an option when the underlying changes by one unit. Therefore, delta represents a rate of change of the option in relation to the underlying, and it is essential to understand price variation in options. Consider, for example, an option for IBM stock that expires in 30 days. The strike price is $100, and the stock is currently trading at $100. Suppose that the price of the stock increases by $1. It is an interesting problem to calculate the expected change in the option price.

It turns out that when the underlying price is close to the strike price, the delta of a call option is close to 0.5. One can also think of this in terms of probabilities of the option getting in the money, in which case this means that the value of the option is equally probable to go up or down by the same quantity. Therefore, it makes sense that the change per unit of price will be just half of the change in the underlying asset.

The value of delta increases as the option becomes more and more in the money. In that case, the delta gets close to one, since each dollar of change will have a larger impact in the intrinsic value of the option. Conversely, the value of delta decreases as the option becomes more and more out of the money. In that case, delta gets closer to zero, since each dollar of change will have less impact on the value of an option that is out of the money.

The second important option Greek is called gamma, and it is also related to delta. The gamma of an option is described as the rate of change of delta with a unit change in price of the underlying. As you have seen, delta changes in different ways when the option is in the money, out of the money, or at the money. But the rate of change of delta will also vary depending on other factors. For example, delta will change more quickly if the option is close to expiration, because there is so little time for a movement to happen. To see why this happens, consider the delta for an option that is 30 days before expiration and for a second option that is just one day before expiration. Delta is also dependent on time, because an option closer to expiration has less probability of change. As a result, the delta will move from zero to one more slowly if there are 30 days to go, because there is still plenty of time left for changes to occur. But an option with only one day left to expiration will have a delta quickly moving from close to zero to nearly one, since there is no time left for future changes. This is described by saying that the first option has lower gamma than the second option. Other factors such as volatility can also change an option gamma. Figure 1-2 illustrates the value of gamma for a particular option at different times before expiration.

A multiple-line graph represents gamma values at different dates. The parameters are 5, 10, 8, and 3 days. All the lines follow a bell-shaped trend and peak at the price of 50. The line 3 days has the highest altitude. Data are estimated

Figure 1-2

Value of gamma at different dates before expiration

Another option Greek that is closely related to time is theta. The theta of an option varies as a function of the time left to expiration, and its value decays when it gets closer to the expiration date. You can think of theta as a measure of time potential for the option. For option buyers, higher theta is a desirable feature, since buyers want more probability of changes for the options they own. On the other hand, option sellers benefit from decreased theta, so short-term options are ideal for sellers due to the lower theta.

Finally, we have an option Greek that is not really named after a Greek letter: vega. The vega of an option measures the amount of volatility of the underlying asset that is priced into an option. The higher the volatility, the more expensive an option has to be in order to account for the increased possibility of price changes. The differential equations that define the price of an option (as you will see in future chapters) take into account this volatility. Vega can be used to determine how much relative volatility is embedded in the option price. An important use of this measure is to help option buyers and sellers determine if this implied volatility is consistent with their expectations for future changes in the price of the underlying. High vega means that a large movement is implied by the price of the option. Low vega means that only small movements are priced into the option.

There are other option Greeks that have been used in the academic community and in some financial applications; however, they are not as widely known as the ones mentioned here and have limited used. You can see a summary of the most commonly used option Greeks in Table 1-2.

Table 1-2

Option Greeks and Their Common Meanings

Greek

Meaning

Delta

Option price’s rate of change with respect to the price of the underlying asset.

Gamma

Option delta’s rate of change with respect to the price of the underlying asset.

Rho

Option price’s rate of change with respect to changes in interest rates.

Theta

Option price’s rate of change with respect to time left to expiration.

Vega

Option price’s rate of change with respect to the volatility of the underlying asset.

Lambda

Option price’s rate of change with respect to percentual changes in the price of the underlying asset.

Using C++23 to Analyze Options

C++ has unique features that make it especially useful for programming software for the financial industry. With the new standard version of the language, C++23, these advantages became even more pronounced. Over the years, developers have migrated to C++ as a practical way to meet the requirements of intensive numeric, real-time algorithms used by the investment community. When it comes to creating decision support software for fast-paced investment strategies, it is very difficult to beat the C++ programming language in the areas of performance and stability. Several financial algorithms and libraries have been made available in C++, which makes this language a great choice for the implementation of algorithms used in analysis and trading.

While it is true that several newer programming languages may be available for the implementation of financial software, very few of them provide the combination of advantages available when using C++. Let’s now look at some of the areas where C++ provides a unique advantage when compared to other programming languages that could be used to implement financial and investment software.

Availability

When looking for a programming language to implement investment software, one of the first concerns you need to address is the ability to run the code in a variety of computational environments. Targets for such investment software can range from small and mobile processors to large-scale parallel systems and supercomputers. Moreover, it is not uncommon to have to interact with different operating systems, including the common software platforms based on Linux, Windows, and macOS.

Because modern computer systems are so heterogeneous, it makes economic sense to use languages that can be deployed in a large variety of hardware and software configurations with little or no source code modifications. Programmers for financial firms also work on diverse platforms, which makes it even more attractive to use software that can run in different computers and operating systems with little or no changes.

A strong characteristic of C++ is its wide availability over different platforms. Due to its early success as a multiparadigm language, C++ has been ported to nearly any imaginable operating system and hardware combination. While other mainstream languages such as Java require the implementation of a complex runtime environment for proper operation, C++ was designed from the beginning with simplicity and portability in mind. The language does not require a runtime system, and only a minimal support system, provided by the C++ standard library, needs to work in order to support a new target. Therefore, it is relatively easy to port C++ compilers and build systems to new platforms with minimal changes.

Another advantage is the availability of multiple compilers provided by commercial vendors as well as free software. Given the importance of C++ applications, it is possible to find compilers with both free and commercial licenses so that you can use the option that best suits your objectives. Open source developers can still use state-of-the-art free compilers such as gcc and clang. Commercial groups, on the other hand, can take advantage of mature compilers licensed by companies such as Intel and IBM.

Performance

It is a fact that programmers using C++ benefit from the high performance provided by the language. Because C++ was explicitly designed to require a minimum amount of overhead in most platforms, typical C++ programs run very efficiently, even without further optimization steps. Moreover, compilers for the language are known for their ability to aggressively apply optimizations that further improve performance. As a result, programs coded in C++ will frequently outperform code created in most other languages, even when the programmer has not spent much time on code optimization.

Part of the performance advantage provided by C++ is the result of mature compilers and other building tools. Since C++ is such a well-established language, major companies and well-known open source projects have created optimized compilers for practically every major architecture. Common examples include GNU gcc, MS Visual C++, LLVM clang, and Intel cc. Such compilers provide huge speed improvements in typical running time, frequently beating nonoptimized (and even optimized) code that is produced by other languages.

When considering performance, C++ shares the same philosophy of the C programming language. The general idea is to provide high-level features while, whenever possible, avoiding any overhead in the implementation of such features on standard processors. This means that the features provided by C++ generally match very closely with low-level processor instructions.

Other solutions for improved performance in C++ include the use of templates in addition to runtime polymorphism. With templates, the compiler can generate code that matches the types used in a particular algorithm exactly. In this way, programs can avoid the large overhead of polymorphic code that needs to make different runtime decisions depending on the particular type. Programmers can control algorithms in a much finer-grained scale when using templates, while still retaining the ability to use high-level types.

Last but not least, C++ simplifies the use of memory and other resources with the help of smart pointers and other techniques based on the RAII (Resource Acquisition Is Initialization) technique. These techniques allow C++ programs to control memory usage without having to rely on a runtime GC (garbage collection) system. Employing such strategies, C++ programmers can considerably reduce the overhead of frequently used dynamic allocation algorithms, without the need to resort to manual bookkeeping of memory and other resources.

Standardization

Another great advantage of C++ is that it’s based on an international standard, which is recognized by practically every software vendor. While some vendors are not as fast in updating the compiler, they definitely move into the direction of the standard. Unlike some languages that are practically defined by an actual implementation or controlled by a powerful company, C++ has for decades being defined as the work of the C++ committee, which has representatives from major companies and organizations with an interest in the future development of the language.

In fact, some of the big financial companies also have representatives in the C++ committee. This means that the future of C++ is not controlled by a single institution, such as what happens with Java (controlled by Oracle), Go (controlled by Google), C# (controlled by Microsoft), or Objective-C and Swift (controlled by Apple). The fact that the standards committee has members from several organizations protects C++ users from commercial manipulation that would benefit a single company or operating system, to the detriment of the larger community of programmers.

The C++ standards committee has been effective in improving the language in ways that address many of the modern needs of its users. For example, the last three versions of the language standard (C++17, C++20, and C++23) introduced many changes that simplify common aspects of programming, such as simpler initialization methods, more advanced type detection, and generalized control structures.

The standard library has also been the target of many improvements over the last few years. A main focus has been the introduction of containers and smart pointers, which can be used to simplify a large part of modern applications. The standard library also has been augmented to support parallel and multithreaded algorithms, using primitives that can be reused on different operating systems and architectures.

It is necessary to remember that the standardization process has a few drawbacks too. One of the issues is the time it takes to introduce new features. Since the standardization process requires a lot of organization and formal meetings, it takes a few years before a new version of the standard is approved. This has been improved in the last decade, as the committee has decided to create new C++ releases every three years on average. Also, there is the risk of including features that go against previous design decisions. In this case, however, the committee has been very careful in introducing only features that have been thoroughly tested and considered to improve the language according to its design philosophy.

In general, having a standardized language has certainly helped the C++ community to grow and improve the whole programming ecosystem over the last few decades. This is just another reason why developers in financial institutions have embraced C++ as a language suitable for the implementation of options- and derivatives-based financial algorithms.

Expressiveness

Last but not least, C++ is a multiparadigm language that provides the expressiveness and features necessary for the implementation of complex software projects. Unlike some languages, which define themselves as following a single programming paradigm (such as object oriented or functional), C++ allows the use of multiple paradigms in a single application. In this way, you can use the best approach for problem solving, independent of the underlying implementation techniques: object-oriented programming, functional programming, template-based programming, or just simple structured programming.

Because C++ allows programmers to express themselves using different paradigms, it makes easier to find a solution that matches the problem at hand, instead of demanding changes to the way you think in order to match language requirements. For example, a language such as Java, which is designated as object oriented, requires programmers to create code based on objects and classes even if this does not match directly the fundamental requirements of the problem. In C++, on the other hand, you have a choice of using OO techniques as well as functional or even more traditional structured techniques, if this is what your algorithm requires.

The fact that you can use different techniques for different parts of your application also improves your ability to concentrate on algorithms, instead of on programing techniques. Sometimes, using a template-based strategy is the easiest way to achieve a particular algorithmic goal, and C++ allows you to do that without getting in your way. Other parts of the application may benefit from using objects, such as the GUI code. In each case, it is important to be able to express algorithms in the most natural way.

In this book, you will have the opportunity to use many of the features of C++ in different contexts. It will become clear that some features such as object-oriented programming are best used with a particular class of problems, whereas functional techniques may be the best approach in other situations. The fact that the C++ language provides the flexibility to tackle such distinct problems is a clear advantage.

Modeling Options in C++

In this section, you will learn how to code a basic class that can later be used as a starting point for more complex options analysis and trading. In this first example, you will see a C++ class that can be used as the basis for a framework for options value calculation. The class is named GenericOption, since it can be used for any type of underlying and for calls and puts. Before I present how the class works, let’s review a basic concept of class design that is unique to the C++ language and that will be followed on other examples in this book.

Creating Well-Behaved Classes

One of the most important parts of designing classes in C++ is to make sure that they can combine appropriately with other libraries in the system. In particular, the C++ standard library, which includes the STL (standard template library), is the most important set of classes that you will encounter when developing C++ applications. It is essential that your classes play well with the classes and templates provided by the standard library.

To work properly with other parts of the C++ library, classes need to define (or use the default definition for) the four special member functions. These member functions are mainly used to create and copy objects and are required in general to guarantee their proper behavior. These four special member functions are as follows:

The default constructor: Each class can have one or more constructors that define how to initialize objects of the class. The constructor is named after the class, and it can be overloaded so that you can create classes with different parameters. The constructor that receives no parameters is also known as the default constructor, and the compiler automatically creates one if you don’t supply it. Most of the time, you should avoid using the default constructor because it doesn’t properly initialize the native C++ types, such as the double and int variables. To avoid such initialization issues, you should always provide a constructor for new classes.

The copy constructor: This specialized constructor performs an initialization function similar to the default constructor. However, it is called only when creating new objects based on a copy of an existing object. The C++ compiler can also generate a default copy constructor, which automatically copies the values stored in the original object into the new object. However, the default copy constructor also has a problem: it doesn’t know the semantic details of some values stored in the object. This causes issues when you’re storing a pointer to allocated memory or some object that shouldn’t be copied. To avoid such problems, you should provide your own definition for the copy constructor; we recommend that you should always write a copy constructor for new classes.

The destructor: A destructor is a member function called when the object is deallocated. It defines how data used by the class is released when the object is destroyed. Like the other special member functions, the compiler automatically generates a default empty constructor. You should add your own constructor to properly handle the release of private data, for example. This is especially important when a class contains virtual members, in which case the destructor should also be marked as virtual. A virtual destructor tells the compiler to adjust the destructor for each inherited class. In this way, the compiler guarantees that the inherited objects will be properly destructed, even when the destructor is called through a pointer to the base class.

The assignment operator: When copying data between objects, the assignment operator is invoked automatically. Even though this operator appears as a method, and therefore not equivalent to a constructor, it does similar work. Consequently, you should apply the same strategy when dealing with the assignment operator and make sure that it properly handles initialization and copies of the required data members.

To avoid potential problems with C++ classes, it is frequently best to include these four member functions in the classes you create. They are pretty straightforward. The only member function that needs further explanation is the assignment operator. Suppose that you’re implementing a class called GenericOption. The assignment operator would read as follows:

GenericOption &GenericOption::operator=(const GenericOption &p)

{

    if (this != &p)

    {

        m_type = p.m_type;

        m_strike = p.m_strike;

    }

    return *this;

}

The first instruction inside the method is an if statement that checks if the “this” pointer is different from the pointer for the passed object. The reason for this check is that you don’t want to perform the private data member assignment unless the objects in the left and right side of the assignment operator are different:

if (this != &p)

While performing the auto-assignment might not be a problem for some types of variables (especially for native data types such as int and float), it can be time-consuming for complex objects that need to perform several steps during initialization and release. For example, if a member variable contains a large matrix, the assignment may trigger an expensive copy operation that would become unnecessary.

Computing the Option Value at Expiration

Our first example class, GenericOption, provides only the minimum necessary to calculate the value of options at expiration. The first thing you should notice about this class is that it follows the recommended practice described in the previous section. Therefore, it contains a default constructor, a regular copy constructor, a destructor, and an assignment operator.

The main constructor of GenericOption does very little and is responsible only for the initialization of private variables. Although this is common in a simple class like this, using constructors with very few responsibilities is a pattern that should be adopted in many cases. Since constructors are called in various places in a program written in C++, it is important to make them as fast as possible—and relegate any complex operations to member functions that can be called after the object is created.

Tip

Avoid designing classes with complex constructors. Constructors are frequently called for the creation of temporary objects and used when passing parameters by value, for example. Complex constructors can cause your code to run slower and make classes harder to debug and maintain.

There are two types of options recognized by the GenericOption class. This is defined by the enumeration OptionType, which contains the values OptionType_Call and OptionType_Put. Depending on the value passed to the constructor, the object will behave accordingly as a call or as a put option. The constructor also requires the strike value of the option and the cost of the option when it was bought. You will see later in this book how this option cost can be calculated from other parameters, but for now, you can assume that the cost of the option is provided by the exchange.

The main functionality of the class is contained in two member functions: valueAtExpiration and profitAtExpiration. The first member function simply calculates the value of the option at the time of expiration, which in this case is the same as the intrinsic value. To perform this calculation, it needs to know the current price of the underlying asset.

The member function valueAtExpiration first needs to determine if the option is a put or a call. In the case of a put, it takes the difference between the current price and the strike price as its value, with the value being 0 when the strike is lower than the current price. In the case of a call, this calculation is reversed, with the value being 0 when the strike price is higher than the current price. The full calculation can be coded as follows:

double GenericOption::valueAtExpiration(double underlyingAtExpiration)

{

    auto value = 0.0;

    if (m_type == OptionType_Call)

    {

        if (m_strike < underlyingAtExpiration)

        {

            value = underlyingAtExpiration - m_strike;

        }

    }

    else  // it is an OptionType_Put

    {

        if (m_strike > underlyingAtExpiration)

        {

            value = m_strike - underlyingAtExpiration;

        }

    }

    return value;

}

The profitAtExpiration function is similar to valueAtExpiration, but it also considers the price that was paid by the option. Thus, a profit in the option is achieved only after it surpasses the break-even price (for call options). The calculation uses the m_cost member variable to determine the price paid by the option, and it returns the net profit of the option (without considering transaction costs). The function can be coded as follows:

double GenericOption::profitAtExpiration(double underlyingAtExpiration)

{

    auto value = 0.0;

    auto finalValue = valueAtExpiration(underlyingAtExpiration);

    if (finalValue > m_cost)

    {

        value = finalValue - m_cost;

    }

    return value;

}

Complete Listing

The complete code for the example described previously is shown in Listings 1-1 and 1-2. The code is split into a header file called GenericOption.h and an implementation file called GenericOption.cpp.

Here are some observations:

The header file contains the interface of the class, which will be exposed to other files that need to use GenericOption. The separation of class definition and declaration is one of the features of C++ that make your code reusable. The header file has a “.h” extension, while the implementation file has a “.cpp” extension.

The header file is contained inside the “#ifdef” directives, which are interpreted by the C++ pre-processor. This is necessary because if a file includes this interface more than once, the definition of “__CppOptions__GenericOption__” will prevent the content of the file to be added more than once. This is not only good for performance but also avoids errors resulting from duplicate definitions.

The enumeration “enum OptionType” introduces two values used for calls and puts.

The class declaration is introduced with the “class” keyword.

The destructor is declared using the notation “~GenericOption()”.

The public members of the class are declared using the keyword “public”.

The private members of the class are declared using the keyword “private”.

//

//  GenericOption.h

#ifndef __CppOptions__GenericOption__

#define __CppOptions__GenericOption__

#include <stdio.h>

//

// Option types based on direction: call or put

enum OptionType {

    OptionType_Call,

    OptionType_Put

};

//

// Class that represents a generic option

//

class GenericOption {

public:

    GenericOption(double strike, OptionType type, double cost);

    GenericOption(const GenericOption &p);

    ~GenericOption();

    GenericOption &operator=(const GenericOption &p);

    double valueAtExpiration(double underlyingAtExpiration);

    double profitAtExpiration(double underlyingAtExpiration);

private:

    double m_strike;

    OptionType m_type;

    double m_cost;

};

#endif /* defined(__CppOptions__GenericOption__) */

Listing 1-1

Interface of the GenericOption Class

The complete implementation of the GenericOption class is displayed in Listing 1-2. Here are some observations:

The file includes the header file as its first line.

The file also includes the <iostream> header file from the standard library. This is necessary to use the output stream “cout”.

Notice how the constructor initializes the member variables of the class, right before the beginning of the constructor body.

//

//  GenericOption.cpp

#include "GenericOption.h"

#include <iostream>

using std::cout;

using std::endl;

// This is a constructor for this class

//

GenericOption::GenericOption(double strike, OptionType type, double cost)

: m_strike(strike),

  m_type(type),

  m_cost(cost)

{

}

GenericOption::GenericOption(const GenericOption &p)

: m_strike(p.m_strike),

  m_type(p.m_type),

  m_cost(p.m_cost)

{

}

GenericOption::~GenericOption()

{

}

//

// Assignment operator

GenericOption &GenericOption::operator=(const GenericOption &p)

{

    if (this != &p)

    {

        m_type = p.m_type;

        m_strike = p.m_strike;

        m_cost = p.m_cost;

    }

    return *this;

}

//

// Computes the value of the option at expiration date.

// Value depends on the type of option (CALL or PUT) and strike.

//

double GenericOption::valueAtExpiration(double underlyingAtExpiration)

{

    auto value = 0.0;

    if (m_type == OptionType_Call)

    {

        if (m_strike < underlyingAtExpiration)

        {

            value = underlyingAtExpiration - m_strike;

        }

    }

    else  // it is an OptionType_Put

    {

        if (m_strike > underlyingAtExpiration)

        {

            value = m_strike - underlyingAtExpiration;

        }

    }

    return value;

}

//

// Return the profit (value at expiration minus option cost)

//

double GenericOption::profitAtExpiration(double underlyingAtExpiration)

{

    auto value = 0.0;

    auto finalValue = valueAtExpiration(underlyingAtExpiration);

    if (finalValue > m_cost)

    {

        value = finalValue - m_cost;

    }

    return value;

}

int main()

{

    GenericOption option(100.0, OptionType_Put, 1.1);

    auto price1 {120.0};

    auto value = option.valueAtExpiration(price1);

    cout << " For 100PUT, value at expiration for price "

         << price1

         << " is "

         << value << endl;

    auto price2 {85.0};

    value = option.valueAtExpiration(85.0);

    cout << " For 100PUT, value at expiration for price "

         << price2

         << " is "

         << value << endl;

    // Test profitAtExpiration

    auto limit = 120.0;

    for (auto price = 80.0; price <= limit; price += 0.1)

    {

        value = option.profitAtExpiration(price);

        cout << price << ", "  << value << endl;

    }

    return 0;

}

Listing 1-2

Implementation of the GenericOption class

Using the auto Keyword

The code in Listing 1-2 gives a good example of using auto variable declarations. When declaring a variable, C++ allows you to determine the type of the variable. For example:

float price1 = 120.0;

string name = "test";

The first line declares variable price1 to be of type float. The second line declares variable name as a string. C++ also allows the type of the variable to be automatically determine from the content at initialization. That is the purpose of the auto keyword:

auto price1 = 120.0;

string name = "test";

auto full_name = name + "_name";

In this example, the type of price1 is now automatically detected using the initialization value. The same happens to the string variable full_name, whose type is determined by the given initialization expression.

Initializing Variables in C++

C++ provides two main ways to initialize variables: the first method is using the assignment syntax. For example:

float option_price = 200.0;

int days_to_expiration = 14;

This syntax for variable initialization maintains the ability of conversion between types. For example, the second line in the preceding example could be rewritten as

int days_to_expiration = 14.0;

even though 14.0 is a float number. In this case, the compiler will allow the float value to be converted to an integer. A second way to initialize variables in C++ is to enclose the initial value in braces:

float option_price {200.0};

int days_to_expiration {14};

This syntax is stricter and can be used to remove the possibility of type conversion, which can at times create confusion and allow conversion bugs that are difficult to track. Using this syntax, it is not possible to convert integers to floats, for example:

int days_to_expiration {14.0}; // error: this conversion is not possible

Printing Values in C++

Another concept demonstrated in Listing 1-2 is printing values through the use of streams. The following line:

cout << price << ", "  << value << endl;

will print the price and value to the standard terminal. The cout variable is called a stream and defined in the standard library iostream. You can print variables and strings by sending the value to the cout stream, using the operator <<. A sequence of values separated by the operator << will result in them being directed to the stream, which in this case represents the output terminal.

C++ has many other stream types, including terminals for input and output, files, network interfaces, etc. The stream concept is a generic way to represent data input and/or output methods.

Another way of performing data output is using the std::format function. It makes the formatting operating explicit, so you can make sure that the output will be displayed as desired:

#include <iostream>

#include <format>

int main() {

    auto value =  "world";

    std::cout << std::format("Hello {}!\n",value);

}

Building and Testing

To compile the code presented in the previous sections, you need a standards-compliant C++ compiler. I have tested this code with gcc and LLVM clang, although most modern compilers should work without any problems. Here are the commands that I used to compile this on my machine:

gcc -o GenericOption.o -c GenericOption.cpp

gcc -o GenericOption GenericOption.o    # creates the executable

The executable file can then be used to run the sample application like this (I used the bash shell to run the application on UNIX):

$ ./GenericOption

 For 100PUT, value at expiration for price 120 is 0

 For 100PUT, value at expiration for price 85 is 15

80, 20

80.1, 18.8

80.2, 18.7

80.3, 18.6

80.4, 18.5

80.5, 18.4

...

You can check the output to determine if the results match your expectations. I used the data to create a chart with the results, as shown in Figure 1-3. Since the example is a put, notice that the profit is negative for any price higher than the break-even price of $98.90. Below that value, the profit rises steadily, attaining its maximum value at price $0 (not shown in the chart).

A line graph represents profit versus price. A line follows a decreasing trend from (90, 9) to (100, minus 1), then falls flat till (110, minus 1). Data are estimated.

Figure 1-3

Profit chart calculated with the GenericOption class for sample option with strike price $100

Further References

In this chapter, I provided an introduction to most common concepts of options investing and how C++ programmers can model them. You can turn to several other sources for further clarity on the concepts introduced in this chapter. If you need additional information on options and related financial investments, here are a few books that cover not only the basics but also the mathematical details of options investing:

Option Volatility & Pricing by Sheldon Natenberg, McGraw Hill, 1994. This is the standard reference on options and their properties. This book explains in great detail how options are defined, how option Greeks work, and their basic economic interpretation.

Investment Science by David Luenberger, Oxford University Press, 1998. This is an undergraduate-level book that describes the basic theory of investment. Most of the book explains the fundamentals of fixed income investments, but the included algorithms can be used for other common problems in finance.

Mathematics for Finance by Marek Capinski and Tomasz Zastawniak, Springer Press, 2011. This book is more for the mathematically inclined. It explains not only the basics of fixed income investments but also provides a lot of mathematical methods that are useful in their analysis. Many of these techniques are also used in the analysis of options-based investments.

Investments by Zvi Bodie, Alex Kane, and Alan J. Marcus, McGraw Hill/Irwin, 2004. This is a standard textbook on investment theory that explains, among other topics, the ideas behind equity-based investments and their derivatives.

Conclusion

In this chapter, I provided an overview of the themes and ideas that will be discussed in the remainder of the book. Options are basic financial vehicles that can serve multiple investment goals such as providing risk protection, supplying short-term income, or serving as a speculation method based on perceived future prices of a financial instrument. These methods are used throughout the financial industry, including banks, hedge funds, and other market institutions.

I started with a basic description of options and how they fit in the landscape of the investment industry. You learned the most important properties of options and how they define standard contracts that are traded by stock, futures, and commodity exchanges. I also described how this information may be useful to software engineers who want to create solutions for the financial industry using C++ as the main implementation language.

You have seen how options can be described by option Greeks: a set of standard attributes associated with option contracts that can be used to determine properties of the option. In particular, these option Greeks are useful for evaluating the price at which options should be bought and sold, as you will see in the algorithms introduced in the later part of this book.

This chapter also discussed the advantages of C++23 as a language for financial and options-related programming. Many of the features of C++ make it an ideal language to implement algorithms and large-scale software packages to analyze and trade options. You have seen an example C++ class that can be used to compute the profit or loss for a single option contract.

In the next chapter, you will learn about derivatives in general and how they expand on the ideas of standard options. You will also see how such financial derivatives can be modeled using the C++ language.

© The Author(s), under exclusive license to APress Media, LLC, part of Springer Nature 2023

C. OliveiraOptions and Derivatives Programming in C++23https://doi.org/10.1007/978-1-4842-9827-5_2

2. Financial Derivatives

Carlos Oliveira1  

(1)

Seattle, WA, USA

Derivative is a general term used for contracts that have their price based on the value of an underlying asset. In particular, options are a standardized type of derivatives that gives the right to buy or sell the underlying asset at a particular price. Unlike options, however, general derivatives include a large number of nonstandard features that allow them to be created even for illiquid assets such as corporate credit risk or real estate mortgages.

In the last decades, the financial industry has created and popularized a multitude of derivatives to collateralize disparate assets, including items such as fixed income instruments, mortgage-backed securities, and risk of default. Pricing and trading of these derivatives have become a large part of the work performed daily in the trading desks of large banks by analysts and quantitative programmers.

This chapter focuses on characteristics of general derivatives and presents a few C++ techniques that are useful to model specific aspects of these financial instruments. This chapter also introduces you to topics that you will learn in more depth in the remainder of this book. The main items covered in the chapter are as follows:

Models for derivative pricing: You will learn the basic ideas used to determine the price of various derivatives along with a few examples of how they are applied.

Credit default swaps: A particular type of derivative where investors want to buy protection against the default of a third-party institution.

Interest rate derivatives: A derivative in which the underlying asset is an interest rate that is paid in predefined time periods.

FX derivatives: A quick introduction to some foreign exchange derivative contracts.

A Monte Carlo model for derivatives: You will explore a simple computation of Monte Carlo models for pricing derivatives in C++.

Using the STL for derivative pricing: Using the STL makes it possible to create fast containers for generic objects, without incurring runtime inefficiencies.

Models for Derivative Pricing

In the last chapter, you learned some basic information about options and how to use C++ to model this type of contracts. Recall that an option is a kind of financial derivative that is traded on exchanges and is defined by a standard agreement between buyers and sellers. General derivatives, however, are not restricted to the fixed requirements of standard option contracts. In this section, you will learn more about generic derivatives, including how they are handled in the financial industry.

In its general sense, a financial derivative is just a contract that assigns a value to a particular set of rights linked to an underlying asset. For example, options give the right to buy or sell an asset such as a stock or a commodity. But complex derivatives can be created if you want to perform a more exotic transaction between buyers and sellers. For example, credit default swaps are contractual exchanges that require a payment to occur only when a particular institution is in default (i.e., bankrupt). For another example, collateralized debt obligations will require payments that depend on the risk level of certain borrowers.

The common aspect shared between different derivatives is the way their prices are modeled, that is, the mathematical characteristics of price changes for these instruments. All derivatives that are traded in the market can be analyzed using a generalized random walk model that was discovered and applied in the twentieth century by American economists. Such a model for derivative pricing and its associated mathematical equations were developed and popularized by Robert Merton, in a work that was itself a generalization of the Nobel prize winning Black-Scholes model for options pricing.

In a random walk model, the prices of securities are studied under the assumption that their changes are random. That is, these prices can move up or down by a random value that is given by a normal distribution, as shown in Figure 2-1.

A line graph represents price versus time. The line fluctuates and follows an erratic pattern. The highest peak is at (90, 32.25). Data are estimated.

Figure 2-1

An example of random walk

While this is only an approximation of the complex market behavior, it is most of the time so close to what has been observed in the marketplace that models based on random walks have been extremely successful. These models are frequently used in the financial industry to accurately determine prices for options and more complex derivatives. As a result, most of what you will learn in this book is in some way or another related to this pricing model, whether we’re using it to analyze existing derivatives or to trade them.

The first thing to understand about the random walk model for derivative pricing is that it results in a set of equations that determine the behavior of prices as time passes. This equation is, by the nature of its assumptions, probabilistic, but it can be solved to give a value for the fair price of a particular investment instrument.

The fair price, according to economic conventions, is the price at which neither the sellers nor the buyers would have an unfair advantage. In other words, both sides in the transaction are satisfied with the result, and there is no known way to extract more value from one of the sides in the transaction without breaking this equilibrium. Because the model used is probabilistic, this also means that each side of the transaction has the same probability of making money after the transaction is concluded. This fair price element of the model allows you to calculate a fixed value using only a probabilistic assumption about future expectations. A list of common derivative types is given in Table 2-1.

Table 2-1

A List of Common Derivatives

Derivative Type

Description

Credit default swaps

A contract that pays its holder in the case of default (bankruptcy) of a target corporation.

Collateralized debt obligations

A financial product where debt is paid to investors according to levels of collateral risk from borrowers.

FX derivatives

A derivative where the underlying asset is composed of foreign currencies, with prices varying according to foreign exchange rates.

Interest rate derivatives

A derivative in which the underlying asset is an interest rate that is paid in predefined time periods.

Mortgage-backed security

A type of derivative that is defined in terms of mortgage contracts.

Energy derivative

Derivative in which the underlying asset is an energy product or asset, such as oil, natural gas, coal, or electricity.

Inflation derivative

Derivative contracts that have prices defined by the level of inflation in a particular economy.

Another consequence of fair pricing hypothesis is that the resulting theoretical model allows no arbitrage. Arbitrage is a method of making money in financial markets where you buy some asset for a price and immediately sell it for a higher price for a sure profit. This kind of arbitrage cannot be allowed in a financial model, because it indicates that the original price was unfair for at least one of the participants. It also corresponds to the known fact that, in liquid and free markets, opportunities for arbitrage will be nonexistent or disappear as soon as they are identified.

Credit Default Swaps

A credit default swap (CDS) is a derivative that allows investors to bet on the solvency of a particular institution. In this case, the underlying asset is defined as the value of a business minus the liabilities it currently has. Solvency is then defined as the situation in which the value of the business is superior to its liabilities.

Credit default swaps have been used as a way to protect large corporations against the risk of default of a counterpart, which is a type of risk suffered by contracts with large institutions. For example, the 2008 financial meltdown proved that counterpart risk is very difficult to avoid when only a few participants dominate a large portion of the market. The ability to use mathematical techniques to model this type of risk is therefore very important for institutions that deal with such large-scale operations.

In the recent years, most banks and other investment institutions have become active in the development of CDS models as a way to mitigate such risks. Much of the software for solving CDS pricing models is based on modern C++, which you will learn in the next chapters.

Collateralized Debt Obligations

A collateralized debt obligation (CDO) is a financial derivative product based on the cash flow of a collection of loans. The collateralization process makes it possible to split the cash flows among different investors based on the characteristics of individual loan originations.

In particular, CDOs are used to split cash flows based on the risk of each loan. Parts of the cash flow are classified as low risk (e.g., loans that are labeled as AAA by credit rating institutions) and sold for higher profit, while other parts of the package are sold as higher-risk investments. CDOs have acquired a bad public reputation after the financial crisis of 2008, but they remain a valuable tool for defining the risk associated with particular investment classes (see Figure 2-2).

A stacked bar and a line graph represent dollars and percentages versus years from 1996 to 2008. The bars represent securitized and non-securitized. 2005 has the highest bar of $620. The line represents the subprime share of the entire mortgage market. It peaks at 23.5% in 2006. Data are estimated.

Figure 2-2

Securitization levels of mortgage loans during the 1990s and 2000s (from the official government publication, “Financial Crisis Inquiry Commission Report”)

CDO pricing relies heavily on the derivative-pricing techniques that will be discussed in this book. The development of Black-Scholes-Merton methods gave institutions the ability to price more complicated products using similar ideas. By extending these pricing methods to collateralized loans, quantitative trading desks have been able to create a completely new category of financial products that are now used by most banks and other financial institutions.

FX Derivatives

Derivatives based on foreign currencies are a relatively simple extension of the concepts already used on options. The underlying price is defined by a foreign currency. The basic difference between such products and standard options is that they depend on the price variation of currency pairs, such as USD/EUR.

FX derivatives play an important role in markets that rely on foreign trade. For example, it is used in the production planning for companies that need protection against variations in currency prices. Most global companies that buy or sell products in a foreign market will use FX derivatives as a tool to avoid the unpredictability of currency fluctuations.

FX derivatives can also be an investment vehicle. Hedge funds have for a long time used foreign exchange products as a way to hedge against possible losses in foreign investments. They can also be used to speculate on the rise or fall of foreign currencies as compared to local currencies. For all these reasons, the pace of development of mathematical models for FX derivatives has been significant in the industry. Because of the high volatility and near real-time needs of FX traders, C++ has become the language of choice for developing applications that handle FX derivative pricing.

Equations for Derivative Modeling

The set of equations that has been used to model the future price of derivatives is generally called the Black-Scholes-Merton equations. These models, which are based on similar differential equations from physics, describe the properties of price movements when considering a number of input parameters. Here are the most commonly used parameters for these differential equations:

The price of the underlying asset: This is the price of the asset that is the basis for the derivative. In the case of stock options, this is the price of the stock at the present time.

The current interest rates: Interest rates have an important role in the modeling of derivatives, because they are the safest way to get a return on your money. The price of a derivative has to take into consideration the prevailing interest rates and the money that the investor could be earning in a risk-free investment.

The strike price: The price at which a transfer of value will happen. For call options, this is the price above which a profit is made. More complex models will have different definitions for the strike price.

Volatility: The volatility for the underlying asset is very important in derivative models, because it determines how fast the underlying prices move. This information then can be used to calculate the probabilities that are part of the general model for derivative pricing. Volatilities are described in terms of standard deviation.

Time left in the contract: Time is another important variable, because the more time is left to expiration, the higher the probability that the underlying asset will move in price. This directly affects the probability of profit for the derivative.

These parameters are used as part of the differential equation that determines the price of a derivative. Here is the basic equation that is generally called the Black-Scholes model:

This differential equation determines the relationship between the following quantities:

V: The price of the derivative

t: The time to expiration

σ: The volatility

S: The price of the underlying asset

r: The current interest rate

This equation can be interpreted for different purposes, depending on the type of contract you want to price. For example, when working with stock options, this equation will result in a formula that returns the price of a call or put option, which will depend on the desired strike. Moreover, the exact formula used will change depending on the exercising style: either an American- or European-style option. You will see in later chapters a few examples of how this general equation can be used with different derivatives.

Numerical Models

As discussed in the previous section, the existing models for options pricing are based on the Black-Scholes differential equation, which describes the variation of derivative prices with time, along with a number of other parameters. Later, Merton successfully expanded this model to deal with other derivative types. All these models share in common the fact that prices are assumed to be random and to change according to a predefined probability distribution.

In order to solve these models, you have to learn a few techniques to calculate the desired prices, given the set of input parameters required by the equations. There are two main strategies that have been devised for this purpose: numerical methods and simulation methods.

Numerical methods refer to a set of mathematical and computational techniques to solve, or at least approximate, differential equations. While numerical methods were invented to solve problems in physics and engineering, they have been recently used with success to solve financial pricing problems for options and derivatives. Many of the techniques studied in this book will be used to solve one or more parts of the derivative-pricing models previously described.

Examples of mathematical tools that can be used in the numerical solution of complex derivative models include linear algebra, optimization, and approximation methods, probability, numerical root calculation, and finite difference methods. These mathematical tools can be used in isolation or combined to form more complex algorithms for the solution of Black-Scholes equations.

The other side of solving numerical models is the development of fast algorithms. While the mathematical tools are important, they need to be implemented in a fast and memory-efficient manner to be useful for financial applications. Pricing models normally need to be solved very frequently, and the performance and accuracy of solutions can make the difference between a profitable and a losing financial transaction.

Binomial Trees

Another technique used to determine the price of derivatives is the method of binomial trees. A binomial tree is a technique to organize the computation necessary to determine derivative prices in a step-by-step fashion. The root of the tree is the initial price at the time origin. Starting from each node of the binomial tree, there are two possible directions for the new price, which can then be calculated by solving a simple equation.

Once the complete tree has been calculated, it is possible to answer questions about the fair price of the derivative at particular strike prices and time period. The complete algorithm for binomial trees has three main steps:

The forward phase: This phase happens when the tree is constructed, starting at time zero with an initial price. Then, the total time is divided into discrete steps, and at each step, a new set of nodes is created. The nodes represent the two directions in which the underlying price can change, either going up or down in value. This phase ends when the tree nodes reach the maturity or expiration date.

The payoff phase: In this phase, the profit (payout) of each node is calculated. The calculation starts from the maturity date, since the profit in that case is easy to calculate.

The backward phase: In this phase, the computation of the payout continues moving backward in time, using the values calculated in the previous phase as the starting point. This process continues until the initial node is reached.

Simulation Models

Simulation models, also called Monte Carlo models, are a different approach to solve problems involving differential equations, such as the equations necessary for derivative pricing. The main motivation behind simulation models is that the equations for derivative pricing generally don’t have a closed mathematical solution. In that case, instead of finding the exact solution, a possible strategy is to run a simulation of the price evolution while considering that price changes can occur according to a random distribution, as assumed by the Black-Scholes equations.

Monte Carlo methods have a long history. Since the initial development of probability theory, researchers have found that simulating a random event is a good way to learn about certain physical or engineering models. With the introduction of digital computers, it is now possible to perform very complex simulations in an efficient way. This is an area where using C++ has a big advantage, since simulation accuracy is directly related to the number of repetitions of a basic random experiment.

To find the price of a derivative security, the basic step is to develop a random walk model for the security. As discussed previously, derivative pricing is based on the idea that underlying prices are always moving in an unpredictable, random way. A Monte Carlo algorithm uses this property to simulate the movements of the underlying asset for a large number of times. The random fluctuations are determined with a random number generator, according to the parameters that have been previously observed for the asset, such as volatility, current interest rate, and observed price of the underlying instrument.

If the simulation is properly performed, a Monte Carlo algorithm will converge to a particular value of derivative price, according to the assumptions of the Black-Scholes model. The interpretation of these simulated runs can then be used to determine the price of a particular contract.

Another consideration is that numeric and Monte Carlo methods are not necessarily exclusive choices. You can code numerical methods to solve a particular pricing problem while at the same time using Monte Carlo methods for confirmation of the results. You can also start using Monte Carlo methods to explore different scenarios and then code a more precise numerical algorithm to find the solution of the more interesting and/or important scenarios. Still another possibility is to use numerical algorithms to solve particular subproblems and use a Monte Carlo simulation to put these values together in a more complicated scenario. In summary, there are many ways to combine numerical algorithms and simulation to achieve the desired results.

Coding in C++ with the STL

One of the main goals of C++ is to act as an efficient and high-level language for application development. One of the tools used by programmers to achieve this goal is the standard template library (STL). With the STL, it is possible to create fast containers for generic objects, without incurring runtime inefficiencies.

The C++ STL provides a list of software components that you can use in several contexts. The library can be described as having three main groups of templates:

Containers: A container is a template class that provides generic logic to handle a group of objects. They typically implement traditional data structures using the facilities provided by C++ templates. Table 2-2 displays a quick list of containers provided with the STL and a short description of each one.

Table 2-2

List of STL Containers

Container

Description

std::vector

A dynamically allocated array of elements, where members are guaranteed to be allocated contiguously.

std::list

A linked list data structure.

std::map

An associative data structure, where elements are associated with keys of a particular type.

std::multimap

A version of std::map template that can also contain repeated elements.

std::queue

A first-in last-out data structure.

std::dqueue

A double queue, where elements can be added or removed from both sides of the queue.

std::set

A data structure that contains ordered values and provides quick lookup functionalities.

std::multiset

A data structure similar to set, but where elements can appear more than once.

Iterators: Along with containers, you also need to manipulate the contents of data structures stored in them. This is possible in the STL with the use of iterators. With an iterator, you can easily access individual elements in a container and perform common operations such as inspecting, adding, removing, and modifying single elements.

Algorithms: The last major piece of the STL is a set of algorithms that have been optimized for each container. Because templates are parameterized, the algorithms in the STL can be specialized for each container so that users can have the fastest algorithm for each data type while using the same interface. This means that you just need to learn a small set of algorithms that are applicable to a large class of containers. The STL implementation will guarantee that you’re using the most efficient version of an algorithm for that particular container. Table 2-3 displays a quick list of useful algorithms in the STL.

Table 2-3

List of STL Algorithms

Algorithm

Description

std::for_each

Performs a given function for each element of the target container.

std::find

Searches the container for a given element, given a range indicating the beginning and end of the data sequence.

std::find_if

Similar to std::find, but searches the container for a given element satisfying a given predicate.

std::find_first_of

Searches the container for the first match of a particular element, given a range of elements.

std::count

Counts the number of elements in the container defined by the given parameter.

std::count_if

Counts the number of elements in the container that satisfies a given predicate.

std::copy

Copies elements from a given origin position to a destination.

std::move

Moves elements from a given origin position to a destination position.

std::reverse

Reverses the current order of the container.

std::sort

Sorts the container according to a comparison function.

std::binary_search

Performs binary search for a particular element on a given container.

Generating a Random Walk in C++

This section gives an example of using the STL and describes a simple way to generate random walks in C++. While the method presented is not optimal, it shows most of the elements necessary to create realistic random walks. In the later chapters, you’ll learn about the statistical techniques that can be used to create more realistic random walks, suitable for derivative-pricing algorithms.

A random walk is a process that simulates stochastic movement. That is, under a random walk, a certain quantity can increase or decrease its value according to a probabilistic rule. Random walks are important in the analysis of price movements: if we assume that such movements are random (as we can assume at least for relatively small timeframes), then a random walk can be used to model the change in prices for several classes of financial assets. For example, the price of a set of stocks can be analyzed as a random walk, from which we can derive the probability of its change in the near future. A random walk can be used not only as a practical simulation device (as we’ll do in this chapter) but also as a mathematical model, from which one can derive rules for derivative pricing.

The class that we create in this section is called RandomWalkGenerator, and it exposes a main member function called generateWalk(). This class has the single responsibility of creating a sequence of numbers that represent a random walk. This means that starting on a particular value (the initial price), the sequence will change according to random increments, as determined by the given step parameter. Finally, the size of the sequence (which corresponds to the time to expiration of a contract) is also given as a parameter to the class. This results in a class with the following signature to the constructor:

RandomWalkGenerator(int size, double start, double step);

The class contains three member variables controlling the behavior of the random walk. They are as follows:

m_numSteps: An integer that gives the number of steps desired

m_stepSize: A double number that gives size of each step (in percentage points)

m_initialPrice: A double number that specifies the starting price

The main member function, generateWalk(), performs the task of sequentially generating new steps in the price simulation. The function receives no parameters and uses the member data already stored in the RandomWalkGenerator class.

The way the generateWalk() member function operates is based on the std::vector container, which is used to store all the intermediate prices created by this Monte Carlo simulation process. The constructor used in this case is the default constructor, which results in an empty vector, called walk.

The walk vector is then populated inside the for loop using vector::push_back, a member function of the std::vector container that adds a new element at the end of the vector, resizing the vector if more space is necessary. The fragment displayed as follows uses the value returned by the member function computeRandomStep(), starting from the previous price stored in the local variable prev:

//

// Generates a random walk and stores the data in a std::vector

// that is returned at the end.

//

std::vector<double> RandomWalkGenerator::generateWalk()

{

    std::vector<double> walk;

    double prev = m_initialPrice;

    for (int i=0; i<m_numSteps; ++i)

    {

        double val =  computeRandomStep(prev);

        walk.push_back(val);

        prev = val;

    }

    return walk;

}

Finally, you can check the computeRandomStep member function, which generates a new random price according to the given simulation arguments. The idea used in this example is that there is a 1/3 chance that the price will change up, down, or stay the same. I use a simple random number generator to return uniformly generated numbers (the standard function rand is not the best choice for such applications, but you’ll learn about better options in a latter chapter). The result is that you have a “three-sided dice” that determines the direction of the next step in the simulation. Here is the complete code for this member function:

//

// Returns a random step size, following the parameters given in the

// constructor.

//

double RandomWalkGenerator::computeRandomStep(double currentPrice)

{

    const int num_directions = 3;

    int r = rand() % num_directions;

    double option_value = currentPrice;

    if (r == 0)

    {

        option_value += (m_stepSize * val);

    }

    else if (r == 1)

    {

        option_value -= (m_stepSize * val);

    }

    return option_value;

}

Finally, I present next a test stub that can be used to verify the correctness of the code. It is always a great idea to perform some testing of the algorithm as you implement it. This kind of testing can be used to avoid obvious mistakes as you code a complex algorithm. The test case is to generate a random walk starting from price $30, for 100 steps with a step size of $0.01. The first step is to create an object of class RandomWalkGenerator, with the described parameters. Then, we call the generateWalk member function to calculate each step of the random walk. Finally, we print the results of the random walk using the std::cout output stream. Here is the code used:

//

// Test function for RandomWalkGenerator

//

int main()

{

    // 100 steps starting at $30

    RandomWalkGenerator rw(100, 30, 0.01);

    vector<double> walk = rw.generateWalk();

    for (int i=0; i<walk.size(); ++i)

    {

        cout << ", " << i << ", " << walk[i] << std::endl;

    }

    cout << endl;

    return 0;

}

Complete Listing

The complete code for the example is listed next in Listings 2-1 and 2-2. The code is split into a header file called GenericOption.h and an implementation file called GenericOption.cpp.

The header file follows the C++ convention and starts with preprocessor guards to allow multiple inclusion. The RandomWalkGenerator class has three member variables: m_numSteps, m_stepSize, and m_initialPrice.

//

//  RandomWalkGenerator.h

//

// Interface for random walk generator class.

#ifndef __CppOptions__RandomWalkGenerator__

#define __CppOptions__RandomWalkGenerator__

// The class uses a vector to hold the elements

// of the random walk, so they can be later plotted.

#include <vector>

//

// Simple random walk generating class. This class can be

// used for price simulation purposes.

//

class RandomWalkGenerator {

public:

    //

    // Class constructors

    RandomWalkGenerator(int size, double start, double step);

    RandomWalkGenerator(const RandomWalkGenerator &p);

    // Destructor

    ~RandomWalkGenerator();

    // Assignment operator

    RandomWalkGenerator &operator=(const RandomWalkGenerator &p);

    // Main method that returns a vector with

    // values of the random walk

    std::vector<double> generateWalk();

    // Returns a single step of the random walk

    double computeRandomStep(double currentPrice);

private:

    int m_numSteps;        // the number of steps

    double m_stepSize;     // size of each step (in percentage points)

    double m_initialPrice; // starting price

};

#endif /* defined(__CppOptions__RandomWalkGenerator__) */

Listing 2-1

Interface of the RandomWalkGenerator Class

//

//  RandomWalkGenerator.cpp

//

//  Simple random walk implementation.

#include "RandomWalkGenerator.h"

#include <cstdlib>

#include <iostream>

using std::vector;

using std::cout;

using std::endl;

//

// Constructor. The supplied parameters represent the number

// of elements in the random walk, the initial price, and the

// step size for the random walk.

//

RandomWalkGenerator::RandomWalkGenerator(int size, double start, double step)

: m_numSteps(size),

m_stepSize(step),

m_initialPrice(start)

{

}

RandomWalkGenerator::RandomWalkGenerator(

               const RandomWalkGenerator &p)

        : m_numSteps(p.m_numSteps),

m_stepSize(p.m_stepSize),

m_initialPrice(p.m_initialPrice)

{

}

RandomWalkGenerator::~RandomWalkGenerator()

{

}

RandomWalkGenerator &RandomWalkGenerator::operator=(

               const RandomWalkGenerator &p)

{

    if (this != &p)

    {

        m_numSteps = p.m_numSteps;

        m_stepSize = p.m_stepSize;

        m_initialPrice = p.m_initialPrice;

    }

    return *this;

}

//

// Returns a single step of the random walk

//

double RandomWalkGenerator::computeRandomStep(double currentPrice)

{

    const int num_directions = 3;

    int r = rand() % num_directions;

    double val = currentPrice;

    if (r == 0)

    {

        val += (m_stepSize * val);

    }

    else if (r == 1)

    {

        val -= (m_stepSize * val);

    }

    return val;

}

//

// This is the main method in this class. It will generate

// random steps within the constraints set by the constructor.

//

std::vector<double> RandomWalkGenerator::generateWalk()

{

    vector<double> walk;

    double prev = m_initialPrice;

    for (int i=0; i<m_numSteps; ++i)

    {

        double val =  computeRandomStep(prev);

        walk.push_back(val);

        prev = val;

    }

    return walk;

}

//

// This is a testing stub. It generates a sequence of points

// following a random walk.

//

int main()

{

    // 100 steps starting at $30

    RandomWalkGenerator rw(100, 30, 0.01);

    vector<double> walk = rw.generateWalk();

    for (int i=0; i<walk.size(); ++i)

    {

        cout <<  i << ", " << walk[i] << std::endl;

    }

    cout << endl;

    return 0;

}

Listing 2-2

Implementation of the RandomWalkGenerator Class

Building and Testing

You can build the code presented in the last section using any standards-compliant C++ compiler. The code was tested on Linux and macOS X. You can use a compiler such as gcc, which is freely available on all major platforms. The commands used in this case were

cc++ -std=c++2b -o RandomWalkGenerator.o -c RandomWalkGenerator.cpp

cc++ -std=c++2b -o RandomWalkGenerator RandomWalkGenerator.o

The option -std=c++2b is used to select C++23 features in the compiler, since the 2023 standard was still not selected by default by the compiler at the time of writing. The code contains a test stub that generates a sample random walk. You can run the application to see the sequence of random prices created by the RandomWalkGenerator class. Here is sample output from my machine:

$ ./RandomWalkGenerator

0, 29.7,

1, 29.403,

2, 29.403,

3, 29.403,

4, 29.109,

5, 29.109,

6, 29.4001,

7, 29.4001,

8, 29.4001,

9, 29.1061,

10, 29.3971,

...

Using the data provided in this sample output, it is easy to create a chart that shows the price behavior over a simulated period of time, as shown in Figure 2-3. Notice how this simple output already looks similar to the behavior of a traded asset. You will later learn to change the parameters in this type of simulation so that it more closely resembles a particular asset class.

A line graph represents price versus time. The line fluctuates and follows a decreasing to increasing trend. The highest peak is at (15, 30). Data are estimated.

Figure 2-3

Profit chart for a random walk produced by the application RandomWalkGenerator

Further References

Derivatives are a broad subject, and several books have been written on theoretical and practical aspects of these investment vehicles. Here is a quick list of references that can be used to get additional information on this topic:

Practical C++20 Financial Programming by C. Oliveira. This book covers most of the basic algorithms necessary for derivative pricing. Examples in C++20 are provided in each chapter.

The “Financial Crisis Inquiry Commission Report,” which is a publication of the US government (available at www.gpo.gov/fdsys/pkg/GPO-FCIC/pdf/GPO-FCIC.pdf), provides an overview of derivatives trading activity that led to the financial crisis of 2008.

Options, Futures, and Other Derivatives by John C. Hull. This is the standard textbook introduction to derivatives.

Derivatives Markets by Robert L. McDonald. This book provides an in-depth look at the several markets in which financial derivative methods have been applied.

Conclusion

This chapter introduced the main ideas about generic financial derivatives and their pricing mechanisms. Derivatives allow investors and traders to enter into contracts that are based on a particular asset while having some of their rights defined by associated price levels of the underlying asset. The prices are set along with certain parameters, such as interest rates, volatility, and time to expiration. The concepts behind derivatives make it possible to create financial products that uniquely target different patterns of risk and reward. Derivatives can be used in practice to mitigate the risk associated with many credit- and asset-based transactions. They can also be used to make risky and speculative bets on particular markets.

In this chapter, you have seen the basic ideas behind the models used for derivative pricing. These models are ultimately based on the equations developed by economists Black, Scholes, and Merton. The resulting partial differential equations determine with precision the price of the derivative as time passes while making a small number of assumptions about the underlying assets. The main assumption used is that the changes in the underlying asset are randomly distributed, with known volatility.

I described next the main approaches used to solve derivative-pricing models. In general terms, you will be able to apply numerical algorithms, based on the interactive solution of mathematical equations; binary tree techniques; or Monte Carlo methods, which are simulation algorithms that replicate the price movements of the desired financial asset.

As an example of C++ programming for derivative pricing, I introduced a C++ class that implements a random walk. This class illustrates in a simple way how Monte Carlo methods operate and will be later used as a basic algorithm for more complex pricing methods.

The next chapter introduces other basic algorithms used in the implementation of option and derivative-pricing models. You will see how these algorithms can be efficiently coded in C++23. I will also review some of the most used C++ libraries in finance.

© The Author(s), under exclusive license to APress Media, LLC, part of Springer Nature 2023

C. OliveiraOptions and Derivatives Programming in C++23https://doi.org/10.1007/978-1-4842-9827-5_3

3. Basic C++ Algorithms

Carlos Oliveira1  

(1)

Seattle, WA, USA

To become a proficient software developer, it is essential to get a solid understanding of the basic algorithms used in an application area. This is especially applicable to financial derivatives, where some basic problems and algorithms are recurring. In this chapter, I examine some common algorithms encountered in C++ applications for analyzing and processing options and derivatives.

The basic algorithms in this area involve frequent tasks such as time series processing, date and time handling, and data access and storage. While these algorithms are useful in most applications, they are especially important in code that handles financial data, such as options and other financial derivatives. This chapter will also prepare you for the type of C++23 coding skills that are necessary for more advanced topics covered in the following chapters.

The chapter is organized to survey some basic algorithms and their implementation in C++, including the following topics:

Date and time handling: Date representations are important for many of the underlying tasks in financial engineering. You will learn about the main operations performed on dates and how they can be implemented in C++23.

Compact date implementation: Another aspect of date processing is efficient memory use for long-time series. I discuss some of the alternative representations for date objects and explain how they can be implemented in C++23.

Networks and graphs: Data elements and their relationships are often described as a network of connections. This is true for many of the data entities used in financial analysis. You will see a quick overview of networks and their representation using C++23 and the STL, along with an example of their use.

Date and Time Handling

Among the basic algorithms employed on financial applications, date and time handling are some of the most commonly used. This happens because dates are needed to process time series, which are used almost everywhere in financial applications. These time series can span time periods ranging from a few minutes to several years. For this reason, it is important to use date-handling data structures that are efficient and accurate so that you don’t need to worry about the correctness of financial calculations depending on dates.

In this section, you’ll learn about the most common ways to represent dates in C++ applications. This will also help you choose a date representation that matches the requirements of your particular application.

The first thing is to realize that there are several ways to represent dates in a computer program. The simplest technique is to use a class that directly stores the values for day, month, and year. This is the representation used for the Date class, as introduced in this section. A more compact representation of dates will be presented in the next section. Finally, standard C++ comes already with a few classes that can be used to store and manipulate dates. Between these choices, you can select the best one for your particular purpose.

Date Operations

A number of operations are commonly required to work on dates. Table 3-1 presents some of the most common date operations that will be discussed in this chapter.

Table 3-1

List of Common Operations Performed on Date Objects

Operation

Description

add

Add a certain number of days to the current date.

subtract

Subtract a certain number of days from the current date.

addTradingDays

Add a number of trading days to the current date.

subtractTradingDays

Subtract a number of trading days from the current date.

dateDifference

Return the difference in days from the current date.

tradingDaysDifference

Return the difference in trading days between two dates.

dayOfTheWeek

Return the day of the week corresponding to the given date.

isWeekDay

True if the date is a weekday.

isHoliday

True if the date is a holiday.

isTradingDay

True if the date is a trading day.

isLeapYear

True if the year is a leap year (i.e., it has 366 days).

nextDay

Increment the current date to the next valid day.

nextTradingDay

Increment the current date to the next valid trading day.

Let’s introduce a Date class that implements the operations described in Table 3-1. The declaration for the Date class is the following:

class Date {

public:

    Date(int year, int month, int day);

    Date(const Date &p);

    ~Date();

    Date &operator=(const Date &p);

    void setHolidays(const std::vector<Date> &days);

    std::string month();

    std::string dayOfWeek();

    void add(int numDays);

    void addTradingDays(int numDays);

    void subtract(int numDays);

    void subtractTradingDays(int numDays);

    int dateDifference(const Date &date);

    int tradingDateDifference(const Date &date);

    DayOfTheWeek dayOfTheWeek();

    bool isHoliday();

    bool isWeekDay();

    Date nextTradingDay();

    bool isLeapYear();

    bool isTradingDay();

    void print();

    Date &operator ++();

    Date &operator --();

    bool operator<(const Date &d) const;

    bool operator==(const Date &d);

private:

    int m_year;

    int m_month;

    int m_day;

    DayOfTheWeek m_weekDay;

    std::vector<Date> m_holidays;

};

Notice that the data members for this class store the year, month, and day, which are passed to the constructor. There are two other data members:

m_weekDay, which stores the current day of the week (if it is known)

m_holidays, which stores a list of given holidays

Computing the Day of the Week

First, let’s consider how to calculate the day of the week, given the desired date as input. One way to calculate the day of the week is by adding days starting from January 1, 1900, which was a Monday. This process may be suboptimal, but it can be improved by storing the result on member variable m_weekDay so that it doesn’t need to be recomputed.

The member function, called dayOfTheWeek(), is implemented as follows:

DayOfTheWeek Date::dayOfTheWeek()

{

    if (m_weekDay != DayOfTheWeek_UNKNOWN)

       return m_weekDay;

    int day = 1;  // Monday

    Date d(1900, 1, 1);

    for (;d < *this; ++d)

    {

        if (day == 6) {

           day = 0;

        }

        else {

           day++;

        }

    }

    m_weekDay = static_cast<DayOfTheWeek>(day);

    return m_weekDay;

}

The logic of this method is simple: just iterate through dates, and move from Monday to Tuesday, etc., until we get to the current date. The date is increased using the ++ operator.

Using the ++ Operator

In C++, operator++ is an increment operator that is used to increase the value of a variable by one. It is also known as the "unary increment operator" because it operates on a single operand.

There are two versions of the operator++ in C++: pre-increment and post-increment. The pre-increment operator (++variable) increments the value of the variable before its use in an expression, whereas the post-increment operator (variable++) increments the value of the variable after its use in an expression.

It is important to remember some facts about this C++ operator:

The operator++ can be used with any numeric data type, including integers, floating-point numbers, and pointers.

The pre-increment and post-increment operators have different return types. The pre-increment operator returns a reference to the incremented variable, while the post-increment operator returns a copy of the original value of the variable.

The operator++ can be overloaded for user-defined types, which allows the programmer to define its behavior for custom classes and structures. For example, the operator can be defined to increment a value in the object or to modify some other aspect of the object's state.

The operator++ can also be used in combination with other operators, such as the addition operator (+). For example, the expression a = b++ + c will increment the value of b after it is used in the addition operation.

The operator++ can be used with great effect in the implementation of the Date class. This member function in fact overrides the default autoincrement ++. It will update the object so that it represents the next valid date. In most cases, only the m_day field needs to be incremented. However, when the day is 28, 29, 30, or 31, both the month and day need to be updated. Then, the right thing to do depends on the month, as shown in the following code fragment:

    if (m_day == 31)

    {

        m_day = 1;

        m_month++;

    }

    else if (m_day == 30 &&

             std::find(monthsWithThirtyOneDays.begin(),

                       monthsWithThirtyOneDays.end(), m_month)

                    == monthsWithThirtyOneDays.end())

    {

        m_day = 1;

        m_month++;

    }

    else if (m_day == 29 && m_month == 2)

    {

        m_day = 1;

        m_month++;

    }

    // ...

The logic behind this method is that we need to check the month we’re in, so as to determine the right way to increment date. For example, if the date is 30, we need to check if we’re not in a month that has 31 days, in which case we need to move to day 1 and increase the month.

In this code, monthsWithThirtyOneDays is an std::vector containing a set of months that have 31 days. The test performed by the code with std::find checks that the current month is not found in the vector. Other tests are analogous to this example. Similarly, operator-- is another operation that adjusts the current date to the previous valid date. Its implementation is quite simple: if the current day is 1, it finds the right date based on the number of days in the previous month.

Determining Trading Dates

The isTradingDay member function is very useful when dealing with trading data. It returns true if the current date is not a holiday or a day of the weekend:

// Returns true if not a holiday or a day of the weekend.

//

bool Date::isTradingDay()

{

    if (!isWeekDay()) return false;

    if (m_holidays.size() == 0) return true;

    if (isHoliday()) return false;

    return true;

}

Note

Notice that holidays are different per country, and when used in a realistic application, this code should be updated to consider international holiday dates.

Most other functions of the Date class are implemented based on these primitive functions. For example, here is how you can add days to the current date:

void Date::add(int numDays)

{

    for (int i=0; i<numDays; ++i)

    {

        ++*this;

    }

}

The difficult part was dealt with on the operation++ method. All the code needs to do is iterate through dates using the ++ operator to reach the desired date.

And here is how you can add trading days to the current date. Initially, you find the first trading day starting from the given date. Then, for each trading day, add one to the current date and skip all upcoming nontrading days. The implementation is as follows:

void Date::addTradingDays(int numDays)

{

    while (!isTradingDay())

    {

        ++*this;

    }

    for (int i=0; i<numDays; ++i)

    {

        ++*this;

        while (!isTradingDay())

        {

            ++*this;

        }

    }

}

Complete Listing

Here, you can find the complete code for the Date class. Listing 3-1 contains the header file, and Listing 3-2 shows the implementation file for Date.

The header file contains an enumeration called DayOfTheWeek, which is used to initialize the week day more easily. Similarly, the enumeration Month is used to create symbols for each month of the year. The class Date contains the member functions discussed in the previous section.

//

//  Date.h

#ifndef __CppOptions__Date__

#define __CppOptions__Date__

#include <vector>

#include <string>

enum DayOfTheWeek {

    DayOfTheWeek_Sunday,

    DayOfTheWeek_Monday,

    DayOfTheWeek_Tuesday,

    DayOfTheWeek_Wednesday,

    DayOfTheWeek_Thursday,

    DayOfTheWeek_Friday,

    DayOfTheWeek_Saturday,

    DayOfTheWeek_UNKNOWN

};

enum Month {

    Month_January = 1,

    Month_February,

    Month_March,

    Month_April,

    Month_May,

    Month_June,

    Month_July,

    Month_August,

    Month_September,

    Month_October,

    Month_November,

    Month_December,

};

class Date {

public:

    Date(int year, int month, int day);

    Date(const Date &p);

    ~Date();

    Date &operator=(const Date &p);

    void setHolidays(const std::vector<Date> &days);

    std::string month();

    std::string dayOfWeek();

    void add(int numDays);

    void addTradingDays(int numDays);

    void subtract(int numDays);

    void subtractTradingDays(int numDays);

    int dateDifference(const Date &date);

    int tradingDateDifference(const Date &date);

    DayOfTheWeek dayOfTheWeek();

    bool isHoliday();

    bool isWeekDay();

    Date nextTradingDay();

    bool isLeapYear();

    bool isTradingDay();

    void print();

    Date &operator ++();

    Date &operator --();

    bool operator<(const Date &d) const;

    bool operator==(const Date &d);

private:

    int m_year;

    int m_month;

    int m_day;

    DayOfTheWeek m_weekDay;

    std::vector<Date> m_holidays;

};

#endif /* defined(__CppOptions__Date__) */

Listing 3-1

Interface of the Date Class

//

//  Date.cpp

//  CppOptions

#include "Date.h"

#include <string>

#include <iostream>

#include <algorithm>

using std::cout;

using std::endl;

using std::string;

Date::Date(int year, int month, int day)

: m_year(year),

  m_month(month),

  m_day(day),

  m_weekDay(DayOfTheWeek_UNKNOWN)

{

}

Date::~Date()

{

}

Date::Date(const Date &p)

: m_year(p.m_year),

  m_month(p.m_month),

  m_day(p.m_day),

  m_weekDay(p.m_weekDay),

  m_holidays(p.m_holidays)

{

}

Date &Date::operator=(const Date &p)

{

    if (&p != this)

    {

        m_day = p.m_day;

        m_month = p.m_month;

        m_year = p.m_year;

        m_weekDay = p.m_weekDay;

        m_holidays = p.m_holidays;

    }

    return *this;

}

bool Date::operator<(const Date &d) const

{

    if (m_year < d.m_year) return true;

    if (m_year == d.m_year && m_month < d.m_month) return true;

    if (m_year == d.m_year && m_month == d.m_month

                           && m_day < d.m_day) return true;

    return false;

}

bool Date::operator==(const Date &d)

{

    return d.m_day == m_day && d.m_month == m_month

                            && d.m_year == m_year;

}

void Date::setHolidays(const std::vector<Date> &days)

{

    m_holidays = days;

}

bool Date::isHoliday()

{

    return std::find(m_holidays.begin(), m_holidays.end(), *this)

                  != m_holidays.end();

}

// The member function Date::month() returns a string representation

// for the month stored in the Date object.

// It converts enumeration values to month strings.

//

std::string Date::month()

{

    switch (m_month) {

        case Month_January:  return "January";

        case Month_February: return "February";

        case Month_March:  return "March";

        case Month_April:  return "April";

        case Month_May:    return "May";

        case Month_June:   return "June";

        case Month_July:   return "July";

        case Month_August: return "August";

        case Month_September: return "September";

        case Month_October:   return "October";

        case Month_November:  return "November";

        case Month_December:  return "December";

        default: throw std::runtime_error("unknown month");

    }

    return "";

}

std::string Date::dayOfWeek()

{

    switch (this->dayOfTheWeek()) {

        case DayOfTheWeek_Sunday: return "Sunday";

        case DayOfTheWeek_Monday: return "Monday";

        case DayOfTheWeek_Tuesday: return "Tuesday";

        case DayOfTheWeek_Wednesday: return "Wednesday";

        case DayOfTheWeek_Thursday: return "Thursday";

        case DayOfTheWeek_Friday: return "Friday";

        case DayOfTheWeek_Saturday: return "Saturday";

        default: throw std::runtime_error("unknown day of week");

    }

}

void Date::add(int numDays)

{

    for (int i=0; i<numDays; ++i)

    {

        ++*this;

    }

}

void Date::addTradingDays(int numDays)

{

    while (!isTradingDay())

    {

        ++*this;

    }

    for (int i=0; i<numDays; ++i)

    {

        ++*this;

        while (!isTradingDay())

        {

            ++*this;

        }

    }

}

void Date::subtract(int numDays)

{

    for (int i=0; i<numDays; ++i)

    {

        --*this;

    }

}

void Date::subtractTradingDays(int numDays)

{

    while (!isTradingDay())

    {

        --*this;

    }

    for (int i=0; i<numDays; ++i)

    {

        --*this;

        while (!isTradingDay())

        {

            --*this;

        }

    }

}

/*

The dateDifference method is responsible for returning the difference between the current date and the date passed as a parameter. This is useful to determine the number of days since or until a particular target date.

The logic of the method is different if the target date is in the future or in the past. In the first case, we create a copy of the current date and increment its value until we reach the target date. Then, we return the number of times we had to increment. In the second case, the logic is similar, but the algorithm decrements the current date, until the target date is reached.

*/

int Date::dateDifference(const Date &date)

{

    Date d = *this;

    if (d < date)

    {

        int diff=0;

        while (d < date)

        {

            ++d;

            ++diff;

        }

        return diff;

    }

    int diff=0;

    while (date < d)

    {

        --d;

        --diff;

    }

    return diff;

}

/*

The method tradingDateDifference is similar to the previous method, but it only counts the number of trading days between two dates. The logic is analogous to what was described previously.

*/

int Date::tradingDateDifference(const Date &date)

{

    Date d = *this;

    if (d < date)

    {

        int diff=0;

        while (!d.isTradingDay()) ++d;

        while (d < date)

        {

            ++d;

            ++diff;

            while (!d.isTradingDay()) ++d;

        }

        return diff;

    }

    int diff=0;

    while (!d.isTradingDay()) --d;

    while (date < d)

    {

        --d;

        --diff;

        while (!d.isTradingDay()) --d;

    }

    return diff;

}

DayOfTheWeek Date::dayOfTheWeek()

{

    if (m_weekDay != DayOfTheWeek_UNKNOWN) return m_weekDay;

    int day = 1;

    Date d(1900, 1, 1);

    for (;d < *this; ++d)

    {

        if (day == 6) day = 0;

        else day++;

    }

    m_weekDay = static_cast<DayOfTheWeek>(day);

    return m_weekDay;

}

bool Date::isWeekDay()

{

    DayOfTheWeek dayOfWeek = dayOfTheWeek();

if (dayOfWeek == DayOfTheWeek_Sunday || dayOfWeek == DayOfTheWeek_Saturday)

    {

        return false;

    }

    return true;

}

bool Date::isTradingDay()

{

    if (!isWeekDay()) return false;

    if (m_holidays.size() == 0) return true;

    if (isHoliday()) return false;

    return true;

}

Date Date::nextTradingDay()

{

    Date d = *this;

    if (d.isTradingDay())

    {

        return ++d;

    }

    while (!d.isTradingDay())

    {

        ++d;

    }

    return d;

}

/*

The method isLeapYear checks if the current year is a leap year. The logic is simple: a year is a leap year if it is divisible by 4, unless it is a century year (divisible by 100) and this century year is not divisible by 400.

*/

bool Date::isLeapYear()

{

    if (m_year % 4 != 0)   return false;

    if (m_year % 100 != 0) return true;

    if (m_year % 400 != 0) return false;

    return true;

}

/*

The next two methods implement the -- and ++ operators. These methods do the work of adjusting dates forward and backward, according to the month and year. The tests are straightforward, but you need to consider different situations, like leap years, months that end on 31 days, etc.

*/

Date &Date::operator--()

{

    if (m_weekDay != DayOfTheWeek_UNKNOWN) // update weekday

    {

        if (m_weekDay == DayOfTheWeek_Sunday)

            m_weekDay = DayOfTheWeek_Saturday;

        else

            m_weekDay = static_cast<DayOfTheWeek>(m_weekDay - 1);

    }

    if (m_day > 1)

    {

        m_day--;

        return *this;

    }

    if (m_month == Month_January)

    {

        m_month = Month_December;

        m_day = 31;

        m_year--;

        return *this;

    }

    m_month--;

    if (m_month == Month_February)

    {

        m_day = isLeapYear() ? 29 : 28;

        return *this;

    }

    // List of months with 31 days

    std::vector<int> monthsWithThirtyOneDays = {

           1, 3, 5, 7, 8, 10, 12

    };

    if (std::find(monthsWithThirtyOneDays.begin(),

                  monthsWithThirtyOneDays.end(), m_month)

               != monthsWithThirtyOneDays.end())

    {

        m_day = 31;

    }

    else

    {

        m_day = 30;

    }

    return *this;

}

Date &Date::operator++()

{

    // List of months with 31 days

    std::vector<int> monthsWithThirtyOneDays = {

        1, 3, 5, 7, 8, 10, 12

    };

    if (m_day == 31)

    {

        m_day = 1;

        m_month++;

    }

    else if (m_day == 30 &&

             std::find(monthsWithThirtyOneDays.begin(),

                       monthsWithThirtyOneDays.end(), m_month)

                    == monthsWithThirtyOneDays.end())

    {

        m_day = 1;

        m_month++;

    }

    else if (m_day == 29 && m_month == 2)

    {

        m_day = 1;

        m_month++;

    }

    else if (m_day == 28 && m_month == 2  && !isLeapYear())

    {

        m_day = 1;

        m_month++;

    }

    else

    {

        m_day++;

    }

    if (m_month > 12)

    {

        m_month = 1;

        m_year++;

    }

    if (m_weekDay != DayOfTheWeek_UNKNOWN) // update weekday

    {

        if (m_weekDay == DayOfTheWeek_Saturday)

            m_weekDay = DayOfTheWeek_Sunday;

        else

            m_weekDay = static_cast<DayOfTheWeek>(m_weekDay + 1);

    }

    return *this;

}

void Date::print()

{

    cout << m_year << "/" << m_month << "/" << m_day << endl;

}

// Finally, the following main function performs a basic test of

// the functionality for this class.

int main()

{

    Date d(2015, 9, 12);

    DayOfTheWeek wd = d.dayOfTheWeek();

    cout << " day of the week: " << wd <<  " "

         << d.dayOfWeek() <<   endl;

    d.print();

    d.add(25);

    d.print();

    d.addTradingDays(120);

    d.print();

    cout << " day of the week: " << d.dayOfTheWeek()

         <<  " " << d.dayOfWeek() <<   endl;

    return 0;

}

Listing 3-2

Implementation File of the Date Class

A Compact Date Representation

While the Date class presented in the previous section is an adequate implementation of the concept of dates in C++, it still may not be perfect for all applications. One problem with it is that you need to use integers to store each of the different parts of the date, which include year, month, and day. In today’s common 64-bit CPU, this takes 24 bytes, which is a lot of space for such a small piece of information.

There are a few ways that you can improve the memory use for Date objects. In this section, I explain how to do this using a simple format for date storage that uses a character string. If you use 4 bytes for the year and 2 bytes for the month as well as the day, the required memory is reduced to just 8 bytes. This format is also commonly used as a date stamp in several applications, so it is easy to verify the correctness of a particular date.

To show how this implementation works, I created a new class called DateCompact, which is a compact representation of Date objects. I only present a few of the operations required from this data type to avoid duplication of the previous code, but you can implement all other methods provided in the Date class using the underlying representation provided by DateCompact.

The only date member of class DateCompact is a string, declared using the old-style array type of C, for compactness:

    char m_date[8];

Dates are stored using the following member functions:

    void setYear(int y);

    void setMonth(int m);

    void setDay(int d);

These dates can be retrieved using three corresponding methods:

    int year();

    int month();

    int day();

For example, to store the year, you just need to convert the given number into a fourcharacter string:

void DateCompact::setYear(int year)

{

    m_date[3] = '0' + (year % 10);  year /= 10;

    m_date[2] = '0' + (year % 10);  year /= 10;

    m_date[1] = '0' + (year % 10);  year /= 10;

    m_date[0] = '0' + (year % 10);

}

You need to add each number to the character '0' so that the resulting string is printable. The reverse process is easy; you just need to add the characters in the right way:

int DateCompact::year()

{

    // (x - '0')  computes the numeric value

    // corresponding to each character.

    return  1000 * (m_date[0] - '0') + 100 * (m_date[1] - '0')

            + 10 * (m_date[2] - '0') +       (m_date[3] - '0');

}

The comparison operators can be easily implemented with the help of the strncmp function from the C string library. The function strncmp returns a negative number if the first argument is lexicographically less than the first, a positive number if the first argument is greater than the second, and 0 if the two strings are equal. For example, the equality operator can be implemented as follows:

bool DateCompact::operator==(const DateCompact &d) const

{

    return strncmp(m_date, d.m_date, 8) == 0;

}

Similarly, the less than operator has the following implementation:

bool DateCompact::operator<(const DateCompact &d) const

{

    // strcmp returns negative values if the

    // first argument is less than the second.

    return strncmp(m_date, d.m_date, 8) < 0;

}

Complete Listings

The full code for the DateCompact class, described in the previous section, is presented in Listings 3-3 and 3-4.

//

//  DateCompact.h

#ifndef __CppOptions__DateCompact__

#define __CppOptions__DateCompact__

//

// A compact representation for dates, using a character string

//

class DateCompact {

public:

    DateCompact(int year, int month, int day);

    DateCompact(const DateCompact &p);

    ~DateCompact();

    DateCompact &operator=(const DateCompact &p);

    void setYear(int y);

    void setMonth(int m);

    void setDay(int d);

    int year();

    int month();

    int day();

    void print();

    bool operator==(const DateCompact &d) const;

    bool operator<(const DateCompact &d) const;

private:

    char m_date[8];

};

#endif /* defined(__CppOptions__DateCompact__) */

Listing 3-3

Interface of the DateCompact Class

//

//  DateCompact.cpp

//

//  Implementation for the DateCompact class

#include "DateCompact.h"

#include <cstring>

#include <iostream>

using std::cout;

using std::endl;

DateCompact::DateCompact(int year, int month, int day)

{

    setYear(year);

    setMonth(month);

    setDay(day);

}

DateCompact::DateCompact(const DateCompact &p)

{

    strcpy(m_date, p.m_date);

}

DateCompact::~DateCompact()

{

}

DateCompact &DateCompact::operator=(const DateCompact &p)

{

    if (&p != this)

    {

        strcpy(m_date, p.m_date);

    }

    return *this;

}

//

// Use string comparison to determine if the dates are equal.

//

bool DateCompact::operator==(const DateCompact &d) const

{

    return strncmp(m_date, d.m_date, 8) == 0;

}

// Use the strncmp function to determine if a date is less than the other.

bool DateCompact::operator<(const DateCompact &d) const

{

    // strcmp returns negative values if the first

    // argument is less than the second.

    return strncmp(m_date, d.m_date, 8) < 0;

}

//

// Functions to calculate the year, month, and days as integers,

// based on the characters contained in the string 'm_date'.

//

int DateCompact::year()

{

    // (x - '0')  computes the numeric value

    // corresponding to each character.

    return  1000 * (m_date[0] - '0') + 100 * (m_date[1] - '0')

            + 10 * (m_date[2] - '0') +       (m_date[3] - '0');

}

int DateCompact::month()

{

    return  10 * (m_date[4] - '0') +  (m_date[5] - '0');

}

int DateCompact::day()

{

    return  10 * (m_date[6] - '0') +  (m_date[7] - '0');

}

void DateCompact::print()

{

    // Copy the m_date string into a NULL terminated

    // string (with 9 characters).

    char s[9];

    strncpy(s, m_date, 8);

    s[8] = '\0';             // properly terminate the string

    cout << s << endl;

}

//

// Calculate the string corresponding to the given numeric parameter.

//

void DateCompact::setYear(int year)

{

    m_date[3] = '0' + (year % 10);

    year /= 10;

    m_date[2] = '0' + (year % 10);

    year /= 10;

    m_date[1] = '0' + (year % 10);

    year /= 10;

    m_date[0] = '0' + (year % 10);

 }

void DateCompact::setMonth(int month)

{

    m_date[5] = '0' + (month % 10);  month /= 10;

    m_date[4] = '0' + (month % 10);  month /= 10;

}

void DateCompact::setDay(int day)

{

    m_date[7] = '0' + (day % 10);  day /= 10;

    m_date[6] = '0' + (day % 10);  day /= 10;

}

#include "Date.h"

int main()

{

    DateCompact d(2008, 3, 17);

    DateCompact e(2008, 5, 11);

    cout << " size of DateCompact: " << sizeof(DateCompact) << endl;

    d.print();

    e.print();

    if (d < e)

    {

        cout << " d is less than e " << endl;

    }

    else

    {

        cout << " d is not less than e " << endl;

    }

    Date date(2008, 3, 17);

    cout << " size of Date: " << sizeof(Date) << endl;

    return 0;

}

Listing 3-4

Implementation of the DateCompact Class

Building and Testing

The previous code can be built using any standards-compliant C++ compiler. Here are the commands used to build the application on macOS X using gcc:

cc++ -std=c++2a  –o DateCompact.o –c DateCompact.cpp

cc++ -std=c++2a  –o Date.o –c Date.cpp

cc++ –o main  DateCompact.o Date.o

The main function provides a quick test of the DateCompact class, which also compares the size of the objects created using DateCompact and Date. Notice how Date occupies much more memory than DateCompact.

./DateCompact

size of DateCompact: 8

20080317

20080511

 d is less than e

 size of Date: 48

Using the Standard Chrono Header

In C++23, you can also represent dates using the std::chrono library. Specifically, there are two main classes that can be used to represent dates and time intervals. You can use the std::chrono::system_clock class to get the current date and time, or the std::chrono::time_point class to represent a specific date and time.

The following example shows how to get the current date and time using the system_clock class:

#include <iostream>

#include <chrono>

int main() {

    auto now = std::chrono::system_clock::now();

    std::time_t current_time = std::chrono::system_clock::to_time_t(now);

    std::cout << "Current date and time: " << std::ctime(&current_time) << std::endl;

    return 0;

}

In this code, the method call std::chrono::system_clock::now() returns an std::chrono::time_point object representing the current time, which is saved to the current_time variable. We then use the std::chrono::system_clock::to_time_t() function to convert this time point to an std::time_t value. Next, the code uses the std::ctime() method to get a human-readable string representing the current date and time.

If you need to represent a specific date and time, the chrono header file also provides a simple way to do that. In this case, you can use the std::chrono::time_point class. Here's an example:

#include <iostream>

#include <chrono>

int main() {

    std::chrono::system_clock::time_point tp = std::chrono::system_clock::now() + std::chrono::hours(24);

    std::time_t time = std::chrono::system_clock::to_time_t(tp);

    std::cout << "Date and time in 24 hours: " << std::ctime(&time) << std::endl;

}

Working with Networks

As a final example of C++ flexibility, we will discuss an example of handling network structures. Network structures commonly appear in many fields of software development. Such networks are ideal for representing the connections between entities as diverse as people, investments choices, trading institutions, countries, or sale contracts. In financial applications, for example, elements of a network may represent stocks or other asset classes. Connections between elements of the network may represent correlation between assets, among other uses. This type of algorithm is used in the analysis of investment portfolios, for example.

In this section, I provide an overview of networks and explain how they can be presented in C++ applications. The particular example used demonstrates the way in which such algorithms can be designed and implemented.

The sample problem presented here is called word production. A word is a sequence of characters, and it can represent, among other things, stock tickers in a financial application, for example. Therefore, IBM and CAT (stock tickers for companies IBM and Caterpillar) may be viewed as application-specific words. These elements are then stored in a dictionary of useful words. The word production problem determines how a word can be derived from another using a dictionary. For example, the word CAT can be derived from the word CAR by just changing a single character. Complex string production can be performed using multiple productions. Therefore, using this rule, it is possible to connect elements of a dictionary using a set of links, where each link represents a single word production.

In the string-production problem, you are given a starting and a destination word. You also have a dictionary of words (e.g., a set of stock tickers that you may be interested in trading). Then, the goal is to find the shortest set of productions that can connect the initial word to the final word. For a concrete example, consider the dictionary containing the words LOB, DAG, LOG, CAR, DOG, CAT, COB, CAB, and CAG. If you start from the word CAT and end with the word DOG, a possible solution to the problem is this sequence:

CAT, CAG, DAG, and DOG

This is not a unique solution, but it has minimum size (three productions). Another candidate solution is

CAT, CAB, COB, LOB, LOG, DOG

This is also valid but is clearly not the shortest solution, since it needs more productions than the previous example. For simplicity, it is assumed that all words in the dictionary have the same size.

Creating a Dictionary Class

The first step to solve this problem is to find an efficient representation for the Dictionary object. For this purpose, I created a class that stores the set of words using a vector called m_values. Here is the class definition:

class Dictionary {

public:

    Dictionary(int wordSize);

    ~Dictionary() {}

    Dictionary &operator=(const Dictionary &p); // not implemented

    //   ...

    void addElement(const std::string &s);

    void buildAdjancencyMatrix();

    bool contains(const std::string &s);

    const std::vector<std::vector<bool> > &adjList();

    int elemPosition(const std::string &s);

    int size() { return (int)m_values.size(); }

    std::string elemAtPos(int i);

private:

    std::vector<std::string> m_values;

    std::map<std::string, int> m_valuePositions;

    std::vector<std::vector<bool> > m_adjacencyList;

    int m_wordSize;

};

There are other three member variables used by the class:

m_wordSize is used to store the size of words in the dictionary.

m_valuePositions is a variable used to store a mapping between words and numeric positions.

m_adjacencyList, an adjacency list.

The first step in the implementation is to define member functions that add elements to the dictionary. For example, this is how you add new words to the dictionary:

void  Dictionary::addElement(const string &s)

{

    if (s.size() != m_wordSize)

    {

        throw std::runtime_error("invalid string size");

    }

    m_values.push_back(s);

    m_valuePositions[s] = (int)m_values.size() - 1;

    cout << " added " << s << endl;

}

You can use member functions in std::vector to interact with the underlying m_values collection. In this case, the function uses push_back to add new words of the right size. Notice that when a word is stored, the position of the word is also stored in an std::map named m_valuePositions.

The member function elementAtPos returns the word stored in a certain position of the m_values vector:

string Dictionary::elemAtPos(int i)

{

    return m_values[i];

}

The member function contains returns true if a word is already stored in the dictionary. It uses the find member function of std::map, which when given a map m returns the value associated with the given key when the element is found, or the value m.end() when the element is not in the map.

bool Dictionary::contains(const string &s)

{

    return m_valuePositions.find(s) != m_valuePositions.end();

}

Another feature of the Dictionary class is that it returns the position of an element that has been stored in the vector m_values. To speed up this process, Dictionary uses std::map m_valuePositions, which maps between strings and their respective positions. Using this map, it is possible to define the member function elemPosition. The implementation is straightforward:

int Dictionary::elemPosition(const string &s)

{

    return m_valuePositions[s];

}

Finally, the Dictionary class is responsible for building an adjacency matrix, that is, a matrix that stores the connectivity information for the network of words stored in this dictionary. The way this works is that the matrix has size n by n, where n is the number of words stored. The entries Aij in the matrix are true or false, and true means that the words stored at positions i and j differ by just one character.

The first thing that you need to do is create the adjacency matrix for the given set of words stored in the dictionary. This is done using the buildAdjacencyMatrix member function:

void Dictionary::buildAdjancencyMatrix()

{

    m_adjacencyList.clear();

    int n = (int)m_values.size();

    for (int i=0; i<n; ++i)

    {

        m_adjacencyList.push_back(vector<bool>(n));

        for (int j=0; j<n; ++j)

        {

            if (diffByOne(m_values[i], m_values[j]))

            {

                m_adjacencyList[i][j] = true;

            }

        }

    }

}

The original adjacency data is cleared, and a loop is run through each pair of words stored in m_values. Then, the algorithm checks if the words differ by just one character using the diffByOne member function. If that is true, then the algorithm can set the value of the adjacency to true. The diffByOne algorithm is also straightforward:

bool diffByOne(const string &a, const string &b)

{

    if (a.size() != b.size()) return false;

    int ndiff = 0;

    for (unsigned i=0; i<a.length(); ++i)

    {

        if (a[i] != b[i]) ndiff++;

    }

    return ndiff == 1;

}

You just need to count the number of different characters occurring in both strings. The function returns true only if the number of differences is equal to one.

Calculating a Shortest Path

The challenging part of this algorithm is to find the shortest path between the two given nodes of the network, represented by the initial and final words. There are a few alternative algorithms to find a shortest path, but this implementation uses the well-known Dijkstra’s algorithm.

The central idea of this algorithm is to maintain the known distances starting from the initial node. Then, at each iteration, you can look for the neighbors of a node and see if at least one can reduce the known shortest path by traversing that node. If that is possible, then the shortest path passing through that node is updated. This process continues until all nodes in the network have been considered.

I present a simple implementation of this algorithm in the StringProduction class. The definition of the class is as follows:

class StringProduction {

public:

    StringProduction(Dictionary &d);

    StringProduction(const StringProduction &p);

    ~StringProduction();

    StringProduction &operator=(const StringProduction &p);

    bool produces(const std::string &src,const std::string &dest,

                        std::vector<std::string> &path);

    void shortest_path(int v, int dest, int n,

                       std::vector<std::string> &path);

    std::vector<int> recoverPath(int src, int dest,

               const std::vector<int> &P, std::vector<int> &path);

private:

    Dictionary &m_dic;

};

The StringProduction class keeps a reference to a dictionary, which contains all the nodes in the network for use by the shortest-path algorithm. The central member function for this class is shortest_path, which returns the shortest path between the two given words (which should be part of the underlying dictionary). The first part of the function initializes the data structures used by the algorithm:

    // Initialize the set of distances and the set of nodes

    for (int i = 0; i <n; i++) {

        Q.insert(i);

        if (i != v) {

            dist[i] = INF;

        }

    }

The object named Q has type std::set, and it can quickly add and remove elements that will later be checked by the algorithm. The loop is just adding all nodes to Q and setting the initial distances in the vector dist to a large number (INF). The only exception is the distance between the initial node v and itself, which is known to be zero.

Another important part of the algorithm is the so-called relaxation step, where the distance is updated to the latest known shortest-path value:

        for (int i=0; i<n; ++i){

            if (A[u][i]) {           // nodes u and i are neighbors

                int d = dist [u] + 1;

                if (d < dist[i]) {

                    dist[i] = d;

                    prev[i] = u;

                }

            }

        }

    }

The vector prev stores the node that is known to be the previous one in the shortestpath sequence. The last part of the algorithm is the path-recovery step, where the complete path is retrieved using the information stored in prev:

    vector<int> npath;

    recoverPath(v, dest, prev, npath);

    for (auto elem : npath) {

       path.push_back(m_dic.elemAtPos(elem));

    }

This algorithm uses the member function called recoverPath to find the numeric sequence of nodes used in the shortest path. The for loop then uses that numeric sequence to recover the words from the dictionary. The implementation of the recoverPath method iterates through the previous nodes to construct a sequence:

vector<int> StringProduction::recoverPath(

            int src, int dest, const vector<int> &P,

            vector<int> &path){

    int v = dest;

    while (v != src) {

        path.push_back(v);

        v = P[v];

    }

    path.push_back(src);

    std::reverse(path.begin(),path.end());

    return path;

}

Finally, the produces member function uses the algorithm explained previously to find and return the shortest production. First, it checks that the initial and destination words are stored in the dictionary. Then, the function shortest_path is called with the right parameters. The word sequence is returned using the parameter path. The return value is true if there is a valid sequence with size greater than zero.

bool StringProduction::produces(const string &src,

          const string &dest, vector<string> &path) {

    if (!m_dic.contains(src) || !m_dic.contains(dest))

       return false;

    shortest_path(m_dic.elemPosition(src),

                  m_dic.elemPosition(dest), m_dic.size(), path);

    return path.size() > 0;

}

Complete Listings

Here is the complete listing for the network-based algorithm described in the preceding section. There are five files that contain the full solution. Two files are used for the Dictionary class. Two other files are used for the StringProduction class. Finally, a main file is provided so that you can run a test on the two classes. The files are displayed in Listings 3-5 to 3-9.

//

//  Dictionary.h

#ifndef __StringProduction__Dictionary__

#define __StringProduction__Dictionary__

#include <string>

#include <vector>

#include <map>

//

// Stores the words in the dictionary and provides an adjacency matrix for the words

class Dictionary {

public:

    Dictionary(int wordSize);

    ~Dictionary() {}

    Dictionary &operator=(const Dictionary &p); // not implemented

private:

    Dictionary(const Dictionary &p);            // not implemented

public:

    void addElement(const std::string &s);

    void buildAdjancencyMatrix();

    bool contains(const std::string &s);

    const std::vector<std::vector<bool> > &adjList();

    int elemPosition(const std::string &s);

    int size() { return (int)m_values.size(); }

    std::string elemAtPos(int i);

private:

    std::vector<std::string> m_values;

    std::map<std::string, int> m_valuePositions;

    std::vector<std::vector<bool> > m_adjacencyList;

    int m_wordSize;

};

#endif /* defined(__StringProduction__Dictionary__) */

Listing 3-5

Interface of the Dictionary Class

//

//  Dictionary.cpp

#include "Dictionary.h"

#include <iostream>

#include <vector>

#include <map>

#include <set>

#include <queue>

using std::string;

using std::vector;

using std::set;

using std::map;

using std::cout;

using std::endl;

using std::cerr;

Dictionary::Dictionary(int wordSize)

: m_values(),

m_valuePositions(),

m_adjacencyList(),

m_wordSize(wordSize)

{

}

const std::vector<std::vector<bool> > &Dictionary::adjList()

{

    return m_adjacencyList;

}

Dictionary &Dictionary::operator=(const Dictionary &p)

{

    if (&p != this)

    {

        m_adjacencyList = p.m_adjacencyList;

        m_valuePositions = p.m_valuePositions;

        m_values = p.m_values;

        m_wordSize = p.m_wordSize;

    }

    return *this;

}

//

// True if the words a and b differ by just one character

//

bool diffByOne(const string &a, const string &b)

{

    if (a.size() != b.size()) return false;

    int ndiff = 0;

    for (unsigned i=0; i<a.length(); ++i)

    {

        if (a[i] != b[i]) ndiff++;

    }

    return ndiff == 1;

}

bool Dictionary::contains(const string &s)

{

    return m_valuePositions.find(s) != m_valuePositions.end();

}

int Dictionary::elemPosition(const string &s)

{

    return m_valuePositions[s];

}

void  Dictionary::addElement(const string &s)

{

    if (s.size() != m_wordSize)

    {

        throw std::runtime_error("invalid string size");

    }

    m_values.push_back(s);

    m_valuePositions[s] = (int)m_values.size() - 1;

    cout << " added " << s << endl;

}

string Dictionary::elemAtPos(int i)

{

    return m_values[i];

}

void Dictionary::buildAdjancencyMatrix()

{

    m_adjacencyList.clear();

    int n = (int)m_values.size();

    for (int i=0; i<n; ++i)

    {

        m_adjacencyList.push_back(vector<bool>(n));

        for (int j=0; j<n; ++j)

        {

            if (diffByOne(m_values[i], m_values[j]))

            {

                m_adjacencyList[i][j] = 1;

            }

        }

    }

}

Listing 3-6

Implementation of the Dictionary Class

//

//  StringProduction.h

#ifndef __StringProduction__StringProduction__

#define __StringProduction__StringProduction__

#include <vector>

#include <string>

class Dictionary;

class StringProduction {

public:

    StringProduction(Dictionary &d);

    StringProduction(const StringProduction &p);

    ~StringProduction();

    StringProduction &operator=(const StringProduction &p);

bool produces(const std::string &src, const std::string &dest, std::vector<std::string> &path);

void shortest_path(int v, int dest, int n, std::vector<std::string> &path);

std::vector<int> recoverPath(int src, int dest, const std::vector<int> &P, std::vector<int> &path);

private:

    Dictionary &m_dic;

};

#endif /* defined(__StringProduction__StringProduction__) */

Listing 3-7

Interface of the StringProduction Class

//

//  StringProduction.cpp

#include "StringProduction.h"

#include "Dictionary.h"

#include <algorithm>

#include <climits>

#include <map>

#include <set>

using std::vector;

using std::string;

using std::map;

using std::set;

StringProduction::StringProduction(Dictionary &d)

: m_dic(d)

{

}

StringProduction::StringProduction(const StringProduction &p)

: m_dic(p.m_dic)

{

}

StringProduction::~StringProduction()

{

}

StringProduction &StringProduction::operator=(const StringProduction &p)

{

    if (&p != this) {

        m_dic = p.m_dic;

    }

    return *this;

}

//

// Recovers the path from a list of previous nodes (P)

vector<int> StringProduction::recoverPath(int src, int dest, const vector<int> &P, vector<int> &path){

    int v = dest;

    while (v != src) {

        path.push_back(v);

        v = P[v];

    }

    path.push_back(src);

    std::reverse(path.begin(),path.end());

    return path;

}

//

// Computes the shortest path.

// Node v is the source, dest is destination. If the path can be found, it is stored on parameter path

void StringProduction::shortest_path(int v, int dest, int n, vector<string> &path)

{

    const std::vector<std::vector<bool> > &A = m_dic.adjList(); //

get the adjacency matrix

    path.clear();

    vector<int> dist(n, 0);

    vector<int> prev(n, 0);

    set<int> Q;              // set of nodes

    const int INF = INT_MAX; // a large number

    // Initialize the set of distances and the set of nodes

    for (int i = 0; i <n; i++) {

        Q.insert(i);

        if (i != v) {

            dist[i] = INF;

        }

    }

    // This is Dijkstra's algorithm

    while (!Q.empty()) {

        int min = INF;

        int u = -1;

        for (set<int>::iterator it = Q.begin(); it != Q.end(); ++it) {

            // Find the minimum value in queue

            if (dist[*it] < min) {

                min = dist[*it];

                u = *it;

            }

        }

        Q.erase(u);   // remove min vertex u from set

        // Relaxation step

        for (int i=0; i<n; ++i){

            if (A[u][i]) {              // this is a neighbor

                int d = dist [u] + 1;

                if (d < dist[i]) {

                    dist[i] = d;

                    prev[i] = u;

                }

            }

        }

    }

    // Recover the path from vector prev

    vector<int> npath;

    recoverPath(v, dest, prev, npath);

    for (auto elem : npath) {

       path.push_back(m_dic.elemAtPos(elem));

    }

}

//

// Returns true if the word src produces dest using the

// dictionary dic. If true, then path will contain the path

// between src and dest.

//

bool StringProduction::produces(const string &src,

            const string &dest, vector<string> &path) {

    if (!m_dic.contains(src) || !m_dic.contains(dest)) return false;

    shortest_path(m_dic.elemPosition(src),

                  m_dic.elemPosition(dest), m_dic.size(), path);

    return path.size() > 0;

}

Listing 3-8

Implementation of the StringProduction Class

//

//  main.cpp

//  StringProduction

//

#include "StringProduction.h"

#include "Dictionary.h"

#include <iostream>

using std::vector;

using std::string;

using std::cout;

using std::endl;

//

// main function is a test case for the algorithm.

//

int main(int argc, const char * argv[]) {

    if (argc != 3) {

        cout << "prog word1 word2" << endl;

        return 1;

    }

    Dictionary dic(3);

    dic.addElement("lob");

    dic.addElement("dag");

    dic.addElement("log");

    dic.addElement("car");

    dic.addElement("dog");

    dic.addElement("cat");

    dic.addElement("cob");

    dic.addElement("cab");

    dic.addElement("cag");

    dic.buildAdjancencyMatrix();

    vector<string> path;

    StringProduction sp(dic);

    if (sp.produces(argv[1], argv[2], path)) {

        cout << " -- the first string produces the second" << endl;

        cout << " -- that path has size " << path.size()   << ":\n";

        for (unsigned i=0; i<path.size(); ++i) {

            cout << path[i] << "; ";

        }

    } else {

        cout << " the second string does not produce the second"

             << endl;

    }

    return 0;

}

Listing 3-9

The main Function with a Simple Test for the StringProduction Class

Building and Testing

You can build the code presented in the last section using any standards-compliant C++ compiler. I tested the code on Linux and macOS X. The commands used to build the project in gcc are the following:

gcc -o StringProduction.o -c StringProduction.cpp

gcc -o Dictionary.o –c Dictionary.cpp

gcc -o main.o –c main.cpp

gcc -o StringProduction Dictionary.o StringProduction.o main.o

The main function contains test code that creates a new Dictionary object, inserts a small set of words, and uses the StringProduction class to calculate the shortest path. Here is a sample of the generated output in my system:

./StringProduction cat dog

added lob

added dag

added log

added car

added dog

added cat

added cob

added cab

added cag

-- the first string produces the second

-- that path has size 4:

cat; cag; dag; dog;

A quick note about the complexity of this algorithm. As explained, the Dijkstra’s algorithm for shortest-path calculation is used. The current implementation uses a matrix of adjacencies, with complexity O(n²), where n is number of words in the dictionary. This could be improved using more complex implementation schemes (such as adjacency lists and priority queues); however, I decided to use the simplest data structures in order to concentrate on the algorithm itself.

Conclusion

In this chapter, I presented a few basic algorithms implemented in C++23. These algorithms provide examples of how to solve computational problems using C++ and containers from the STL. You read an overview of two interesting problems with financial applications: date calculation and shortest paths on data networks.

The first two sections dealt with date representations and their associated operations. Dates are needed in nearly all financial- and derivative-related applications. They are an intrinsic part of time series for prices, volatility, and other financial information used in the analysis of derivatives. You saw how to implement commonly used functions to manipulate dates, such as adding and subtracting dates, finding trade dates, and computing date intervals. You have also learned how to design a compact date representation so that only a small amount of memory is necessary to store a large number of date objects.

Finally, I discussed the common problem of implementing a network, with nodes that represent individual data elements and connections between these nodes. I discussed a simple problem based on a dictionary of strings, which can represent stocks from a universe of interest, for example. Then, you learned how to create an algorithm that calculates the shortest paths between elements of this basic dataset.

In the next chapter, you will see more examples of using C++ for financial programming. This time, you will learn more about object-oriented techniques, including how they can be used to create high-performance applications to process options and derivative contracts.

© The Author(s), under exclusive license to APress Media, LLC, part of Springer Nature 2023

C. OliveiraOptions and Derivatives Programming in C++23https://doi.org/10.1007/978-1-4842-9827-5_4

4. Object-Oriented Techniques

Carlos Oliveira1  

(1)

Seattle, WA, USA

For the last 30 years, object-oriented techniques have become the standard for software design and development. Since C++ fully supports OO programming, it is essential that you have a good understanding of OO techniques in order to solve many of the challenges presented by options and derivatives programming.

This chapter presents a practical summary of the programming topics you need to understand in order to become proficient in the relevant OO concepts and techniques used in the field of options and derivatives analysis. Some of the topics covered in this chapter include the following:

Fundamental OO concepts in C++: A quick review of object-oriented concepts as implemented in C++, with examples based on derivatives and options

Problem partitioning: How to partition a problem into classes and related OO concepts using specific C++ techniques

Designing a solution: How to use classes and objects to solve problems in financial engineering

Reusing OO components: How to create reusable C++ components that can be integrated to your own full-scale applications, or even distributed as an external library

OO Programming Concepts

Object-oriented programming provides a set of principles that can facilitate the development of computer software. Using OO programming techniques, you can easily organize your code and create high-level abstractions for application logic and commonly used component libraries. In this way, OO techniques can be used to improve and reuse existing components, as well as simplify the overall development. OO programming promotes a way of creating software that uses logical elements operating at a higher level of abstraction.

When considering different styles of software programming, it is important to use tools and languages that provide an adequate level of support for the desired programming style. C++ was designed to be a multiparadigm programming language (see Figure 4-1); therefore, it can properly support more than one style of programming, including the following:

Structured programming: In structured programming, code is organized in terms of functions and data structures. Each function uses standard control flow structures, such as for, while, do, and if/then/else, to organize code. While this programming style was previously used in isolation, nowadays it is more commonly used as part of an OO or functional approach.

Functional programming: In this style of programming, functions are the most important element of composition. Functions are also used as first-class citizens: they can be stored and passed as parameters to other functions in this programming paradigm. The C++11 standard has improved support for functional programming, as seen in Chapter 8.

Generic, or template-based programming: Templates allow programmers to create parameterized types. Such types can be used to implement concepts that are independent of the specific type employed. A common example is a container class such as std::vector, which can be used to store values of any type in a sequence of elements stored in contiguous memory.

Object-oriented programming: A programming style where code is organized in classes and shared in the form of objects. In the OO paradigm, objects can respond to operations that are implemented as member functions in C++. Encapsulation and inheritance are common mechanisms used to support the implementation of OO systems.

4 lists of concepts in programming paradigms. A. Structured includes data structure, functions, and control flow. B. O O includes classes, polymorphism, and encapsulation. C. Functional includes closures, currying, and recursion. D. Template includes templates, containers, and generic algorithms.

Figure 4-1

A comparison of concepts used in four programming paradigms enabled by C++

C++ offers complete support for OO concepts. Some of these support elements have already been used in the previous chapters of this book, including classes, objects that can be instantiated from these classes, as well as their members such as constructors and destructors, among others. In this chapter, you will learn more about OO concepts that are frequently used in real-world applications, with examples that are directly used in the implementation of options and derivatives in C++.

Remember that the main elements of OO programming can be summarized as follows:

Encapsulation: This concept refers to the division of programmatic responsibilities into different language elements. C++ offers classes that can be used to encapsulate desired functionality in a clear way. When planning applications and coding them in C++, it is always a good idea to determine the main concepts that need to be represented as classes and encapsulate the related procedural code into member functions of that class.

Inheritance: C++ allows programmers to extend a class with new operations. This is possible through the concept of inheritance, when a new class assumes all operations previously available in an existing class, called its parent. Inheritance also allows programmers to add new functionality to existing classes, through the inclusion of new member functions that provide the required functionality.

Polymorphism: Inherited classes in C++ extend available classes through the addition of new member functions. Inherited classes also modify the behavior of existing member functions that have been marked with the virtual keyword. Polymorphism in C++ is defined through the use of virtual functions, which are then dispatched using a virtual function table, as implemented by most compilers.

Although C++ provides much more than pure OO programming, these elements alone can nonetheless be used to create very complex and efficient applications in various areas, such as the case of financial applications. In the remaining of this chapter, you will see how these OO concepts can be utilized to solve problems occurring on financial derivatives.

Note

Software development using OO techniques not only allows separation between implementation and interface but also requires the clear definition of such concepts. A good C++ programmer will excel at decomposing problems into smaller components, which can then be coded into separate classes. While I can only give examples of this process in this book, design and analysis of OO software are a complex and important phase that should be part of your effort during each software project.

Encapsulation

The idea of encapsulation is to define abstract operations that can be implemented by a single class. Once these operations have been made available through the class interface, clients of a class can use them without being exposed to the internal details of the implementation such as variables, constants, and other internal code that is only used locally to implement the required features.

The most important aspect of encapsulation is the ability to hide data, which then becomes the member variables of the target class. Consider, for example, a class that represents a credit default swap. The class should contain enough information to determine how to store and trade such financial instruments. For an example of data that must be encapsulated into such a class, you might want to consider the following:

Underlying instrument: The financial instrument that is the basis for the contract. It could be, for example, a set of bonds for a particular company, cash, or some other preestablished financial instrument.

Counterpart: The institution that is the target of the default swap payments. The payment is generally made when the target institution defaults.

Payoff value: The monetary value of the default swap contract. This payoff is transferred between institutions if the contract payment condition is triggered.

Term: The term of the contract, after which it ceases to exist.

Spread cost: The recurring payment made by the buyer to maintain the contract. Many contracts require equal payments of a spread that is due at regular periods, such as every month or every year.

By using encapsulation to represent a CDS contract, a C++ developer can simply create a class that contains all these data elements. For example, here is a simple CDS class that represents the concepts described previously. We first present an enumeration of possible underlying values.

Note

An enum, such as CDSUnderlying, is a technique used in C and C++ to define related constants with different integer values, which can later be reused in the code.

enum CDSUnderlying {

    CDSUnderlying_Bond,

    CDSUnderlying_Cash,

    // Other values here...

};

class CDSContract {

public:

    CDSContract();

    CDSContract(const CDSContract &p);

    ~CDSContract();

    CDSContract &operator=(const CDSContract &p);

    // Other member functions here...

private:

    std::string m_counterpart;

    CDSUnderlying m_underlying;

    double m_payoff;

    int m_term;

    double m_spreadCost;

};

With this definition, you encapsulate all the information that corresponds to a CDS contract into a single class. Because the data members are private, this means that only variables and methods internal to the class can access elements defined as private. The main advantage of such an arrangement is that no code outside the CDSContract class is allowed to access the private data, achieving true encapsulation.

If it is necessary to provide access to one or more data members of a class, there are two options. The data member could be moved to the public section of the class, but this would make it possible for the data member to change without knowledge of the CDSContract class.

A better way of doing this is to provide an access member function in a case-by-case way. You could, for example, allow the counterpart and payoff member variables to be accessed by other objects through member functions, as shown here:

class CDSContract {

public:

    CDSContract();

    CDSContract(const CDSContract &p);

    ~CDSContract();

    CDSContract &operator=(const CDSContract &p);

    std::string counterpart() { return m_counterpart; }

    void setCounterpart(const std::string &s) { m_counterpart = s;    }

    double payoff() { return m_payoff;   }

    void setPayoff(double payoff) { m_payoff = payoff; }

private:

    std::string m_counterpart;

    CDSUnderlying m_underlying;

    double m_payoff;

    int m_term;

    double m_spreadCost;

};

Using this strategy, any change happening to the m_counterpart and m_payoff will occur only through an operation on the CDSContract class. This means that the class can react to any changes in these values, providing proper encapsulation of that data. For example, suppose that you want to reset the payoff value whenever the counterpart for the CDS contracts changes. This could be done the following way:

class CDSContract {

public:

    // ...

    std::string counterpart() { return m_counterpart; }

    void setCounterpart(const std::string &s);

    double payoff() { return m_payoff;   }

    void setPayoff(double payoff) { m_payoff = payoff; }

private:

    std::string m_counterpart;

    CDSUnderlying m_underlying;

    double m_payoff;

    int m_term;

    double m_spreadCost;

    static double kStandardPayoff;

};

void CDSContract::setCounterpart(const std::string &s)

{

    m_counterpart = s;

    setPayoff(kStandardPayoff);

}

Whenever the counterpart for the contract changes, the class reacts by resetting the payoff to a standard value (defined by the constant kStandardPayoff). That would not be possible if the m_counterpart data member were not properly encapsulated into the CDSContract class.

Inheritance

The benefits of encapsulation make it easy to implement and maintain code written in C++. However, it is commonly necessary to extend that code to handle situations that could not be anticipated by the designer of the original class. In that case, you can use inheritance as a powerful way to adapt your classes to new requirements.

Note

Although inheritance is a powerful tool, its overuse may result in an implementation that is difficult to maintain and extend. The main problem is that it is very easy to create deep hierarchies of classes with different behavior at each level. This makes it difficult to analyze the behavior of classes in isolation.

With the use of inheritance, it is possible to create a new class that contains the same data and behavior as an existing class. The new class is called a derived class, and the original class is called a base or parent class. For example, a loan-only credit default swap is a CDS where the protection is based on secured loans made on the target entity.

This useful type of CDS could be modeled as a new class that inherits from the original CDSContract class. If you need to create a derived class LoanOnlyCDSContract from a base class CDSContract, the C++ syntax would be the following:

class LoanOnlyCDSContract : public CDSContract {

public:

    // Constructors go here

    void changeLoanSource(const std::string &source);

private:

    std::string m_loanSource;

};

The public keyword is used to indicate that the public interface of the base class CDSContract is still available to the new class. The changeLoanSource member function is used to determine the source of the loan used by the CDS contract. The loan source is then stored in the m_loanSource member variable.

Notice that inheritance creates a new class that has access to all of the public and protected interfaces of the base class. So you still can call any method from the original CDSContract class when working with LoanOnlyCDSContract. On the other hand, private functions and data members are not available to the derived class. If you envision that a class could be used as the base for a hierarchy, it should provide access to some of the nonpublic interface using protected variables and functions. As a result, inheritance also requires a certain level of cooperation between base and derived classes.

Note

Inheritance requires that the new class be used in a context similar to the original class. Therefore, inheritance shouldn’t be used to create classes that have just a superficial similarity to the original class. In particular, a class that inherits from a base class could be used in the same code as the original class. If this is not true for the new class you need, it is better to create a separate class with a specialized interface.

Inheritance is the base technology used to accomplish many of the other techniques available in OO programming. Therefore, ideas such as polymorphism and abstract functions are possible due to the use of inheritance.

Polymorphism

While inheritance in itself provides a useful extension mechanism, its biggest advantage is the possibility of changing the original behavior of the base class in specific situations. In C++, this is enabled by using the virtual keyword to mark member functions that have polymorphic behavior.

For example, suppose that the CDSContract class is required to calculate the contract value at a particular date. This operation can be performed at the class level, but it will be slightly different for each particular implementation. Concrete implementations of the class may want to take into consideration particular factors that are not available at the base class level, such as differences in underlying, contract structures, and calculation models.

For these and other reasons, determining the best way to calculate the contract value may not be possible at the base class, and it must be delegated to derived class. Such derived class will possess additional data that can be used to compute the contract price with more precision than what is possible on the base class.

This behavioral change can be performed in the derived classes if you use C++ virtual mechanism. Syntactically, this polymorphic behavior can be implemented as long as the member function is modified with the virtual keyword in the original class. The virtual keyword is a C++ tool that allows functions to behave differently according to the concrete instance that is executing the function call.

For example, to support the required polymorphic behavior to calculate the contract value, the CDSContract base class should be coded as follows:

class CDSContract {

public:

    CDSContract();

    CDSContract(const CDSContract &p);

    ~CDSContract();

    CDSContract &operator=(const CDSContract &p);

    std::string counterpart() { return m_counterpart; }

    void setCounterpart(const std::string &s);

    double payoff() { return m_payoff;   }

    void setPayoff(double payoff) { m_payoff = payoff; }

    virtual double computeCurrentValue(const Date &d);

private:

    std::string m_counterpart;

    CDSUnderlying m_underlying;

    double m_payoff;

    int m_term;

    double m_spreadCost;

    static double kStandardPayoff;

};

The virtual double computeCurrentValue(const Date &d); line declares a new member function that can be overridden by derived classes.

Note

Virtual methods need to be recognized by the compiler. Therefore, the virtual keyword has to appear directly in the base class, not only in the derived classes. If a member function is supposed to have polymorphic behavior, you have to use virtual to signal this information to the compiler. Overriding a non-virtual member function doesn’t create a polymorphic object and will result in a warning in most compilers.

The classes derived from CDSContract can implement the virtual member function declared previously so that it can be invoked when instances of that derived class are created. Here is how this can be done for the LoanOnlyCDSContract subclass.

The isTradingDay member function returns true if the current date is not a holiday or a weekend day:

class LoanOnlyCDSContract : public CDSContract {

public:

    // Constructors go here

    void changeLoanSource(const std::string &s);

    virtual double computeCurrentValue(const Date &d);

private:

    std::string m_loanSource;

};

The implementation for a virtual function, both in the base class and the derived classes, is not different from the syntax used in other member functions. It is used in the compiler to determine the correct way to handle virtual functions that are called.

The use of a virtual function is determined by its polymorphic invocation through pointers and references. For example, consider the following code using CDSContract and LoanOnlyCDSContract:

void useContract(bool isLOContract, Date &currentDate)

{

    CDSContract *contract = nullptr;

    if (isLOContract)

    {

        contract = new LoanOnlyCDSContract();

    }

    else

    {

        contract = new CDSContract(); // normal CDS contract

    }

    contract->computeCurrentValue(currentDate);

    delete contract;

}

The useContract function is passed two arguments: the Boolean value isLOContract, which indicates that the contract used is a loan-only CDS. The second argument is the current date for use of the contract. The first line in the function

CDSContract *contract = nullptr;

determines the base class of the object that will be created. As with any OO object in C++, a pointer (or reference) to a base class can be used to point to objects of any descent class. In this case, a pointer to the CDSContract class (being the base class) can also be used to point to objects of type LoanOnlyCDSContract. The pointer is initialized to nullptr.

Note

The keyword nullptr was introduced in the C++11 standard. It provides a way to initialize pointers with a null value without the use of a macro such as NULL (which is used in C but normally avoided in C++), or the value 0, which can be easily confused with a numeric expression. The nullptr keyword can be used to initialize a pointer so that it contains a known value. In C++, nullptr can also be used when a particular result cannot be retrieved or computed.

The next lines determine the exact type that will be instantiated. If the isLOContract flag is set to true, a new object of type LoanOnlyCDSContract is created using the new keyword. Otherwise, the function creates an object of type CDSContract as the default value. In a more complex application, types should not be encoded using flags but passed as a parameter or supplied by some of the part of the application.

The next line

contract->computeCurrentValue(currentDate);

uses the pointer contract to perform a polymorphic call to computeCurrentValue. The polymorphic call mechanism will determine the correct implementation for the member function, depending on the exact class of the instance pointed to by the contract pointer. The next section explains how this mechanism works in practice and how it affects the creation and use of objects in C++.

Polymorphism and Virtual Tables

The first step in using polymorphism via virtual functions is to understand how they differ from regular member functions. When a virtual function is called, the compiler has to determine the type of call and translate it into binary code that will perform the call to the correct implementation. This is done in C++ using the so-called virtual table mechanism.

A virtual table is a vector of functions that is created for each class that uses at least one virtual function. The virtual table stores the addresses of virtual functions that have been declared for that particular type, as shown in Figure 4-2.

An illustration represents 3 blocks, such as class A, class B colon public A, and class C colon public A. Each lists a set of functions from f 1 to f 5.

Figure 4-2

Virtual functions shared by classes A, B, and C and stored in their respective virtual function tables

As shown in Figure 4-2, class A is the base class, and it contains a number of virtual functions, here denoted by the names f1 to f5. The slots in these tables store pointers to the implementation used by the class. Two other classes—B and C—are declared as derived classes via a public interface. This makes classes B and C inherit each a virtual table that contains at least the same function pointers (derived classes can add more virtual functions if they wish to do so).

Each class can define its own version of the virtual function, and as a result, the pointer to that function is stored in the corresponding location of the virtual table. The virtual table is populated in the compiler as it creates the data structures necessary for each class. At execution time, the virtual table is available for code executed by each of the classes defined in this example.

During runtime, the code generated by the C++ compiler can retrieve the location in the table where the function pointer is stored. Then, the function is called with the given parameters. First, the compiler retrieves the location of the virtual table associated with the class. Then, the compiler finds the function pointer at a predefined displacement from the beginning of the table. Finally, the program makes an indirect call using the function pointer stored at that location.

If you use this information to understand how C++ code works, you can see how the CDSContract and its derived class would execute a call to the computeCurrentValue member function, as shown in the following line of code:

contract->computeCurrentValue(currentDate);

The first step performed by the implementation is to find the virtual table for the particular object that is stored in the contract pointer. Then, the slot corresponding to the virtual function computeCurrentValue is searched, usually at a fixed distance from the beginning of the vector as determined by the compiler. Finally, the function pointer retrieved in this way is called indirectly, resulting in a function call to the correct implementation.

Although the sequence of steps necessary to call a virtual function appears to be complex, modern compilers can generate very efficient code using the virtual table technique. By means of code optimization, virtual function calls frequently end up as just a call to a function pointer.

Virtual Functions and Virtual Destructors

Another member function that can be annotated with the virtual keyword is the destructor. As you may remember, a destructor is called automatically (in the code generated by the compiler) when an object goes out of scope, with the objective of reclaiming resources used by the object.

The destructor may also be used through the keyword delete. When a delete is used, the code calls the destructor and frees the memory used by the object up to that moment. As a result, the pointer is not valid after the delete is called.

It is important to consider the role of the destructor when virtual functions are part of a class. The reason is that object cleanup is a class-specific activity, which needs to be overridden for each individual derived class that contains additional resources (such as memory, network connections, or graphical contexts). As a result, the destructor usually has different implementations that are necessary to perform the proper cleanup and deallocation activities.

For these reasons, the correct way to handle destructors in polymorphic classes is to use the virtual mechanism in their definition. This provides the means for each subclass to call a specific destructor even when called from a base pointer.

For example, consider what happens when the destructor in the base class is not virtual.

class CDSContract {

public:

    CDSContract();

    CDSContract(const CDSContract &p);

    ~CDSContract() { std::cout << " base class delete " << std::endl; }

    CDSContract &operator=(const CDSContract &p);

    std::string counterpart() { return m_counterpart; }

    void setCounterpart(const std::string &s);

    double payoff() { return m_payoff;   }

    void setPayoff(double payoff) { m_payoff = payoff; }

    virtual double computeCurrentValue(const Date &d);

// ...

};

The derived class LoanOnlyCDSContract would have the following simple definition, which just prints an informational message:

class LoanOnlyCDSContract : public CDSContract {

public:

LoanOnlyCDSContract() { std::cout << " derived class delete "

<< std::endl; }

    // Constructors go here

    void changeLoanSource(const std::string &s);

    virtual double computeCurrentValue(const Date &d);

private:

    std::string m_loanSource;

};

If called from client code, these definitions may result in undefined behavior. For example, consider the following fragment:

void useBasePtr(CDSContract *contract, Date &currentDate)

{

    contract->computeCurrentValue(currentDate);

    delete contract;

}

This code receives a pointer of type CDSContract, uses it to call a virtual function, and then uses the delete operator on it. When called in the following way:

void callBasePtr()

{

    Date date(1,1,2010);

    useBasePtr(new LoanOnlyCDSContract(), date);

}

the code has undefined behavior, because the compiler cannot guarantee that the destructor of the derived class will be found and executed. From the compiler point of view, a nonvirtual destructor doesn’t need to be called when the object is destroyed.

To fix this problem, the right thing to do is to declare the destructor as virtual in the base class. A simple change in this definition can accomplish this:

class CDSContract {

public:

    CDSContract() {}

    CDSContract(const CDSContract &p);

    virtual ~CDSContract() { std::cout << " base delete " << std::endl; }

    CDSContract &operator=(const CDSContract &p);

    // ... other members here

};

Once a virtual destructor has been declared in the base class, all descendant classes will also contain a virtual destructor, independent of using the virtual keyword. This is guaranteed by the presence of a virtual table containing the address of the destructor, as described in the previous section. The result of the callBasePtr function after this change is guaranteed to be the following:

$ ./CDSApp

 derived class delete

 base class delete

Abstract Functions

Another mechanism used to implement polymorphism in C++ is abstract functions. Such abstract functions are closely related to virtual functions, but their presence marks the containing class as an abstract class, which cannot be directly instantiated.

An abstract class is frequently used when a function should be provided in derived classes, but there is no clear default behavior that could be provided by the base class. This is a common situation when a base class provides only the framework for an algorithm, with details that are purposefully left unspecified. The idea is that the derived classes will necessarily provide the missing functionality that would make the derived classes useful for a particular application.

The syntax for abstract functions is similar to the syntax for virtual functions. The member function is preceded with the virtual keyword as previously seen. In addition, the syntax = 0; is used to terminate the declaration of the abstract function. Notice that only a declaration is needed, since no implementation is necessary for an abstract function (although it can be provided if available).

For an example, consider that the CDSContract class has a member function to process a credit event. In the world of credit default swaps, a credit event is what happens when a company calls for bankruptcy. Processing this event is different for each entity and CDS type; therefore, I would like to have such a member function as an abstract virtual function:

class CDSContract {

public:

    CDSContract() {}

    CDSContract(const CDSContract &p);

    virtual ~CDSContract() { std::cout << " base delete " << std::endl; }

    CDSContract &operator=(const CDSContract &p);

    std::string counterpart() { return m_counterpart; }

    void setCounterpart(const std::string &s);

    double payoff() { return m_payoff;   }

    void setPayoff(double payoff) { m_payoff = payoff; }

    virtual double computeCurrentValue(const Date &d);

    virtual void processCreditEvent() = 0;

    // ...

};

If a base class includes even one abstract virtual function, it becomes an abstract class that cannot be itself instantiated. The reason is that the class can be thought of as “incomplete,” since at least one of its virtual functions has no implementation. Given these definitions, the following code would become invalid:

CDSContract *createSimpleContract()

{

CDSContract *contract = new CDSContract();   ///

Wrong: CDSContract is now Abstract

    contract->setCounterpart("IBM");

    return contract;

}

Once an abstract member function has been defined, the classes that are direct descents are required to implement that function, or else they will become abstract too. For example, the descendant class LoanOnlyCDSContract now has to implement processCreditEvent in order to be used by client code. Even a trivial implementation would allow LoanOnlyCDSContract to be instantiated.

class LoanOnlyCDSContract : public CDSContract {

public:

LoanOnlyCDSContract() { std::cout << " derived class delete "

<< std::endl; }

    // Constructors go here

    void changeLoanSource(const std::string &s);

    virtual double computeCurrentValue(const Date &d);

    virtual void processCreditEvent();

private:

    std::string m_loanSource;

};

void LoanOnlyCDSContract::processCreditEvent()

{

}

Abstract member functions can be freely used even inside the abstract class, where the body of that member function is not defined. For example, this is a valid definition for the CDSContract::computeCurrentValue member function:

double CDSContract::computeCurrentValue(const Date &d)

{

    if (!counterpart().empty())

    {

        processCreditEvent(); // make sure there is no credit event;

    }

    return calculateInternalValue();  //

use an internal calculation function

}

Building Class Hierarchies

One of the advantages of OO code is the ability to organize your application around conceptual frameworks defined by classes. A class hierarchy allows the sharing of common logic that can be easily reused in other contexts. Proper use of class hierarchy can reduce the amount of code duplication and lead to applications that are more understandable and easier to maintain.

A class hierarchy can be developed around important concepts used by the application. For example, in a derivatives-based application, the class CDSContract would be a candidate to become the root of a class hierarchy. Figure 4-3 shows a possible class hierarchy for CDS contracts, containing derived classes for the following types of contracts:

LoanOnlyCDSContract: CDS contracts that are based on loans to other institutions and have special logic for processing these loans

HedgedCDSContract: A CDS contract type where hedging is performed using other asset classes with the goal of reducing contract risk

NakedCDSContract: A particular CDS contract where the contract seller does not own the underlying asset negotiated in the contract

FixedInterestCDSContract: A CDS type where the contract requires a fixed interest rate for the duration of the specified agreement

VariableInterestCDSContract: A type of CDS where the contracts are defined using variable interest rates, using a well-known benchmark for interest rates

TaxAdvantagedCDSContract: A particular type of CDS contract that takes advantage of a special tax structure

A flow chart represents the class hierarchy for the C D S Contract. They are loan-only, hedged, naked, fixed interest, variable interest, and tax-advantaged C D S contracts.

Figure 4-3

A class hierarchy rooted on the base class CDSContract

All these CDS contract derivatives would benefit from code sharing from the base class CDSContract. As a result, common functionality such as CDS pricing, contract creation, and contract maintenance can be stored in a central place and used by as many different types of CDS contracts as possible.

Although creating class hierarchies is a useful technique for code maintenance and sharing, inheritance may not be the best strategy for code organization in some cases. It is important to be able to identify the situations in which other approaches would work better. Here are some potential disadvantages of using inheritance:

Increased coupling between classes: Once you decide to use inheritance, there is a big interdependence between classes. A small change in the base class can affect all descendant classes. If there is a situation where the base class can vary frequently in functionality and responsibilities, then inheritance may not be the best solution.

Physical dependencies at compilation time: In C++, inheritance also creates a compile-time dependency between classes. To generate correct code, the C++ compiler needs to access the definition of each base class. This may result in increased compilation time, which is sometimes undesirable, especially in large software projects.

Increased information coupling: Class hierarchies may also require developers to learn the multiple implementations of different classes at different levels. This is necessary especially when classes are not well designed and information about their operations is not clear.

Object Composition

Another strategy to organize and code using OO techniques is object composition. Composition is an alternative to inheritance, where you can use the behavior of an object without the dependency caused by direct class/subclass relationship.

To use object composition, you need to store the object that has the desired behavior as a member variable for the containing object. This is the basic strategy, which can be implemented in at least three ways in C++:

Storing a pointer to an object: In this case, only a pointer to the object is stored as part of the class. This option allows an object to be created inside the class or passed as a parameter from a user of the class and then stored in a member variable.

Storing a reference to an object: This option allows the class to receive a reference to an existing object but doesn’t allow the object to be created after the constructor is executed. A reference in C++ cannot be reassigned, which leads to a requirement that the stored object needs to be valid the whole time the container object exists.

Storing the object directly as a member variable: In this case, the containing class assumes complete responsibility for storing the required object. Here, it is also necessary for the compiler to know the exact size and type of the object stored as a member variable, which reduces the flexibility of this method.

With object composition, a class can use the functionality provided by another class without the use of inheritance.

For example, suppose that the CDSContract class needs a fast method for calculating integrals. In this case, a good approach is to use an object-composition strategy to access the functionality of integration, instead of adding this functionality to the base class. You could do this, for example, by passing to the CDSContract constructor a pointer to a MathIntegration object and storing that pointer as a member function. The code would look like this:

class MathIntegration;

class CDSContract {

public:

    CDSContract() {}

    CDSContract(MathIntegration *mipt);

    CDSContract(const CDSContract &p);

    virtual ~CDSContract() { std::cout << " base delete " << std::endl; }

    CDSContract &operator=(const CDSContract &p);

    // Other member functions here

private:

    std::string m_counterpart;

    CDSUnderlying m_underlying;

    double m_payoff;

    int m_term;

    double m_spreadCost;

    MathIntegration *m_mipt;

    static double kStandardPayoff;

};

When necessary, the pointer could be used to access the functionality stored in the MathIntegration class. The best thing about this kind of design is that there is little coupling between the CDSContract and MathIntegration classes. Each one can evolve separately, by adding new functions as necessary, without the need for mutual dependencies.

When to Use Object-Oriented Features?

Since C++ is a language that can be used with different coding styles, the question arises of when to use object-oriented features instead of functional, template-based, or other programming techniques. Object-oriented features in C++ should be used when you want to organize your code around objects, encapsulate data and behavior, and promote modularity and reusability.

Here are some situations where object-oriented features are particularly beneficial:

Code organization: Object-oriented programming (OOP) provides a natural way to organize code by grouping related data and behavior into objects. This improves code readability and maintainability and allows for better organization of complex systems.

Encapsulation: OOP allows you to encapsulate data and behavior within objects, providing data hiding and abstraction. Encapsulation helps enforce access control and promotes modular design by allowing objects to communicate through well-defined interfaces.

Code reuse: Inheritance and polymorphism, key features of OOP, facilitate code reuse. Inheritance enables the creation of derived classes that inherit and extend the functionality of base classes, promoting code reuse and reducing duplication. Polymorphism allows objects of different types to be treated interchangeably, enabling flexible and reusable code.

Modularity and extensibility: OOP promotes modularity, allowing you to break down complex systems into smaller, manageable components (objects). This modularity facilitates maintenance, testing, and extensibility, as changes in one object have limited impact on other parts of the system.

Design patterns: Object-oriented features align well with common software design patterns. Many design patterns, such as Factory, Observer, and Strategy, rely on polymorphism and encapsulation to solve recurring design problems. OOP makes it easier to implement and understand these patterns.

Team collaboration: OOP provides a common paradigm and vocabulary for developers, making it easier for team members to collaborate and understand each other’s code. OOP concepts, such as classes, objects, and inheritance, are widely understood and used in industry.

Modeling real-world entities: Object-oriented programming allows you to model real-world entities and their interactions more naturally. By representing objects as classes and their relationships as inheritance or composition, you can create software that mirrors the real-world problem domain.

Objects and C++23

Object orientation capabilities in C++ have been enhanced with the new features introduced in the recent versions of the standard, particularly with C++23. The trend in the language is to provide strong support for compile-time interfaces. This emphasis makes it possible to write software that interacts with objects in a safer way while reducing the amount of checking that needs to be done at runtime and, therefore, increasing performance.

One example is the introduction of concepts. The concept feature is complex and its full explanation cannot be provided in this chapter, but in a few words, it means that programmers can now request features from objects at compilation time. For example, if a function works with objects that contain dates, we can write the requirement that any object passed to a certain function contains an is_date member function.

The concept feature of C++23 makes it possible to avoid some runtime checking of object types by expressing such requirements at compilation time. Concepts also help in the generation of clear requirements, with better error messages produced by compilers.

Here is a simple example of how objects in C++ interact with concepts:

#include <concepts>

#include <vector>

// Simple option contracts class

class OptionContract {

    public:

    virtual void sell() {

        // Sell implementation...

    }

};

template <class OptionContainer>

void sellAllContracts(OptionContainer &oc)

  requires std::same_as<

          typename OptionContainer::value_type, OptionContract *>

{

  std::for_each(oc.begin(), oc.end(),

           [](OptionContract* aContract) { aContract->sell(); } );

}

void useContracts() {

    std::vector<OptionContract*> contracts;

    // ...

    sellAllContracts(contracts);

}

In this code fragment, the concepts header file is used to provide library support for concepts. Then, we create a simple class called OptionContract. This class is responsible for holding option contracts of a particular type, and it knows how to sell contracts using the sell() member function. Later, a template function named sellAllContracts is introduced. The special feature of this function is that it contains a requires statement, which can be used to specify clear requirements on types that it manipulates. In this case, the code only requires that the parameter be a container that has same value_type as OptionContract *. In other words, the parameter must be a container for OptionContract object pointers.

With this requirement in place, the code of the function is now able to safely call the sell() method of the OptionContract class, since the compiler already knows that the elements of the oc container must be of type OptionContract.

In general, the Concepts feature allows you to define requirements on template arguments, improving the readability and expressiveness of generic and object-oriented code. Here is another example of how Concepts could be used to check your code:

#include <iostream>

#include <concepts>

// Concept: ArithmeticType

template <typename T>

concept ArithmeticType = std::is_arithmetic_v<T>;

// Function template

template <ArithmeticType T>

T add(T a, T b) {

    return a + b;

}

int main() {

    int result = add(5, 10);

    std::cout << "Result: " << result << std::endl;

    // Error: 'const char*' does not satisfy the 'ArithmeticType' concept

    // int invalidResult = add("Hello", "World");

    return 0;

}

The preceding code was compiled with gcc using the -std=cxx2a compiler option to activate the concepts keyword. In this example, we define a concept ArithmeticType that checks whether a type is an arithmetic type using the std::is_arithmetic_v trait. Then, we define a function template add that takes two arguments of the same type satisfying the ArithmeticType concept. The function adds the two arguments and returns the result.

In the main function, we demonstrate the use of the add function by calling it with two integers. Since int satisfies the ArithmeticType concept, the code compiles and executes successfully, printing the result.

If we attempt to call add with arguments that do not satisfy the ArithmeticType concept, such as two string literals, the code will fail to compile and produce an error.

Conclusion

In this chapter, I presented an overview of OO concepts provided in C++ and how they are used in the financial development community to solve problems occurring with options and derivatives.

The first part of this chapter summarized the basic characteristics of OO as implemented in C++, including the main concepts of encapsulation, inheritance, and polymorphism. You learned about the technique used in C++ to implement polymorphic behavior through virtual functions. You also saw how virtual functions are stored in virtual tables that are created for each class that contains virtual functions.

This chapter also presented some examples of using OO to efficiently solve common problems in financial programming, as applied to options and derivatives. The next chapter proceeds to template-based concepts and explains how they can be used to create high-performance solutions to problems in the area of financial derivatives processing.

© The Author(s), under exclusive license to APress Media, LLC, part of Springer Nature 2023

C. OliveiraOptions and Derivatives Programming in C++23https://doi.org/10.1007/978-1-4842-9827-5_5

5. Design Patterns for Options Processing

Carlos Oliveira1  

(1)

Seattle, WA, USA

Design patterns are a set of common programming design elements that can be used to simplify the solution of recurring problems. With the use of OO techniques, design patterns can be cleanly implemented as a set of classes that work toward the solution of a common goal. These designs can then be reused and shared across applications.

Over the last few years, design patterns have been developed for common problems occurring in several areas of programming. When designing algorithms for options and other derivatives, design patterns can provide solutions that are elegant and reusable (when supporting libraries are employed). Thanks to the inherent ability of the C++ language to create efficient code, these solutions also have high performance.

In this chapter, you will learn about the most common design patterns employed when working with financial options and derivatives, with specific examples of their usage. The chapter covers the following topics:

Overview of design patterns: You will learn how design patterns can help in the development of complex applications, with the ability to reuse common patterns of programming behavior. Using design patterns can also make solutions more robust and easier to understand, because patterns provide a common language that allows developers to discuss complex problems. Such design techniques have also been made available through libraries that implement some of the best-known design patterns.

Factory method pattern: A factory method is a design pattern that allows objects to be created in a polymorphic way, so the client doesn’t need to know the exact type of the returned object, only the base class that provides the desired interface. It also helps to hide a complex set of creation steps to instantiate particular classes.

Singleton pattern: The singleton pattern is used to model situations in which you know that only one instance of a particular class can validly exist. This is a situation that occurs in several applications, and in finance, I present the example of a clearing house for options trading.

Observer pattern: Another common application of design patterns is in processing financial events such as trades. The observer design patterns allow you to decouple the classes that receive trading transactions from the classes that process the results, which are the observers. Through the observer design pattern, it is possible to simplify the logic and the amount of code necessary to support these common operations, such as the development of a trading ledger, for example.

Visitor pattern: We also investigate the visitor pattern that allows two or more class hierarchies to cooperate in performing dynamic method dispatching.

Introduction to Design Patterns

Design patterns have been introduced as a set of programming practices that simplify the implementation of common coding problems. As you study the behavior of OO applications, there are tasks and solution strategies that occur frequently and can be captured as a set of reusable classes.

Object-oriented programming provides a set of principles that can facilitate the development of computer software. Using OO programming techniques, you can easily organize your code and create high-level abstractions for application logic and commonly used component libraries. In this way, OO techniques can be used to improve and reuse existing components, as well as simplify the overall development. OO programming promotes a way of creating software that uses logical elements operating at a higher level of abstraction.

Here are some of the most common design patterns that can be used in software development in general and for algorithms to process options and derivatives in particular, as viewed in Figure 5-1:

Factory method: In the factory method design pattern, the objective is to hide the complexity and introduce indirection when creating an instance of a particular class. Instead of asking clients to perform the initialization steps, factory methods provide a simple interface that can be called to create the object and return a reference.

Singleton: A singleton is a class that can have at most one active instance. The singleton design pattern is used to control access to this single object and avoid creating copies of this unique instance.

Observer: The observer pattern allows objects to receive notifications for important events occurring in the system. This pattern also reduces the coupling between objects in the system, since the generator of notification events doesn’t need to know the details of the observers.

Visitor: The visitor pattern allows a virtual function to be called in response to another dynamic invocation in a separate class. The visitor pattern provides a way to dispatch messages based on a combination of two objects, instead of the single object-based dispatch that is the norm for C++ virtual member functions.

A block diagram represents 8 design patterns. They are singleton, visitor, adapter, strategy, factory method, abstract factory, decorator, and observer.

Figure 5-1

A few common design patterns used in OO programming

In the next few sections, you will see how these design patterns can be implemented in C++, with examples of how they occur in options and derivatives applications.

The Factory Method Design Pattern

A factory design pattern is a technique used to indirectly create objects of a particular class. This pattern is important because it is frequently useful to access newly allocated objects without having to directly perform the work necessary to create them. For example, using the factory method design pattern, it is possible to avoid the use of the new keyword to create an object, along with the parameters required by the constructor. You can also define a factory method as creating a family of objects that are of the same class hierarchy.

The factory design pattern allows an object to be created through a member function of the desired class so that the client doesn’t need to create the object directly. This can be useful for the following reasons:

Most of the time, there is no need for the client to provide parameters for construction of the object (however, a parameter may be used to determine the type of object to create). For example, if the objects require the allocation of additional resources, such as a file or a network connection, the client is relieved from acquiring these resources.

Sometimes, the object depends on internal implementation details, such as a private class, that are not available to clients. In this case, providing a factory method is the only way to create new instances of the object.

The exact sequence of events necessary to create an object may change. In that case, it is better to provide a factory method that hides this complexity. Users of the class will not have to worry if the way the object is created is updated.

More importantly, factory methods can be used to simplify polymorphic object creation. For example, when an object is created using the new operator, the concrete type of the returned object has to be known by the client. On some applications, this might be undesirable, because the real type of the needed object could be any one within a set of derived classes. Using a factory method, it is possible to delegate the creation of the object so that the client code doesn’t need to know about the concrete type, and only the base class is known by clients. As a result, the returned object may be any one of the subtypes of the original type.

Static member methods can be used to implement the Factory pattern. Such a member function doesn’t depend on an instance of the class to be executed. The syntax for member functions is simply ClassName::functionName(), with parameters added as needed.

Note

The factory method design pattern is also used as a foundation for more complex design patterns. For example, you will notice that other patterns such as singleton use a factory method to control the creation of new instances of a particular class.

In options and derivatives applications, the factory method is commonly used. A situation where the use of a factory method is desirable is when you need to load data objects. The data source used can vary from a local file to a URL, and the parsing of that data is not an important part of the overall algorithm. In that case, abstracting the creation of the data source can be an important application of the factory method.

In the example that follows, you can see how a DataSource class can be implemented. The goal of this class is to hide the process of creating a new data source, so the clients have no access to the real constructor of the class. Instead, clients need to use a factory method, which is implemented as a static member function of the DataSource class.

When using factory methods, it is frequently useful to hide the real implementation of the constructor. This can be done through careful use of the private modifier. The goal is to grant access to the constructor only to the class itself (and to any declared friends of the class). This is done to the standard constructor as well as to the copy constructor.

The interface to the DataSource class is presented in Listing 5-1. Both constructors and the assignment operator are declared as private. The destructor, however, needs to be accessible so that the delete keyword can be called on allocated objects. The readData member function is an interface for the main responsibility attributed to this class, and its implementation will vary according to the read data source used. The createInstance member function is a static function that creates and returns new instances of the data type, functioning as the factory method.

//

//  DataSource.hpp

#ifndef DataSource_hpp

#define DataSource_hpp

#include <string>

class DataSource {

private:

    DataSource(const std::string &name);

    DataSource(const DataSource &p);

    DataSource &operator=(const DataSource &p);

public:

    ~DataSource();  // must be public so clients can use delete

    static DataSource *createInstance();

    void readData();

private:

    std::string m_dataName;

};

#endif

Listing 5-1

Declaration of the DataSource Class

The implementation of the DataSource class is shown in Listing 5-2. The constructors and destructor are standard, considering the fact that the constructor is private. The interesting part of the DataSource implementation is the getInstance method, which returns a new data source. This implementation receives only one parameter that is created by the method, but consider the general case in which a list of complex or implementation-dependent objects needs to be retrieved in order to call the new operator for the DataSource class.

Note

At the end of getInstance, the member function returns a pointer to the newly created object. Another option is to return a smart pointer, such as std::shared_ptr, which would make it easier to manage the lifetime of the allocated object.

//

//  DataSource.cpp

#include "DataSource.hpp"

DataSource::DataSource(const std::string &name)

: m_dataName(name)

{

}

DataSource::DataSource(const DataSource &p)

: m_dataName(p.m_dataName)

{

}

DataSource &DataSource::operator=(const DataSource &p)

{

    if (this != &p)

    {

        m_dataName = p.m_dataName;

    }

    return *this;

}

DataSource::~DataSource()

{

}

// Create the object instance.

// Note: this is not thread safe.

DataSource *DataSource::createInstance()

{

    std::string sourceName;

// Complex method used here to find sourceName and other construction parameters...

    DataSource *ds = new DataSource(sourceName);

    return ds;

}

void DataSource::readData()

{

    // Read data here...

}

void useDataSource()

{

    // DataSource *source = new DataSource(""); // this will not work!

    DataSource *source = DataSource::createInstance();

    source->readData();

    // Do something else with data

    delete source;

}

Listing 5-2

Implementation of the DataSource Class

The Singleton Pattern

One of the simplest and most used design patterns is the singleton. With this design pattern, a single object is used from each class so that there is a central location where services managed by that class can be directed.

Unlike standard classes, a singleton is a class that will only create one instance and therefore represents a single resource that cannot be replicated. Because of this, the singleton pattern restricts the ability to create new objects of a particular class, using a few techniques that will be discussed later in this section. C++ provides all the features necessary to implement singleton patterns with high performance.

In programming, the notion of an entity that is unique across the application is frequently encountered. An example in options programming is an entity called a clearing house. A clearing house is an institution that provides clearing services for trades on options and derivatives. The clearing house makes sure that every trade has collateral so that counterpart risk is reduced, among other attributions. For example, if a trader sells options in a particular instrument, the clearing house will make sure that the trader has enough margin to satisfy the requirements of that particular trade.

While a clearing house provides important services in the trading industry, most applications need to connect to a single clearing house. Thus, creating a single object to represent the clearing house is an obvious implementation technique for this situation. Table 5-1 presents a few examples of objects that could be modeled using a singleton.

Table 5-1

Example Objects That Can Be Implemented As a Singleton Design Pattern

Object

Notes

Clearing house (finance)

A single clearing house is used for all trades.

Root window (GUI)

Each GUI application communicates with only one root window.

Operating system

An object representing operating system services is unique through the application.

Company CEO

An object representing the CEO has only one instance.

Memory allocator (system services)

Each application uses a single memory allocator, which can be represented by a singleton.

To implement a singleton in C++, the first step is to make sure that there is only one object of that class in the application. To do this, it is necessary to disallow the creation of new objects of that particular class. You can take advantage of the ability provided by C++ to make class members inaccessible to users of the class through the private keyword. Users then cannot use the new keyword to generate new objects of that particular class.

On the other hand, it is necessary to create some mechanism for clients to access an instance of the singleton class. This is usually done using a static member function that returns the single existing object or creates a new object if necessary before returning it. Using such an access member function, clients can access the public interface of the singleton object. At the same time, they’re not allowed to create or manage the life cycle of that object.

Using the Singleton Design Pattern

The singleton design pattern is used when you want to ensure that only one instance of a class is created and that instance is globally accessible. There are some situations where using the singleton pattern may be appropriate.

If you have a limited resource that needs to be shared across different parts of your application, such as a database connection, thread pool, or configuration settings, the singleton pattern can be used to manage the access and ensure that all components use the same instance.

In another case, when you need a single point of control for coordinating actions in a system, the singleton pattern can be helpful, for example, a logging service that needs to be accessed from different parts of an application to record events in a centralized manner.

In scenarios where you need to cache objects to improve performance, a Singleton can be used to provide a global cache manager. The Singleton can control the creation, storage, and retrieval of cached objects.

Moreover, if you want to ensure that only one instance of a class can be created throughout the lifetime of an application, the singleton pattern enforces this restriction. This is useful in cases where multiple instances could cause issues or conflicts.

However, it’s important to consider the downsides and limitations of the singleton pattern as well. It can introduce global state, making your code harder to test and maintain. It can also lead to tight coupling and hinder flexibility in the future. Therefore, it’s essential to carefully evaluate whether the singleton pattern is the most appropriate solution for your specific use case before implementing it.

Clearing House Implementation in C++

A clearing house, also known as a clearing corporation or a clearing agency, is a financial institution or entity that acts as an intermediary between buyers and sellers in financial markets. Its primary role is to facilitate the settlement of trades and ensure the smooth functioning of the market.

When a trade is executed between a buyer and a seller, the clearing house receives trade data from the respective parties. It verifies the details of the trade, such as the quantity, price, and terms, to ensure accuracy and eliminate discrepancies.

The clearing house becomes the central counterparty to all trades, acting as the buyer to every seller and the seller to every buyer. This process helps to mitigate counterparty risk, as the clearing house becomes responsible for ensuring the completion of each trade.

The clearing house establishes and enforces margin requirements for market participants. Margin is a deposit or collateral that traders must maintain to cover potential losses. By setting margin requirements, the clearing house helps manage risk and ensures that market participants have sufficient funds or collateral to meet their obligations.

Clearing houses play a crucial role in managing and mitigating risks associated with trading. They employ risk management systems and mechanisms to monitor and control potential defaults, market volatility, and other risks that could disrupt the settlement process.

The clearing house facilitates the settlement of trades by ensuring the timely and accurate transfer of securities and funds between buyers and sellers. It maintains accounts for market participants and facilitates the delivery of securities from sellers to buyers and the transfer of funds accordingly.

Finally, clearing houses employ risk containment measures such as collateral requirements, default funds, and loss-sharing arrangements to protect against potential losses. These measures are in place to ensure the financial integrity and stability of the clearing house and the broader financial system.

A possible implementation in C++ for the clearing house class using the singleton pattern is presented in Listings 5-1 and 5-2. The class has two parts: the first part deals with the management of the singleton object. This is done through the definition of private constructor and destructors, as well as the presence of a static member function getClearingHouse, which returns a reference to the singleton instance.

The second part of the implementation deals with the responsibilities of the clearing house, represented here as the member function clearTrade. This function receives as an argument a Trade object, which is not defined here but contains all the data associated with the transaction.

Listing 5-3 shows the interface, which follows the singleton design pattern. Listing 54 contains the implementation of the member functions declared in the class interface, as well as the static member variable s_clearingHouse.

//

//  DesignPatterns.hpp

//  CppOptions

#ifndef DesignPatterns_hpp

#define DesignPatterns_hpp

class Trade {

    //  ....

};

class ClearingHouse {

private:                   //

These are all private because this is a singleton

    ClearingHouse();

    // The copy constructor is not implemented

    ClearingHouse(const ClearingHouse &p);

    ~ClearingHouse();

    // Assignment operator is not implemented

    ClearingHouse &operator=(const ClearingHouse &p);

public:

    static ClearingHouse &getClearingHouse();

    void clearTrade(const Trade &);

private:

    static ClearingHouse *s_clearingHouse;

};

#endif /* DesignPatterns_hpp */

Listing 5-3

Header File for the ClearingHouse Class, Which Implements the Singleton Design Pattern

The implementation file contains the member function ClearingHouse::getClearingHouse. This function first checks the static variable s_clearingHouse to determine if it has been previously allocated. If the object doesn’t exist, then the static function can create a new object, store it for further use, and return a reference. In a real-world case, one needs to make sure that this is done in a single thread in order to avoid racing conditions. That can be done using a mutex, for example.

The function useClearingHouse is an example of how the ClearingHouse class can be used. The first step is to have a variable hold a reference to the singleton object. Then, by calling the static function getClearingHouse, you can access the singleton. In this example, the singleton is used to process another trade through the member function clearTrade.

//

//  DesignPatterns.cpp

#include "DesignPatterns.hpp"

ClearingHouse *ClearingHouse::s_clearingHouse = nullptr;

ClearingHouse::ClearingHouse()   //

private constructor, cannot be used by clients

{

}

ClearingHouse::~ClearingHouse()  //

this is private and cannot be used by clients

{

}

ClearingHouse &ClearingHouse::getClearingHouse()

{

    if (!s_clearingHouse)

    {

        s_clearingHouse = new ClearingHouse();

    }

    return *s_clearingHouse;

}

void ClearingHouse::clearTrade(const Trade &t)

{

     // Trade is processed here

}

void useClearingHouse()

{

    Trade trade;

    ClearingHouse &ch = ClearingHouse::getClearingHouse();

    ch.clearTrade(trade);

}

Listing 5-4

Implementation File for the ClearingHouse Class, Which Uses the Singleton Design Pattern

The Observer Design Pattern

A frequent situation that happens in complex systems is the occurrence of events that trigger further actions. For example, an event that occurs on financial systems is the completion of an options trade. When a new trade is completed, several actions need to be performed to update the state of the system and reflect the new positions in the ledger.

The observer design pattern is a very powerful strategy to manage event updates. It is based on a technique that gives clients the ability to listen to events, perform updates to a particular object, and react accordingly.

There are two parts of the observer design pattern (see Figure 5-2). First, there is an observer, which implements an abstract interface capable of receiving notifications. The abstract interface consists of a single member function, called notify. This member function is called by the second part of the design pattern, the Subject, when a new event occurs (the arrow between them means that the observer has a reference to the Subject).

A block flow diagram represents an observer design pattern that calls for the notify function and is mapped to the subject and concrete observer. The subject consists of add observer, remove observer, and trigger notification functions. The concrete observer consists of a notify function.

Figure 5-2

Simplified scheme of the observer design pattern

The Subject class has at least three member functions that enable the functionality of the observer design pattern. The first function is addObserver, which takes as a parameter a reference to an observer object. The addObserver function maintains the reference in an internal list of objects that are interested in receiving notifications.

The second member function in the Subject interface is removeObserver, which simply removes a given observer from the notification list. Finally, there is a member function called triggerNotifications that is used to send the notifications to all objects that registered with the Subject class.

The observer design pattern can readily be implemented in C++ using abstract classes. You can see a sample implementation in Listings 5-5 and 5-6. The first class that is considered is the Observer class. This class has the purpose of providing a simple interface for the observer. Its only nontrivial member function is notify, which is an abstract function called by the subject when a new event occurs. As a result, any class deriving from observer needs to process the notification in a user-defined way.

The interface is the following:

class Observer {

public:

    // Constructor and destructor definitions

    virtual void notify() = 0;

};

Note

Consider how the Observer class is independent of any implementation detail for the trading ledger system. This definition could be reused as part of a design pattern’s library. Similar techniques can be used to simplify the creation of other design patterns as well.

Next, it is necessary to define a class that implements the abstract observer interface. In this case, the goal is to implement a trade observer, which can be specified in the following way:

class TradeObserver : public Observer {

public:

    TradeObserver(TradingLedger *t);

    TradeObserver(const TradeObserver &p);

    ~TradeObserver();

    TradeObserver &operator=(const TradeObserver &p);

    void notify();

    void processNewTrade();

private:

    Trade m_trade;

    TradingLedger *m_ledger;

};

The constructor for this class receives as a parameter a pointer to the TradingLedger object, which will be defined later. The class provides an implementation for notifications and a member function to process new trades. These two member functions are implemented as follows:

void TradeObserver::notify()

{

    this->processNewTrade();

}

void TradeObserver::processNewTrade()

{

    m_trade = m_ledger->getLastTrade();

    // Do trading processing here

}

Here, the notification implementation just calls the processNewTrade function, which stores the trade returned by the ledger object.

Finally, you can also see a definition for the TradingLedger class. The class contains the three member functions that comply with the subject interface (addObserver, removeObserver, and triggerNotifications). The class also contains two simple member functions to add and return trades, as shown in the following definitions:

class TradingLedger {

public:

    TradingLedger();

    TradingLedger(const TradingLedger &p);

    ~TradingLedger();

    TradingLedger &operator=(const TradingLedger &p);

    void addObserver(std::shared_ptr<Observer> observer);

    void removeObserver(std::shared_ptr<Observer> observer);

    void triggerNotifications();

    void addTrade(const Trade &t);

    const Trade &getLastTrade();

private:

    std::set<std::shared_ptr<Observer>> m_observers;

    Trade m_trade;

};

The addObserver and removeObserver functions operate with std::shared_ptr templates for the observer object. The goal is to avoid unnecessary memory issues by delegating the memory deallocation to shared pointers from the standard library. These two functions operate as an interface to the internal m_observers container.

The triggerNotifications function can be implemented as follows:

void TradingLedger::triggerNotifications()

{

    for (auto i : m_observers)

    {

        i->notify();

    }

}

It simply loops through all elements stored in the m_observers set and sends a notification to these registered objects. Each such object that implements the observer interface can now respond to the event as needed.

Complete Code

The complete example previously described can be seen in Listings 5-5 and 5-6. The first file contains only the interface for the main classes used in the system. Listing 5-6 shows the implementation of these classes, along with a sample main function that creates the ledger and two observer objects.

//

//  Observer.hpp

#ifndef Observer_hpp

#define Observer_hpp

#include <set>

#include <memory>

class Observer {

public:

    Observer();

    Observer(const Observer &p);

    ~Observer();

    Observer &operator=(const Observer &p); // not implemented

    virtual void notify() = 0;

};

class Trade {

    //

    // .... Implementation not shown here

};

class TradingLedger;

class TradeObserver : public Observer {

public:

    TradeObserver(TradingLedger *t);

    TradeObserver(const TradeObserver &p);

    ~TradeObserver();

    TradeObserver &operator=(const TradeObserver &p);

    void notify();

    void processNewTrade();

private:

    Trade m_trade;

    TradingLedger *m_ledger;

};

class TradingLedger {

public:

    TradingLedger();

    TradingLedger(const TradingLedger &p);

    ~TradingLedger();

    TradingLedger &operator=(const TradingLedger &p);

    void addObserver(std::shared_ptr<Observer> observer);

    void removeObserver(std::shared_ptr<Observer> observer);

    void triggerNotifications();

    void addTrade(const Trade &t);

    const Trade &getLastTrade();

private:

    std::set<std::shared_ptr<Observer>> m_observers;

    Trade m_trade;

};

#endif /* Observer_hpp */

Listing 5-5

Header File Containing Interfaces for the Observer Design Pattern

//

//  Observer.cpp

#include "Observer.hpp"

using std::shared_ptr;

typedef shared_ptr<Observer> PObserver;

typedef shared_ptr<TradeObserver> PTradeObserver;

Observer::Observer()

{

}

Observer::Observer(const Observer &p)

{

}

Observer::~Observer()

{

}

void Observer::notify()

{

}

TradeObserver::TradeObserver(TradingLedger *t)

: m_ledger(t)

{

}

TradeObserver::TradeObserver(const TradeObserver &p)

: m_trade(p.m_trade),

  m_ledger(p.m_ledger)

{

}

TradeObserver::~TradeObserver()

{

}

TradeObserver &TradeObserver::operator=(const TradeObserver &p)

{

    if (this != &p)

    {

        m_trade = p.m_trade;

        m_ledger = p.m_ledger;

    }

    return *this;

}

void TradeObserver::notify()

{

    this->processNewTrade();

}

void TradeObserver::processNewTrade()

{

    m_trade = m_ledger->getLastTrade();

    // Do trading processing here

}

// -- TradingLedger implementation

TradingLedger::TradingLedger()

{

}

TradingLedger::TradingLedger(const TradingLedger &p)

: m_observers(p.m_observers),

  m_trade(p.m_trade)

{

}

TradingLedger::~TradingLedger()

{

}

TradingLedger &TradingLedger::operator=(const TradingLedger &p)

{

    if (this != &p)

    {

        m_observers = p.m_observers;

        m_trade = p.m_trade;

    }

    return *this;

}

void TradingLedger::addObserver(PObserver observer)

{

    m_observers.insert(observer);

}

void TradingLedger::removeObserver(PObserver observer)

{

    if (m_observers.find(observer) != m_observers.end())

    {

        m_observers.erase(observer);

    }

}

void TradingLedger::triggerNotifications()

{

    for (auto i : m_observers)

    {

        i->notify();

    }

}

void TradingLedger::addTrade(const Trade &t)

{

    m_trade = t;

    this->triggerNotifications();

}

const Trade &TradingLedger::getLastTrade()

{

    return m_trade;

}

//

//  Simple test stub for the TradingLedger and TradeObserver classes.

int main()

{

    TradingLedger tl;

    PTradeObserver observer1 = PTradeObserver(new TradeObserver(&tl));

    PTradeObserver observer2 = PTradeObserver(new TradeObserver(&tl));

    tl.addObserver(observer1);

    tl.addObserver(observer2);

    // Perform trading system here

    Trade aTrade;

    tl.addTrade(aTrade);

    // Observers should receive a notification at this point

    return 0;

}

Listing 5-6

Implementation File with C++ Definitions for the Observer Design Pattern

The Visitor Pattern

Another useful pattern that has been used in several real-life applications is the visitor pattern. In this pattern, the goal is to allow dynamic dispatching of objects in two separate hierarchies of types. This design pattern has application in many common problems occurring in finance.

The problem solved by the visitor pattern is the application of dynamic rules to two or more polymorphic objects at the same time. This is necessary because C++, like some other object-oriented languages, uses single dispatch to process polymorphic calls.

The visitor design pattern is generally used when you have a complex object structure consisting of multiple types of objects and you want to perform operations on those objects without modifying their individual classes. There are some situations where using the visitor pattern may be the most appropriate.

If you have a set of classes that represent a structure or hierarchy and you want to add new operations or behaviors to these classes without modifying their code, the visitor pattern can be useful. Instead of modifying each class individually, you can create a visitor class that defines the new operations, and each class can accept the visitor to perform those operations.

Moreover, when you have a complex object structure with many different types of objects and you want to define algorithms that operate on these objects, the visitor pattern can help separate the algorithm implementation from the object structure. You can define a visitor class for each algorithm, and the objects accept the visitor to perform the algorithm-specific operations.

The visitor pattern is particularly helpful when you need to perform operations based on both the type of the object being visited and the type of the visitor. This allows you to achieve double dispatch or multiple dispatch, where the specific implementation of the operation is determined at runtime based on both the object and the visitor types.

Finally, the visitor pattern supports the open-closed principle, which states that classes should be open for extension but closed for modification. With the visitor pattern, you can add new visitors and operations to the object structure without modifying the existing classes, promoting extensibility and reducing the risk of introducing bugs.

Consider, for example, the case of a class representing derivative contracts. The class can have several polymorphic (virtual) methods, including one for displaying the profit/loss chart.

#include <list>

class ChartDisplay;

class SimpleDerivativeContract {

  public:

  virtual void chartProfitLoss(ChartDisplay *c);

};

class ChartDisplay {

public:

   virtual void displayContracts(

                 std::list<SimpleDerivativeContract*> &contracts);

   virtual void addToChart(SimpleDerivativeContract *c);

};

The SimpleDerivativeContract class has a virtual method that is able to present a chart with profit/loss for the position. But to do this, the derivative object needs to coordinate with a second class, called ChartDisplay. Both ChartDisplay and SimpleDerivativeContract have polymorphic methods that interact with each other, but in C++, the virtual dispatch is done in just a single method. For example, ChartDisplay might have specialized subclasses such as PDFChart and HTMLChart.

To make this possible, the visitor design pattern enables the dynamic interaction of two classes by the use of virtual methods that call each other. In summary, one of the objects becomes responsible to implement the visitation strategy, by which the virtual method on the second object is called. Here is the implementation of our example classes:

void ChartDisplay::displayContracts(

    std::list<SimpleDerivativeContract*> &contracts) {

    for (auto c : contracts) {

        c->chartProfitLoss(this);

    }

}

void SimpleDerivativeContract::chartProfitLoss(ChartDisplay *disp) {

      // ...

      // Use ChartDisplay virtual methods:

      disp->addToChart(this);

}

The first method, displayContracts, is responsible for displaying each of the contract objects stored in the container passed as parameter. To do this, the virtual method chartProfitLoss is called with the ChartDisplay as a parameter. On the other hand, the chartProfitLoss method also calls a virtual method from ChartDisplay: addToChart. This relation between the two methods is what makes the dual virtual dispatch to work, allowing two separate hierarchies to work together in a dynamic fashion:

class PDFDisplay : public ChartDisplay {

public:

    virtual void addToChart(SimpleDerivativeContract *c) {

        // Add contract to a PDF chart here

    }

};

class HTMLDisplay : public ChartDisplay {

public:

    virtual void addToChart(SimpleDerivativeContract *c) {

        // Add contract to an HTML chart here

    }

};

Conclusion

Design patterns are commonly used to develop reusable code, especially when OO techniques are employed. C++ provides strong support for the creation of classes that follow designed patterns such as the ones discussed in the preceding sections.

In this chapter, you saw examples and implementation in C++ for three common design patterns. First, I presented an overview of design patterns, listing some of the patterns that are most commonly used in the implementation of algorithms for options and derivatives. Then, you learned about the factory method design pattern, which is one of the easiest and most widely used patterns of OO programming.

The singleton pattern is used when it is necessary to enforce the existence of a single instance for a particular class. You saw the example of a clearing house implementation, where the single instance must be accessible to all clients in the application.

The observer pattern is a third example of how to implement such designs in C++. You saw how this pattern can be employed to solve the problem of trading processing. Using this design pattern, it is possible to decouple the classes that receive the events from specific classes that listen to the events and perform further processing.

While object-oriented design patterns provide several elegant solutions for commonly found problems in financial programming, there are situations in which a non-OO strategy may be a better solution. In these situations, C++ promotes the use of templates, an implementation technique in which the compiler is allowed to generate code based on parameterized types. In the next chapter, you will see several examples in which template-based algorithms can be used to improve the performance and flexibility of algorithms for options and derivatives trading.

© The Author(s), under exclusive license to APress Media, LLC, part of Springer Nature 2023

C. OliveiraOptions and Derivatives Programming in C++23https://doi.org/10.1007/978-1-4842-9827-5_6

6. Template-Based Techniques

Carlos Oliveira1  

(1)

Seattle, WA, USA

C++ templates allow programmers to write generic code, which works without modification on different data types. Through the careful use of templates, C++ programmers can write expressive code with high performance and low overhead, without the need to rely exclusively on more computationally expensive object-oriented techniques, such as the design patterns presented in the previous chapter.

This chapter explores a few template-based programming practices that can be used to solve options- and derivatives-based financial problems. Here are some of the topics discussed in this chapter:

Understanding the use of templates: You will learn about the basics of templates, including their syntax and how they can be implemented as template functions or template classes.

Using compile-time algorithms: This is a quick overview of how compile-time algorithms work, with some examples such as recursive algorithms, which allow compile-time definitions that depend on themselves recursively.

Containers and smart pointers: One of the most common uses of templates is to maintain containers of objects. Smart pointers are also frequently employed to simplify the code necessary for memory management.

Best practices: You will learn a few best practices that will improve your template-based code.

Templates in C++23: You will see how template code has become simpler and more powerful in the recent revisions of the C++ standard.

Introduction to Templates

A template is a mechanism to generate parameterized code so that different versions of the same programming definition (a class or a function) can be generated for each given parameter. A combination of parameters can also be used when required by the algorithm. In C++, the parameters passed to a template may be a concrete data type (native or user-defined data types) or an integer number, as you will see in the following examples.

You have already seen how to use basic templates in some of the previous examples that employed standard template library containers. Such containers include vectors, maps, and sets, as provided by the C++ standard library. In this section, you will learn more about the implementation of new templates and the features they can provide to application programmers.

One of the applications of templates is to perform compile-time computations. Performing some operations at compilation time instead of at runtime is a performanceenhancing technique that can save a lot of CPU and make your application run more smoothly. Examples of such cases include the use of integer recursive functions, conditional code that depends on particular data types, and container objects and smart pointers.

Recursive functions: A recursive function based on integer numbers can be easily calculated ahead of time using compile-time techniques. For example, some numerical algorithms depend on the use of factorials of numbers, which may be known at compilation time. Transforming a runtime computation into a compile-time transformation is an easy way to make your algorithms run faster.

Compile-time polymorphism: Another example of compile-time performance enhancement is the removal of conditional code based on types. When different operations need to be performed for different types, the standard procedure in OO code is to create a hierarchy that provides a different implementation for each type involved. With templates, you can replace this type of runtime polymorphism with compilation-time polymorphism. In that case, the right template is executed based on the type that is already known at compilation time, and as a result, no decision is necessary at runtime, avoiding extra computational effort.

Container objects: Container objects provide a big advantage to template users. They simplify the coding required to maintain and employ commonly used data containers. The STL provides several containers based on templates that streamline the task of storing objects using different memory allocation strategies. For example, std::map allows programmers to map from a key type to a value type in a generic way. The use of templates also simplifies common tasks such as iterating through the elements of the container. Since templates know the type of objects stored at the time of compilation, there is no need to use a cast or other time-consuming polymorphic techniques such as is used by OO code.

Smart pointers: Finally, templates also give C++ the ability to automatically manage memory using smart pointers. A smart pointer is a template that has the sole purpose of managing an object that has been passed as a pointer. The exact semantics of a smart pointer changes according to the particular template and the desired results, including, for example, the ability to use reference counting, or to be owned by a single client. The standard C++ library provides a small number of smart pointers, such as std::auto_ptr, std::unique_ptr, and std::shared_ptr, among others.

Note A possible disadvantage of templates is the possibility of duplication of generated code in the resulting binary application. For example, if a large template has a type parameter, the compiler needs to duplicate the generated code for each different type that is used. This has the potential of creating bloated executables with several redundant compiled templates. Thankfully, modern computers have enough memory that this is not a common concern, but as application sizes grow, software developers need to consider this issue.

In the next few sections, you will see some examples of template-based techniques and learn how these techniques can be effectively implemented in C++.

Compilation-Time Polymorphism

One of the techniques you learned in the previous chapter is the use of polymorphism based on object-oriented features such as the virtual keyword. One of the advantages of templates is that they can be used to work with different types, while at the same time they avoid the need for runtime checking that is inherent to the use of polymorphic classes.

With templates, you can use compile-time polymorphism in several situations where types can be known by the compiler. This makes it possible to write code that’s independent of the type used while at the same time avoiding the expense of runtime lookups.

An example that is commonly used in financial code is applying mathematical operations to different datasets. This can be done in several ways, but templates can be used to make the process efficient and transparent to the programmer. Consider the operation of normalizing a dataset. To apply such an operation to different sets, you could create a Normalize template, as demonstrated in the following code. First, you assume that there are two implementations available for the normalization operation: one for vectors and another for sets:

void array_normalize(std::vector<double> &array);

void set_normalize(std::set<double> &set);

The next part of this example shows the main template class, called Normalization. This class provides the main declaration used. In a more complete implementation, Normalization would also contain a number of static definitions other than a single function, but that is enough to demonstrate the usefulness of the class template.

The member function normalize performs the work of normalization in a generic way; therefore, it must receive as argument a type that is a template parameter:

template <class T>

class Normalization

{

public:

    typedef T Type;

    static void normalize(T &arg);

    // Other methods here...

};

Now, you’re ready to implement as many specializations of the normalize function as necessary. I present two specializations here: one using a vector of doubles and another using a set of doubles. These two implementations use the regular functions that have been declared previously, and their implementations are now shown here.

template <>

void Normalization<std::vector<double>>::normalize(std::vector<double> &a)

{

    array_normalize(a);

}

template <>

void Normalization<std::set<double>>::normalize(std::set<double> &a)

{

    set_normalize(a);

}

Note

Consider how the parameter list for the template is empty. This syntax indicates that this is a specialization of a previously defined member function.

Notice how these definitions are independent of the original class definition. This means that if you create a new type of normalization function that can be applied to a particular type, the only thing you need to do is declare a new template specialization that uses that function. Therefore, the Normalization class is essentially an open definition that can be extended by any library that decides to implement a new normalization strategy. And this can be done without any runtime overhead, since the right normalization strategy will be chosen during compilation.

Finally, I present a template function that simplifies a call to the normalization member function. This template function is called normalize and just calls the desired static member function:

template <class T>

void normalize(T &val)

{

    Normalization<T>::normalize(val);

}

Here is an example of how such a function can be called for different types. The compiler will generate optimal code by deciding which specialization of the class to use and will make the call without runtime overhead:

void use_normalize()

{

    std::set<double> set;

    std::vector<double> array;

    // Initialize variables here...

    normalize(set);

    normalize(array);

}

Template Functions

A template function is a C++ function that can be parameterized with the use of one or more types or integral values. Using template functions, you can write generic functions that work with any combination of the original parameters, expanding the domain of application for the code contained in the original implementation.

Consider as a first example the function returning the maximum value between the two given parameters. It is easy to write such a function for a particular data type. For example, for integer parameters, this function can be written, in a verbose way, as

int int_max(int a, int b)

{

    if (a > b)

    {

        return a;

    }

    else

    {

        return b;

    }

}

To create a generic version of this function, you just need to create a template function that is parameterized on the types used in the parameter list and return values:

template <class T>

T generic_max(T a, T b)

{

    if (a > b)

    {

        return a;

    }

    else

    {

        return b;

    }

}

With this template, you can not only compute the maximum of two integers but can also do the same for any type that supports a comparison using the > operator. This even includes nonnumeric types such as strings, as you will see next.

The string case is interesting in this example, because it also involves the discussion of partial specialization. A partial specialization is a version of a template where one or more of the parameters have been substituted by concrete types or values. You can specialize the generic_max template function to handle zero-terminated strings using a different implementation, as follows:

template <>

const char * generic_max(const char *a, const char *b)

{

    if (strcmp(a, b) > 0)

    {

        return a;

    }

    else

    {

        return b;

    }

}

This syntax indicates that this is a specialization of the previously defined generic_max function. The parameter type const char * is substituted directly in the function implementation. This function in particular uses the strcmp function from the C standard library to determine if a string is less than another.

Implementing Recursive Functions

One of the applications of compile-time computation through templates is the implementation of recursive functions. A recursive function is one in which the result of the operation for a particular value can be calculated based on another application of the same function.

The reason why it is possible to use templates for computing recursive functions is the ability of these C++ templates to take integral numbers as arguments. For example, a trivial template that prints a static value can be defined using the number as a template argument:

template <int N>

void printNumberPlusOne()

{

    int a = N + 1;

    std::cout << a << std::endl;

}

void usePrintTemplate()

{

    printNumberPlusOne<10>();

}

Here, the integer N is passed not as a function argument but as a compile-time parameter. This means that during compilation, the value of N is already known as a constant value, which eliminates the need for computation during runtime. This makes the operation much more efficient than it would be the case of normal parameter passing.

This example can be further expanded using a recursive strategy to print N numbers at compilation time. Here is a simple version that does this recursively:

template<int N>

void printNumberRecursive()     // general case

{

    std::cout << N << " ";

    printNumberRecursive<N-1>();

}

template<>

void printNumberRecursive<0>()  //  base case

{

    std::cout << std::endl;

}

void usePrintRecursive()

{

    printNumberRecursive<10>();

}

This template is implemented as a general case and a specialization (base case). The general recursion case is what should be done in most cases, which in this case is print the given template parameter N and call the same template with a smaller value N-1. The base case is what should happen to cause the recursion to stop. In this example, the recursion stops when the value 0 is reached, in which case the template simply prints a new line.

Taken together, these two cases for the printNumberRecursive template can print the numbers from N to 0 using only compilation-time expressions. This means that all calculations have already been done by the compiler, dramatically cutting down the computation effort at runtime.

You can use the same strategy to compute more complex and useful recursive functions. Table 6-1 shows a few common recursive functions that involve integer numbers and that can be easily implemented using C++ templates. Notice how each of these functions uses its own definitions in order to compute the next value.

Table 6-1

Common Integer Recursive Functions

Recursive Function

Description

Factorial

Calculate factorials of the form 1×2×3×...×n.

Fibonacci

Calculate the general recursion F(n) = F(n – 1) + F(n – 2).

Triangular numbers

Calculate the number of items in triangular formation.

Binomial coefficients

Calculate the coefficients of polynomial equations of the form (ax + b)n.

In a more complete example, consider the implementation of the summation of the first N integer values, for a given parameter N. You can do this with a template function that recursively calls itself. Thanks to templates, the C++ compiler can calculate such values during compilation time. Here is an implementation of such a function:

template <int N>

int intSum()

{

    return N + intSum<N-1>();

}

template <>

int intSum<0>()

{

    return 0;

}

void useIntSum()

{

    std::cout << intSum<20>() << std::endl;

}

As before, there is a general case for most values of N and a base case that is used when the parameter is 0. The general case defines the template and its integer parameter. The base case is a template specialization, so the exact argument value needs to be provided.

The intSum template in the general case returns the sum of the argument that was originally passed and adds to that the value of intSum for N-1. Since all these calculations are based on constant values at compilation time, the result is computed using the compiler itself.

The specialization of intSum deals with the base case that terminates the recursion. When the argument is 0, the value 0 is returned as the value of the sum. The function useIntSum instantiates the template, passing the value 20 as its parameter. The result is then printed to standard output.

Recursive Functions and Template Classes

Recursive functions can also be implemented using template classes, instead of simple functions. This is recommended when additional information is supposed to be stored with the result of the function. A template class can also receive as a parameter an integer number, along with specializations based on that template parameter.

Consider an example template class that computes the factorial of a number. The logic of this type of computation is very similar to the functions you have seen before. However, it gives you an opportunity to see how a template class works in this situation.

template <long N>

class Factorial

{

public:

    enum

    {

        Argument = N

    };

    static long value();

};

template <long N>

long Factorial<N>::value()

{

    return N * Factorial<N-1>::value();

}

template <>

long Factorial<0>::value()

{

    return 1;

}

void useFactorial()

{

    Factorial<8> fact;

    std::cout << " factorial for argument " << fact.Argument << " is "

              <<  fact.value() << std::endl;

}

The class Factorial shows how a template class can store useful values as part of the class definition. The enumeration at the beginning of the class definition contains a value called Argument, which stores the argument for further use as a value of the enumeration. This exemplifies a feature that cannot be achieved by a simple function: the use of a class may allow any value to be stored for further use, either as an enumeration or a static variable. The way the template is expanded by the compiler is shown in Figure 6-1.

A block flow diagram represents 4 sets of programs. Each block consists of the factorial metafunction by instantiating the template from 3 to 0 and resulting in value generation.

Figure 6-1

An example of computation using template specialization. The general case of the Factorial template is instantiated with the integer 3, and new instantiations are used until the specialization for Factorial<0> is reached

The Factorial class also contains a static function that computes the desired factorial number. As in the previous examples, the function is implemented with a general case for any integer number and a base case, which is used when the 0 value is passed.

The useFactorial function shows how to invoke the Factorial class for a particular compile-time computation. The factorial of the value 8 is desired, so it is passed as the single argument to the template class. The next line uses the Argument enumeration value so it can retrieve the passed argument.

The value of the factorial is finally accessed using the value member function. Notice that, as usual with templates, the value function is calculated at compilation time and the result is replaced by the compiler at that particular point.

Containers and Smart Pointers

One of the most important applications of templates in C++ is the creation of data containers. A container is a template-based object that maintains and provides access to other underlying objects or data structures. For example, a common container used in C++ is std::vector, which is a representation of sequential memory that can be accessed using a numeric index. Other more complex containers are provided in the STL and in third-party libraries that are commonly used in financial applications.

Here are some of the best-known STL containers and the types of arguments that they expect in the standard library. A short list of available containers is displayed in Table 6-2.

Note

All STL containers receive as a parameter a default Allocator type, which determines how objects are allocated, such as using the global heap or some other preallocated local memory. If this type is not supplied, the standard allocator for the new keyword is used when creating objects.

std::vector<T, Allocator>: The type T passed to std::vector represents the main type of each element stored in the vector. This container guarantees that elements will be stored sequentially.

std::map<K, T, Allocator>: This template requires two parameters. The first parameter represents the type of the key and should be an immutable object. The second type represents the object stored for each key. Maps have variations, such as std::unordered_map, where entries are unordered, and std::multimap, where each key can have more than one associated entry.

std::queue<T, Allocator>: An std::queue provides a first-in first-out mechanism, and the argument T is the type of elements stored in this container. This container also has a variant called std::dequeue, which allows elements to be removed from the front or back of the queue.

std::stack<T, Allocator>: A template object that stores elements in a first-in last-out mechanism. The elements are typically allocated sequentially.

Table 6-2

Common STL Containers and Their Parameters

Container Type

Description

std::vector

Container in which elements are stored in sequential mode. Each element must have the same type, as determined by the template parameter.

std::map

A container where each element is associated with a unique key. The container allows searching by keys.

std::queue

A first-in first-out container that has elements of the same type, the type being the parameter to the template.

std::array

A simple sequential group of elements that can be indexed by a number. The element type is passed as a template argument.

std::list

A linked list where each object has the same type.

std:set

A container that stores an unordered list of objects. Elements of set can be retrieved efficiently.

std::stack

A first-in last-out container where each element has the same type, as determined by the template parameter.

The second important application of templates in the C++ standard library is in the implementation of smart pointers. A smart pointer enables you to manage the memory of objects allocated in the heap. It does this through particular strategies such as using reference counting, or restricting the access to the pointer and deleting the associated memory at the end of the current scope (as is the case with std::auto_ptr).

Smart pointers are possible due to the ability to generate specific code for each data type passed as parameter. Thus, an std::shared_ptr<OptionsContract>, for example, can be created to manage objects of type OptionsContract only.

Table 6-3 presents a few of the most common smart pointer templates. Some of these templates have been available as part of the standard C++ library since C++11.

Table 6-3

Common Smart Pointer Templates

Smart Pointer

Description

std:auto_ptr

A smart pointer that provides automatic deallocation with single ownership semantics.

std:shared_ptr

A smart pointer that provides a reference-counted memory management, with shared ownership semantics.

std::unique_ptr

A smart pointer that provides unique ownership of an object.

std::weak_ptr

A shared pointer that represents a weak reference to an object allocated in the heap.

Avoiding Lengthy Template Instantiations

C++ templates are a powerful mechanism that can be used to create generic code. With templates, it is also possible to remove undesirable code duplication, since the same code can then be applied to data of different types.

On the flip side, however, templates can also create problems due to the potential they have to slow down compilation times, when complex template rules are processed during compilation. Also, because all the code in a template is generally available to the compiler when processing translation units, it is difficult to provide separate compilation for templates. An example of a library that is a victim of this behavior is boost, where typically all the functionality is included in header files. These header files are then included each time the library is referenced in an implementation file, resulting in long build times.

Despite these shortcomings, in some situations, it is possible to reduce the amount of work done by the compiler on behalf of templates. This section shows a simple technique that can be used to achieve faster template compilation speeds when desired instantiations are known ahead of time.

Preinstantiating Templates

Certain templates are used in only a reduced number of cases by design. For example, consider a numerical library that creates code for different types of floating-point numbers. Each class in the library can be instantiated with a particular floating-point type, such as double, long double, or float. Consider, for instance, the following definition:

// file mathop.h

//

// The template class for mathematical operations

//

template <class T>

class MathOperations

{

public:

   static T squared(T value)

   {

      return value * value;

   }

  // ...

};

This class can be used in the following way:

#include "mathop.h"

#include <iostream>

using std::cout;

using std::endl;

MathOperations<double> mathOps;

double value = 2.5;

cout << "result: " << mathOps.squared(2.5) << endl;

Unfortunately, because the MathOperations class is a template class, you have to include its complete definition as part of the header file, where it can be found in the compiler whenever the class is instantiated.

One possible way to reduce the size of the header file is to preinstantiate the template for the types that you know in advance.

The first step is to remove the implementation from the header file. This is clearly possible, since you can implement class member functions outside the class declaration (whether the class is a template or not). Then, you need to add the implementation to a separate source file. Once this step is done, client code can use the template class interface but will not be able to generate code. Therefore, for this to work, you need to instantiate the templates on the implementation file.

// file mathop.h

//

// The template class included by the applications

//

template <class T>

class MathOperations

{

public:

   static T squared(T value);

   // ...

};

// file mathop.cpp

#include "mathop.h"

//

// Template member function definition

//

template <class T>

T MathOperations<T>::squared(T value)

{

   return value * value;

}

//

// Function used to instantiate code for specific datatypes

//

void instantiateMathOps()

{

   double d = MathOperations<double>::squared(2.0);

   float f = MathOperations<float>::squared(2.0);

   int i = MathOperations<int>::squared(2);

   long l = MathOperations<long>::squared(2);

   char c = MathOperations<char>::squared(2);

}

In this example, I chose to instantiate five versions of the original template for numeric types. The main limitation of this technique, as I mentioned, is that your clients will not be able to generate templates for the additional types they may want to use. However, in a few situations, you may really want to restrict how these templates are used, and this technique works as desired.

Templates in C++23

The latest updates of the C++ standards, of which C++23 is the newest, have introduced a number of features that simplify and improve the use of templates. As usual, with the release of a new version of the standard, you can still use every feature that was discussed in the previous sections. However, C++23 adds new ways to use templates with easier syntax and expanded functionality.

For example, a great feature that has been added in the last standard is the ability to write template functions without the use of the template keyword. This functionality is now available using the auto keyword as part of the function declaration.

Here is a case where a template function has become much easier to use:

#include <string>

#include <iostream>

// This is a template that is instantiated depending on

// passed arguments.

auto apply_add_operator(auto x, auto y)

{

    return x + y;

}

int main()

{

    int x = 1, y = 3;

    std::string a{"hello "}, b{"world"};

    std::cout << apply_add_operator(a,b) << "\n";

    int res = apply_add_operator(1,3);

    return 0;

}

The preceding code fragment shows how to create a generic function without the use of the template keyword. The function apply_add_operator will take two arguments and apply the operator plus to the two arguments, returning the result. This works for any data type that supports the operator plus (“+”). In the preceding example, you can see the application of this function to variables of type std::string and int. The compiler will take care of choosing the right data types and generating the code as needed.

For comparison, the old way of doing this is to define the generic function using template arguments, in the following way:

template <class T>

T apply_add_operator2(T x, T y)

{

    return x + y;

}

However, since the standard C++17, there is no need to use template arguments when in the presence of the automatic deduction keyword auto. This means that you can write shorter function declarations and let the compiler do the heavy lifting of defining the exact types of the parameters passed to a generic function. Just as with automatic variable declarations, the auto keyword can help you to avoid a large amount of boilerplate that was needed in previous versions of C++.

The constexpr Syntax in C++23

One of the goals of the C++23 programming language standard is to enhance the capabilities of the "constexpr" keyword. The "constexpr" keyword is currently used to declare functions and variables that can be evaluated at compile-time. It allows computations to be performed during compilation, which can lead to performance improvements and better optimization opportunities.

The extended constexpr syntax in C++23 expands the usage of "constexpr" in several ways:

Function parameters: Currently, "constexpr" functions can only have literal types as parameters. The new syntax aims to remove this restriction, allowing nonliteral types to be used as parameters in "constexpr" functions. This enables more flexible and powerful compile-time computations.

Local variables: Currently, "constexpr" variables must be declared at namespace scope or as static members of classes. C++23 allows extending "constexpr" to permit the declaration of local variables, enabling compile-time evaluation of local computations.

The proposal also suggests adding "constexpr" support to more Standard Library functions and algorithms. This would enable developers to use these functions in constexpr contexts, promoting further compile-time evaluation and optimization.

By expanding the usage of "constexpr" in these ways, the new standard seeks to make compile-time evaluation more powerful and flexible in C++. This can lead to improved performance, increased code reuse, and better opportunities for compile-time optimizations.

Here is some sample code that demonstrates the numerous contexts where constexpr can be used in C++23:

#include <iostream>

#include <string>

#include <vector>

constexpr int square(int x) {

    return x * x;

}

constexpr int sum(const std::vector<int>& numbers) {

    int total = 0;

    for (int num : numbers) {

        total += num;

    }

    return total;

}

constexpr char a_digit(int n) {

  static constexpr char all_digits[] = "0123456789abcdef";

  return all_digits[n];

}

int main() {

    constexpr int result1 = square(5);

    std::cout << "Square of 5: " << result1 << std::endl;

    constexpr int result2 = sum({1, 2, 3, 4, 5});

    std::cout << "Sum of numbers: " << result2 << std::endl;

    return 0;

}

In this example, the square function is declared as constexpr and computes the square of its input parameter. The sum function takes a const std::vector<int>& as a parameter and calculates the sum of the numbers in the vector. Both functions are evaluated at compile-time because they are marked as constexpr and their arguments satisfy the requirements.

In the main function, square is called with a constant expression (5) as an argument, and the result is assigned to the constexpr variable result1. The value of result1 is then printed.

Similarly, the sum function is called with a constexpr vector, and the result is assigned to the constexpr variable result2. The value of result2 is printed.

Since both computations occur at compile-time, the values of result1 and result2 are known and can be used for further compile-time optimizations or as constant expressions. This code was compiled with the latest gcc using the -std=c++23 option.

Conclusion

While object-oriented design patterns provide several elegant methods for the solution of commonly found problems in financial programming, there are cases in which a non-OO strategy may be more indicated. In these situations, C++ promotes the use of templates, an implementation technique in which the compiler is allowed to generate code based on parameterized types.

In this chapter, you learned how to create new template classes and functions that use the template facilities of C++. Among other things, you saw how to create functions and classes that compute their results at compilation time. Compilation-time polymorphism, an alternative to runtime polymorphism that uses the code-generation capabilities of C++ templates, was also discussed.

The next chapter continues exploring templates in C++ with a more detailed view of the standard template library and its algorithms.

© The Author(s), under exclusive license to APress Media, LLC, part of Springer Nature 2023

C. OliveiraOptions and Derivatives Programming in C++23https://doi.org/10.1007/978-1-4842-9827-5_7

7. STL for Derivatives Programming

Carlos Oliveira1  

(1)

Seattle, WA, USA

Modern financial programming in C++ makes heavy use of template-based algorithms. Many of the basic algorithms related to trading options and their derivatives are implemented in terms of function and class templates. This is done due to the superior advantages of templates in terms of performance as well as their ability to improve code reuse.

Several template-based algorithms are implemented right into the standard template library (STL), which is one of the main parts of C++ standard library. Therefore, it is important to become familiar with the concepts of algorithms in the STL and to understand how they can be used and extended to more complex applications.

In this chapter, I discuss STL algorithms and how they can be employed in quantitative finance and other programming projects. In particular, I attempt to cover how these template-based algorithms are used in practice to solve common problems with options and other financial derivatives. After reading this chapter, you will get a better understanding of how the STL interacts with other parts of the C++ libraries and how it imposes a certain structure on classes developed in the language.

Here are some of the concepts discussed in this chapter:

STL-based algorithms: Here, I present an introduction to the basic concepts of algorithms in the STL, how they interact with the container, and their basic performance characteristics.

Functional techniques on STL: The STL algorithms can simplify your code with the use of a functional style of programming, whereby you can use functions as a first-class object of abstraction.

Working on STL containers: STL algorithms have been developed so that they work in tandem with containers. You need to understand the usage patterns of STL algorithms and how they can efficiently employ the most common containers provided by the standard C++ library.

Efficient iterators: Another way in which algorithms interact with containers is through the use of iterators. Developers can use iterators in flexible ways, thanks to the support available in the STL algorithms.

Improvements in C++23: You will also learn how the latest C++ standard has turned the STL into an even more efficient and easy-to-use library.

Introduction to Algorithms in the STL

The STL offers a set of templates that can be used to solve some of the most common problems encountered in C++ programming. Among such templates, you will find a list of algorithms that implement tasks such as copying, sorting, selecting, iterating, and adding elements to generic collections such as vectors, sets, maps, and their variations.

With STL algorithms, C++ designers created a set of template functions that manipulate generic collections. Once these algorithms have been implemented as templates, developers are free to use them for any class that satisfies the functional requirements of its container. For example, based on the STL, you can create vectors of any custom class and apply template algorithms such as sort and reverse to manipulate these objects, without having to write any additional code. Table 7-1 presents a list of algorithm types available in the STL.

Table 7-1

A List of Algorithm Types Available in the STL

Algorithm Type

Description

Conditional testing

Performs a test of a given condition against elements of a container. Algorithms include operations such as all_of (testing if all elements are true), any_of (testing if any of the given elements are true), and none_of (testing if none of the elements are true).

Iteration

Performs an operation for each element of a container, such as the for_each algorithm.

Searching

Finds elements in a container: find, find_if, find_if_not, find_first_of, and search.

Counting

Returns the number of elements in a container: count and count_if.

Sorting

Puts the elements of the container in a defined sorted order: sort, stable_sort, and partial_sort.

Partitioning

Partitions the container into two ranges according to a given property: partition, partition_copy, and partition_stable.

Merge

Performs the merge of two containers that have been previously sorted: merge, set_union, set_intersection, and set_difference.

Binary search

Implements a binary search for each STL container. Examples are lower_bound, upper_bound, and binary_search.

The generic algorithms in the STL can be imported into a C++ application using the <algorithm> header file. Most of these algorithms are implemented directly as templates in the header file, so they can be available to any client code without the need of external binary components.

The next few sections describe a few common tasks that are implemented as STL algorithms and explain how they can be used from client code, including financial applications.

Sorting

Sorting is a basic activity that is common to many algorithms. For this reason, sorting templates have been created to deploy high-performance sorting algorithms without much effort. Reusing sorting algorithms also allows programmers to avoid re-creating well-known algorithms and the possibility of introducing mistakes into the implementation. STL algorithms provide just what you need in order to apply sorting strategies to containers and other data structures.

The STL has a set of template algorithms that can perform sorting on many different types of containers. The right algorithm should be selected according to the desired properties of the container and the data stored in it. For this purpose, the library gives you several options corresponding to the different desired tasks and their properties. As a developer, you should become acquainted with these types of sorting algorithms. Table 7-2 lists a set of algorithms commonly available from the STL (specific implementations might add their own variants).

Table 7-2

A List of Sorting Algorithms Available in the STL

Sorting Algorithm

Description

sort

Generic sorting algorithm that can be used on most containers. This should be used in the majority of cases.

stable_sort

A stable sorting procedure that maintains the relative positions of elements in the container.

partial_sort

An algorithm that sorts only part of a given container.

partial_sort_copy

An algorithm that performs partial sorting on a copy of the original container.

is_sorted

Returns true if the given container is already sorted. This is useful when working with an unknown container.

nth_element

An algorithm that sorts only one of the largest elements of a container.

The first type of sorting template is the generic sort function. This function can be applied to a range of values that are stored in the container, given by two iterators: one for the start and another for the end of the range. As normally happens in the STL, the container can be anything that can be iterated over, including arrays, vectors, maps, sets, and other container templates. This sort of function can also take as a parameter a comparison function, which is used to determine the proper order of objects in the collection.

Consider, for example, a date type. The goal is to be able to sort objects of type date, which are stored in a standard STL container. To be able to sort based on dates, however, you need to provide a comparison function for the underlying date class. In C++, this is done through the use of a C++ class operator that overloads the standard comparison operator. Here is a quick example:

class Date {

public:

    // Other public methods here...

    bool operator<(const Date &d);

    int year()  const { return m_year;  }

    int month() const { return m_month; }

    int day()   const { return m_day;   }

private:

    int m_day;

    int m_month;

    int m_year;

};

bool Date::operator<(const Date &d)

{

    if (m_year < d.m_year)

    {

        return true;

    }

    if (m_year == d.m_year and m_month < d.m_month)

    {

        return true;

    }

    if (m_year == d.m_year and m_month == d.m_month and

        m_day < d.m_day)

    {

        return true;

    }

    return false;

}

bool operator<(const Date &a, const Date &b)

{

    return a < b;

}

Notice that there are two versions of the < operator. The first version is written as a member function. This is necessary so that the operator has access to the private member data of the date class. The second version of the < operator is a free function, and it is necessary when the first argument is a constant object. The implementation of the free function is directly based on the member function.

void sort_dates()

{

    vector<Date> dates;

    // ....  initialize the dates here

    std::sort(dates.begin(), dates.end()); //  perform comparison

}

The sort_dates function provides an example of using the standard sort template. In this version, the default comparison is used, which in this case is implemented by the < operator. You can, however, use a different comparison function, as shown in the following example:

bool year_comparison(const Date &a, const Date &b)

{

    return a.year() < b.year();

}

Here, the specialized comparison is performed only using the date year fields you stored in each date object. The comparison function can be called in the following way:

void sort_dates()

{

    vector<Date> dates;

    // ....

    // Performs comparison by year only

    std::sort(dates.begin(), dates.end(), year_comparison);

}

In this instance, since you are using a specialized method of comparison, you need to provide a comparison function explicitly. The result of this sorting procedure is a sequence of dates where the elements appear in increasing order of year.

The preceding case can be used to exemplify the use of stable sorting. In a stable sorting, elements that are equal with respect to the sorting strategy appear in the same order as the input. This is an important feature in some sorting applications. Therefore, if you want to maintain the relative sorting position of dates within a year, you should instead use the stable_sort template function. This is exemplified in the modified code:

void stable_sort_dates()

{

    vector<Date> dates;

    // .... initialization here

    // Performs comparison by year only,

    // but relative order is maintained

    std::stable_sort(dates.begin(), dates.end(), year_comparison);

}

Presenting Frequency Data

A simple application of sorting can be seen in the presentation of frequency data. Suppose that you were given a vector of price observations, and the goal is to present this pricing data according to the frequency in which it appears. This is an application that is typically described as a data histogram; that is, the data is presented according to increasing frequency.

To solve this problem, you can use STL containers and the sorting template algorithm to reorganize results. The final function is named compute_frequency. The first step of the algorithm is to calculate the number of bins defined by the data interval. To compute this, you’ll use the variables start, end, and step size. Here is the implementation:

//

//  stl_alg.cpp

//  Sorting algorithm for price data

#include <algorithm>

#include <vector>

#include <cmath>

#include <iostream>

using std::vector;

using std::cout;

using std::endl;

using std::pair;

void compute_frequency(vector<double> &prices, double start,

                       double end, double step)

{

    int nbins = int(std::abs(end-start)/step);

    vector<pair<int, int>> count(nbins, std::make_pair(0,0));

    for (int i=0; i<nbins; ++i)

    {

        count[i].second = i;

    }

    for (int i=0; i<prices.size(); ++i)

    {

        if (start <= prices[i]  && prices[i] <= end)

        {

            int pos = int((prices[i] - start)/step);

            count[pos].first++;

        }

    }

    std::sort(count.begin(), count.end());

    for (int i=0; i<nbins; ++i)

    {

        int k = count[i].second;

        cout << start + k * step << "-" << start + (k+1) * step

             << ": "  << count[i].first;

    }

}

The vector count stores the frequency of each data interval. Each element of the count vector has two members: the first member is the frequency, and the second member is the relative position of the interval. These two values are stored as an STL pair, and the sequence of numbers is initialized in a for loop.

The next step is to store the frequency counts. This is done in a loop that iterates through the given range, adding to the frequency of each data point. Finally, after the frequencies are collected, you can sort them using the STL sort algorithm, which in this case uses the begin and end functions to define the sorting range. Following this, the frequencies are presented to standard output along with the respective ranges, which have been saved in the index variable.

// Sample test of histogram algorithm

int frequency_test()

{

    vector<double> prices = {32.3, 34, 35.6, 39.2,

                             38.7, 31.17, 33.14 };

    compute_frequency(prices, 31.0, 39.0, 0.1);

    return 0;

}

To test this code, I created a sample function frequency_test that calls the compute_frequency function with a few data points. The output of the code execution should look like the following:

31-31.1: 0

31.2-31.3: 0

31.3-31.4: 0

31.4-31.5: 0

33.3-33.4: 0

// ...  more data here ...

38.9-39: 0

31.1-31.2: 1

32.2-32.3: 1

33.1-33.2: 1

34-34.1: 1

35.6-35.7: 1

38.7-38.8: 1

Figure 7-1 shows a histogram, computed from sample data processed by the function frequency test. This kind of ranking function is useful when working with financial data such as price volatility.

A scatterplot depicts the relationship between y and x. The data points are dispersed across the graph. Some of the plots peak between 7 and 15 of x.

Figure 7-1

Histogram displaying number of values computed in sample data given function frequency_test

Copying Container Data

Another common application of template algorithms is to copy elements from one container to another. This can be easily done using the copy template algorithm. This algorithm can perform copies between containers of different types using common conversion techniques already provided by the C++ language.

For example, it is possible to copy a container of integer numbers (int) into a second container that maintains only numbers of type double. Consider the following code:

void copy_int_to_double()

{

    vector<int> ivector(100, 1);

    vector<double> dvector(100);

    std::copy(ivector.begin(), ivector.end(), dvector.begin());

}

Here, the vectors ivector and dvector have different types. The fact that you have a template algorithm means that you don’t need to write separate functions to handle every combination of types that could be presented as an argument to the copy function.

Another useful ability provided by this template is to copy elements from an existing container into the standard output. To do this, you need to wrap the standard output (or any other stream for that matter) with an std::ostream_iterator object, which allows you to iterate through an output stream. Here is an example of a simple way of displaying the contents of an STL container:

void print_prices()

{

    vector<double> prices(100);

    // Initialize prices here

    std::copy(prices.begin(), prices.end(),

              std::ostream_iterator<double>(cout));

}

The print_prices function creates and initializes a vector of doubles. Then, it passes the begin and end iterators for this vector as the first two parameters of find. Finally, the third argument wraps the standard output stream into an iterator for data of type double.

If you need to simplify the use of find (and many other similar algorithms), you could implement your own template algorithm that extracts the correct begin and end iterators. For an example of how you can do this, consider the following template function:

template <class T, class S >

typename T::const_iterator find(const T &a, S val) {

    return std::find (a.begin(), a.end(), val);

}

This template function receives two template parameters: the first is a container class, and the second is a value type. The find template presented here will just call std::find and make sure that the first two arguments are the begin and end of the passed container. This code could be called in a way similar to the previous example:

void find_value()

{

    vector<int> values;

    // ...  initialize the vector

    // Call our template

    vector<int>::const_iterator result = find (values, 42);

    if (result == values.end())

    {

        cout << " the value was not found " << endl;

    }

    else

    {

        cout << " the value found is " << *result << endl;

    }

}

Finally, using std::copy, it is also possible to transform a container template such as list into a different container type, such as vector. This kind of transformation allows programmers to easily convert containers of one type into another, without having to create custom code for each case. Here is an example:

void from_list_to_vector(const list<int> &l)

{

    vector<int> values;

    // Copy contents to destination array values

    std::copy(l.begin(), l.end(), values.begin());

    // Do something with the vector here

}

In this example, the function receives an std::list of integers and copies the content stored in the list into an std::vector<int>. Since std::copy is a template that works with different container types, you can simply rely on the standard library to perform the desired conversions.

Finding Elements

Finding elements in a container is another common operation that can be performed with the help of STL algorithms. The find family of templates allows programmers to search using different options. As usual, the find templates are optimized according to the specific container to which they are applied, but this is done automatically by the STL.

First, you have the simple find algorithm. This algorithm takes as parameters two iterators that specify the start and end of the target data. The next parameter is a value that you want to find in the given container. If the value is found, the algorithm returns an iterator pointing to the desired location. If the value is not found, the algorithm returns the second iterator, named last. Here is an example of how this works:

void find_value()

{

    vector<int> values;

    // ...  initialize the vector

    auto result = std::find(values.begin(),

                            values.end(), 42);

    if (result == values.end())

    {

        cout << " the value was not found " << endl;

    }

    else

    {

        cout << " the value found is " << *result << endl;

    }

}

The find_value function is responsible for searching for a particular number inside a vector of integers. The values variable is declared as a vector container and should be initialized as desired. Next, you need to apply the find function using the beginning and end iterators returned by values. Therefore, the previous example shows how to search for a constant number. The return value of this function is then stored in a vector iterator. If this variable corresponds to the end iterator, you know that the value was not found. Otherwise, the value is printed using the contents pointed to by the returned iterator.

Another type of search is necessary if you use a conditional find. In this case, you should use the find_if template function. This function enables you to use a predicate, in other words, a conditional selection statement (also called a filter) that is true only for the desired values.

Suppose, for example, that I try to search for a particular value inside of a container, such that the value is greater than 100. This is possible by defining a specific predicate and passing it as the last argument to the find_if function template. This can be done as follows:

bool greater_than_100(int num)

{

    return num > 100;

}

void conditional_find()

{

    vector<int> values;

    // ...  initialize the vector

    auto result = std::find_if(values.begin(),

                               values.end(), greater_than_100);

    if (result == values.end())

    {

        cout << " the value was not found " << endl;

    }

    else

    {

        cout << " the value found is " << *result << endl;

    }

}

First, I introduce a new predicate function called call_greater_than_100. This function simply returns true when the number passed as an argument is above 100. Next, you can see the function conditional_find. This function is similar to the previous example, but it uses the find_if template function instead. The first and second arguments to the find_if function also determine the range of values tested. The last argument is simply a pointer to the predicate function that was presented previously.

Another way to do the same thing is to use a lambda function, instead of a normal function. A lambda function is a C++ functional element that can be built inline and can be passed to other functions themselves as a parameter. For example, instead of creating a separate function such as greater_than_100, one can pass the same comparison as a lambda function. The syntax for this is to put square brackets at the beginning of the function, instead of a function name:

   [](int num){return num > 100;}

The empty brackets syntax makes it easy to remember that the function name is not required. A complete version of the previous function that uses a lambda expression can be written as follows:

void conditional_find2()

{

    std::vector<int> values;

    // ...  initialize the vector

    auto result =  std::find_if(values.begin(), values.end(),

                                [](int num){return num > 100;});

    if (result == values.end())

    {

        std::cout << " the value was not found " << std::endl;

    }

    else

    {

        std::cout << " the value found is " << *result << std::endl;

    }

}

Note

The last argument of find_if can be a function or a functional object. A functional object implements the function call operator and therefore can be called using syntax similar to a call to a normal function. Such functional objects are explained in the next chapter. In the case of find_if, the last argument can also be a lambda expression.

Selecting Option Data

This section shows an additional example of how STL functions can be used to speed up option data processing. This example shows a simple implementation of options, where one of the data members is the number of days until expiration.

Let the option class be defined as follows:

class StandardOption {

public:

    StandardOption() : m_daysToExpiration() {}

    StandardOption(int days);

    StandardOption(const StandardOption &p);

    ~StandardOption();

    StandardOption &operator=(const StandardOption &p);

    int daysToExpiration() const { return m_daysToExpiration; }

    // Other function members here...

private:

    int m_daysToExpiration;

    // Other data members here...

};

StandardOption::StandardOption(int days)

: m_daysToExpiration(days)

{

}

StandardOption::StandardOption(const StandardOption &p)

: m_daysToExpiration(p.m_daysToExpiration)

{

}

StandardOption::~StandardOption()

{

}

StandardOption &StandardOption::operator=(const StandardOption &p)

{

    if (this != &p)

    {

        m_daysToExpiration = p.m_daysToExpiration;

    }

    return *this;

}

This class presents a simplified version of a standard option. The number of days to expiration is stored in the member variable m_daysToExpiration and is returned by the daysToExpiration member function. You can also see a few of the standard member functions provided by the class.

The goal of this example is—given a container of StandardOptions objects—to find a set of options that are close to expiration (in this case, closeness is defined as a ten-day period before expiration). The first step in this process is to define a predicate function (a function returning a Boolean value), which will be called is_expiring.

bool is_expiring(const StandardOption &opt)

{

    return opt.daysToExpiration() < 10;

}

This function simply determines the number of days until expiration, and if it corresponds to the given criterion, the predicate returns true.

This predicate can be used to find all the objects of type StandardOption that satisfy the property of being close to expiration. Here is how this can be done, with the help of STL algorithms:

vector<StandardOption>

find_expiring_options(vector<StandardOption> &options)

{

    vector<StandardOption> result(options.size());

    std::copy_if(options.begin(), options.end(),

                 result.begin(), is_expiring);

    if (result.size())

    {

        cout << " no expiring option was found " << endl;

    }

    return result;

}

First, a new vector is declared to hold the results. The final size of this vector is at most the size of the options vector. To perform the search, you can use the std::copy_if algorithm. This template algorithm copies values from the given range into the destination (result), whenever the element satisfies the given predicate function. Since you are passing a function that is true only for options close to expiration, the resulting vector will contain only near-expiration options, which are returned as the result at the end of the function.

STL Improvements in C++23

The changes in the C++ language introduced with the C++23 standard (as well as C++11 and C++14) have made the use of the STL templates much more straightforward. The first feature, introduced in C++11 but amplified in the last standard, is the use of the auto keyword. The auto keyword can be used to substitute for complex types whenever the true type can be deduced by the compiler. For example, code such as this

std::vector<std::string>::iterator it = std::find_if (myvector.begin(), myvector.end(), IsUpperCase);

can now be substituted by the much simpler version:

auto it = std::find_if (myvector.begin(),

                       myvector.end(), IsUpperCase);

But since C++23, you can also do the same to return types of functions:

auto find_my_value(auto myvector) -> auto {

    return std::find_if (myvector.begin(),

                         myvector.end(), IsUpperCase);

}

This feature makes it much easier to work with the complex data types declared in the STL header files. Another simplification that was introduced in C++17 is the ability to deduce types for template classes. For example, instead of writing

std::vector<int> int_vector = {1, 2, 3, 4};

one can now simply remove the type specification for the template argument:

std::vector int_vector = {1, 2, 3, 4};

This is possible because the compiler can now easily determine the type of the vector container from the type of the initialization expression.

Array Slices in C++23

Array slices refer to a programming technique that allows you to extract a portion or subrange of elements from an array or container. It is a new feature in C++ that allows one to access data from an array without incurring in copying.

The idea is to create a view or reference to the subset of elements of a sequence, so to avoid the cost of making a copy. This can be useful when you want to work with a portion of an array without modifying the original data.

It’s worth noting that some libraries and frameworks, such as the GSL (Guidelines Support Library designed by Microsoft) and the proposed Ranges library, provide facilities for working with array slices in C++. These libraries offer functions and types to manipulate and work with ranges or views into collections, including arrays and vectors.

Here is an example of using the slice feature in C++.

#include <iostream>

#include <valarray>

int main() {

    int arr[] = {1, 2, 3, 4, 5};

    std::valarray<int> my_slice(5);

    for (int i=0; i<5; +i) {

        my_slice[i] = arr[i];

    }

    // Accessing elements in the slice

    for (auto element : my_slice) {

        std::cout << element << " ";

    }

    std::cout << std::endl;

    // Modifying elements in the slice

    my_slice[1] = 10;

    // Accessing the original array to verify modifications

    for (int element : arr) {

        std::cout << element << " ";

    }

    std::cout << std::endl;

    return 0;

}

This code shows how we can mix arrays and slices in the same function. The original data is stored in the arr array. Next, these values are copied to the my_slice valarray.

The type of my_slice is determined as a valarray or integers. The valarray type was created to simplify the storage of numbers, along with common math operators associated with them.

Next, the code uses the std::slice function to generate a slice (without performing any copy of the elements).

Conclusion

Templates allow programmers to create concise code that works on different data types. Given the advantage of templates, it is possible to create generic algorithms, which are also implemented in the core STL library. In this chapter, you learned about several template algorithms available in the C++ standard library. You also learned how to combine these algorithms to create efficient code for financial problems.

First, you saw how to use the most basic functional templates found in the STL. These include templates for tasks such as sorting, coping, iterating, and accumulating values restored in an STL container.

Later, you saw examples of how to combine those functional templates into working algorithms. Template algorithms allow programmers to take full advantage of existing high-performance programming techniques coded by implementers of the C++ template library.

The use of template algorithms leads to a different style of programming, which does not rely solely on object-oriented features. Newer versions of C++ also support functional programming. In the functional programming style, problems are solved using combinations of functions and functional objects. In these types of programs, functions are also treated as first-class objects. Treating functions this way can give you a more flexible method to organize code and solve problems. In the next chapter, I will explore the functional style and show how it can be used to solve financial problems occurring in options and derivatives.

© The Author(s), under exclusive license to APress Media, LLC, part of Springer Nature 2023

C. OliveiraOptions and Derivatives Programming in C++23https://doi.org/10.1007/978-1-4842-9827-5_8

8. Functional Programming Techniques

Carlos Oliveira1  

(1)

Seattle, WA, USA

Functional programming is an implementation strategy that focuses on the direct use of functions as first-class objects. This means that in a functional program, you are allowed to create, store, and call functions and otherwise use them as if they were just another variable of the system. Functional code also simplifies programming decisions because it avoids changing state and reduces mutable data. This type of functional manipulation allows programs to more closely express the desired behavior of the system and is particularly suitable to some application areas.

Functional programming is especially useful in the development of mathematical software and in the processing of large datasets, as is the case in the analysis options and derivatives. It can also be successfully used in the development of multithreaded systems, since it helps in the maintenance of lock-free code.

While the practice of functional programming was possible in previous versions of C++, such techniques have more recently been greatly improved with the adoption of the new language standards (from C++11 to C++23), particularly with the introduction of lambda functions. With lambda functions, programmers can now create temporary functions in place and pass them as arguments during the call to other functions. Such features have made it easier to apply functional programming techniques to C++ applications.

In this chapter, you will learn how to use functional programming strategies to solve typical problems that occur in algorithms for trading options and derivatives. The following topics are explored in this chapter:

Functional objects: A functional object allows an instance of a class to be called with the same syntax as a function by using the function call operator.

Functional templates: The STL has support for functional programming through the use of functional templates. With them, programmers can pass functions as parameters, as well as compose functions.

Lambda functions: With the introduction of C++11, a new syntax was created to represent unnamed functions, also known as lambda functions. You will see how to use lambdas in C++ and learn how they simplify the creation and maintenance of functional code.

Functional techniques for options processing: Throughout the chapter, you will see examples of how these functional programming techniques can be effectively used to solve some problems occurring in the analysis of options and derivatives.

Functional Programming Concepts

Functional programming has its roots in the analysis of mathematical algorithms, where functions are the main abstraction. Such functions are typically used to compute results based on mathematical properties of numbers. Functions can be used to express mathematical algorithms as well as used as an effective abstraction for the creation of complex algorithms in other areas.

In particular, functional programming uses functions as building blocks to create solutions for computational problems. Using this programming technique, you can call functions as well as perform several operations on them. You will see examples of these operations later in this chapter.

Here are a few advantages of using functional programming in C++:

It is possible to combine functions to achieve complex behavior, starting from a few simple basic functions. The combination of functions can be more easily done when functions are treated as objects, instead of isolated elements of the language.

Functional programming tries to avoid objects that share state (stored in member variables). Functions are generally easy to reason about, as they depend only on arguments that are passed at each function call. In comparison, traditional objects are complex and store a lot of context that may be hard to validate and understand. The use of functional programming techniques favors the creation of simpler code with less state, since the state needs to be passed at each function call.

Operations such as memoization can be easily performed when functions are first-class objects. With memoization, it is possible to cache the values of function calls so that the next time a result can be immediately returned. This can be done because functions don’t store any mutable state and their result depends only on the parameters passed.

No complex hierarchy of objects is necessary. Unlike OO programming, functional techniques are not based on hierarchies and therefore require no knowledge of the internal relationships of classes. Functions are independent of each other and can be applied in any sequence.

In the next few sections, you will see examples of these functional concepts applied to C++ through different techniques. First, you will see how to use function objects for this purpose. Then, you will see how to use external libraries such as boost::lambda. Finally, you will see how to implement functional programming techniques using C++ lambda functions.

Function Objects

The first step toward working with functional programming in C++ is to use a flexible representation for functions. One of the most common techniques for doing this is to use function objects. A function object (also known as a functor) is a C++ concept that allows programmers to create class instances that behave as if they were functions. The key for this concept to work is the overloading of the function call operator (represented in C++ by a pair of matching parentheses).

The function call operator can be defined as a member function in each class that needs to simulate a function call. The function call operator is called automatically from the compiler when the function call syntax is used. Consider the following example of how this process works. The OptionComparison class defines instances of a function object that compares two financial option contracts (defined here using the class SimpleOption), as defined here:

// A simple option representation

class SimpleOption {

public:

    // Other definitions here

    int daysToExpiration() const { return m_daysToExpiration; }

private:

    int m_daysToExpiration;

};

The first part of the code declares a class that contains option contracts data. In this example, SimpleOption contains only the number of days to expiration. In a normal application, this class would contain a complete representation of the attributes of an option.

class OptionComparison {

public:

    OptionComparison(bool directionLess);

    OptionComparison(const OptionComparison &p);

    ~OptionComparison();

    OptionComparison &operator=(const OptionComparison &p);

    bool operator()(const SimpleOption &o1, const SimpleOption &o2);

private:

    bool m_directionLess;

};

The OptionComparison class is the main focus of this example, since it declares a data type that can be used as a comparison function.

For the purposes here, the most important part of OptionComparison is the declaration of a member function to handle the function call syntax, using operator(). In this example, the arguments passed to the function call operator are two objects of type SimpleOption that you want to compare. The following code shows the details of the implementation for the OptionComparison class:

OptionComparison::OptionComparison(bool directionLess)

: m_directionLess(directionLess)

{

}

OptionComparison::OptionComparison(const OptionComparison &p)

: m_directionLess(p.m_directionLess)

{

}

OptionComparison::~OptionComparison()

{

}

OptionComparison &OptionComparison::operator=(

                  const OptionComparison &p)

{

    if (this != &p)

    {

        m_directionLess = p.m_directionLess;

    }

    return *this;

}

bool OptionComparison::operator()(const SimpleOption &o1,

                                  const SimpleOption &o2)

{

    bool result = false;

    // Check components of opt1 and opt2.

    // In practice this could be more complex.

    if (m_directionLess)

    {

        result = o1.daysToExpiration() < o2.daysToExpiration();

    }

    else

    {

        result = o1.daysToExpiration() > o2.daysToExpiration();

    }

    return result;

}

The first part of the implementation contains a few standard member functions that are required by C++. The next part of the implementation, containing operator(), shows how the comparison functionality is handled by this class. In this simple case, the class considers the m_directionLess flag to determine if a less than test should be used. Otherwise, the function uses a greater than test and returns the results.

The following function shows how to use OptionComparison:

void test_compare()

{

    OptionComparison comparison(true);

    SimpleOption a, b;

    // ...

    // Initialize options a and b here...

    if (comparison(a, b))

    {

        std::cout << " a is less than b " << std::endl;

    }

    else

    {

        std::cout << " b is less than a " << std::endl;

    }

}

The first line of test_compare creates a new instance of the comparator object. Then, the code creates two SimpleOption objects and initializes them as necessary. The comparison object is then called as if it were a function, using operator().

The strategy displayed here can be used to simulate functions with different signatures by creating the appropriate version of the operator(). Also, a single class can decide to implement several versions of the operator(), depending on the ways in which it expects to be called.

Functional Predicates in the STL

As you learned in the previous section, objects can be used to simulate functions in C++ through the definition (or overloading) of the function call operator. This flexible mechanism can be used to create code that behaves as a function but encapsulates complex properties, as any object can do.

Based on the use of function objects, you can build a different style of programming. To facilitate the creation of functional code in this style, the authors of the STL provide a set of basic function templates and classes that automate many common tasks. Some of these template functions are listed in Table 8-1.

Table 8-1

List of Functional Templates Provided by the STL

Functional Template

Description

equal_to

Compares two parameters and determines equality between them.

Greater

Compares the two given parameters and returns true if the first parameter is greater than the second.

greater_equal

Compares the two given parameters and returns true if the first parameter is greater than or equal to the second.

Less

Compares the two given parameters and returns true if the first parameter is less than the second.

less_equal

Compares the two given parameters and returns true if the first parameter is less than or equal to the second.

logical_and

Receives two Boolean parameters and returns true if both parameters evaluate to true.

logical_or

Receives two Boolean parameters and returns true if at least one of the parameters evaluates to true.

logical_not

Receives a Boolean parameter and returns true if the parameter evaluates to false.

Plus

A functional template that receives two numeric parameters and returns their sum.

Minus

A functional template that receives two numeric parameters and returns the first minus the second.

Negate

A functional template that receives a single numeric parameter and returns the negative of that value.

Divides

A functional template that receives two numeric parameters and returns the value of the first parameter divided by the second.

Bind

Receives a function or functional object as a parameter and binds the parameters to that function to constant values or to variable placeholders.

The goal of the functional objects included in the STL is to provide a set of basic operations for creating new functional objects. Notice that through the combination of the given objects, it is possible to create complex functions to encode applicationdependent logic. You can freely combine these functional templates to define larger expressions in a way that represents the desired functionality.

Note

Be aware of the differences between using functional objects and normal C++ operations. A C++ computation specified with operators such as * and + cannot be passed as parameters to other functions, because they are immediately executed in place. Functional objects, on the other hand, form expressions that can be passed to other functions. Moreover, the process of putting these functional objects together is performed by the compiler. This ability to create complex expressions and pass them to other functional objects and templates makes these STL templates useful for the purpose of functional programming.

Consider the following examples of using these functional templates in C++. The first example shows how to use these functional templates to create a sorting predicate.

#include <functional>

void test_operator()

{

    using namespace std;

    vector<int> numbers = { 3, 4, 2, 1, 6 };

    sort(numbers.begin(), numbers.end(), greater<int>() );

}

Here, you first create a sequence of integer values and store it in the variable numbers. In this case, the code is taking advantage of the initialization syntax of C++11, which allows for the sequence type to be left unspecified, while the result is stored in an std::vector.

The next step is to call std::sort on the sequence of numbers. As you have seen before, the last argument of std::sort is a comparison function. Here, you can pass a functional object declared in functional.h, therefore freeing you from having to define a separate function.

Another simple application is to transform two sequences into a third sequence. For example, one can use the plus function to add elements from two lists:

void test_transform()

{

    using namespace std;

    auto list1 = { 3, 4, 2, 1, 6 };

    auto list2 = { 4, 1, 5, 3, 2 };

    vector<int> result(list1.size());

    transform(list1.begin(), list1.end(),

              list2.begin(), result.begin(), plus<int>() );

    // Use transformed list here...

    copy(result.begin(), result.end(),

         ostream_iterator<int>(cout, ", "));

    // Prints 7, 5, 7, 4, 8,

}

This example shows you how to take two lists and perform an arithmetic operation with its respective elements. The operation in this case is the plus functional template, which adds two values and returns the sum. The first step is to create the two sequences. You can use the auto keyword to simplify the declaration of these sequences; they will be represented as vectors of integers. A result vector is also necessary, as declared in the next line of code.

The next step is to use the std::transform function to perform a transformation from the two source sequences into the destination sequence. Each step of the transformation uses the std::plus function. The result of this process is then sent to the standard output using the std::copy template function.

You could modify this example to perform any of the arithmetic or logical operations available in the functional header file, including adding, subtracting, multiplying, and dividing. More complex operations could be performed by combining these functions.

Note

In general, the transform function template is very useful when you want to perform a common action to a list of elements. By using transform, you can reduce the number of explicit for loops in your code, making the resulting program easier to understand.

The Bind Function

In the last section, you saw that several common operations are provided in the standard library using the mechanisms of functional programming. With these templates, you can write transformations to lists of data without having to program explicit loops or use other imperative programming techniques.

However, just using the primitive operations such as subtract and divide is not enough to create complex application logic. Another thing that you can do using the techniques of functional programming is bind parameter values for a given function so that you can have a new, modified function as a result.

Consider, for example, the std::plus<T> function provided in the functional header file. It can be used to add two numbers and can be applied to members of separate containers using the transform function template. A simple modification of this function is to have a constant number as the first parameter so that the resulting function is in fact adding a constant value each time it is applied. Functional programming allows functions to be modified in this way, before they are applied to the required data.

The solution in the STL is provided through the std::bind function. With std::bind, you can bind a particular value to one of the arguments of a given template function. By doing this, you can create as many different functions as there are new combinations of arguments.

To use std::bind, you need to determine the function to be modified and specify one or more values that will be bound to the function arguments. Among these bound parameters, you can also refer to the arguments supplied by the user of the function, at the time that the function is called. These arguments are called placeholder arguments and named as the special variables _1, _2, _3, and so on.

Consider the following example of the std::bind function:

void use_bind()

{

    using namespace std;

    using namespace std::placeholders;

    auto list1 = { 3, 4, 2, 1, 6 };

    vector<int> result(list1.size());

    //  Add 3 to each element of the list

    transform(list1.begin(), list1.end(),

              result.begin(), bind(plus<int>(), _1, 3));

    copy(result.begin(), result.end(),

         ostream_iterator<int>(cout, ", "));

    // Prints 6, 7, 5, 4, 9,

}

In this example, the goal is to use a modification of the std::plus function so that each element of the list is added to the value 3, resulting in a new vector with the results. The example is similar to what you have seen in the previous code fragment, but the bind template now modifies the plus function.

The first two lines of the example are importing std and std::placeholders namespaces. The std::placeholders namespace allows you to write the name of placeholder variable _1 or _2. Then, the original list is created, and a result vector is allocated.

The transform function performs the desired changes, and bind is used to create the operation applied to each element of the list1 vector. As seen in the previous example, there are two arguments for std::plus. These arguments need to be specified in sequence. This is indicated with the second and third parameters of std::bind. The first argument is supposed to be the placeholder for the first parameter. The second argument is bound to a constant number.

The std::bind template can be used in more complex situations. For example, it can be used to find member functions for existing classes. The following example shows how bind can be used to create a variation of a member function for the SimpleOption class:

class SimpleOption {

public:

    // Other definitions here

double getInTheMoneyProbability(int numDays, double currentUnderlyingPrice) const;

};

auto computeInTheMoneyProblExample(

     const std::vector<SimpleOption> &options) -> std::vector<double>

{

    using namespace std;

    using namespace std::placeholders;

    double currentPrice = 100.0;

    vector<double> probabilities(options.size());

    auto inTheMoneyCalc = bind(

      &SimpleOption::getInTheMoneyProbability, _1, 2, currentPrice);

    transform(options.begin(), options.end(),

              probabilities.begin(), inTheMoneyCalc);

    return probabilities;

}

This assumes that SimpleOption contains a member function that calculates the probability that a particular option will be in the money, given a number of days before expiration and the current underlying price. Moreover, the goal is to create a function that will receive a vector of options and return the associated probabilities for the specific case of two days before expiration. The function is called getInTheMoneyProblExample in the previous fragment.

To do this using the STL functional algorithms, you need to find a way to express the desired condition as a functional object and pass the resulting object to std::transform. This can done with the help of std::bind. The idea is to use std::bind to bind the value of the first argument, which in this case is the number 2. Then, the placeholder _1 indicates that the argument passed to the resulting function is used as the second argument to getInTheMoneyProbability. The bound function is then saved to a variable called inTheMoneyCalc and used as an argument to transform, applied to the options vector.

Lambda Functions in C++23

As you saw in the previous sections, classes, templates, and objects can be used to represent functions and other functional objects. Unfortunately, using classes for functional programming requires you to define a separate function outside of the current place where it is being used, thus making the process more difficult than it needs to be. For every single functional argument, you may need to define a separate class that implements the desired interface, therefore making code harder to read. Functional templates such as std::plus and std::multiplies help make this easier, but it is still not as easy as writing plain C++ code.

Other languages such as Lisp and Python have simplified this task with the concept of unnamed functions, also called lambdas. These unmanned functions can be passed as parameters to other functions and objects and can be freely combined into more complex functions. This way, functional programming techniques become much easier to implement and test, when compared to languages in which functions can be created only as a static entity.

One of the big changes since C++11 (and improved in C++23) was the introduction of lambda functions as a syntactical element. With the addition of lambda functions, it is now possible to create unnamed functions that can be saved as variables or passed as parameters to other functions. This considerably simplifies the task of applying functional techniques in C++ programs, as you will see in the next few examples.

A C++ lambda is a piece of C++ code that can be assigned to a variable or passed as a parameter to other functions. With lambdas, the compiler has enough information to understand that the given C++ code will run later, but in an environment that contains the variables passed to and created by the current function.

The syntax of lambda functions starts with a pair of square brackets, followed by arguments and a block of code. Here is an example:

void use_lambda()

{

    auto fun = [](double x, double y) { return x + y; };

    double res = fun(4, 5);

    std::cout << " result is "  << res << std::endl;

}

Here, the lambda function is introduced by [], followed by parameters of type double. The function simply adds the two given parameters. The compiler can deduce the result type for this lambda function. However, you can also declare the return type as part of the code using the -> syntax:

    auto fun = [](double x, double y) -> double { return x + y; };

Lambda functions can also refer to variables that have been declared outside the block of the lambda function. This makes them much more convenient than standard functions, which are independent of the surrounding variables. The process used by lambda functions is called lambda capture, and it allows a lambda function to access the data stored in a local variable of an enclosing function, even after the current function has returned.

There are two types of lambda capture:

Lambda capture by value: Allows lambda functions to use the value stored in a variable that is accessible at the moment of the lambda declaration. The value can be used even after the original variable no longer exists. This is indicated by adding the name of the variable inside the square brackets that introduce the lambda function.

Lambda capture by reference: This allows a lambda function to modify the variable itself, instead of just using its value. This type of capture is indicated with an & operator before the name of the desired variable.

Here is an example of both cases of lambda capture:

void use_lambda2()

{

    int offset = 5;

    auto fun1 = [ offset](double x, double y)

                         { return x + y + offset; };

    auto fun2 = [&offset](double x, double y)

                         { return x + y + offset; };

    double res = fun1(4, 5);

    std::cout << " result is "  << res << std::endl;

    offset = 10;

    std::cout << " result of fun1 is "  << fun1(4, 5) << std::endl;

    std::cout << " result of fun2 is "  << fun2(4, 5) << std::endl;

}

The function named fun1 has been created with a capture of the offset variable. This capture is by value only, so it will always reflect the original value of that variable, in this case, the number 5. The second lambda function fun2 captures the variable offset by reference. This means that each time fun2 is called, it will use a reference to the updated value of the offset. When the variable offset changes from 5 to 10, this will change the results produced by fun2 but will not change the results of the application of fun1, as shown in the following output:

 result is 14

 result of fun1 is 14

 result of fun2 is 19

A lambda function can also be passed as an argument to other functions. When this happens, the compiler creates a template object of type std::function<> that stores all the information used by the lambda function. You can create new functions that receive lambdas and freely use them in your code. The compiler will automatically convert a lambda into an object during the function call. Consider the following example:

void use_function(std::function<int(int,int)> f)

{

    auto res = f(2,3);

    std::cout << " the function returns the value "

              << res << std::endl;

}

This function just receives an std::function object and displays its result when applied to the values 2 and 3. The important part of this code is noticing that std::function defines both the return type and the types for each of the parameters of the given function. You can see how this information is used in the compiler with two sample lambda functions that are passed to use_function as follows:

void test_use_function()

{

    auto f1 = [] (int a, int b) { return a + b; };

    auto f2 = [] (int a, int b) { return a * b; };

    use_function(f1);

    use_function(f2);

}

When called, test_use_function will produce the following results, as expected:

 the function returns the value 5

 the function returns the value 6

Complete Code

The complete code for this chapter is implemented in the Functional.hpp and Functional.cpp files. The functional techniques presented here have as dependencies only the main STL header files.

//

//  Functional.hpp

#ifndef Functional_hpp

#define Functional_hpp

class SimpleOption {

public:

   // Other definitions here

   int daysToExpiration() const { return m_daysToExpiration; }

double getInTheMoneyProbability(int numDays, double currentUnderlyingPrice) const ;

private:

   int m_daysToExpiration;

};

class OptionComparison {

public:

   OptionComparison(bool directionLess);

   OptionComparison(const OptionComparison &p);

   ~OptionComparison();

   OptionComparison &operator=(const OptionComparison &p);

   bool operator()(const SimpleOption &o1, const SimpleOption &o2);

private:

   bool m_directionLess;

};

#endif /* Functional_hpp */

//

//  Functional.cpp

#include "Functional.hpp"

#include <iostream>

#include <vector>

#include <functional>   // for functional STL code

//

// Class SimpleOption

//

double SimpleOption::getInTheMoneyProbability(int numDays, double currentUnderlyingPrice) const

{

    return 0; // implementation here

}

//

//  Class OptionComparison

//

OptionComparison::OptionComparison(bool directionLess)

: m_directionLess(directionLess)

{

}

OptionComparison::OptionComparison(const OptionComparison &p)

: m_directionLess(p.m_directionLess)

{

}

OptionComparison::~OptionComparison()

{

}

OptionComparison &OptionComparison::operator=(const OptionComparison &p)

{

    if (this != &p)

    {

        m_directionLess = p.m_directionLess;

    }

    return *this;

}

bool OptionComparison::operator()(const SimpleOption &o1, const SimpleOption &o2)

{

    bool result = false;

// Check components of opt1 and opt2. In practice this could be more complex.

    if (m_directionLess)

    {

        result = o1.daysToExpiration() < o2.daysToExpiration();

    }

    else

    {

        result = o1.daysToExpiration() > o2.daysToExpiration();

    }

    return result;

}

void test_compare()

{

    OptionComparison comparison(true);

    SimpleOption a, b;

    // ...

    if (comparison(a, b))

    {

        std::cout << " a is less than b " << std::endl;

    }

    else

    {

        std::cout << " b is less than a " << std::endl;

    }

}

void test_operator()

{

    using namespace std;

    vector<int> numbers = { 3, 4, 2, 1, 6 };

    sort(numbers.begin(), numbers.end(), greater<int>() );

}

void test_transform()

{

    using namespace std;

    auto list1 = { 3, 4, 2, 1, 6 };

    auto list2 = { 4, 1, 5, 3, 2 };

    vector<int> result(list1.size());

transform(list1.begin(), list1.end(), list2.begin(), result.begin(), plus<int>() );

    // Use transformed list here...

copy(result.begin(), result.end(), std::ostream_iterator<int>(cout, ", "));

    // Prints 7, 5, 7, 4, 8,

}

void use_bind()

{

    using namespace std;

    using namespace std::placeholders;

    auto list1 = { 3, 4, 2, 1, 6 };

    vector<int> result(list1.size());

    //  Add 3 to each element of the list

transform(list1.begin(), list1.end(), result.begin(),  bind(plus<int>(), _1, 3));

    copy(result.begin(), result.end(), std::ostream_iterator<int>(cout, ", "));

    // Prints 6, 7, 5, 4, 9,

}

auto computeInTheMoneyProblExample(const std::vector<SimpleOption> &options) -> std::vector<double>

{

    using namespace std;

    using namespace std::placeholders;

    double currentPrice = 100.0;

    vector<double> probabilities(options.size());

auto inTheMoneyCalc = bind(&SimpleOption::getInTheMoneyProbability, _1, 2, currentPrice);

transform(options.begin(), options.end(), probabilities.begin(), inTheMoneyCalc);

    return probabilities;

}

void use_lambda()

{

    auto fun = [](double x, double y) -> double { return x + y; };

    double res = fun(4, 5);

    std::cout << " result is "  << res << std::endl;

}

void use_lambda2()

{

    int offset = 5;

    auto fun1 = [ offset](double x, double y) -> double

                         { return x + y + offset; };

    auto fun2 = [&offset](double x, double y) -> double

                         { return x + y + offset; };

    double res = fun1(4, 5);

    std::cout << " result is "  << res << std::endl;

    offset = 10;

    std::cout << " result of fun1 is "  << fun1(4, 5) << std::endl;

    std::cout << " result of fun2 is "  << fun2(4, 5) << std::endl;

}

void use_function(std::function<int(int,int)> f)

{

    auto res = f(2,3);

    std::cout << " the function returns the value "  << res << std::endl;

}

void test_use_function()

{

    auto f1 = [] (int a, int b) { return a + b; };

    auto f2 = [] (int a, int b) { return a * b; };

    use_function(f1);

    use_function(f2);

}

//

// The main entry point for the test application

//

int main()

{

    test_use_function();

    return 0;

}

You can compile this code using any standards-compliant compiler, such as gcc (which is available for all major platforms). The following command line can be used to compile the application called Functional:

g++ -std=gnu++11 -o Functional Functional.cpp

Changes to Lambda Functions in C++23

One of the changes in C++23 was the simplification of lambda syntax, by reducing the amount of parenthesis needed in the declaration of simple lambda functions. The goal of the standard committee with this change was to allow optional parentheses for lambdas without parameters.

In C++23, the use of empty parentheses for lambda functions without parameters becomes optional. Previously, empty parentheses were required for consistency in some cases, such as when using lambda template parameters, constexpr, consteval, mutable, noexcept specifiers, attributes, trailing return types, or a requires clause. But with the proposed change, empty parentheses can be omitted in those circumstances.

Here is a quick example:

// Previous syntax (C++20)

auto empty_lambda = [](){};

// New syntax (C++23)

auto empty_lambda = []{};

Change in the Scope of Lambda Trailing Return Type

Currently, when using a trailing return type in a lambda function, the type deduction considers variables from the outer scope instead of the lambda’s capture list. In C++23, the proposed change would modify the name lookup rules for trailing return types to prioritize captures before looking outside. This change aims to align the behavior with the developer’s intent and provide more intuitive results.

Example:

// C++20: the trailing return type is the type of my_var (double).

double my_var = 50.0;

auto counter = [my_var =0]() mutable -> decltype(my_var) {

    return my_var--;

};

In this example, the trailing return type is that of the variable my_var. Before C++23, my_var would be searched in the scope outside the lambda function, resulting in a return type of double.

However, in C++23, the rule was changed so that the variable my_var in the capture list is used instead. In that case, the return type of the function will be int. This is a better result because my_var is initialized with an integer value in the lambda function.

Attributes on Lambdas

In C++23, the proposed change allows attributes to be applied directly to the lambda function call operator. Currently, attributes are placed in the lambda declarator, either before or after the parameter declaration clause, affecting the type of the function call operator. With the change, attributes can be declared immediately after the lambda or template parameter list, thus directly affecting the function call operator itself.

Here is an example:

// C++23

auto int_func = [][[nodiscard]]()->int { return 3; };

auto res = int_func(); // this compiles because the result is used

int_func();  // but this doesn't compile!

Conclusion

Using templates is a good way to organize your code into generic functions that work across different data types. However, it’s only when you start to compose these functions that you start to reap the benefits of a functional programming style. Functional tools in the STL and other libraries allow programmers to use functions as first-class objects.

In this chapter, you learned a few of the techniques available for programmers who want to explore functional programming in C++. Some of these techniques include the use of functional objects, which implement the function call operator to simulate native functions. The STL provides several template functions that support the use of functional objects.

You have also seen how to create and use lambda functions, a new syntactical element introduced in C++11. With lambda functions, programmers can create unnamed functions that can be saved as variables or passed as parameters to other functions. Even more interestingly, such lambda functions can refer to variables that occur in the environment in which they were created. In this way, lambda functions reduce the need to create additional classes just for the purpose of simulating function calls.

This chapter concludes the book’s overview of C++ programming techniques used on derivatives programming. In the next chapter, you will start to learn about mathematical tools that can be used to price and analyze options and other derivatives. In particular, you will learn about linear programming methods and their C++ implementations.

© The Author(s), under exclusive license to APress Media, LLC, part of Springer Nature 2023

C. OliveiraOptions and Derivatives Programming in C++23https://doi.org/10.1007/978-1-4842-9827-5_9

9. Linear Algebra Algorithms

Carlos Oliveira1  

(1)

Seattle, WA, USA

Linear algebra (LA) techniques are used throughout the area of financial engineering and, in particular, in the analysis of options and other financial derivatives. These techniques are used, for example, to calculate the value of large portfolios, or to quickly price derivative instruments. This chapter contains an overview of LA algorithms and their implementation in C++.

Linear algebra algorithms consist of simple operations on sets of values arranged as vectors or matrices. There is a rich mathematical theory behind the use of vectors and matrices. Although it is out of the scope here to explain this mathematical theory, it is nonetheless essential to understand how such algorithm can be implemented in C++.

It is important to recognize how the traditional methods of linear algebra can be translated to a multiparadigm language such as C++. As a high-performance language, C++ has been used by software engineers to efficiently encode numerical algorithms, such as the ones used in linear algebra. With this goal in mind, this chapter presents a few examples that illustrate how to use some of the most common linear algebra algorithms. In this chapter, you will also learn how to integrate the following types of LA algorithms into your code:

Vector operations: Operations on vectors are some of the most common ways to explore linear algebra algorithms.

Implementing matrices: A matrix is a set of numbers ordered in a two-dimensional array. Even though matrices are very common, there is no standard support for matrices in the C++ library. In this chapter, you will see how to easily create a Matrix class that supports many of the most common matrix operations.

Using linear algebra libraries: There is a set of LA functions, named BLAS (Basic Linear Algebra Subprograms), that have become a de facto standard in the world of numerical computing. You will see in this chapter how to use BLAS and similar implementations, which provide the basic blocks used by most LA software (both free and commercial) available nowadays.

Vector Operations

As you will see in the following examples, linear algebra is concerned about the mathematical properties of vector spaces. Many of the operations either produce vectors or take vectors as their arguments. Therefore, the first step to properly use LA algorithms is to have a good implementation of vectors.

Notice that, on the positive side, the C++ standard library already contains an optimized container called std::vector, which you have used extensively in the last few chapters. On the other hand, std::vector doesn’t implement some of the most important operations that are conventionally used in linear algebra algorithms. The first step in implementing such an algorithm is therefore to provide such missing operations.

There are two kinds of mathematical operations that are needed when using vectors:

Operations between numbers and vectors: Some mathematical operations involve a single number (also called a scalar number) and a vector as arguments. For example, you may need to multiply a vector by a scalar, or add the same number to each entry in the vector. Such operations are not available in std::vector but are so common that they should be supported by any linear algebra software package.

Operations between two or more vectors: Another class or mathematical operations take two or more vectors and calculate a result based on their values. A common example is a vector product, where all members in both vectors are pairwise multiplied and finally added. Other operations like vector summation are also commonly used.

The next few examples will show how to implement some of these operations using the existing containers of the STL, such as std::vector.

Scalar-to-Vector Operations

Scalar operations on vectors allow a vector to be modified by a single scalar number. The two most common scalar operations are scalar addition and scalar multiplication. You can use these operations as building blocks for more complex operations, which will be explored in the following sections.

Because the std::vector class is already part of the STL, the strategy used here is to create free functions (not members of a particular class) that operate on vector containers. This way, you are free to continue to use the well-known functions available for std::vector when necessary. You can also overload these functions with other types if you feel the need to extend these definitions.

The scalar addition to vectors consists in adding the same constant number to each component of the vector. The operation of scalar addition is used in many linear algebra algorithms. This can be implemented in the following way:

#include <iostream>

#include <vector>

typedef std::vector<double> Vector;

Vector add(double num, const Vector &v)

{

    int n = (int)v.size();

    Vector result(n);

    for (int i=0; i<n; ++i)

    {

        result[i] = v[i] + num;

    }

    return result;

}

The first statement is a typedef that allows you to use the type name Vector instead of std::vector in this and the other examples in this chapter. Another advantage of using such a typedef in numerical algorithms like this is the possibility of changing the definition of Vector if necessary. In such a case, all the code would still compile to comply with another vector type with just a few or no changes.

The add function creates a new vector with a size equal to the length of the argument vector. Then, it fills the resulting vector with the original plus the number in the first argument. Next, you can see the scalar multiplication operation:

Vector multiply(double num, const Vector &v)

{

    int n = (int)v.size();

    Vector result(n);

    for (int i=0; i<n; ++i)

    {

        result[i] = v[i] * num;

    }

    return result;

}

The multiply function is implemented similarly to add. It receives a double number and a vector. The resulting vector is created as the same size as the argument v. The resultant vector is computed element by element to comply with the definition of the scalar product operation.

These two functions create and return a new vector. This is an effective way to perform the operations, but it can be less than optimal when used in inner loops of complex algorithms. One way to speed up this process is to create a version of these functions that modifies the vector in place. That is, one of the vectors is passed using a non-const reference, and it is modified to contain the result of the calculation.

Here is the scalar addition function, implemented as an in-place modifying operation:

void in_place_add(double num, Vector &v)

{

    int n = (int)v.size();

    for (int i=0; i<n; ++i)

    {

        v[i] += num;

    }

}

As you can see, this is the equivalent of the += operator, but applied to a vector and a scalar number argument. A similar implementation also works for the scalar product operation:

void in_place_multiply(double num, Vector &v)

{

    int n = (int)v.size();

    for (int i=0; i<n; ++i)

    {

        v[i] *= num;

    }

}

Last, you can take advantage of C++ operator overloading when defining these functions. With operator overloading, you can write code much more naturally, so instead of typing

multiply(5, add(10, a));

(assuming that a is a vector), you can type

5 * (10 * a);

which is much easier to understand and maintain. You can create operator versions of the previous functions using the following definitions:

inline Vector operator +(double num, const Vector &v)

{

    return add(num, v);

}

inline Vector operator *(double num, const Vector &v)

{

    return multiply(num, v);

}

inline void operator +=(double num, Vector &v)

{

    in_place_add(num, v);

}

inline void operator *=(double num, Vector &v)

{

    in_place_multiply(num, v);

}

Because these are inline functions, they don’t add any runtime penalty to the functions that have already been defined. In fact, you can think about these definitions as shortcuts to the full definition of the vector operators so that they are easy to type.

Vector-to-Vector Operations

The vector-to-vector operations allow you to form mathematical expressions involving two or more vectors. The most common such operations are vector addition and vector product. They can be implemented using strategies similar to the ones used previously.

First, you will see the implementation of vector addition:

Vector add(const Vector &v1, const Vector &v2)

{

    int n = (int)v1.size();

    Vector result(n);

    for (int i=0; i<n; ++i)

    {

        result[i] = v1[i] + v2[i];

    }

    return result;

}

Here, the function allocates a resultant vector, which is populated using element-wise addition of vector entries.

void  in_place_add(Vector &v1, const Vector &v2)

{

    int n = (int)v1.size();

    for (int i=0; i<n; ++i)

    {

        v1[i] += v2[i];

    }

}

Next, you can apply the same strategy to the implementation of vector products. The first kind of vector product is called inner product, or dot product, and is defined as the sum of products of each correspondent element of each vector. Here is a simple implementation in C++:

double prod(const Vector &v1, const Vector &v2)

{

    double result = 0;

    int n = (int)v1.size();

    for (int i=0; i<n; ++i)

    {

        result += v1[i] * v2[i];

    }

    return result;

}

Another type of product between two vectors is known as the cross product and has several applications in physics and engineering. Unlike the inner product, which returns a single number, the cross product results in a new vector. The cross product generates a new vector that is orthogonal to the given parameters. Its definition for threedimensional vectors is given using the equations presented in the following function:

Vector cross_prod_3D(const Vector &a, const Vector &b)

{

    assert(a.size()==3); // definition is 3D vectors only

    int n = (int)a.size();

    Vector v(n);  // the resulting vector

    v[0] = (a[1] * b[2] - a[2] * b[1]);

    v[1] = (a[2] * b[0] - a[0] * b[2]);

    v[2] = (a[0] * b[1] - a[1] * b[0]);

    return v;

}

Just as you can use in-place operations for scalar-to-vector functions, you can also implement vector-to-vector operations in place, therefore saving some of the effort needed to create temporary data structures. Here are the versions of these two functions designed for in-place updates:

void  in_place_add(Vector &v1, const Vector &v2)

{

    int n = (int)v1.size();

    for (int i=0; i<n; ++i)

    {

        v1[i] += v2[i];

    }

}

void  in_place_product(Vector &v1, const Vector &v2)

{

    int n = (int)v1.size();

    for (int i=0; i<n; ++i)

    {

        v1[i] *= v2[i];

    }

}

Vector in_place_cross_prod_3D(const Vector &a, const Vector &b, Vector &v)

{

assert(a.size()==3); // definition is 3D vectors only

    int n = (int)a.size();

    v[0] = (a[1] * b[2] - a[2] * b[1]);

    v[1] = (a[2] * b[0] - a[0] * b[2]);

    v[2] = (a[0] * b[1] - a[1] * b[0]);

    return v;

}

Finally, you can also simplify the use of these vector operations with the help of C++ operator overloading. Instead of typing a complex set of function calls, it is much more elegant to apply the standard addition and multiplication operations whenever possible. Therefore, you can use the following inline definitions to call the given vector operations without any runtime performance penalty:

inline Vector operator +(const Vector &v1, const Vector &v2)

{

    return add(v1, v2);

}

inline void  operator +=(Vector &v1, const Vector &v2)

{

    in_place_add(v1, v2);

}

inline double operator *(const Vector &v1, const Vector &v2)

{

    return prod(v1, v2);

}

inline void  operator *=(Vector &v1, const Vector &v2)

{

    in_place_add(v1, v2);

}

The next operation I want to discuss is a very common function defined over a single vector. The norm of a vector can be defined as the square root of the vector product of a vector with itself. Basically, the norm of a vector is a numeric quantity that can be applied to describe the whole vector. You can very easily implement a norm in the following way:

double norm(const Vector &v)

{

    double result = 0;

    int n = (int)v.size();

    for (int i=0; i<n; ++i)

    {

        result += v[i] * v[i];

    }

    return std::sqrt(result);

}

Matrix Implementation

In the previous section, you learned about the most basic level of linear algebra functions, dealing with single numbers and vectors. A second level of operations is defined on a two-dimensional arrangement of numbers, also known as a matrix. Matrices arise naturally as the result of linear algebra calculations, and they provide a convenient way to manipulate data.

Matrices are fundamental to the implementation of linear algebra algorithms that are frequently used in the analysis of options and other derivatives. Unfortunately, C++ does not support matrices directly. Programmers need to create a separate abstraction to represent a matrix or use some third-party library that contains such a data type.

For the purpose of illustrating linear algebra and related algorithms, a Matrix class will be introduced in this section. This Matrix data type implements some of the most common operations that are needed in a financial application. However, the Matrix class presented here doesn’t include all the necessary checks that a robust implementation would require, and some of these features are left as exercise for the reader.

In particular, the Matrix class presented in this section offers the following abilities:

Creation of square and rectangular matrices, which handle the allocation of memory for a two-dimensional container of real (floating-point) numbers

Copy constructor and assignment operator that support the basic copy operations used in C++ libraries

Indexing operator, so that values can be accessed with the familiar square bracket notation

Common linear algebra operations, such as transpose, add, and multiply, implemented as member functions

The first step in defining a Matrix class is to define the basic organization of the stored data. In this class, the data is stored as a sequence of rows, making maximum use of the existing vector container to help manage the data.

The header file is presented in Listing 9-1, and it includes the class declaration and a few free operators that simplify the use of the class.

//

//  Matrix.h

//

#ifndef __FinancialSamples__Matrix__

#define __FinancialSamples__Matrix__

#include <vector>

class Matrix {

public:

    typedef std::vector<double> Row;

    Matrix(int size);

    Matrix(int size1, int size2);

    Matrix(const Matrix &s);

    ~Matrix();

    Matrix &operator=(const Matrix &s);

    void transpose();

    double trace();

    void add(const Matrix &s);

    void subtract(const Matrix &s);

    void multiply(const Matrix &s);

    void multiply(double num);

    Row & operator[](int pos);

    int numRows() const;

private:

    std::vector<Row> m_rows;

};

// Free operators

//

Matrix operator+(const Matrix &s1, const Matrix &s2);

Matrix operator-(const Matrix &s1, const Matrix &s2);

Matrix operator*(const Matrix &s1, const Matrix &s2);

#endif /* defined(__FinancialSamples__Matrix__) */

Listing 9-1

Declarations for the Matrix Class

Notice that a Row is defined as an std::vector of double numbers using a typedef. Next, you see the usual definitions for constructors, destructors, and the assignment operator.

The Matrix class contains a few common operations, implemented as member functions. Last, you see a few operator overloads so that the class can be comfortably used along with other linear algebra types discussed previously.

The first part of the Matrix class implementation is concerned with the constructors. The class has two constructors: the first constructor creates a square matrix, that is, one that has the same number of rows and columns. This is done by instantiating each row of the matrix and adding it to the top-level m_rows vector, until the complete matrix has been allocated.

//

//  Matrix.cpp

//

#include "Matrix.h"

#include <stdexcept>

Matrix::Matrix(int size)

{

    for (int i=0; i<size; ++i )

    {

        std::vector<double> row(size, 0);

        m_rows.push_back(row);

    }

}

The second way to create a matrix is to give a number of rows and a number of columns, therefore creating a rectangular matrix. The underlying algorithm is similar to the previous case:

Matrix::Matrix(int size, int size2)

{

    for (int i=0; i<size; ++i )

    {

        std::vector<double> row(size2, 0);

        m_rows.push_back(row);

    }

}

The next constructor allows you to make a copy of an existing matrix. It simply takes advantage of how vectors copy all of their contents by default. The destructor is also trivial, because of the use of std::vector to manage the data.

Matrix::Matrix(const Matrix &s)

: m_rows(s.m_rows)

{

}

Matrix::~Matrix()

{

}

The assignment operator also takes advantage of the use of an std::vector. The only thing it needs to do is copy the underlying m_rows data member.

Matrix &Matrix::operator=(const Matrix &s)

{

    if (this != &s)

    {

        m_rows = s.m_rows;

    }

    return *this;

}

The Matrix class provides an easy way to access elements using square brackets. For this purpose, it needs to define the operator[] member function. Because an std::vector is returned, the result can also be accessed using square brackets. Therefore, if a is an object of class Matrix, users of this class can just type a[2][3] to access the fourth element of the third row.

Matrix::Row &Matrix::operator[](int pos)

{

    return m_rows[pos];

}

Transposition is one of the most common operations in a matrix. The goal of transposition is to convert rows into columns, changing the orientation of the data stored. This class does this by creating a new set of rows, where each new row contains the elements of the corresponding column. At the end, you just need to replace the existing rows with this new set of rows. This is done using the swap member function of the underlying std::vector. This way, you don’t need to worry about the details of data allocation, taking full advantage of STL data management techniques.

void Matrix::transpose()

{

    std::vector<Row> rows;

    for (unsigned i=0;i <m_rows[0].size(); ++i)

    {

        std::vector<double> row;

        for (unsigned j=0; j<m_rows.size(); ++j)

        {

            row[j] = m_rows[j][i];

        }

        rows.push_back(row);

    }

    m_rows.swap(rows);

}

Next, the Matrix class contains another very common operation called trace. The trace of a matrix is defined as the summation of elements in the diagonal positions of the matrix. That is, for a given matrix a, you need to sum all elements a[i][i], or in mathematical notation:

This function is not defined for nonsquare matrices.

double Matrix::trace()

{

    if (m_rows.size() != m_rows[0].size())

    {

        throw new std::runtime_error("invalid matrix dimensions");

    }

    double total = 0;

    for (unsigned i=0; i<m_rows.size(); ++i)

    {

        total += m_rows[i][i];

    }

    return total;

}

The add member function implements matrix addition. Just as with vector addition, matrix addition performs the element-wise summation of entries in the matrix. This operation is defined only when the two matrices have the same dimensions; otherwise, a runtime exception is thrown.

void Matrix::add(const Matrix &s)

{

    if (m_rows.size() != s.m_rows.size() ||

        m_rows[0].size() != s.m_rows[0].size())

    {

        throw new std::runtime_error("invalid matrix dimensions");

    }

    for (unsigned i=0; i<m_rows.size(); ++i)

    {

        for (unsigned j=0; j<m_rows[0].size(); ++j)

        {

            m_rows[i][j] += s.m_rows[i][j];

        }

    }

}

The subtract operation is similar to addition. It is here just to avoid the need to multiply the whole matrix by -1 in order to do a simple subtraction.

void Matrix::subtract(const Matrix &s)

{

    if (m_rows.size() != s.m_rows.size() ||

        m_rows[0].size() != s.m_rows[0].size())

    {

        throw new std::runtime_error("invalid matrix dimensions");

    }

    for (unsigned i=0; i<m_rows.size(); ++i)

    {

        for (unsigned j=0; j<m_rows[0].size(); ++j)

        {

            m_rows[i][j] -= s.m_rows[i][j];

        }

    }

}

The product operation is implemented by the member function multiply. When you’re multiplying two matrices, the resulting matrix has entries that correspond to the vector product of the ith row and the jth column. In mathematical notation, this is represented as

The multiply member function updates the matrix in place; therefore, it just needs to create a new set of rows and swap the results at the end of the function.

void Matrix::multiply(const Matrix &s)

{

    if (m_rows[0].size() != s.m_rows.size())

    {

        throw new std::runtime_error("invalid matrix dimensions");

    }

    std::vector<Row> rows;

    for (unsigned i=0; i<m_rows.size(); ++i)

    {

        std::vector<double> row;

        for (unsigned j=0; j<s.m_rows.size(); ++j)

        {

            double Mij = 0;

            for (unsigned k=0; k<m_rows[0].size(); ++k)

            {

                Mij += m_rows[i][k] * s.m_rows[k][j];

            }

            row.push_back(Mij);

        }

        rows.push_back(row);

    }

    m_rows.swap(rows);

}

The Matrix class also defines a multiply member function that performs multiplication by a scalar number. This is analogous to the scalar multiplication of vectors and multiplies each element of the matrix by the same number.

void Matrix::multiply(double num)

{

    for (unsigned i=0; i<m_rows.size(); ++i)

    {

        for (unsigned j=0; j<m_rows[0].size(); ++j)

        {

            m_rows[i][j] *= num;

        }

    }

}

The numRows member function just returns the number of rows in the matrix.

int Matrix::numRows() const

{

    return (int)m_rows.size();

}

Finally, three operators are defined that simplify the use of the class. These operators use the in-place implementations you have seen previously, and they allow the use of convenient expressions involving matrices. These operators just give you an idea of how this works in practice; you can extend these definitions to include other common operators, such as /, +=, and *=.

Matrix operator+(const Matrix &s1, const Matrix &s2)

{

    Matrix s(s1);

    s.subtract(s2);

    return s;

}

Matrix operator-(const Matrix &s1, const Matrix &s2)

{

    Matrix s(s1);

    s.subtract(s2);

    return s;

}

Matrix operator*(const Matrix &s1, const Matrix &s2)

{

    Matrix s(s1);

    s.multiply(s2);

    return s;

}

Using the uBLAS Library

In the previous sections, you saw simple implementations of linear algebra concepts in C++. While they are useful for the examples provided in this book, sometimes, you will be required to create high-performance implementations of complex numerical algorithms involving vectors and matrices. In such cases, it is useful to use well-tested and optimized libraries that provide linear algebra–related code.

The most used library for linear algebra algorithms is LAPACK. Originally written in Fortran, LAPACK (linear algebra package) aims at providing high-performing and well-tested algorithms for basic operations involving vectors and matrices.

One interesting aspect of LAPACK is that it relies on another library called BLAS (Basic Linear Algebra Subprograms) to implement basic vector and matrix routines. The result is that BLAS became a standard for implementing vector and matrix routines. Several versions of BLAS have been released, providing optimized performance for specific architectures. BLAS also has versions targeting C and C++ that are used in many commercial products and other applications that need extensive support for numerical algorithms.

BLAS defines three levels of routines for support of linear algebra algorithms:

BLAS Level 1 supports only vector-to-scalar and vector-to-vector operations. It is the most basic level of support, upon which other levels may be built.

BLAS Level 2 offers optimized routines for vector-to-matrix calculations.

BLAS Level 3 expands the previous levels to support matrix-to-matrix calculations, including operations such as matrix multiplication.

There are several implementations of BLAS, both in Fortran and in C++. Boost uBLAS is an implementation that is free and mostly compatible with the original BLAS library. It contains the same three support levels listed previously.

For an example of how to use uBLAS, assume that you want to access a fast implementation of the premultiply operations. That is, given a vector and a matrix, you want to write an algorithm that multiplies the vector by the matrix, giving a vector as a result.

To solve this problem, you can import the uBLAS libraries and create a function that receives two arguments: a vector and a matrix object. Here is a possible implementation for this function:

#include "Matrix.h"

#include <boost/numeric/ublas/matrix.hpp>

#include <boost/numeric/ublas/io.hpp>

#include <boost/numeric/ublas/lu.hpp>

namespace ublas = boost::numeric::ublas;

std::vector<double> preMultiply(const std::vector<double> &v, Matrix &m)

{

    using namespace ublas;

    ublas::vector<double> vec;

    std::copy(v.begin(), v.end(), vec.end());

    int d1 = m.numRows();

    int d2 = (int)m[0].size();

    ublas::matrix<double> M(d1, d2);

    for (int i = 0; i < d1; ++i)

    {

        for (int j = 0; j < d2; ++j)

        {

            M(i,j) = m[i][j];

        }

    }

    vector<double> pv = prod(vec, M);

    std::vector<double> result;

    std::copy(pv.begin(), pv.end(), result.end());

    return result;

}

The first step is to include the header files for the boost numerical libraries. (You also need to make sure that the program will link to the necessary libraries; check your boost documentation for details.) Then, a function called preMultiply is defined, receiving a vector and a matrix as its parameters.

Note

For more information about how to install and use boost libraries, check Chapter 14 of this book.

One of the first things this function needs to do is to convert the parameters into types required by the uBLAS library. In particular, uBLAS provides the vector<double> and matrix<double> types. You need to convert your data to these types before calling any uBLAS functions.

Once the data has been prepared, you may call the prod function from uBLAS, which knows how to calculate the product of a vector and a matrix. The result is then saved into an std::vector container and returned to the caller.

Complete Code

This section contains the complete code for the vector operations. These functions may be used as the basis for a complete LA package, which is a common requirement in the analysis of options and derivatives.

The code is spread over two source files—LAVectors.hpp is the header file and LAVectors.cpp is the implementation file—which you’ll find in Listings 9-2 and 9-3.

//

//  LAVectors.hpp

#ifndef LAVectors_hpp

#define LAVectors_hpp

#include <vector>

typedef std::vector<double> Vector;

// Scalar-to-vector operations

Vector add(double num, const Vector &v);

Vector multiply(double num, const Vector &v);

void in_place_add(double num, Vector &v);

void in_place_multiply(double num, Vector &v);

inline Vector operator +(double num, const Vector &v)

{

    return add(num, v);

}

inline Vector operator *(double num, const Vector &v)

{

    return multiply(num, v);

}

inline void operator +=(double num, Vector &v)

{

    in_place_add(num, v);

}

inline void operator *=(double num, Vector &v)

{

    in_place_multiply(num, v);

}

// Vector-to-vector operations

Vector add(const Vector &v1, const Vector &v2);

void  in_place_add(Vector &v1, const Vector &v2);

double product(const Vector &v1, const Vector &v2);

void  in_place_product(Vector &v1, const Vector &v2);

inline Vector operator +(const Vector &v1, const Vector &v2)

{

    return add(v1, v2);

}

inline void  operator +=(Vector &v1, const Vector &v2)

{

    in_place_add(v1, v2);

}

inline double operator *(const Vector &v1, const Vector &v2)

{

    return product(v1, v2);

}

inline void  operator *=(Vector &v1, const Vector &v2)

{

    in_place_add(v1, v2);

}

double norm(const Vector &v);

#include <stdio.h>

#endif /* LAVectors_hpp */

Listing 9-2

Header File LAVectors.hpp

//

//  LAVectors.cpp

#include "LAVectors.hpp"

#include <cmath>

//

// Adds a scalar number to a vector "v"

//

Vector add(double num, const Vector &v)

{

    int n = (int)v.size();

    Vector result(n);

    for (int i=0; i<n; ++i)

    {

        result[i] = v[i] + num;

    }

    return result;

}

//

// Premultiply a number "num" by the given vector "v"

//

Vector multiply(double num, const Vector &v)

{

    int n = (int)v.size();

    Vector result(n);

    for (int i=0; i<n; ++i)

    {

        result[i] = v[i] * num;

    }

    return result;

}

//

// Perform vector addition in place (modifying the given vector)

//

void in_place_add(double num, Vector &v)

{

    int n = (int)v.size();

    for (int i=0; i<n; ++i)

    {

        v[i] += num;

    }

}

//

// Perform vector multiplication in place

// (modifying the given vector)

//

void in_place_multiply(double num, Vector &v)

{

    int n = (int)v.size();

    for (int i=0; i<n; ++i)

    {

        v[i] *= num;

    }

}

//

// Perform vector addition of two vectors  (v1 and v2)

//

Vector add(const Vector &v1, const Vector &v2)

{

    int n = (int)v1.size();

    Vector result(n);

    for (int i=0; i<n; ++i)

    {

        result[i] = v1[i] + v2[i];

    }

    return result;

}

//

// Perform the vector product of vectors v1 and v2

//

double product(const Vector &v1, const Vector &v2)

{

    double result = 0;

    int n = (int)v1.size();

    for (int i=0; i<n; ++i)

    {

        result += v1[i] * v2[i];

    }

    return result;

}

//

// In-place addition of vectors v1 and v2

//

void  in_place_add(Vector &v1, const Vector &v2)

{

    int n = (int)v1.size();

    for (int i=0; i<n; ++i)

    {

        v1[i] += v2[i];

    }

}

//

// In-place product of vectors v1 and v2

//

void  in_place_product(Vector &v1, const Vector &v2)

{

    int n = (int)v1.size();

    for (int i=0; i<n; ++i)

    {

        v1[i] *= v2[i];

    }

}

//

// Computes the cross product for two three-dimensional vectors

//

Vector cross_prod_3D(const Vector &a, const Vector &b)

{

    assert(a.size()==3); // definition is 3D vectors only

    int n = (int)a.size();

    Vector v(n);  // the resulting vector

    v[0] = (a[1] * b[2] - a[2] * b[1]);

    v[1] = (a[2] * b[0] - a[0] * b[2]);

    v[2] = (a[0] * b[1] - a[1] * b[0]);

    return v;

}

//

// In-place version of cross product for 3D vectors

//

Vector in_place_cross_prod_3D(const Vector &a, const Vector &b, Vector &v)

{

    assert(a.size()==3); // definition is 3D vectors only

    int n = (int)a.size();

    v[0] = (a[1] * b[2] - a[2] * b[1]);

    v[1] = (a[2] * b[0] - a[0] * b[2]);

    v[2] = (a[0] * b[1] - a[1] * b[0]);

    return v;

}

//

// Computes the norm of a vector

//

double norm(const Vector &v)

{

    double result = 0;

    int n = (int)v.size();

    for (int i=0; i<n; ++i)

    {

        result += v[i] * v[i];

    }

    return std::sqrt(result);

}

Listing 9-3

Implementation File LAVectors.cpp

Syntax Change in C++23: Multidimensional Subscripts

As you have seen in the examples in this chapter, the syntax for accessing arrays uses the [] operator, which receives as argument a single index. This was the only valid syntax until C++20. However, the new C++23 standard has provided yet another way to work with matrices: using an indexing operator with multiple indices.

Here is a quick example of what is now possible:

#include <array>

template <typename T, size_t R, size_t Cols>

struct M_Matrix

{

   T& operator[](size_t const a, size_t const b) noexcept

    {

    return data_[a * Cols + b];

    }

    T const & operator[](size_t const a, size_t const b) const noexcept

    {

    return data_[a * Cols + b];

    }

   static constexpr size_t Rows = R;

   static constexpr size_t Columns = Cols;

private:

   std::array<T, R* Cols> data_;

};

int call_m_matrix()

{

    M_Matrix<double, 5,6> my_matrix;

    // ...

    my_matrix[3,5] = 1.0;

}

In this example, the type M_Matrix (multidimensional matrix) has an indexing operator with two parameters (a and b). This means that we can now pass two indices (separated by comma) to determine the exact location of an element in the underlying data, as you can see in the function call_m_matrix.

Conclusion

In this chapter, you learned about linear algebra algorithms that are commonly used in the development of software for the analysis of options and other derivatives. Linear algebra provides many of the techniques that are applied to important problems such as options pricing and the numerical approximation of certain derivatives occurring in finance.

First, you learned about the basic algorithms that involve a vector and a scalar number. These operations can be implemented in C++ using functions that are applied to standard vectors, as you could observe in the given examples.

Next, you learned how to implement a useful matrix data type. Matrices are not directly provided by the STL, but you can take advantage of existing support to vectors as a building block for matrix representations. You also learned about the basic operations that can be performed over matrix objects.

Finally, I discussed linear algebra libraries that provide efficient implementations for some of the functionality discussed in the previous sections. In particular, BLAS has been created and improved by some of the greatest specialists in the implementation of numerical algorithms. The BLAS library is organized into different levels of support for linear algebra algorithms. You saw an example of how to take advantage of this highly optimized library to improve the performance of your own linear algebra code.

In the next chapter, you will learn about another building block for financial derivatives: numerical algorithms used to solve mathematical equations. This type of algorithms is at the core of many techniques used in the pricing of options and more exotic derivatives, as you will see in the next few chapters.

© The Author(s), under exclusive license to APress Media, LLC, part of Springer Nature 2023

C. OliveiraOptions and Derivatives Programming in C++23https://doi.org/10.1007/978-1-4842-9827-5_10

10. Algorithms for Numerical Analysis

Carlos Oliveira1  

(1)

Seattle, WA, USA

Equation solving is one of the main building blocks for financial algorithms used in the analysis of options and financial derivatives. This happens because of the nature of options pricing, which is based on the Black-Scholes pricing model. Many of the techniques that involve options pricing require the efficient solution of differential equations and other mathematical formulations.

Given the importance of mathematical techniques in the pricing of such derivatives, it is important to be able to calculate the solution for particular mathematical models. Although this is a vast area of numerical programming, I will present a few illustrations of numerical algorithms that can be used as a starting point for developing your own C++ code.

In this chapter, you will see programming examples for a few fundamental algorithms in numerical programming. In particular, you will learn techniques to calculate equation roots and integrate functions in C++, with a discussion of how they work and how they are applied. The chapter also discusses numerical error and stability issues that present a challenge for developers in the area of quantitative financial programming.

Mathematical function representation: I initially discuss a representation for mathematical functions that can be used as the starting point for algorithms that manipulate these mathematical abstractions.

Root-finding algorithms: One of the most common types of numerical algorithms, root-finding techniques are used to find one or more roots of an equation, which are the points where the equations have zero value.

Integration algorithms: Another common type of numerical algorithms, integration techniques are used to calculate the numerical value of an integral (which can also be described as the area under a function, for single dimensional equations).

Numerical examples in C++: This chapter also includes C++ code that implements many of these concepts, with concrete examples of how to code these algorithms.

Representing Mathematical Functions

The first step in this short overview of numerical algorithms is to find a reasonable way to represent mathematical functions in C++. As you saw in the previous chapter, functions can be easily represented in C++ using functional objects, which declare a function call operator as one of its member functions. Using this strategy, it is possible to convert a class instance into a callable object, with semantics similar to native functions.

A similar strategy can be used to represent mathematical functions. The main difference between generic C++ and mathematical functions is that the latter operate only over numeric domains, more commonly using float or double values.

In the following example, a new MathFunction class is declared using this strategy. The declaration of MathFunction as an abstract interface allows programmers to extend this definition as necessary to represent concrete functions, as you will see next.

The abstract class can be defined as presented in Listing 10-1.

#include <iostream>

#include <vector>

using std::cout;

using std::endl;

class MathFunction {

public:

    virtual ~MathFunction() {}

    virtual double operator()(double x) = 0;

private:

    // This is just an interface

};

Listing 10-1

Definition for the Abstract Class MathFunction

Note

Because MathFunction is a polymorphic base class, it needs to define its own virtual destructor. This is necessary because clients will receive pointers or references to the base class. Without a virtual destructor, the compiler cannot determine the right destructor to be called, and as a result, such objects will not be properly cleaned up.

The great thing about using this type of interface class is that once you have a class like MathFunction, you can start writing code that uses it directly. Your code is insulated from any worries about the exact representation of objects. For example, consider a useful class called PolynomialFunction, which implements the interface described by MathFunction:

//

//  Polynomial has the form

//    c_1 x^n + c_2 x^n-1 + ....  + c_n-1 x^1 + c_n

//

class PolynomialFunction : public MathFunction {

public:

    PolynomialFunction(const std::vector<double> &coef);

    PolynomialFunction(const PolynomialFunction &p);

    virtual ~PolynomialFunction();

    virtual PolynomialFunction &operator=(const PolynomialFunction &p);

    virtual double operator()(double x) override;

private:

    std::vector<double> m_coefficients;

};

The PolynomialFunction class derives from MathFunction so that it can implement the same interface. However, it is only usable to represent polynomial functions, that is, functions that are determined by a polynomial of the form

The polynomial is determined using the coefficients passed as vectors to the constructor of PolynomialFunction. The constructors are responsible for updating the m_coefficients data member using this information.

PolynomialFunction::PolynomialFunction(const std::vector<double> &coef)

: m_coefficients(coef)

{

}

PolynomialFunction::PolynomialFunction(const PolynomialFunction &p)

: m_coefficients(p.m_coefficients)

{

}

PolynomialFunction::~PolynomialFunction()

{

}

PolynomialFunction &PolynomialFunction::operator=(const PolynomialFunction &p)

{

    if (this != &p)

    {

        m_coefficients = p.m_coefficients;

    }

    return *this;

}

Using Horner’s Method

The main part of the PolynomialFunction class is the implementation for the method call operator. Since this class represents a polynomial, this operator needs to receive a real number x and evaluate the function at that particular point. This is done using the so-called Horner’s method.

Horner’s method is just a quick way to evaluate a polynomial so that you don’t need to explicitly evaluate the terms xi, for i from 1 to n. This can be done using a loop, where at each step you add a coefficient and multiply the result by x. A simple implementation of this idea can be done as follows:

double PolynomialFunction::operator()(double x)

{

    int n = (int)m_coefficients.size();

    double y = 0;

    int i;

    for (i=0; i<n-1; ++i)

    {

        y += m_coefficients[i];

        y *= x;

    }

    if (i < n) {

        y += m_coefficients[i];

    }

    return y;

}

To test these classes, you create a sample function that evaluates a polynomial function in a particular range. The function tested here is simply x² in the real range of –2 to 2. The function also prints the results so that you can visualize the data.

int test_poly_function()

{

    PolynomialFunction f( { 1, 0, 0 } );

    double begin = -2, end = 2;

    double step = (end - begin) / 100.0;

    for (int i=0; i<100; ++i)

    {

        cout <<  begin + step * i << ", ";

    }

    cout << endl;

    for (int i=0; i<100; ++i)

    {

        cout << f( begin + step * i) << ", ";

    }

    return 0;

}

I ran this function and plotted the results as a graph of the function. Figure 10-1 shows the output of the plot.

A line graph of Y versus X. It plots an open upward parabolic curve with a vertical dotted line at o on the x-axis. The curve extends between (negative 2, 4) and (2, 4), and passes through (0, 0). Values are estimated.

Figure 10-1

Plot of results printed by the test_poly_function function

Finding Roots of Equations

Once you have a good representation for mathematical functions, it becomes possible to solve a few numerical problems. The first one I discuss in this section is finding the roots of an equation, a common problem that occurs as part of several numerical algorithms. Finding roots of an equation consists of determining one or more points in a numerical domain (usually the real numbers) where the equation has a value of zero.

This problem has a long history in mathematics, and for some types of equations, it is possible to calculate their roots exactly. For example, you can find such roots for polynomials in general. For other equations, however, this problem can be too complicated to solve using analytical methods, which leads to the need for an algorithm capable of generating approximate solutions to such equations.

A number of numerical algorithms have been proposed in the mathematical literature to find the roots of equations. In this section, you see how to do this using Newton’s method, which is one of the most common algorithms for this problem, and learn how it can be implemented in C++.

Newton’s Method

Newton’s method is based on the use of the derivative as an approximation to the function on a particular neighborhood. To understand how this method works, notice that the derivative of a function at a particular point is known to be the slope of a line segment that is tangent to the function.

Using this property, it is very easy to improve the approximation to the equation root with a new point that is determined by the tangent. Newton’s method will essentially iterate through this process, until the difference between successive approximations is very small. Figure 10-2 shows an example of finding the root of function 5x + cos (9x) − 2 using Newton’s method. The method starts at a given point and at each step computes a better approximation, until the root of the equation (represented as a large dot) is found.

A screenshot displays a set of 4 graphical representations for the root of function 5 x + cos of 9 x minus 2. Every graph has a starting point of 0.5, indicated by a sizable dark-shaded dot. The waves arrive at a more accurate estimate. The starting point, zoom, and function are indicated at the top.

Figure 10-2

Finding the root of a function using Newton’s method (image created with Wolfram Mathematica)

This method can be readily implemented in C++ using the tools that you already have. The first part consists of creating a class that encapsulates the necessary data for the approximation procedure. Here is the definition for the NewtonMethod class:

#include "MathFunction.hpp"

//

// A Newton method implementation

//

class NewtonMethod {

public:

    // Takes as parameter the function and its derivatives

    //

    NewtonMethod(MathFunction &f, MathFunction &derivative);

    NewtonMethod(MathFunction &f,

                 MathFunction &derivative, double error);

    NewtonMethod(const NewtonMethod &p);

    virtual ~NewtonMethod();

    NewtonMethod &operator=(const NewtonMethod &p);

    double getFunctionRoot(double initialValue);

private:

    MathFunction &m_f;

    MathFunction &m_derivative;

    double m_error;

};

The NewtonMethod class contains the commonly used member functions, and in addition, it provides a function called getFunctionRoot, which receives as a parameter an initial value (a first guess that will work as a starting point).

The class stores as its data a reference to the function for which you want to find roots and another reference to its derivative. Although it is technically possible to find the derivative for most functions, the techniques to do this in a generic way are beyond the capabilities of this class, so you need to receive the derivative as a constructor parameter and store it.

#include <iostream>

#include <cmath>

using std::endl;

using std::cout;

namespace {

    const double DEFAULT_ERROR = 0.0001;

}

NewtonMethod::NewtonMethod(MathFunction &f, MathFunction &derivative)

: m_f(f),

  m_derivative(derivative),

  m_error(DEFAULT_ERROR)

{

}

NewtonMethod::NewtonMethod(MathFunction &f, MathFunction &derivative, double error)

: m_f(f),

  m_derivative(derivative),

  m_error(error)

{

}

NewtonMethod::NewtonMethod(const NewtonMethod &p)

: m_f(p.m_f),

  m_derivative(p.m_derivative),

  m_error(p.m_error)

{

}

NewtonMethod::~NewtonMethod()

{

}

NewtonMethod &NewtonMethod::operator=(const NewtonMethod &p)

{

    if (this != &p)

    {

        m_f = p.m_f;

        m_derivative = p.m_derivative;

        m_error = p.m_error;

    }

    return *this;

}

These member functions are necessary just to maintain the state of NewtonMethod objects. The m_f member stores the function that needs to be solved. The m_derivative member stores a reference to the derivative of the main function. You can also tweak the expected error of the solutions found by this class using the m_error member function. If the error is not supplied, this class uses the value stored in the DEFAULT_ERROR error constant.

Next, you’re ready for the implementation of Newton’s method using the given infrastructure. The getFunctionRoot function provides the necessary code for finding the root of the equation. This member function is essentially a loop in which at each step a new approximation for the function root is provided. The loop ends when the absolute difference between the two last approximations is at least equal to the acceptable error:

double NewtonMethod::getFunctionRoot(double x0)

{

    double x1 = x0;

    do

    {

        x0 = x1;

cout << " x0 is " << x0 << endl;  //

this line just for demonstration

        double d = m_derivative(x0);

        double y = m_f(x0);

        x1 = x0 - y / d;

    }

    while (std::abs(x0 - x1) > m_error);

    return x1;

}

Inside the main loop, the steps are as follows:

1.

Find the value at the derivative at the current estimate point using the m_derivative member.

2.

Find the value of the function itself at the current estimate using the m_f member.

3.

The derivative gives the slope d of the tangent, which can now be used to calculate another estimate point starting from the previous estimate. The equation for this new estimate is given by

where x₀ is the previous estimate and x₁ is the new estimate.

You can use a few sample functions to test the accuracy of this method. I created a SampleFunction class for this purpose. This class inherits publicly the MathFunction interface and can be used to compute the function f(x) = (x–1)³, which has 1 as a root solution.

class SampleFunction : public MathFunction {

public:

    virtual ~SampleFunction();

    virtual double operator()(double value);

}.

SampleFunction::~SampleFunction()

{

}

double SampleFunction::operator ()(double x)

{

    return (x-1)*(x-1)*(x-1);

}

To use this class with NewtonMethod, you also need to supply its derivative. I have implemented the Derivative class, which again is derived from MathFunction. Simple math shows you that the derivative is given by f'(x) = 3(x–1)².

class Derivative : public MathFunction {

public:

    virtual ~Derivative();

    virtual double operator()(double value);

};

// Represents the derivative of the sample function

Derivative::~Derivative()

{

}

double Derivative::operator ()(double x)

{

    return 3*(x-1)*(x-1);

}

With these two classes, you can create a simple main function that puts them together and finds the root of the desired function. This code instantiates both SampleFunction and Derivative objects and creates an object of the NewtonMethod class. Finally, the code prints the value for a given initial estimate of 100.

int main()

{

    SampleFunction f;

    Derivative df;

    NewtonMethod nm(f, df);

    cout << " the root of the function is "

         << nm.getFunctionRoot(100) << endl;

    return 0;

}

Running this function gives as a result a set of points, each one closer to the desired equation root. You can view the sequence of results in Table 10-1.

Table 10-1

Sequence of Values Found by Newton’s Method Applied to Function (x–1)³ and with Initial Guess of 100

Iteration

Estimate

Difference

1

100

2

67

33.00000

3

45

22.00000

4

30.3333

14.66670

5

20.5556

9.77770

6

14.037

6.51860

7

9.69136

4.34564

8

6.79424

2.89712

9

4.86283

1.93141

10

3.57522

1.28761

11

2.71681

0.85841

12

2.14454

0.57227

13

1.76303

0.38151

14

1.50868

0.25435

15

1.33912

0.16956

16

1.22608

0.11304

17

1.15072

0.07536

18

1.10048

0.05024

19

1.06699

0.03349

20

1.04466

0.02233

21

1.02977

0.01489

22

1.01985

0.00992

23

1.01323

0.00662

24

1.00882

0.00441

25

1.00588

0.00294

26

1.00392

0.00196

27

1.00261

0.00131

28

1.00174

0.00087

29

1.00116

0.00058

30

1.00077

0.00039

31

1.00052

0.00025

32

1.00034

0.00018

33

1.00023

0.00011

34

1.00015

0.00008

Integration

Another problem that frequently requires the help of mathematical algorithms is the integration of functions. The integral of a function can be visualized as the area under its graph, and it has many applications in finance, engineering, and physics. Several algorithms used in the analysis of options need to evaluate integrals numerically, using techniques similar to the ones covered in this section.

Functions can be integrated analytically or numerically. For some functions, it is possible to find an analytic solution, that is, a closed formula that can be directly evaluated to compute the integral of a function between two points. For example, polynomial functions can be easily integrated analytically, using the antiderivative. For example, if the function is f (x) = x², the antiderivative

can be used to calculate the value of the integral between points a and b, which becomes F (b) – F(a).

Many functions, however, are too complicated to be integrated analytically. In these cases, you need to use numerical algorithms that slice the function into small parts and calculate the integral while trying to reduce the error in this process.

In this section, I present an implementation for one of the simplest integration techniques, known as Simpson’s method. Simpson’s method is based on the decomposition of an area that needs to be integrated into a large number of very small pieces.

First, you need to define a class that presents the interface for this solution method. The SimpsonsIntegration class contains data members such as m_f, a reference to the function that will be integrated, and m_numIntervals, the number of intervals used to approximate the integral.

#include "MathFunction.hpp"

class SimpsonsIntegration {

public:

    SimpsonsIntegration(MathFunction &f);

    SimpsonsIntegration(const SimpsonsIntegration &p);

    ~SimpsonsIntegration();

    SimpsonsIntegration &operator=(const SimpsonsIntegration &p);

    double getIntegral(double a, double b);

    void setNumIntervals(int n);

private:

    MathFunction &m_f;

    int m_numIntervals;

};

The implementation for this class is in the next code fragment. The class uses a default number of intervals, in case you don’t want to set up this value. The DEFAULT_NUM_INTERVALS constant is used for this purpose.

#include "Integration.hpp"

#include "MathFunction.hpp"

#include <iostream>

#include <cmath>

using std::cout;

using std::endl;

namespace  {

    const int DEFAULT_NUM_INTERVALS = 100;

}

SimpsonsIntegration::SimpsonsIntegration(MathFunction &f)

: m_f(f),

m_numIntervals(DEFAULT_NUM_INTERVALS)

{

}

SimpsonsIntegration::SimpsonsIntegration(const SimpsonsIntegration &p)

: m_f(p.m_f),

m_numIntervals(p.m_numIntervals)

{

}

SimpsonsIntegration::~SimpsonsIntegration()

{

}

SimpsonsIntegration &SimpsonsIntegration::operator=(const SimpsonsIntegration &p)

{

    if (this != &p)

    {

        m_f = p.m_f;

        m_numIntervals = p.m_numIntervals;

    }

    return *this;

}

The main part of this implementation is the getIntegral member function. The two parameters for this function define the interval in which the integration will be performed. The intSize variable is used to define the size of each interval used for Simpson’s method.

The algorithm operates as follows. For each slice of the required interval, you need to compute the approximate area under the function. The formula used by Simpson’s method is

where a and b are the beginning and end points of the current interval. This rule has been observed as one of the most effective for evaluating an integral in a short interval.

double SimpsonsIntegration::getIntegral(double a, double b)

{

    double S = 0;

    double intSize = (b - a)/m_numIntervals;

    double x = a;

    for (int i=0; i<m_numIntervals; ++i)

    {

S += (intSize / 6) * ( m_f(x) + m_f(x+intSize) + 4* m_f

((x + x+intSize)/2) );

        x += intSize;

    }

    return S;

}

This class also provides a method to change the number of intervals, therefore improving the accuracy of the method (at the expense of additional running time).

void SimpsonsIntegration::setNumIntervals(int n)

{

    m_numIntervals = n;

}

To test the results of this integration method, I provide a simple mathematical function as an example. The function to be integrated here is sin(x).

// Example function

namespace  {

    class SampleFunc : public MathFunction

    {

    public:

        ~SampleFunc();

        double operator()(double x);

    };

    SampleFunc::~SampleFunc()

    {

    }

    double SampleFunc::operator()(double x)

    {

        return sin(x);

    }

}

The main function can be used as a driver to test the SimpsonsIntegration class. It creates an instance of SampleFunc and uses it to initialize a SimpsonsIntegration object. Then, this code will call the function getIntegral for the interval 0.5 to 2.5. Next, the number of intervals changes to 200, and the same calculation is performed again.

int main()

{

    SampleFunc f;

    SimpsonsIntegration si(f);

    si.setNumIntervals(200);

    double integral = si.getIntegral(0.5, 2.5);

    cout << " the integral of the function is " << integral << endl;

    si.setNumIntervals(200);

    integral = si.getIntegral(0.5, 2.5);

    cout << " the integral of the function with 200 intervals is "

         << integral << endl;

    return 0;

}

The result of this function is the following:

 the integral of the function is 1.67876

 the integral of the function with 200 intervals is 1.67873

This is a very effective method, and with only four intervals, it is possible to achieve a reasonable approximation in this case.

Complete Code

Here is the code used in this chapter:

//

//  MathFunction.hpp

#ifndef MathFunction_hpp

#define MathFunction_hpp

#include <stdio.h>

#include <vector>

class MathFunction {

public:

    virtual ~MathFunction() {}

    virtual double operator()(double x) = 0;

private:

    // This is just an interface

};

//

//  Polynomial has the form  c_1 x^n + c_2 x^n-1 + ....  + c_n-1 x^1 + c_n

class PolynomialFunction : public MathFunction {

public:

    PolynomialFunction(const std::vector<double> &coef);

    PolynomialFunction(const PolynomialFunction &p);

    virtual ~PolynomialFunction();

    PolynomialFunction &operator=(const PolynomialFunction &p);

    virtual double operator()(double x) override;

private:

    std::vector<double> m_coeficients;

};

#endif /* MathFunction_hpp */

//

//  MathFunction.cpp

#include "MathFunction.hpp"

#include <iostream>

using std::cout;

using std::endl;

PolynomialFunction::PolynomialFunction(const std::vector<double> &coef)

: m_coeficients(coef)

{

}

PolynomialFunction::PolynomialFunction(const PolynomialFunction &p)

: m_coeficients(p.m_coeficients)

{

}

PolynomialFunction::~PolynomialFunction()

{

}

PolynomialFunction &PolynomialFunction::operator=(const PolynomialFunction &p)

{

    if (this != &p)

    {

        m_coeficients = p.m_coeficients;

    }

    return *this;

}

double PolynomialFunction::operator()(double x)

{

    int n = (int)m_coeficients.size();

    double y = 0;

    int i;

    for (i=0; i<n-1; ++i)

    {

        y += m_coeficients[i];

        y *= x;

    }

    if (i < n) {

        y += m_coeficients[i];

    }

    return y;

}

//

// Test function

int main_afunc()

{

    PolynomialFunction f( { 1, 0, 0 } );

    double begin = -2, end = 2;

    double step = (end - begin) / 100.0;

    for (int i=0; i<100; ++i)

    {

        cout <<  begin + step * i << ", ";

    }

    cout << endl;

    for (int i=0; i<100; ++i)

    {

        cout << f( begin + step * i) << ", ";

    }

    return 0;

}

//

//  NewtonMethod.hpp

#ifndef NewtonMethod_hpp

#define NewtonMethod_hpp

#include "MathFunction.hpp"

//

// A Newton method implementation

//

class NewtonMethod {

public:

    // Takes as parameter the function and its derivatives

    //

    NewtonMethod(MathFunction &f, MathFunction &derivative);

    NewtonMethod(MathFunction &f, MathFunction &derivative, double error);

    NewtonMethod(const NewtonMethod &p);

    virtual ~NewtonMethod();

    NewtonMethod &operator=(const NewtonMethod &p);

    double getFunctionRoot(double initialValue);

private:

    MathFunction &m_f;

    MathFunction &m_derivative;

    double m_error;

};

#endif /* NewtonMethod_hpp */

//

//  NewtonMethod.cpp

#include "NewtonMethod.hpp"

#include <iostream>

#include <cmath>

using std::endl;

using std::cout;

namespace {

    const double DEFAULT_ERROR = 0.0001;

}

NewtonMethod::NewtonMethod(MathFunction &f, MathFunction &derivative)

: m_f(f),

m_derivative(derivative),

m_error(DEFAULT_ERROR)

{

}

NewtonMethod::NewtonMethod(MathFunction &f, MathFunction &derivative, double error)

: m_f(f),

m_derivative(derivative),

m_error(error)

{

}

NewtonMethod::NewtonMethod(const NewtonMethod &p)

: m_f(p.m_f),

m_derivative(p.m_derivative),

m_error(p.m_error)

{

}

NewtonMethod::~NewtonMethod()

{

}

NewtonMethod &NewtonMethod::operator=(const NewtonMethod &p)

{

    if (this != &p)

    {

        m_f = p.m_f;

        m_derivative = p.m_derivative;

        m_error = p.m_error;

    }

    return *this;

}

double NewtonMethod::getFunctionRoot(double x0)

{

    double x1 = x0;

    do

    {

        x0 = x1;

cout << " x0 is " << x0 << endl;  //

this line is just for demonstration

        double d = m_derivative(x0);

        double y = m_f(x0);

        x1 = x0 - y / d;

    }

    while (std::abs(x0 - x1) > m_error);

    return x1;

}

// ---- A function used as example

namespace {

    class SampleFunction : public MathFunction {

    public:

        virtual ~SampleFunction();

        virtual double operator()(double value);

    };

    SampleFunction::~SampleFunction()

    {

    }

    double SampleFunction::operator ()(double x)

    {

        return (x-1)*(x-1)*(x-1);

    }

    class Derivative : public MathFunction {

    public:

        virtual ~Derivative();

        virtual double operator()(double value);

    };

    // Represents the derivative of the sample function

    Derivative::~Derivative()

    {

    }

    double Derivative::operator ()(double x)

    {

        return 3*(x-1)*(x-1);

    }

}

int main()

{

    SampleFunction f;

    Derivative df;

    NewtonMethod nm(f, df);

cout << " the root of the function is " << nm.getFunctionRoot(100) << endl;

    return 0;

}

//

//  Integration.hpp

#ifndef Integration_hpp

#define Integration_hpp

#include "MathFunction.hpp"

class SimpsonsIntegration {

public:

    SimpsonsIntegration(MathFunction &f);

    SimpsonsIntegration(const SimpsonsIntegration &p);

    ~SimpsonsIntegration();

    SimpsonsIntegration &operator=(const SimpsonsIntegration &p);

    double getIntegral(double a, double b);

    void setNumIntervals(int n);

private:

    MathFunction &m_f;

    int m_numIntervals;

};

#endif /* Integration_hpp */

//

//  Integration.cpp

#include "Integration.hpp"

#include "MathFunction.hpp"

#include <iostream>

#include <cmath>

using std::cout;

using std::endl;

namespace  {

    const int DEFAULT_NUM_INTERVALS = 100;

}

SimpsonsIntegration::SimpsonsIntegration(MathFunction &f)

: m_f(f),

m_numIntervals(DEFAULT_NUM_INTERVALS)

{

}

SimpsonsIntegration::SimpsonsIntegration(const SimpsonsIntegration &p)

: m_f(p.m_f),

m_numIntervals(p.m_numIntervals)

{

}

SimpsonsIntegration::~SimpsonsIntegration()

{

}

SimpsonsIntegration &SimpsonsIntegration::operator=(const SimpsonsIntegration &p)

{

    if (this != &p)

    {

        m_f = p.m_f;

        m_numIntervals = p.m_numIntervals;

    }

    return *this;

}

double SimpsonsIntegration::getIntegral(double a, double b)

{

    double S = 0;

    double intSize = (b - a)/m_numIntervals;

    double x = a;

    for (int i=0; i<m_numIntervals; ++i)

    {

        S += (intSize / 6)

          * ( m_f(x) + m_f(x+intSize) + 4* m_f ((x + x+intSize)/2) );

        x += intSize;

    }

    return S;

}

void SimpsonsIntegration::setNumIntervals(int n)

{

    m_numIntervals = n;

}

// Example function

namespace  {

    class SampleFunc : public MathFunction

    {

    public:

        ~SampleFunc();

        double operator()(double x);

    };

    SampleFunc::~SampleFunc()

    {

    }

    double SampleFunc::operator()(double x)

    {

        return sin(x);

    }

}

int main()

{

    SampleFunc f;

    SimpsonsIntegration si(f);

    si.setNumIntervals(4);

    double integral = si.getIntegral(0.5, 2.5);

    cout << " the integral of the function is " << integral << endl;

    si.setNumIntervals(100);

    integral = si.getIntegral(0.5, 2.5);

cout << " the integral of the function with 200 intervals is " << integral << endl;

    return 0;

}

Conclusion

Numerical algorithms are one of the main parts of an analytical system for options and derivatives. These algorithms have been refined for decades, and many of them have been implemented in C++ for the purpose of options pricing and related tasks.

In this chapter, you saw a few examples of numerical algorithms and learned how they can be efficiently implemented. I started with an explanation of how mathematical functions can be modeled as classes that are independent of the underlying algorithm. You also learned how to create a generic polynomial function class that efficiently computes the value of a function at each point using Horner’s method.

Next, you learned how to find roots of equations using Newton’s method. This traditional method employs the derivative of a function to estimate the value of its root and continually improves this estimate until a solution is found. You learned how this method can be relatively easily implemented using the tools developed in the previous sections.

Finally, this chapter also covered the important problem of function integration. To find the integral of a function, you need to evaluate a function in a given range and use those values to estimate the area covered by the function graph. Using the algorithmic methods introduced here, you learned how to implement one of the most common techniques for integrating functions, known as Simpson’s method.

While this chapter introduced simple numerical techniques, in the next chapter, you will learn how these techniques can be combined to solve some of the complex differential equations that occur when analyzing options and similar derivatives.

© The Author(s), under exclusive license to APress Media, LLC, part of Springer Nature 2023

C. OliveiraOptions and Derivatives Programming in C++23https://doi.org/10.1007/978-1-4842-9827-5_11

11. Models Based on Differential Equations

Carlos Oliveira1  

(1)

Seattle, WA, USA

Differential equations are equations that involve in their terms both a function and its mathematical derivatives. Many of these equations arise naturally from the analysis of economic models used for the pricing of options, such as the Black-Scholes model.

Solving specific partial differential equations (PDEs) is at the core of many techniques used in the analysis of options and related financial derivatives. As you will see in this chapter, there are several techniques for solving and analyzing the results of PDEs that can be implemented in C++. In the next few sections, I present programming examples that cover important aspects of differential equations–based option modeling and their applications using C++.

Here are a few of the topics covered in this chapter:

Basic techniques for solving differential equations (DEs): Several techniques have been developed by practitioners in order to find solutions for differential equations. I provide a quick summary of these methods and explain how they can be used in financial applications.

Ordinary differential equations: ODEs are equations that contain only functions and derivatives of one value. ODEs can be used to represent problems in several areas, and solving them gives you an excellent basis for solving more complex differential equations.

Euler’s method for solving ODEs: Euler’s method is a traditional algorithm that can be easily implemented in C++, providing a numerical evaluation method for a large number of DEs.

Runge-Kutta (RK) method: The RK method provides a more accurate way to determine numerical solutions for differential equations. The RK method uses a Taylor expansion as a way to approximate the desired equation, which makes it possible to find solutions with fewer iterations of the algorithm.

General Differential Equations

Differential equations (DEs) are defined as equations that include one or more derivatives of a function. They have an important role in modeling several types of phenomena occurring in diverse areas such as physics, engineering, social sciences, and economy. In physics, for instance, differential equations are typically used to model the dynamics of motion and forces. In economics, it is possible to use DEs to model financial systems that involve interest rates and time decay.

Differential equations are very useful because they encode information about the rate of variation of a particular quantity. The derivative is the concept that represents the rate of change of a function with respect to a particular variable. The second derivative, in its turn, represents the rate of change of the first derivative with respect to the original variable. The same strategy can be used for as many derivatives as needed by the application.

Differential equations are classified according to the terms they contain, involving functions and their derivatives. Here are some examples of differential equations:

This is a differential equation involving quantities x and y, with a first derivative of y with respect to x and a few other standard terms.

This is a differential equation that involves a second derivative of y with respect to x, as well as the first derivative.

The order of a differential equation is the maximum order of the derivatives appearing in it. For example, a first-order differential equation includes only the first derivative. A second-order differential equation may also contain second-order derivatives, such as

To solve differential equations, it is frequently useful to separate them into particular categories and develop solution techniques that can handle such specific categories. In the next sections, you will see specific types of DEs as well as some solution techniques developed for these types of equations.

Ordinary Differential Equations

An ordinary differential equation is a type of DE in which functions of only a single (ordinary) variable are allowed to appear. As with other types of differential equations, ODEs include variables, functions, and their derivatives. A formal definition of an ODE is a function

that depends on a variable x, a function f (x) of x, and their derivatives. The order of the ODE is the maximum order of derivatives appearing in the equation.

You can solve ODEs in two ways:

Using analytical methods: If the function can be solved explicitly using mathematical methods, then a closed expression can be found and used to calculate its value at different points. This method is preferred whenever possible, because it produces results that are usually easier to calculate and interpret. Unfortunately, it is not always possible to find closed solutions to complex differential equations.

Using numerical methods: More generally, it is difficult to find closed solutions for several classes of ODEs. In this case, the analyst may resort to using numerical techniques that approximate the value of the ODE for a particular value or range of values. These numerical techniques usually involve the approximation of the value of a complex function in a piecewise fashion so that the solution of the differential equation is found after a large number of small approximation steps.

Since the goal of this chapter is to consider computational techniques to solve ODEs, you will see a few techniques to solve them numerically, using programming strategies. First, you will learn about Euler’s method for solving ODEs. Then, you will see how this method can be implemented in C++.

Euler’s Method

One of the most common methods used for solving ODEs is called Euler’s method. It was one of the first algorithms developed for this purpose and was proposed by the famous eighteenth-century mathematician Leonhard Euler. The method belongs to a class of ODE algorithms called predictor-corrector, because it tries to make a prediction for the next step in the evaluation, followed by successive corrections of the current result.

The basic idea behind Euler’s method is to approximate the solution to an ODE by successively approximating the derivative of the solution at different points along the domain of interest. The method is based on the concept of a tangent line.

Given an initial condition, typically in the form of the solution value and the derivative value at a specific point, Euler’s method allows you to compute the approximate solution at subsequent points using a step-by-step process.

Euler’s method works by approximating the curve determined by a differential equation through sequential steps. First, to start the solution process, you need to represent the ODE in its most generic form:

Here, y = f(x) is a function that depends on the variable x, and y' is the derivative of f(x) with respect to x. The general goal of the method is to improve the approximation step by step, using a simple formula to calculate small increments and using the result as the next starting point. Figure 11-1 shows an example of how the general approach works, when applied to the sample differential equation dT(t)/dt = -k Δ T.

A dual-line graph of f of x versus x. The parameters are analytical and 10 steps of the Euler method. Both lines follow a descending trend from (0, 100) to (100, 0). Data are estimated.

Figure 11-1

Euler’s method applied to function dT(t)/dt = -k Δ T, with ten steps

Each step starts at a known place of the solution space and moves into the required direction by a small quantity. If you denote by c the desired destination point and start moving from location x₀ in N steps, then the increment h can be calculated as follows:

Now, at each step of this algorithm, you will have the current location (at the beginning, the location is (x₀, y₀), a given parameter passed to the algorithm), and the goal is to compute the next location that approximates the real curve. As long as h is small enough, this new location can be calculated by taking the derivative of the curve, given by y', which represents the slope of the equation, and using a simple line segment to move in that direction. This is fairly easy to calculate numerically, as you will see next.

The equation needed to implement this idea is the following:

In other words, at each step, you’re adding to the previous result a quantity that depends on the step size and the average value of the target function at two points: the current point and the next incremental point. You can think of the averaging (dividing by two) as a correction of the procedure, which will make it closer to the real value that needs to be computed.

Implementing the Method

Euler’s method can be implemented with little effort. First, you need to update the MathFunction class so that it can also be used when a variable and an initial condition are provided. This requires that the function call operator take two parameters instead of one, such as was presented in the last chapter. I coded this as a class called DEMathFunction, with the following interface:

class DEMathFunction   {

public:

    virtual ~DEMathFunction() {}

virtual double operator()(double x, double y) = 0; //

version with two variables

private:

    // This is just an interface.

};

The new version of operator() takes as parameters the value of coordinates x and y. Now, you can implement versions of this class for each desired function. Here is an example that will later be used with the main implementation:

class EulerMethodSampleFunction : public DEMathFunction {

public:

    double operator()(double x, double y);

};

double EulerMethodSampleFunction::operator()(double x, double y)

{

    return  3 * x + 2 * y + 1;

}

The main class implementing Euler’s method is presented next. The interface contains a single function called solve, which receives four parameters:

The number of steps used by the algorithm

The initial x value

The initial y value (which represents the initial condition of the function)

The target value for the ODE, which is the coordinate for which the solution is required

The class also contains a data member to store the instance of DEMathFunction, which is used to compute new values for the desired function.

//

//  EulersMethod.hpp

#ifndef EulersMethod_hpp

#define EulersMethod_hpp

class DEMathFunction  {

public:

    virtual ~DEMathFunction() {}

virtual double operator()(double x, double y) = 0; // version with two variables

private:

    // This is just an interface.

};

class EulersMethod {

public:

    EulersMethod(DEMathFunction &f);

    EulersMethod(const EulersMethod &p);

    ~EulersMethod();

    EulersMethod &operator=(const EulersMethod &p);

    double solve(int n, double x0, double y0, double c);

private:

    DEMathFunction &m_f;

};

#endif /* EulersMethod_hpp */

The implementation of the EulersMethod class contains the steps of the algorithm explained in the previous section. First, here are some of the required methods used by the class:

//

//  EulersMethod.cpp

#include "EulersMethod.hpp"

#include <iostream>

using std::cout;

using std::endl;

EulersMethod::EulersMethod(DEMathFunction &f)

: m_f(f)

{

}

EulersMethod::EulersMethod(const EulersMethod &p)

: m_f(p.m_f)

{

}

EulersMethod::~EulersMethod()

{

}

EulersMethod &EulersMethod::operator=(const EulersMethod &p)

{

    if (this != &p)

    {

        m_f = p.m_f;

    }

    return *this;

}

Next, the solve function contains the main algorithm for Euler’s method. The algorithm assumes that x0 is the initial coordinate and y0 is the corresponding initial value for that coordinate.

double EulersMethod::solve(int n, double x0, double y0, double c)

{

    // problem :   y' = f(x,y) ;  y(x0) = y0

    auto x = x0;

    auto y = y0;

    auto h = (c - x0)/n;

    cout << " h is " << h << endl;

    for (int i=0; i<n; ++i)

    {

        double F = m_f(x, y);

        auto G = m_f(x + h, y + h*F);

        cout << " F: " << F << " G: " << G << "";

        // Update values of x, y

        x += h;

        y += h * (F + G)/2;

        cout << " x: " << x << " y: " << y << endl;

    }

    return y;

}

The first part of the algorithm uses the given values to calculate the desired increment h. Then, for each step, the algorithm will calculate the function at the current point (x, y), as well as at the next incremental point (x + h, y + hF). The values of x and y are then updated according to the equation presented in the previous section.

You can quickly test the implementation with the help of the EulerMethodSampleFunction class. Here is the sample code necessary to instantiate the class and use it to test the method:

int test_euler()

{

    EulerMethodSampleFunction f;

    EulersMethod m(f);

    double res = m.solve (100, 0, 0.25, 2);

    cout << " result is " << res << endl;

    return 0;

}

The sample function is instantiated in the first line, and the resulting function object is passed to the EulersMethod class. The member function solve is called, with a few initial parameters. The results are printed as the last step. Table 11-1 shows the sequence of values obtained when you run the test function.

Table 11-1

Results of Euler’s Method Iterations for the Test Code for the EulersMethod Class

i

F

x

y

i

F

x

y

i

F

x

y

1

1.5

0.02

0.2812

34

9.72643

0.68

3.57223

67

40.5109

1.34

18.6025

2

1.6224

0.04

0.314897

35

10.1845

0.7

3.7806

68

42.2249

1.36

19.4645

3

1.74979

0.06

0.351193

36

10.6612

0.72

3.99868

69

44.0089

1.38

20.3628

4

1.88239

0.08

0.390193

37

11.1574

0.74

4.2269

70

45.8657

1.4

21.2991

5

2.02039

0.1

0.432009

38

11.6738

0.76

4.46564

71

47.7982

1.42

22.2748

6

2.16402

0.12

0.476755

39

12.2113

0.78

4.71535

72

49.8096

1.44

23.2915

7

2.31351

0.14

0.524551

40

12.7707

0.8

4.97647

73

51.903

1.46

24.3509

8

2.4691

0.16

0.575521

41

13.3529

0.82

5.24947

74

54.0818

1.48

25.4548

9

2.63104

0.18

0.629794

42

13.9589

0.84

5.53484

75

56.3496

1.5

26.6049

10

2.79959

0.2

0.687505

43

14.5897

0.86

5.83306

76

58.7098

1.52

27.8032

11

2.97501

0.22

0.748796

44

15.2461

0.88

6.14469

77

61.1664

1.54

29.0516

12

3.15759

0.24

0.81381

45

15.9294

0.9

6.47025

78

63.7232

1.56

30.3522

13

3.34762

0.26

0.882702

46

16.6405

0.92

6.81031

79

66.3843

1.58

31.707

14

3.5454

0.28

0.955628

47

17.3806

0.94

7.16548

80

69.154

1.6

33.1183

15

3.75126

0.3

1.03275

48

18.151

0.96

7.53636

81

72.0367

1.62

34.5885

16

3.96551

0.32

1.11425

49

18.9527

0.98

7.92359

82

75.037

1.64

36.1198

17

4.1885

0.34

1.2003

50

19.7872

1

8.32785

83

78.1597

1.66

37.7149

18

4.42059

0.36

1.29108

51

20.6557

1.02

8.74983

84

81.4098

1.68

39.3763

19

4.66215

0.38

1.38678

52

21.5597

1.04

9.19024

85

84.7925

1.7

41.1066

20

4.91357

0.4

1.48762

53

22.5005

1.06

9.64985

86

88.3132

1.72

42.9088

21

5.17524

0.42

1.5938

54

23.4797

1.08

10.1294

87

91.9776

1.74

44.7858

22

5.44759

0.44

1.70553

55

24.4989

1.1

10.6298

88

95.7915

1.76

46.7405

23

5.73105

0.46

1.82304

56

25.5596

1.12

11.1518

89

99.761

1.78

48.7762

24

6.02608

0.48

1.94657

57

26.6637

1.14

11.6964

90

103.892

1.8

50.8962

25

6.33314

0.5

2.07637

58

27.8127

1.16

12.2643

91

108.192

1.82

53.104

26

6.65274

0.52

2.21268

59

29.0087

1.18

12.8567

92

112.668

1.84

55.403

27

6.98537

0.54

2.35578

60

30.2535

1.2

13.4745

93

117.326

1.86

57.797

28

7.33157

0.56

2.50595

61

31.549

1.22

14.1187

94

122.174

1.88

60.29

29

7.6919

0.58

2.66346

62

32.8974

1.24

14.7904

95

127.22

1.9

62.8859

30

8.06693

0.6

2.82863

63

34.3008

1.26

15.4907

96

132.472

1.92

65.5889

31

8.45726

0.62

3.00176

64

35.7615

1.28

16.2209

97

137.938

1.94

68.4034

32

8.86351

0.64

3.18317

65

37.2817

1.3

16.982

98

143.627

1.96

71.334

33

9.28635

0.66

3.37321

66

38.864

1.32

17.7754

99

149.548

1.98

74.3854

Euler’s method is a simple technique that finds solutions to several ODE problems. However, in terms of quality of approximation, it requires a large number of steps, which can also cause numerical errors and instability. To avoid these problems, more precise methods have been proposed for solving ODEs, as you will learn next.

The Runge-Kutta Method

The next technique for solving ODEs is an extension of Euler’s method called the Runge-Kutta (RK) method (named after its inventors). This technique is an effective way to improve the accuracy of Euler’s method and reduce the possibility of the numerical errors that are common when using a linear approximation.

The main idea of the RK method is to use a higher-order approximation for the given functions, instead of relying on linear interpolation, as you saw with the previous algorithm. By doing this, the RK method can achieve faster convergence, in many cases using a smaller number of steps to achieve the same results. This is an advantage both in terms of reduced computational time as well as higher accuracy.

As explained in the book Computational Physics: An introductory course by Fitzpatrick:

There are two main reasons why Euler’s method is not generally used in scientific computing. Firstly, the truncation error per step associated with this method is far larger than those associated with other, more advanced, methods (for a given value of h). Secondly, Euler’s method is too prone to numerical instabilities.

The methods most commonly employed by scientists to integrate ODEs were first developed by the German mathematicians C.D.T. Runge and M.W. Kutta in the latter half of the nineteenth century. The basic reasoning behind so-called Runge-Kutta methods is outlined in the following.

The main reason that Euler’s method has such a large truncation error per step is that in evolving the solution from xn to xn+1 the method only evaluates derivatives at the beginning of the interval: i.e., at xn. The method is, therefore, very asymmetric with respect to the beginning and the end of the interval.

We can construct a more symmetric integration method by making an Euler-like trial step to the midpoint of the interval, and then using the values of both x and y at the midpoint to make the real step across the interval.

For additional details of the method, remember that to solve an ODE, you have to consider a very general form that is amenable to solution, using the following relation:

Here, y’ is the derivative of the function, and f(x, y) is a function of variable x (the independent variable) and y.

As before, given a starting point for the calculation and the number of steps, it is possible to easily calculate the size of the increment h for each iteration of the RK method, using the equation

In its basic design, the RK method has the same structure of Euler’s algorithm. The main difference is how the RK method approximates the function to generate the next step of the algorithm. While Euler’s method just uses a linear interpolation, the RK method can use any one of a family of approximating equations.

The RK method can be implemented using one of several approximation strategies, but they are frequently calculated as a Taylor series applied to the original function. The Taylor method is a basic tool from calculus that provides a family of approximations for functions around a particular starting value. For example, using the simplest Taylor approximation, you can compute the next (x,y) values in the following way:

Another possibility is to use higher-order approximations, that is, versions of the Taylor series that contain additional terms. By adding more terms of higher order, it is possible to achieve a more accurate result in fewer steps. Here is another commonly used approximation, this time based on a fourth-order expansion:

Runge-Kutta Implementation

To implement this algorithm, it is possible to extend the Euler’s method class. To avoid dependencies between these two methods, I decided to implement a separate class called RungeKuttaMethod.

Here is the interface of the RungeKuttaMethod class. It exposes the solve method, which is used to compute the desired value of the function.

//

//  Class providing an interface for RungeKutta method

class RungeKuttaMethod {

public:

    RungeKuttaMethod(DEMathFunction &f);

    RungeKuttaMethod(const RungeKuttaMethod &p);

    ~RungeKuttaMethod();

    RungeKuttaMethod &operator=(const RungeKuttaMethod &p);

    double solve(int n, double x0, double y0, double c);

private:

    DEMathFunction &m_func;

};

First, the common member functions of RungeKuttaMethod are implemented, including the constructor that receives the DEMathFunction reference as a parameter.

//

//  RungeKutta.cpp

#include "RungeKutta.hpp"

#include <iostream>

using std::cout;

using std::endl;

RungeKuttaMethod::RungeKuttaMethod(DEMathFunction &f)

: m_func(f)

{

}

RungeKuttaMethod::RungeKuttaMethod(const RungeKuttaMethod &p)

: m_func(p.m_func)

{

}

RungeKuttaMethod::~RungeKuttaMethod()

{

}

RungeKuttaMethod &RungeKuttaMethod::operator=(

            const RungeKuttaMethod &p)

{

    if (this != &p)

    {

        m_func = p.m_func;

    }

    return *this;

}

The member function solve is used to compute the numerical value of the ODE, given starting conditions and a target value. The function implements the Runge-Kutta method with fourth-degree Taylor expansion, as described in the previous section.

The parameters for this member function are the following:

The number of steps in the process, which indirectly also determines the increment for each step

The initial value for the variable x

The initial corresponding y for the given value x

The target value for which the ODE is being calculated

// Runge-Kutta method with fourth-order approximation

//

double RungeKuttaMethod::solve(int n, double x0, double y0, double c)

{

    // Initial conditions

    auto x = x0;

    auto y = y0;

    auto h = (c - x0)/n;

    for (int i=0; i<n; ++i)

    {

        // Compute the intermediary steps

        //

        auto k1 = h * m_func(x, y);

        auto k2 = h * m_func(x + (h/2), y + (k1/2));

        auto k3 = h * m_func(x + (h/2), y + (k2/2));

        auto k4 = h * m_func(x + h, y + k3);

        // Use terms to compute next step

        x += h;

        y += ( k1 + 2*k2 + 2*k3 + k4)/6;

        cout << " x: " << x << " y: " << y << endl;

    }

    return y;

}

As in the previous algorithm, the RK method starts by defining the initial conditions, including the values for the variables x and y, and the size of the step determined by h.

The RK method then proceeds to compute each iteration of the algorithm. This consists of successive terms of approximation, as described in the previous section. These terms are then used to compute the new values for x and y.

To test the results of the RK method implementation, I provide a simple test function. But first it is necessary to implement a function that will be later used in the test code:

class RungeKuttaSampleFunc : public DEMathFunction {

public:

    double operator()(double x, double y);

};

double RungeKuttaSampleFunc::operator()(double x, double y)

{

    return  3 * x + 2 * y + 1;

}

The RungeKuttaSampleFunc is derived from DEMathFunction, so it can be passed as a parameter to the RungeKuttaMethod class. It is a simple polynomial function. The test function is the following:

int test_RKMethod()

{

    RungeKuttaSampleFunc f;

    RungeKuttaMethod m(f);

    double res = m.solve (100, 0, 0.25, 2);

    cout << " result is " << res << endl;

    return 0;

}

This test code first instantiates the RungeKuttaSampleFunc class and then uses the resulting instance to create a RungeKuttaMethod object. Next, the result of the function is computed for some test parameters.

Complete Code

The complete listing for the RungeKuttaMethod class is shown in this section. The code is divided into a header file and an implementation file, which appear in Listings 11-1 and 11-2, respectively.

//

//  RungeKutta.hpp

#ifndef RungeKutta_hpp

#define RungeKutta_hpp

#include "EulersMethod.hpp"

class RungeKuttaMethod {

public:

    RungeKuttaMethod(DEMathFunction &f);

    RungeKuttaMethod(const RungeKuttaMethod &p);

    ~RungeKuttaMethod();

    RungeKuttaMethod &operator=(const RungeKuttaMethod &p);

    double solve(int n, double x0, double y0, double c);

private:

    DEMathFunction &m_func;

};

#endif /* RungeKutta_hpp */

Listing 11-1

Header File for the RungeKuttaMethod Class

//

//  RungeKutta.cpp

#include "RungeKutta.hpp"

#include <iostream>

using std::cout;

using std::endl;

RungeKuttaMethod::RungeKuttaMethod(DEMathFunction &f)

: m_func(f)

{

}

RungeKuttaMethod::RungeKuttaMethod(const RungeKuttaMethod &p)

: m_func(p.m_func)

{

}

RungeKuttaMethod::~RungeKuttaMethod()

{

}

RungeKuttaMethod &RungeKuttaMethod::operator=(

            const RungeKuttaMethod &p)

{

    if (this != &p)

    {

        m_func = p.m_func;

    }

    return *this;

}

// Runge-Kutta method with fourth-order approximation

//

double RungeKuttaMethod::solve(int n, double x0, double y0, double c)

{

    // Initial conditions

    auto x = x0;

    auto y = y0;

    auto h = (c - x0)/n;

    for (int i=0; i<n; ++i)

    {

        // Compute the intermediary steps

        //

        auto k1 = h * m_func(x, y);

        auto k2 = h * m_func(x + (h/2), y + (k1/2));

        auto k3 = h * m_func(x + (h/2), y + (k2/2));

        auto k4 = h * m_func(x + h, y + k3);

        // Use terms to compute next step

        x += h;

        y += ( k1 + 2*k2 + 2*k3 + k4)/6;

        cout << " x: " << x << " y: " << y << endl;

    }

    return y;

}

/// -----

class RungeKuttaSampleFunc : public DEMathFunction {

public:

    double operator()(double x, double y);

};

double RungeKuttaSampleFunc::operator()(double x, double y)

{

    return  3 * x + 2 * y + 1;

}

int main()

{

    RungeKuttaSampleFunc f;

    RungeKuttaMethod m(f);

    double res = m.solve (100, 0, 0.25, 2);

    cout << " result is " << res << endl;

    return 0;

}

Listing 11-2

Implementation File for the RungeKuttaMethod Class

Conclusion

Solving differential equations is a task commonly required when analyzing complex financial contracts. This is true due to the mathematical nature of options and derivatives, which are based on the Black-Scholes model.

In this chapter, you saw a few examples of differential equations and learned how they can be effectively solved using computational techniques. First, you learned about Euler’s method, the simplest technique used to compute numerical solutions for ODEs. Next, you learned about the Runge-Kutta method, a commonly used technique that provides improved accuracy over Euler’s method, but still with great performance.

This chapter can be used as an overview of the implementation of differential equations in C++. In the next chapter, you will take a closer look at how these mathematical models can be directly applied to options pricing. In particular, you will see how these techniques can be used when pricing option contracts.

© The Author(s), under exclusive license to APress Media, LLC, part of Springer Nature 2023

C. OliveiraOptions and Derivatives Programming in C++23https://doi.org/10.1007/978-1-4842-9827-5_12

12. Basic Models for Options Pricing

Carlos Oliveira1  

(1)

Seattle, WA, USA

Options pricing is the task of determining the fair value of a particular option, given a set of parameters that exactly determine the features of the option contracts, such as its expiration date, current volatility, and prevailing interest rates. Pricing options requires the use of efficient algorithms, because of frequent changes in prices and market volatility. For this reason, a number of models have been employed for this task in the area of quantitative finance.

This chapter discusses some of the most popular models for options pricing. First, there are models that use tree-based methods, such as binomial and trinomial trees. Second, the most important mathematical model uses the Black-Scholes model, which provides the theoretical basis for the analysis of most options and derivative contracts.

Here is a summary of the topics discussed in this chapter:

Binomial trees: A binomial tree is a technique used to compute option prices by simulating a number of probabilistic price changes starting from the current stock price. Such prices are organized in a tree-based structure and used to compute the option’s corresponding price. You will see the calculations necessary to use these tree-based algorithms for options pricing.

Calculating American-style options: Options in the American style give their buyers the ability to exercise the option at any time before expiration. This exercise style needs to be reflected in the price of the option.

Black-Scholes method: The most famous method for computing option prices is based on the equations developed by Black and Scholes. These differential equations can be solved using PDE techniques, which are explored later in this chapter.

Implementation strategies: You will see examples of implementation techniques for the pricing methods described previously.

Lattice Models

The goal of options pricing is to compute the fair value of an option at a particular time. This problem has been solved theoretically by Black and Scholes, the creators of the famous PDE model that defines prices for options. However, solving complex PDEs is not an easy job, and for this reason, several methods have been developed to perform this computational task in less time.

The Black-Scholes model, also known as the Black-Scholes-Merton model, is a widely used mathematical formula for pricing European-style options. It was developed by economists Fischer Black and Myron Scholes in collaboration with mathematician Robert Merton in the early 1970s. For their contributions to the model, Black, Scholes, and Merton were awarded the Nobel Prize in Economic Sciences in 1997.

The model provides a way to calculate the fair market value of options based on various factors such as the underlying asset’s price, the option’s strike price, time to expiration, risk-free interest rate, and market volatility. It assumes that the financial markets are efficient and that there are no arbitrage opportunities.

A common class of algorithms for computing option prices is the lattice model. A lattice model is a technique of calculating derivative prices that divides the solution space into discrete steps. Each step corresponds to a small time increment and corresponding price change. Starting this way from a given starting point, this technique results in the creation of a tree of nodes that correspond to possible price changes.

There are a few particular methods that have been devised based on the general strategy put forward by lattice models. The best-known such methods are as follows:

Binomial model: In the binomial model, the possible changes are organized in a tree rooted at the given starting point (the current price). To each node of the tree, two nodes are added, representing two possible directions of movement: up (price increases) or down (price decreases). For performance reasons, the binary tree can also be created implicitly, where nodes are calculated only as needed for the evaluation of the next time period.

Trinomial model: The trinomial model is an extension of the binomial model, and it tries to improve the accuracy by considering nodes where the price is unchanged. Depending on the volatility of the underlying, such models can achieve higher accuracy than binomial models, at the expense of a slight increase in computational time.

Mixed models have also been used that combine features of the binomial and trinomial models, producing more complex lattice models for particular uses. In this chapter, you learn how to implement a binomial model for options pricing. The complete model is explained along with the equations frequently used to evaluate such models.

Later, this general model was extended to handle American-style options, where the owners of the option can exercise the option at any time before expiration. These models also show how this type of algorithm can be efficiently coded in C++ using OO concepts. In this particular case, you will see how to use inheritance to override parts of the class according to the desired pricing strategy.

Binomial Model

The first model that’s discussed is called the binomial model for options pricing. In this model, option prices are evaluated interactively. Possible values are organized in a tree-based structure where the root is the original (unknown) price and leaves are the possible prices at a particular target time.

The binomial options pricing model is a popular mathematical method for evaluating option prices. It was first introduced by Cox, Ross, and Rubinstein in 1979, and it provides a more intuitive approach to valuing options compared to the complex mathematical equations used in the Black-Scholes model. The binomial model is based on a discrete-time framework and can be used for both European- and American-style options.

The basic idea behind the binomial model is to model the price movement of the underlying asset over time in a series of discrete steps. The model assumes that during each time step, the underlying asset’s price can move up or down by a certain factor. By constructing a binomial tree of possible price movements, the option’s value at each node of the tree can be calculated. The model then works backward through the tree to determine the option’s value at the initial node (current time).

Using this structure, the binomial model traverses the tree with the goal of computing the desired price (the root value) starting from some known prices. The natural way of doing this is to look at the values for the option at expiration date and use these prices to compute the value at other times. Remember that at expiration price, the value of an option is defined by contract. For example, if you are given the current stock price (denoted by S) and the strike price (denoted by K), then the price of a call option at expiration is given by

For a put option, the price is also straightforward and determined by contract as

The question, however, is which values of stock prices should be used in a tree-based model to make it realistic? A possible answer to this question is that at each time step, the stock price can move either up or down. The exact probabilities for this jump can be derived using a few mathematical assumptions, but the expressions most commonly used are as follows:

Change of value for an up move:

Change of value for a down move:

In these two expressions, σ is a measure of the volatility of the stock (i.e., the typical amount of movement), and t is time. These expressions allow you to construct a tree where each node contains information about the time and the value of the stock at that moment. The tree can be visualized as shown in Figure 12-1.

A binomial tree illustration. S 0 is mapped to S (1, 1) and S (1, 2). S (1, 1) is mapped to (2, 1) and (2, 2). S (1, 2) is mapped to (2, 2) and S (2, 3). S (2, 1) is mapped to S (3, 1) and S (3, 2). S (2, 2) is mapped to S (3, 2) and S (3, 3). S (2, 3) is mapped to (3, 3) and S (3, 4).

Figure 12-1

A visualization of the binomial tree determined by possible stock prices

Now consider the task of pricing a call option at a date immediately before expiration. While the price is initially unknown, it cannot be very far away from the price at expiration, since the time premium at this point is very small. A way to calculate this value is to assume a probability for two events: either going up a small amount or going down a small amount. With this probability, you can estimate the value of the option as the expected value (the mean) based on these two possibilities.

Using these observations, you can devise a method for calculating the price of an option. The general algorithm can be described in the following way:

Calculate stock prices for the nodes of the tree, starting from the root node at time zero and stock price given by the current known price.

Apply the equations for price fluctuations to create up and down nodes starting from the root. The goal of this phase is to calculate the stock prices for nodes at expiration time.

Start to compute the option prices from the leaves of the tree. These leaves have a known price by definition of the option contracts. The value of the option depends on three characteristics:

The strike price

The stock price

If the option is a put or a call

Then, progress from nodes at expiration date toward earlier dates, always using the expected value based on the known probabilities. Repeat this process until you reach the root node.

Binomial Model Implementation

To implement an algorithm for the binomial model as previously described, I introduce a class called BinomialModel. The class provides all the necessary steps for the calculation of option prices, along with the ability to be extended to other open types, as you will see later.

The first step is to provide an interface to the C++ class, as shown in the next code fragment. The class contains a number of data members that are necessary for the computation of option prices using the binomial model approach. Here are these data members:

The expiration date, denoted as m_T.

The initial stock price, that is, the stock price at the root of the binomial tree, denoted by m_S.

The interest rate, which is used as one of the factors necessary to calculate future prices and is denoted as m_r.

The volatility, which is the volatility of the underlying stock, as measured from stock prices in the last few days and denoted by m_sigma.

The dividend yield, which is the amount of dividend paid by the underlying stock during the desired period. This quantity is denoted by m_q.

The number of steps, used by the binomial method to determine the depth of the tree. It is denoted by m_n.

The type of option. This is the class record if the option type is a call or put. This information is stored in the member variable m_call, a Boolean value.

The class BinomialModel also offers a member function that can be used to calculate the option price, named optionPriceForStrike. This function receives as a parameter a strike value and returns the option price corresponding to that strike.

A second function, computePriceStep, is used to compute option prices for a single step. You will see later how this is implemented and extended for more complex option types.

#include <vector>

#include <cmath>

using vec = std::vector<double>;

class BinomialModel {

public:

    BinomialModel(const BinomialModel &p);

    virtual ~BinomialModel();

    BinomialModel &operator=(const BinomialModel &p);

    BinomialModel(double T, // expiration time

                  double S,   // stock price

                  double r,   // interest rate

                  double sigma,

                  double q,   // dividend yield

                  int n,      // number of steps

                  bool call

    );

    double optionPriceForStrike(double K);

    virtual void computePriceStep(int i, int j, double K, vec &prices,

                                  double p_u, double p_d, double u);

protected:

    double getStockPrice() { return m_S; }

private:

    double m_T;     // expiration time

    double m_S;     // stock price

    double m_r;     // interest rate

    double m_sigma; // volatility

    double m_q;     // dividend yield

    int m_n;        // number of steps

    bool m_call;    // true = call, false = put

};

The next few member functions are part of the constructor and destructor code. They are used to properly initialize each of the data members in the BinomialModel class.

BinomialModel::BinomialModel(double T, double S, double r,

              double sigma,

              double q,

              int n, bool call)

: m_T(T),

  m_S(S),

  m_r(r),

  m_sigma(sigma),

  m_n(n),

  m_q(q),

  m_call(call)

{

}

BinomialModel::BinomialModel(const BinomialModel &p)

: m_T(p.m_T),

  m_S(p.m_S),

  m_r(p.m_r),

  m_sigma(p.m_sigma),

  m_n(p.m_n),

  m_q(p.m_q),

  m_call(p.m_call)

{

}

BinomialModel::~BinomialModel()

{

}

BinomialModel &BinomialModel::operator=(const BinomialModel &p)

{

    if (this != &p)

    {

        m_T = p.m_T;

        m_S = p.m_S;

        m_r = p.m_r;

        m_sigma = p.m_sigma;

        m_n = p.m_n;

        m_q = p.m_q;

        m_call = p.m_call;

    }

    return *this;

}

The computePriceStep member function is used to compute the immediate price for a single step of the algorithm. The indices i and j represent the position in the binomial tree. Other arguments are the necessary parameters used to calculate the price of this step. Notice that this member function is declared as virtual, and it can be later overridden for the use of American-style options.

void BinomialModel::computePriceStep(int i, int j, double K,

                                     vec &prices, double p_u,

                                     double p_d, double u)

{

    prices[i] = p_u * prices[i] + p_d * prices[i+1];

}

The main member function in the BinomialModel class is the function that computes the option price for a given strike, determined by the parameter K. The algorithm is essentially a C++ implementation of the ideas presented in the previous section. The first step is to calculate the price delta using the period and the number of steps. Next, the amount of price changes in the up side is calculated using the exp(m_sigma * sqrt(delta)) expression.

Next, the function computes the probabilities of moving up or down in the binomial tree using the equations described previously. The probabilities are denoted by p_u and p_d.

double BinomialModel::optionPriceForStrike(double K)

{

    double delta = m_T / m_n;   // size of each step

    double u = exp(m_sigma * sqrt(delta));

    double p_u = (u * exp(-m_r * delta) - exp(-m_q * delta)) * u / (u*u - 1);

    double p_d = exp(-m_r * delta) - p_u;

    vec prices(m_n);

    //  Compute last day values (leaves of the tree)

    for (int i= 0; i<m_n; ++i)

    {

        if (m_call)

        {

            prices[i] =  std::max(0.0, m_S * pow(u, 2*i - m_n) - K);

        }

        else

        {

            prices[i] =  std::max(0.0, K - m_S * pow(u, 2*i - m_n));

        }

    }

    for (int j = m_n-1; j>=0; --j)

    {

        for (int i = 0; i<j; ++i)

        {

            computePriceStep(i, j, K, prices, p_u, p_d, u);

        }

    }

    return prices[0];

}

The first for loop in this member function is responsible for computing the stock price at the last level of the binomial tree. This is done using the property that defines the price of an option at expiration. Therefore, there are two cases that need to be handled, depending on if the option is a call or a put.

The last for loop is the main computation that traverses the binomial tree from the last level to the root node. The step calculation is performed by the computePriceStep member function. The main idea, which you can see by looking at that member function, is to first compute the average (expected) price of the node. This is done by taking the expected value of the known prices that have been previously calculated according to the probabilities p_u and p_d.

After the option prices have been computed in this way, the algorithm will determine the price at the root node. Therefore, the price required is stored in position zero of the prices vector. The last line of this member function returns prices[0] as the desired solution.

Note

The pricing strategy presented in this section works for options that cannot be exercised until the date of expiration. This type of option is commonly known as a European-style option. For American-style options, which can normally be exercised at any time, a slightly different pricing method needs to be used, as shown in the next section.

Pricing American-Style Options

This section presents a slight modification of the binomial method that can be used to price American-style option contracts. An American-style option is defined in such a way that buyers of such options can exercise their rights (i.e., buying or selling the underlying) at any time until expiration. This is in contrast to what is called European-style options, whereby option rights can be exercised only at expiration.

You can use the AmericanBinomialModel class to price American options. Looking at the code, you can see clearly how American options differ from European ones in terms of the option prices. The binomial model determines this by checking the possible exercise price of the option and taking that value into consideration if it is higher than the expected price.

The class interface is defined as follows. The public inheritance from BinomialModel allows you to share the methods defined in that class. The resulting interface is very simple because no additional member variables are necessary. It contains the standard copy constructor, a constructor that forwards the received parameters to the base class, and a destructor.

#include <vector>

#include <cmath>

using vec = std::vector<double>;

class AmericanBinomialModel : public BinomialModel {

    AmericanBinomialModel(const BinomialModel &p);

    ~AmericanBinomialModel();

    AmericanBinomialModel &operator=(const BinomialModel &p);

    AmericanBinomialModel(double T, // expiration time

                  double S,   // stock price

                  double r,   // interest rate

                  double sigma,

                  double q,   // dividend yield

                  int n,      // number of steps

                  bool call

                  );

    virtual void computePriceStep(int i, int j, double K, vec &prices,

                                  double p_u, double p_d, double u);

};

The constructor just needs to forward the received parameters to the base class BinomialModel.

AmericanBinomialModel::AmericanBinomialModel(const BinomialModel &p)

: BinomialModel(p)

{

}

AmericanBinomialModel::~AmericanBinomialModel()

{

}

Because there are no extra member variables, the assignment operator can use the nice trick of calling the operator on the superclass to do the assignment work, as follows:

AmericanBinomialModel &AmericanBinomialModel::operator=(

                 const BinomialModel &p)

{

    BinomialModel::operator=(p);  // no new data members in this class

    return *this;

}

AmericanBinomialModel::AmericanBinomialModel(double T, // expiration time

                      double S,   // stock price

                      double r,   // interest rate

                      double sigma,

                      double q,   // dividend yield

                      int n,      // number of steps

                      bool call)

: BinomialModel(T, S, r, sigma, q, n, call)

{

}

Next, you can see the real change that characterizes American options. The computePriceStep member function overrides the member function in the base class and allows the price of an American option to be calculated.

The first thing to do here is to call the member function from the superclass, so you don’t need to repeat the same code, with potential duplication errors. Then, the function proceeds to calculate the exercise value. This is done by taking the adjusted stock price and subtracting it from the strike price. If the calculated exercise price is higher than the calculated price, then the price is updated with this exercise price. In other words, at each moment, the price of the option has to be the highest of the potential value and the exercise value.

void AmericanBinomialModel::computePriceStep(int i, int j, double K, vec &prices, double p_u, double p_d, double u)

{

    BinomialModel::computePriceStep(i, j, K, prices, p_u, p_d, u);

    // Compute exercise price for American option

    //

    double exercise = K - getStockPrice() * pow(u, 2*i - j);

    if (prices[i] < exercise)

    {

        prices[i] = exercise;

    }

}

Solving the Black-Scholes Model

The previous sections explored discrete methods used to compute the price of options. These methods work by approximating the solution through the use of price trees, where each node represents a discrete step into the solution of the problem.

While the binomial tree method is appropriate in many situations, it is sometimes necessary to use a more rigorous method based on the Black-Scholes partial differential equation (PDE). The model, developed by economists F. Black and M. Scholes in the 1970s, provides a full mathematical description of how option prices evolve over time and with respect to the changes in the underlying prices.

The Black-Scholes model uses a few input parameters that describe the option and the conditions under which prices evolve. The parameters are

Expiration date

Stock price

Stock volatility

Interest rates (paid on short-term cash)

Dividends paid by the underlying stock

Using these parameters, the model provides a partial differential equation that contains the information necessary to determine the price of the option. The result from this model can be summarized in the following PDE:

In this differential equation, the quantities represented are as follows:

V: The price of the desired derivative

t: The time

σ: The volatility of the underlying stock

S: The stock price

r: The interest rate

If you know the previous information about the underlying security, such as prices, interest rates, and previous volatility, the Black-Scholes equation allows you to compute the value of a call or put option based on those assumptions. The solution of this equation can be achieved using several methods, such as simulation techniques and piecewise integration using numerical approximations. The next section presents a simple numerical technique that can be applied to find solutions to the Black-Scholes model.

Numerical Solution of the Model

To solve the Black-Scholes model computationally, it is necessary to apply numerical techniques to solve the associated PDE. It is important to note that there are several methods used to compute this class of equations, with results that depend on the required accuracy, computational effort, and implementation difficulty.

This section explores a simple strategy to solve the Black-Scholes model. The strategy is based on what is called the forward method for the solution of PDEs. The forward method is an extension of Euler’s method for the solution of ODEs, as described in the previous chapter. Unlike Euler’s method, the forward method needs to find a solution for a differential equation that contains more than one variable.

The forward method (also known as the explicit method or forward Euler method) is a numerical technique used to solve partial differential equations (PDEs). It is particularly well suited for solving parabolic PDEs, which involve time-dependent problems, such as the heat equation or diffusion equation.

In a PDE problem, the goal is to find a function g(x,t) that satisfies the partial differential equation with given initial conditions and boundary conditions. The forward method approximates the solution at discrete points in both space and time.

The forward method solves this problem by dividing the domain of the desired equation into smaller, rectangular pieces, which can be easily computed. Once this is completed, the algorithm propagates those values forward, and at each step, a small area dS is considered.

For this method to work, it is necessary to provide a set of initial conditions for the PDE. In the case of options pricing, the natural set of initial conditions is the price at expiration, which is well known for each possible value of the stock. Therefore, the implementation of the forward in fact starts from the expiration date and proceeds backward in time to the desired date.

The C++ solution is implemented in the BlackScholesMethod class. This class provides a simple interface, where the main member function is called solve, and it returns the price at the desired date and under the conditions defined by the given parameters.

class BlackScholesMethod {

public:

BlackScholesMethod(double expiration, double maxPrice, double strike, double intRate);

    BlackScholesMethod(const BlackScholesMethod &p);

    ~BlackScholesMethod();

    BlackScholesMethod &operator=(const BlackScholesMethod &p);

    std::vector<double> solve(double volatility, int nx, int timeSteps);

private:

    double m_expiration;

    double m_maxPrice;

    double m_strike;

    double m_intRate;

};

In the implementation file, which is listed next, you will first find the constructors and assignment operator. These member functions just initialize the private variables, which include

Expiration date, denoted by m_expiration

Maximum price that will be considered by the algorithm, denoted by m_maxPrice

Strike price, denoted by m_strike

Current interest rate, denoted by m_intRate

#include "BlackScholes.hpp"

#include <cmath>

#include <algorithm>

#include <vector>

#include <iostream>

#include <iomanip>

using std::vector;

using std::cout;

using std::endl;

using std::setw;

BlackScholesMethod::BlackScholesMethod(double expiration, double maxPrice,

                                       double strike, double intRate)

: m_expiration(expiration),

m_maxPrice(maxPrice),

m_strike(strike),

m_intRate(intRate)

{

}

BlackScholesMethod::BlackScholesMethod(const BlackScholesMethod &p)

: m_expiration(p.m_expiration),

m_maxPrice(p.m_maxPrice),

m_strike(p.m_strike),

m_intRate(p.m_intRate)

{

}

BlackScholesMethod::~BlackScholesMethod()

{

}

BlackScholesMethod &BlackScholesMethod::operator=(const BlackScholesMethod &p)

{

    if (this != &p)

    {

        m_expiration = p.m_expiration;

        m_maxPrice = p.m_maxPrice;

        m_strike = p.m_strike;

        m_intRate = p.m_intRate;

    }

    return *this;

}

The solve method is the heart of the algorithm. The first part of this member function is responsible for initializing common expressions that are used throughout the algorithm. These expressions are stored in vectors a, b, and c. In mathematical notation, these factors can be presented as

The third for loop is the place where the initial conditions are prepared, by direct calculation of the price at expiration date. The last loop is where the forward algorithm is used. Each step of the loop will compute the contributions for that particular time period, assuming that the period j-1 is known. At the end, the u vector, where the option prices have been stored, is returned to the caller.

vector<double> BlackScholesMethod::solve(double volatility, int nx, int timeSteps)

{

    double dt = m_expiration /(double)timeSteps;

    double dx = m_maxPrice /(double)nx;

    vector<double> a(nx-1);

    vector<double> b(nx-1);

    vector<double> c(nx-1);

    int i;

    for (i = 0; i < nx - 1; i++)

    {

        b[i] = 1.0 - m_intRate * dt - dt * pow(volatility * (i+1), 2);

    }

    for (i = 0; i < nx - 2; i++)

    {

c[i] = 0.5 * dt * pow(volatility * (i+1), 2) + 0.5 * dt * m_intRate * (i+1);

    }

    for (i = 1; i < nx - 1; i++)

    {

a[i] = 0.5 * dt * pow(volatility * (i+1), 2) - 0.5 * dt * m_intRate * (i+1);

    }

    vector<double> u((nx-1)*(timeSteps+1));

    double u0 = 0.0;

    for (i = 0; i < nx - 1; i++)

    {

        u0 += dx;

        u[i+0*(nx-1)] = std::max(u0 - m_strike, 0.0);

    }

    for (int j = 0; j < timeSteps; j++)

    {

        double t = (double)(j) * m_expiration /(double)timeSteps;

        double p = 0.5 * dt * (nx - 1)

                       * (volatility*volatility * (nx-1) + m_intRate)

                       * (m_maxPrice-m_strike * exp(-m_intRate*t ) );

        for (i = 0; i < nx - 1; i++)

        {

            u[i+(j+1)*(nx-1)] = b[i] * u[i+j*(nx-1)];

        }

        for (i = 0; i < nx - 2; i++)

        {

            u[i+(j+1)*(nx-1)] += c[i] * u[i+1+j*(nx-1)];

        }

        for (i = 1; i < nx - 1; i++)

        {

            u[i+(j+1)*(nx-1)] += a[i] * u[i-1+j*(nx-1)];

        }

        u[nx-2+(j+1)*(nx-1)] += p;

    }

    return u;

}

Finally, I present a simple test function that can be used to illustrate the use of the BlackScholesMethod class. This function first initializes some parameters with reasonable values. Then, it creates a new object of type BlackScholesMethod, passing to the constructor some of the previously defined parameters.

The blackScholes object is then used to solve the pricing problem. The result is a vector of prices, one for each of the steps used by the algorithm (in practice, only the last value would be used). Finally, the function prints the result so that you can inspect the convergence of the algorithm.

void test_bsmethod()

{

    auto strike = 5.0;

    auto intRate = 0.03;

    auto sigma = 0.50;

    auto t1 = 1.0;

    auto numSteps = 11;

    auto numDays = 29;

    auto maxPrice = 10.0;

    BlackScholesMethod blackScholes(t1, maxPrice, strike, intRate);

    vector<double> u = blackScholes.solve(sigma, numSteps, numDays);

    double minPrice = .0;

    for (int  i=0; i < numSteps-1; i++)

    {

        double s = ((numSteps-i-2) * minPrice+(i+1)*maxPrice)

                 / (double)(numSteps-1);

        cout << "  " << s << "  "

             << u[i+numDays*(numSteps-1)] << endl;

    }

}

Complete Code

This section presents the complete code for the BlackScholesMethod class. The code depends only on the STL and functions in the standard C++ library. As such, it can serve as a first step toward a complete solution for options valuation processes.

The code is divided into a header file called BlackScholes.hpp and an associated implementation file. These files are presented in Listings 12-1 and 12-2, respectively.

//

//  BlackScholes.hpp

#ifndef BlackScholes_hpp

#define BlackScholes_hpp

#include <vector>

class BlackScholesMethod {

public:

BlackScholesMethod(double expiration, double maxPrice, double strike, double intRate);

    BlackScholesMethod(const BlackScholesMethod &p);

    ~BlackScholesMethod();

    BlackScholesMethod &operator=(const BlackScholesMethod &p);

std::vector<double> solve(double volatility, int nx, int timeSteps);

private:

    double m_expiration;

    double m_maxPrice;

    double m_strike;

    double m_intRate;

};

#endif /* BlackScholes_hpp */

Listing 12-1

Header File for the BlackScholesMethod Class

//

//  BlackScholes.cpp

#include "BlackScholes.hpp"

#include <cmath>

#include <algorithm>

#include <vector>

#include <iostream>

#include <iomanip>

using std::vector;

using std::cout;

using std::endl;

using std::setw;

BlackScholesMethod::BlackScholesMethod(double expiration, double maxPrice,

                                       double strike, double intRate)

: m_expiration(expiration),

m_maxPrice(maxPrice),

m_strike(strike),

m_intRate(intRate)

{

}

BlackScholesMethod::BlackScholesMethod(const BlackScholesMethod &p)

: m_expiration(p.m_expiration),

m_maxPrice(p.m_maxPrice),

m_strike(p.m_strike),

m_intRate(p.m_intRate)

{

}

BlackScholesMethod::~BlackScholesMethod()

{

}

BlackScholesMethod &BlackScholesMethod::operator=(const BlackScholesMethod &p)

{

    if (this != &p)

    {

        m_expiration = p.m_expiration;

        m_maxPrice = p.m_maxPrice;

        m_strike = p.m_strike;

        m_intRate = p.m_intRate;

    }

    return *this;

}

vector<double> BlackScholesMethod::solve(double volatility, int nx, int timeSteps)

{

    double dt = m_expiration /(double)timeSteps;

    double dx = m_maxPrice /(double)nx;

    vector<double> a(nx-1);

    vector<double> b(nx-1);

    vector<double> c(nx-1);

    int i;

    for (i = 0; i < nx - 1; i++)

    {

        b[i] = 1.0 - m_intRate * dt - dt

                   * pow(volatility * (i+1), 2);

    }

    for (i = 0; i < nx - 2; i++)

    {

        c[i] = 0.5 * dt * pow(volatility * (i+1), 2) + 0.5

                   * dt * m_intRate * (i+1);

    }

    for (i = 1; i < nx - 1; i++)

    {

        a[i] = 0.5 * dt * pow(volatility * (i+1), 2) - 0.5

                   * dt * m_intRate * (i+1);

    }

    vector<double> u((nx-1)*(timeSteps+1));

    auto u0 = 0.0;

    for (i = 0; i < nx - 1; i++)

    {

        u0 += dx;

        u[i+0*(nx-1)] = std::max(u0 - m_strike, 0.0);

    }

    for (int j = 0; j < timeSteps; j++)

    {

        double t = (double)(j) * m_expiration /(double)timeSteps;

        double p = 0.5 * dt * (nx - 1)

                 * (volatility*volatility * (nx-1) + m_intRate)

                 * (m_maxPrice-m_strike * exp(-m_intRate*t ) );

        for (i = 0; i < nx - 1; i++)

        {

            u[i+(j+1)*(nx-1)] = b[i] * u[i+j*(nx-1)];

        }

        for (i = 0; i < nx - 2; i++)

        {

            u[i+(j+1)*(nx-1)] += c[i] * u[i+1+j*(nx-1)];

        }

        for (i = 1; i < nx - 1; i++)

        {

            u[i+(j+1)*(nx-1)] += a[i] * u[i-1+j*(nx-1)];

        }

        u[nx-2+(j+1)*(nx-1)] += p;

    }

    return u;

}

int main()

{

    auto strike = 5.0;

    auto intRate = 0.03;

    auto sigma = 0.50;

    auto t1 = 1.0;

    auto numSteps = 11;

    auto numDays = 29;

    auto maxPrice = 10.0;

    BlackScholesMethod blackScholes(t1, maxPrice, strike, intRate);

    vector<double> u = blackScholes.solve(sigma, numSteps, numDays);

    double minPrice = .0;

    for (int  i=0; i < numSteps-1; i++)

    {

        double s = ((numSteps-i-2) * minPrice+(i+1)*maxPrice)

                 / (double)(numSteps-1);

        cout << "  " << s << "  "

             << u[i+numDays*(numSteps-1)] << endl;

    }

    return 0;

}

Listing 12-2

Implementation File for the BlackScholesMethod Class

Conclusion

Options pricing is a very common problem that needs to be solved if you need to trade these types of financial derivatives. Because underlying prices change so frequently, it is very important that option prices be calculated efficiently. C++ is an ideal language for encoding the solution to these pricing problems.

In this chapter, I provided an introduction to the most common strategies for options pricing. The most popular techniques can be divided into lattice models, such as binomial trees, and PDE-based algorithms, where the Black-Scholes model or some close variation is solved through the use of numerical methods for PDEs.

The first sections of this chapter demonstrated the binomial method, with its assumptions and mathematical ideas. You learned how these ideas can be used in C++ and encapsulated into a class. The model was extended to deal with American-style options, where option buys have the ability to exercise the option at any time before the (or at the) expiration date.

You also saw how to represent the options pricing problem in terms of the Black-Scholes model, which uses a PDE that describes the changes in options pricing. This model is solved using a method that discretizes the domain of the function and calculates the result in a large number of small steps.

In the next chapter, you will learn about Monte Carlo methods, another strategy that is commonly used to solve problems in the area of mathematical finance. In particular, Monte Carlo methods can be used to efficiently solve some difficult problems of derivative pricing without needing to directly compute probabilities, as used by the methods discussed in this chapter.

© The Author(s), under exclusive license to APress Media, LLC, part of Springer Nature 2023

C. OliveiraOptions and Derivatives Programming in C++23https://doi.org/10.1007/978-1-4842-9827-5_13

13. Monte Carlo Methods

Carlos Oliveira1  

(1)

Seattle, WA, USA

Among programming techniques used for trading equity markets, Monte Carlo simulation has a special place due to factors such as its wide applicability and easy implementation. These methods can be used to implement strategies for market analysis such as price forecasting, or to validate options trading strategies, for example.

A great advantage of the Monte Carlo methods is the fact that they can be used to study complex events without the need to solve complicated mathematical models and equations. Using the idea of simulation through the use of random numbers, Monte Carlo methods offer the ability to study a large class of events, which would otherwise be difficult to analyze using exact techniques.

This chapter provides an introduction to stochastic methods and how they can be used as part of simulation-based algorithms applied to options pricing. Here are a few of the topics that will be covered in this chapter:

Random number generation: Generating random numbers is a basic step in creating algorithms that exploit stochastic behavior. Monte Carlo methods require the use of effective random number generation routines, which will be discussed in this chapter.

Probability distributions: Monte Carlo algorithms are based on the properties of stochastic events. Many of these events occur according to well-known probability distributions. In C++, it is possible to generate numbers according to many popular probability distributions, as you will learn.

Random walks: A random walk is a stochastic process where a certain quantity can randomly change with equal probability to positive or negative side. This makes random walk very useful for modeling prices in financial markets, as well as for simulating trading strategies.

Stochastic models for options pricing: Another application of random walks is in the determination of option prices. Using a stochastic method for this purpose is useful if you want to avoid the use of a more complex exact or approximate model, such as the algorithms described in the previous chapter.

Introduction to Monte Carlo Methods

A Monte Carlo algorithm is a computational procedure that uses random numbers to simulate and study complex events. It is based on the idea that you can analyze the results of an event by repeating it several times in different ways, with the help of a computer or other technique to generate random numbers.

This idea behind Monte Carlo methods is not new, having been used for as long as probability methods have been studied. For example, a well-known randomized procedure to determine the area of a geometric shape is to throw darts at the figure. After a while, you can count the percentage of darts inside the shape and use that percentage to determine the area.

Despite their simplicity, Monte Carlo methods may be time-consuming, and they require a large number of repetitions (sometimes in the order of millions) to achieve their goals. The recent development of fast computers, however, made it possible to use such methods in an increasing number of situations, making them practical and capable of finding solutions for problems where explicit mathematical analysis is very difficult.

In general, Monte Carlo methods have been used for the solution of mathematical and computational problems where it is difficult to perform direct observations. Algorithms based on Monte Carlo methods use simulation strategies to determine values that normally occur as the result of random events in several areas, including the financial markets. In fact, the application of Monte Carlo to finance methods is widespread. You will find many algorithms used in the analysis of options and derivatives that exploit Monte Carlo techniques, for example:

Options pricing: It is possible to use randomized algorithms to determine the prices of options and other derivatives.

Trade strategy analysis: Monte Carlo methods can be used to test different trade strategies using simulated prices. This type of analysis is invaluable, since it allows you to test trading techniques on a large amount of data that is independent of the existing market observations.

Analysis of bonds and other fixed income investments: Bonds and their derivatives are tied to fluctuations of interest rates over different time horizons. An effective way to study the behavior of bonds is to construct stochastic models and use them to perform an analysis.

Portfolio analysis: Another area where Monte Carlo methods are useful is when studying a portfolio of investments. The stochastic algorithm allows analysts to vary the rate of exposure to diverse economic scenarios and try to determine the best allocation for a portfolio.

In the next few sections, you will first learn the tools necessary to design and implement Monte Carlo algorithms using the C++ language. You will also see examples of how these tools can be used to analyze options and related instruments.

Random Number Generation

The first topic that is addressed is random number generation. True random numbers are not possible to achieve in digital computers, but there are several techniques to create sequences of pseudorandom numbers. These methods have been made available through the standard C++ libraries, as will be covered in this section.

For C++ programmers, the main source of random number generation routines is the <random> header file provided by the standard library. With these functions, you can generate pseudorandom numbers that are well tested and that can be accessed through an easy interface.

The first thing to learn about random number generation in the standard library is the concept of generators. A generator can be viewed as a source of pseudorandom bits, that is, an algorithm that is capable of returning numbers that are uniformly random. The C++ library offers a small number of generators that can be used by programmers. Here are some of the available generators:

Mersenne twister: This is one of the most popular generators. It is based on an algorithm that uses Mersenne prime numbers as the period length of the sequence of pseudorandom numbers. The Mersenne twister algorithm is considered to be one of the best general-purpose generators of random numbers, and it is frequently used in applications.

Linear congruential engine: This engine is based on a traditional algorithm that uses simple addition, multiplication, and module operations to produce numbers that have pseudorandom properties. This generator is indicated when you need fast sequences of random numbers, due to its efficiency. However, the linear congruential algorithm is known to generate numbers that possess some correlation.

Subtract with carry: This is still another algorithm that is used to generate random numbers in the standard library. The algorithm is called lagged Fibonacci, and it uses a numeric sequence that has properties that are similar to the famous Fibonacci sequence.

These generators represent three of the most common ways to generate random numbers. Other techniques for random number generation have also been proposed in the scientific literature. Table 13-1 shows some of the most commonly used algorithms for random number generation.

Table 13-1

Algorithm for Pseudorandom Number Generation

Algorithm

Description

Linear congruential

Traditional method that uses modulo arithmetic.

Inversive congruential

Uses the modular multiplicative inverse to generate new elements in the sequence.

Mersenne twister

Method developed in 1997; uses Mersenne primes to generate random numbers.

WELL generators

Well Equidistributed Long-period Linear, based on the application for operations on a binary field.

XorShift generators

Fast method that uses exclusive or operations to generate new random numbers.

Linear feedback shift

Method that uses a linear function over the existing sequence of values to generate the next random number.

Park-Miller generator

A linear congruential generator that uses multiplicative groups of integers under the modulo operation.

The second part of the random generation library in C++ is the use of engine instantiations. These instantiations can be viewed as a concrete implementation of a generic algorithm. For example, consider the Mersenne twister engine, which is implemented as a template called mersenne_twister_engine. The easiest way to use this engine is to apply an instantiation such as the minstd_rand (minimal standard pseudorandom number) generator. This particular instantiation is defined by the C++ standard as

typedef linear_congruential_engine<

          uint_fast32_t,

          48271,

          0,

          2147483647> minstd_rand;

The linear_congruential_engine is a common random generator engine that is implemented by the standard library. A list of known engine instantiations in the C++ standard library is presented in Table 13-2. You can choose one of these instantiations as a generator for your own algorithm, or you can create a new instantiation.

Table 13-2

A List of Generator Instantiations Available on the Standard Library

Generator Instantiation

Parameters

default_random_engine

Random engine that is provided as a default option by the library implementation.

knuth_b

Defined as typedef shuffle_order_engine <minstd_rand0,256> knuth_b;.

minstd_rand

Minimal standard generator; it is an instantiation of linear_congruential_engine.

minstd_rand0

Similar to the engine described previously, with particular parameters.

mt19937

Mersenne twister generator.

mt19937_64

Mersenne twister generator for 64-bit types.

ranlux24

Uses the subtract-with-carry generator and returns values that use a 24-bit representation.

Note

Random number generators can be freely instantiated in the standard library. However, you should rarely need to define a new instantiation, unless you have good knowledge about how the parameters for each generator work together. A careful study of parameters is usually necessary to create a new generator, since they are based on statistical properties that have been determined after careful analysis made by researchers in the area.

The generators and their instantiations can be thought of as the original source for pseudorandom bits. Once you have defined a source, it is possible to generate random numbers according to a given probability distribution, as you will see in the next section.

Probability Distributions

A probability distribution is a family of functions that defines the parameters for a stochastic process. For example, the simplest distribution of random numbers is the uniform distribution, where each value is generated with equal probability in a given range. A particular case of the uniform distribution is Uniform[0,1], where each number is randomly generated with equal probability in the range between 0 and 1.

There are a small number of probability distributions that occur very frequently in the analysis of natural events. These common distributions, which have been studied in several branches of stochastic analysis, are now available as part of the C++ <random> header in standard library. For examples of two common probability distributions, see Figure 13-1 (which shows the normal distribution with mean 0 and standard deviation 1), Figure 13-2 (which shows the exponential distribution with mean 1), and Figure 13-3 (which shows the chi-squared distribution).

A line graph represents normal distribution versus x. The line follows a bell-shaped trend. It peaks at (0, 0.4) approximately.

Figure 13-1

Probabilities defined by the normal distribution, with mean 0 and standard deviation 1

A line graph represents exponential distribution versus x. The line follows a descending trend from (0, 1) to (4, 0). Data are estimated.

Figure 13-2

Probabilities defined by the exponential distribution, with mean 1

Consider the most common case of generating uniform random integer numbers in a particular range. This can be easily handled in the standard library by using the std::uniform_int_distribution template. This template is capable of creating integer numbers that have uniform distribution as given by the two parameters: the initial and maximum values. Here is an example of how to code a function that returns such random integer numbers:

#include <iostream>

#include <random>

using std::cout;

using std::endl;

std::default_random_engine generator;

int get_uniform_int(int max)

{

    if (max < 1)

    {

        cout  << "invalid parameter max " << max << endl;

        throw std::runtime_error("invalid parameter max");

    }

    std::uniform_int_distribution<int> uint(0,max);

    return uint(generator);

}

A line graph represents chi-squared distribution versus x. The line follows an ascending trend from (0, 0) to (5, 0.065). Data are estimated.

Figure 13-3

Probabilities defined by the chi-squared distribution

The first step is to define a generator to use as the source of random bits. This is done by instantiating an engine (done at the file scope). The std::default_random_engine is the default generator selected by the compiler’s implementation. It should be a reasonable choice, unless you want to be very specific about the generator for your code.

The get_uniform_int function generates a random integer between 0 and max, where max is a parameter passed to the function. The function first checks if the parameter is valid and throws an exception when that is not the case. The function then uses the parameter to create an object of type uniform_int_distribution. This object receives two parameters that define the distribution: the minimum and maximum values. The resulting object is then used to generate the random number itself.

Note

Traditional C and C++ code used to rely on the rand function to generate random integer numbers. This usage is now deprecated because the algorithm used in rand() is known to have weaknesses. In particular, the idea of using the expression (rand() % N) to generate random integer numbers in the range 0 to N-1 has been proved to be unreliable. Even though the numbers seem random enough for most applications, it fails when you try to perform more complex statistical analysis.

The sequence of steps to use the random number generators and distributions is therefore summarized as follows:

Find a suitable random engine and a corresponding generator according to the needs of your application.

Select a generator instantiation based on the random engine you selected previously. If you don’t have any specific requirements, the default_random_engine could be used.

Select a random distribution according to the needs of your application. A common distribution is the uniform, which produces numbers with the same probability in a given range.

Create an object of the type determined by the probability distribution. In the previous example, you used uniform_int_distribution as the object type.

The resulting object can now be called to generate pseudorandom numbers, once the generator object is passed as the single parameter for the call. This makes it possible to use generators of different types or, more commonly, generators that are used for a specific function of a thread.

Using Common Probability Distributions

This section will show a few examples of common probability distributions and how they can be used in C++. As mentioned, random numbers can be generated according to different probability functions. These families of functions are grouped according to the parameters and shape of the distribution.

One of the simplest probability distributions is the Bernoulli distribution. This is a family of probability distributions that model a yes/no scenario, an event that has only two results. The only parameter for this distribution is the probability of the yes result. The simplest example of this type of model is a coin toss, with parameter 0.5, representing a fair probability of heads or tails.

In the next code example, the function coin_toss_experiment returns a vector of Boolean values, representing the result of a set of fair coin tosses.

#include <iostream>

#include <random>

#include <vector>

using std::cout;

using std::endl;

using std::vector;

std::default_random_engine generator;

vector<bool> coin_toss_experiment(int num_experiments)

{

    if (num_experiments < 1)

    {

        cout  << "invalid number of experiments "

              << num_experiments << endl;

        throw std::runtime_error("invalid number of experiments");

    }

    std::bernoulli_distribution bernoulli(0.5);

    vector<bool> results;

    for (int i=0; i<num_experiments; ++i)

    {

        results.push_back(bernoulli(generator));

    }

    return results;

}

In this code, the first step is to use a generator, which in this case is std::default_random_engine allocated in the file scope, so it is available during the lifetime of the application. The coin_toss_experiment function initially checks the validity of the parameter num_experiments, which gives the number of tries in this random experiment.

The function then allocates a new object from the Bernoulli distribution, with parameter 0.5, which indicates that the yes/no event occurs with even probability for each side. The random values are then generated in the loop, where the bernoulli returns Boolean values according to the desired distribution behavior. The values are stored in a vector<bool> container.

Another common distribution that is used to model natural events is the Poisson distribution. This distribution arises commonly when observing the number of events that occur in a period of time, under the assumption that these events are independent. For example, the number of customers arriving at a coffee shop during a given period could be modeled as a Poisson distribution. The mathematical expression used to model the probability distribution of such events is given by

Here, k is the number of observed events, and λ is the parameter that determines the results of the experiment, which can be interpreted as the average number of events occurring in the given time period.

In the C++ standard library, the Poisson distribution is made available through the std::poisson_distribution template. The parameter for this distribution is the mean, usually represented as the mathematical variable λ as in the previous equation.

The following is an example that can be used to analyze the number of customers buying in a particular store in a time period. For instance, financial analysts perform this type of study when they need to study the buying patterns at a particular business. The code defines a function named num_customers_experiment:

#include <iostream>

#include <random>

#include <vector>

using std::cout;

using std::endl;

using std::vector;

vector<int> num_customers_experiment(double mean, int max, int num_of_tries)

{

    std::default_random_engine generator;

    vector<int> occurrences(max, 0);

    std::poisson_distribution<int> poisson(mean);

    for (int i=0; i< num_of_tries; ++i)

    {

        int result = poisson(generator);

        if (result < max) {

            occurrences[result] ++;

        }

    }

    return occurrences;

}

The num_customers_experiment function can generate a sequence of random values based on the Poisson distribution and return a histogram of these values; that is, for each value, it returns the number of times this value was observed.

The algorithm is similar to what you have seen before with the Bernoulli distribution. The first part is used to define the random generator, and it creates an object of type std::poisson_distribution. The parameter passed represents the mean of the distribution.

The for loop in the algorithm is used to build the histogram. At each step, a number is generated according to the Poisson distribution. Then, if the resulting number is less than the parameter max, that value is incremented in the list of occurrences.

The num_customers_experiment function is used in the next code fragment to print the results of the calculation. These numbers have been saved and used to create the chart displayed in Figure 13-4, which shows the observations between 0 and 20 and the corresponding number of observations for 200 trials.

A line graph represents the number of occurrences versus observations. The line fluctuates and peaks the highest at (12, 27). Data are estimated.

Figure 13-4

Histogram of the data returned by function num_customers_experiment

int test_experiment()

{

    auto data = num_customers_experiment(10.5, 20, 200);

    for (int i=0; i<int(data.size()); ++i)

    {

cout << " event " << i << " occurred "  << data[i] << " times" << endl;

    }

}

The next example shows how to generate and use random values drawn from the normal distribution. The normal distribution, also known as Gaussian distribution, is one of the most common probability distributions used to model real-world data. It is employed in data analysis, in areas ranging from drug design to sociology. The normal distribution represents the distribution of values that are naturally measured in populations. For example, the heights of people living in a particular geographical area follow the normal distribution.

The bell-shaped probability graph of the normal distribution is determined by the Gaussian equation, which takes as parameters the mean and the standard deviation of a random variable. The equation is given by

In this equation, μ is the mean value of these numbers, and σ is the standard deviation, which is a measure of the variability of these random values.

In the following code example, you will see how to generate numbers that follow the normal distribution. The get_normal_observations function returns a list of numbers that have been generated according to the normal distribution according to the parameters mean and stdev.

#include <iostream>

#include <random>

#include <vector>

#include <assert.h>

using std::cout;

using std::endl;

using std::vector;

auto get_normal_observations(int n, double mean,

                             double stdev) -> vector<double>

{

    std::default_random_engine generator;

    vector<double> values;

    std::normal_distribution<double> normaldist(mean, stdev);

    for (int i=0; i<n; ++i)

    {

        values.push_back(normaldist(generator));

    }

    return values;

}

The next function, test_normal, can be used to verify the correctness of this code. The idea of this function is to use the generated values so that it can create a histogram of the normal-distributed data. The first step of the algorithm is to call the get_normal_observations function and save the returned data. The next step is to get some information about the received data, such as the minimum and maximum values. This is done using the std::minmax_element function, which returns a pair of iterators pointing to the minimum and maximum values in the given range.

The algorithm creates a vector with elements corresponding to “bins,” that is, smaller ranges where each observation is recorded. The size of each such bin is stored as the variable h. The first loop then determines the number of elements in each such range so that a histogram can be calculated.

The second loop is responsible for printing the results of the histogram. Each value is printed along with the starting point of the corresponding range.

#include <algorithm>

void test_normal()

{

    auto nv = get_normal_observations(1000, 8, 2);

    auto res = std::minmax_element(nv.begin(), nv.end());

    double min = *(res.first);

    double max = *(res.second);

    int N = 100;

    double h = (max - min)/double(N);

    vector<int> values(N, 0);

    for (int i=0; i<int(nv.size()); ++i)

    {

        double v = nv[i];

        int pos = int((v - min) / h);

        if (pos == N) pos--; // avoid the highest element

        values[pos]++;

    }

    for (int i=0; i<N; ++i)

    {

        cout << min + (i*h) << " " << values[i] << endl;

    }

}

The values created in this way have been plotted and are displayed in Figure 13-5. The horizontal axis represents the value of each observation. The vertical axis represents the number of occurrences of each observation.

A scatterplot graph represents the number of occurrences versus observations. The clustered plots form a bell-shaped trend and peak at (8, 33). Data are estimated.

Figure 13-5

Histogram of values observed using the normal distribution with mean 8

Using Random Walks

One of the main applications of stochastic processes in finance is the study of prices under random variations. This random process is called a random walk, since it implies that changes happen at random as time passes. A random walk model can be used to simulate market conditions and investigate the behavior of trade strategies, portfolios, and market participants in general. In this section, you see how to create a simple random walk using some of the facilities provided by C++.

A random walk is a mathematical concept and statistical model used to describe a sequence of steps or movements that are determined by random or stochastic processes. The term “random walk” was first introduced by mathematician Karl Pearson in the late nineteenth century.

In its simplest form, a random walk takes place on a discrete set of points (such as a grid) and consists of a sequence of steps in which the direction and magnitude of each step are determined by random chance. At each step, the walker can move to one of several neighboring locations with equal probability.

The two common types of random walks are as follows:

One-dimensional random walk: The walker moves along a straight line (often represented as the integer number line). At each step, the walker can either move one unit to the left or one unit to the right with equal probability.

Two-dimensional random walk: The walker moves on a two-dimensional grid, such as moving on the surface of a chessboard. At each step, the walker can move to one of four adjacent grid points (up, down, left, or right) with equal probability.

Random walks have applications in various fields, including physics, finance, biology, and computer science. Random walks have been used to model the price movements of financial assets, especially in the Efficient Market Hypothesis (EMH). The random walk hypothesis suggests that asset prices fully reflect all available information, and future price changes are unpredictable.

Creating Random Walks

A random walk can be designed with the use of a few simple rules that determine the price fluctuations. Notice the exact rules used depend on the kind of market that you need to simulate and the exact conditions that need to be replicated. In this example, I use a few computational commands that will simplify the task; the framework can be readily extended to implement more complex scenarios.

The random walk starts at an initial price given as a parameter to the algorithm. At each step, there are three possibilities for the random walk:

A price decrease, which occurs with probability 1/3.

A price increase, also happening with probability 1/3.

The price remains unchanged.

The amount of increase or decrease is given by a parameter called stepSize.

These rules are implemented in the RandomWalkModel class. The class has an interface that exposes two member functions. getWalk returns a vector with a set of steps in the random walk.

//

//  RandomWalk.hpp

#ifndef RandomWalk_hpp

#define RandomWalk_hpp

#include <vector>

// Simple random walk for price simulation

class RandomWalkModel {

public:

    RandomWalkModel(int size, double start, double step);

    RandomWalkModel(const RandomWalkModel &p);

    ~RandomWalkModel();

    RandomWalkModel &operator=(const RandomWalkModel &p);

    std::vector<double> getWalk();

private:

    int random_integer(int max);

    int m_numSteps;      // number of steps

    double m_stepSize;   // size of each step (in percentage)

    double m_startPrice; // starting price

};

#endif /* defined(__FinancialSamples__RandomWalk__) */

The class interface also contains the following member variables:

The number of steps, m_numSteps, determines the number of steps (time) in the random walk.

The initial price is defined by the m_stepSize member variable.

The starting price is defined by the m_startPrice member variable.

These member variables are initialized in the constructor of RandomWalkModel, as shown in this code listing:

//

//  RandomWalk.cpp

#include "RandomWalk.hpp"

#include <cstdlib>

#include <iostream>

#include <random>

using std::vector;

using std::cout;

using std::endl;

std::default_random_engine engine;

RandomWalkModel::RandomWalkModel(int size, double start, double step)

: m_numSteps(size),

  m_stepSize(step),

  m_startPrice(start)

{

}

RandomWalkModel::RandomWalkModel(const RandomWalkModel &p)

: m_numSteps(p.m_numSteps),

  m_stepSize(p.m_stepSize),

  m_startPrice(p.m_startPrice)

{

}

RandomWalkModel &RandomWalkModel::operator=(const RandomWalkModel &p)

{

    if (this != &p)

    {

        m_numSteps = p.m_numSteps;

        m_stepSize = p.m_stepSize;

        m_startPrice = p.m_startPrice;

    }

    return *this;

}

The random numbers needed by this code are generated using the random_integer member function. This function just uses the standard library random number generator std::default_random_engine. It also uses the uniform distribution returning integer values, as provided by the std::uniform_int_distribution template class.

int RandomWalkModel::random_integer(int max)

{

    std::uniform_int_distribution<int> unif(0, max);

    return unif(engine);

}

The random walk sequence is generated by the member function getWalk. The algorithm has a single loop that repeats the price generation according to the m_numSteps variable. Inside the loop, the code selects a random integer between 0 and 2. Depending on the result, the code makes a decision to increase, decrease, or leave the price unchanged. Each price is then added to a vector, and the vector is returned at the end of the function.

std::vector<double> RandomWalkModel::getWalk()

{

    vector<double> walk;

    double prev = m_startPrice;

    for (int i=0; i<m_numSteps; ++i)

    {

        int r = random_integer(3);

        cout << r << endl;

        double val = prev;

        if (r == 0) {

           val += (m_stepSize * val);

        }

        else if (r == 1) {

            val -= (m_stepSize * val);

        }

        walk.push_back(val);

        prev = val;

    }

    return walk;

}

This code can be tested using the test_random_walk function. This function simply creates a RandomWalkModel object with 200 steps, starting at the $30 price and with steps of $0.01.

int test_random_walk()

{

    RandomWalkModel rw(200, 30, 0.01);

    vector<double> walk = rw.getWalk();

    for (int i=0; i<walk.size(); ++i)

    {

        cout << ", " << walk[i];

    }

    cout << endl;

    return 0;

}

The random walk generated by the test_random_walk function was saved, and using that data, I plotted the results, as shown in Figure 13-6. Notice that, although this model is very simple, the results are not very different from what is observed in the market. Using this kind of synthetic data, you can test trading strategies and determine if they are profitable in such randomized scenarios.

A line graph represents price versus time. The line fluctuates and peaks the highest at (120, 34) and (140, 34). Data are estimated.

Figure 13-6

A random walk generated by the RandomWalkModel class with a starting price of $30

Conclusion

In this chapter, I introduced a few examples of Monte Carlo techniques, which can be used to solve complex problems through simulation of random events. These methods are based on the use of pseudorandom values as a tool for the probabilistic analysis of events. Such models also support the simulation of complex mathematical models, including the evolution of stock prices, as well as their options and related derivative instruments.

In the preceding sections, you learned about the building blocks of Monte Carlo methods. First, you learned how to generate pseudorandom numbers using the C++ standard library. The random numbers can also be generated according to a predefined probability distribution. The C++ standard library contains some of the best-known probability distributions, which makes it easy to integrate these features into user applications.

You also saw how to implement a simple random walk model. In a random walk, values change by small increments in either negative or positive directions. The random walk model can be used to analyze several financial instruments, ranging from fixed income instruments to equities and derivatives.

The next chapter will cover additional library functions and classes that are commonly used to analyze and develop solutions for options and derivatives.

© The Author(s), under exclusive license to APress Media, LLC, part of Springer Nature 2023

C. OliveiraOptions and Derivatives Programming in C++23https://doi.org/10.1007/978-1-4842-9827-5_14

14. Backtesting Trading Strategies in C++

Carlos Oliveira1  

(1)

Seattle, WA, USA

Backtesting is a crucial process in the development and evaluation of trading strategies. It involves testing a trading algorithm or investment strategy using historical market data (instead of simulated data) to assess its performance and potential profitability under different conditions.

Implementing backtesting algorithms in C++ provides the advantage of speed, efficiency, and portability, making it an excellent choice for handling large datasets and complex trading strategies. In this chapter, we’ll walk you through a step-by-step guide on how to perform backtesting of such trading strategies using C++.

This chapter provides an overview of the concepts of backtesting. I also present some sample C++ code that can be used to backtest trading strategies.

Obtaining Historical Market Data

Obtaining high-quality and reliable historical market data is a crucial starting point for backtesting your trading strategies effectively. The historical data will serve as the foundation for testing and evaluating the performance of your algorithm under past market conditions. Here are some ideas on how to obtain and prepare historical market data for backtesting in C++.

The first step is to find a reliable data source. There are several options for obtaining historical market data. Some common sources include financial data providers, online platforms, and APIs (Application Programming Interfaces) that offer historical market data for various financial instruments. Examples of popular financial data providers include Yahoo Finance, Alpha Vantage, Quandl, and Intrinio. Make sure to select a reputable data source with accurate and reliable data, since accurate financial data is crucial to achieve reliable results.

Consider data frequency and the granularity of your tests. You need to decide on the data frequency (e.g., hourly or daily) and granularity (how many data points to collect) in a way that best aligns with your trading strategy and objectives. Notice that historical data is available in various timeframes, such as daily, hourly, minute, or even tick data. The choice of granularity depends on the frequency of your trading strategy. For example, day traders may use minute or tick data, while long-term investors might opt for daily or weekly data.

The next issue to consider is the data format(s) used. It is necessary to ensure that the historical market data is provided in a format compatible with existing code. The most popular and easily parsable format is CSV (comma-separated values). In that case, each row of the CSV file represents a data point, with columns for timestamps, asset prices (open, high, low, close), volume, and any additional relevant information.

Data Cleaning and Preprocessing

Data cleaning is an important consideration when testing your trading strategies. Raw historical data may contain missing values, outliers, or other inconsistencies that could affect the accuracy of your backtesting results. Before using the data, it is essential to perform data cleaning and preprocessing. This involves handling missing data, smoothing out irregularities, and removing outliers.

This type of data cleaning preprocessing may happen in a different way. One possibility is running an automated process, that is, a separate algorithm to check for inconsistencies. For example, one can use this method to remove outliers such as stock prices that are higher than expected for a particular stock.

Another way of performing data cleaning is plotting the values and checking if there are data points that have incorrect values (e.g., prices that are too high or too low for the interval you’re checking).

Adjustments for Corporate Actions

When it comes to financial data, it is always important to check for adjustments to the input data. Some financial instruments may undergo corporate actions like stock splits, dividends, or mergers, which can affect historical prices. It is necessary to ensure that the historical data you obtain has been adjusted for such corporate actions to maintain the accuracy and integrity of your backtesting.

An example of this type of adjustment is processing of splits. A split is a corporate active where a single share of stock is split into two or more. It is also possible to reverse-split, when two or more shares are combined into a single one. When this happens, it is necessary to adjust the past data to represent this change in value for a single share of stock.

Another consideration you need to have is in splitting the available data into training and testing sets. To avoid data bias and overfitting, split your historical data into training and testing sets. The training set will be used for developing and optimizing your trading strategy, while the testing set will be used for the final evaluation and validation. The split ratio depends on the size of your dataset and the time period you want to test your strategy on.

Data storage and organization are another important consideration when designing a backtesting strategy. You need to organize and store the historical market data in a structured manner that is easily accessible by your C++ backtesting engine. A simple technique is to create a separate folder or directory for each financial instrument and store the data files accordingly.

Finally, it is important to think about continuous data updates. Financial markets are dynamic, and historical data may require periodic updates to remain relevant. You need to set up a process for regularly updating your historical data to include the latest market information. Depending on your trading frequency, you may choose to update the data daily, weekly, or at other intervals.

How to Design Your Trading Strategy

Designing a trading strategy is a fundamental and creative aspect of backtesting. It involves formulating a set of rules and conditions that guide your algorithm’s decision-making process. The strategy should be well defined, quantifiable, and based on a robust rationale.

A first step is to define your trading objective. Start by clarifying your trading goals. Are you aiming for short-term gains, long-term growth, or risk management? Having a clear objective will guide your strategy’s development and help you choose appropriate performance metrics for evaluation.

Next, it is important to select the asset or market you want to backtest. Start determining the financial instrument you want to trade. It could be stocks, forex pairs, commodities, cryptocurrencies, or any other tradable asset. Different assets may require specific strategies, so consider factors like liquidity, volatility, and market hours.

Next, you need to choose the desired timeframe. Decide on the ideal timeframe for your trading strategy. Will it be a day trading strategy (single day), swing trading (a few days), or position trading (several days or months)? Each timeframe has its characteristics, and your strategy should align with the frequency of your trades.

Technical or fundamental analysis is a factor that can help you make decisions. Therefore, determine whether your strategy will be based on technical analysis, fundamental analysis, or a combination of both. Technical analysis involves using historical price and volume data, chart patterns, and technical indicators to predict future price movements. Fundamental analysis, on the other hand, examines economic, financial, and company-specific factors that could influence asset prices.

Defining the entry and exit rules for your trading strategy should be the next step. In particular, you need to specify the conditions that will trigger trade entries and exits.

Here is an example of entry and exit rules that you may follow:

Entry: Buy when the 10-day moving average crosses above the 50-day moving average and the Relative Strength Index (RSI) is below 30.

Exit: Sell when the asset’s price reaches a specific target or the RSI crosses above 70.

The secret is to use realistic and automatic strategies for entry and exist. The combination of these rules will guarantee that your simulation is as close as possible to real trading conditions.

Along with entry and exit rules, you should also consider risk management strategies. The idea is to implement risk management rules to protect your capital. For example, you can determine the maximum percentage of your portfolio that can be allocated to a single trade, as a way to avoid losses from single stocks. You may also want to set stop-loss levels to limit potential losses.

Backtest and Validation Phase

Once you have defined your trading strategy following the methodology described previously, it is time to proceed to backtesting and validation. Backtesting your trading strategy will encompass using historical market data to validate the trading strategy performance.

Our algorithm will be responsible for analyzing the results and ensuring that the strategy aligns with your objectives. Based on the backtesting results, you can make necessary adjustments to improve its efficiency and robustness.

Be cautious of overfitting your strategy to historical data. While optimizing parameters can improve performance, it’s crucial to strike a balance and avoid curve-fitting the strategy to fit past data too precisely. A strategy that performs well historically may not necessarily translate to success in real-time trading.

Finally, after getting the results of the backtesting, you should document your strategy. This means to maintain detailed documentation of your trading methods, including the rationale behind each rule, the parameters used, and any modifications made during the development process. Proper documentation will be valuable for future reference and analysis.

Once you have developed a strategy and backtested it, it may be useful to also paper trade using a demo account. The purpose is to compare your live results with the results achieved using backtesting. This way, you can also test your strategy in a real-time simulated environment. It will help you gain confidence in its performance and familiarize yourself with executing trades based on the strategy’s signals.

Developing the Backtesting Engine

Developing the backtesting engine involves creating the core functionality that processes historical market data and executes trades based on your trading strategy. The engine will read historical data, apply your strategy’s rules at each time step, and record the trades and positions. In this section, I provide a simple example that you can use to start a backtesting engine.

Read Historical Market Data

In this step, you’ll need to read the historical market data from a CSV file or any other suitable format. The data should include timestamps, asset prices, and any other relevant information. Here’s a sample C++ function to read data from a CSV file assuming the file has columns “Timestamp”, “Open”, “High”, “Low”, and “Close”:

#include <iostream>

#include <fstream>

#include <vector>

#include <string>

#include <sstream>

struct OHLCData {

    std::string timestamp;

    double open;

    double high;

    double low;

    double close;

};

std::vector<OHLCData> readCSV(const std::string& filename) {

    std::vector<OHLCData> data;

    std::ifstream file(filename);

    if (!file.is_open()) {

        std::cerr << "Error opening file: " << filename << std::endl;

        return data;

    }

    std::string line;

    getline(file, line); // Skip the header

    while (getline(file, line)) {

        std::istringstream iss(line);

        OHLCData record;

        getline(iss, record.timestamp, ',');

        iss >> record.open >> record.high >> record.low >> record.close;

        data.push_back(record);

    }

    file.close();

    return data;

}

Create the Backtesting Engine

Next, implement the backtesting engine that processes the historical data and applies your trading strategy’s rules. Here’s a simplified C++ code snippet for the backtesting engine:

#include <iostream>

#include <string>

#include <vector>

#include <fstream>

#include <sstream>

class YourStrategyClass {

private:

    // Store historical OHLC data for reference

    std::vector<OHLCData> historicalData;

public:

    YourStrategyClass() {

        // Initialize your strategy here, set parameters, etc.

    }

    void processData(const OHLCData& data) {

        // Implement your strategy logic here to process the data

        // For this example, we'll store the historical OHLC data in a vector

        historicalData.push_back(data);

    }

    bool shouldEnterTrade() {

        // Implement your logic to determine if the strategy signals to enter a trade

        // Return true if the conditions for entering a trade are met, otherwise return false

        // In this example, we'll check if the current closing price is higher than the previous close * 1.2

        int min_number_of_points = 2;

        if (historicalData.size() >= min_number_of_points ) {

            double previousClose = historicalData[historicalData.size() - 2].close;

            double currentClose = historicalData.back().close;

            double threshold = previousClose * 1.2;

            if (currentClose > threshold) {

                // Entry condition is met, signal to enter a trade

                std::cout << "Entry signal: Buy at timestamp: " << historicalData.back().timestamp << std::endl;

                return true;

            }

        }

        return false;

    }

    bool shouldExitTrade() {

        // Implement your logic to determine if the strategy signals to exit a trade

        // Return true if the conditions for exiting a trade are met, otherwise return false

        // In this example, we'll check if the current closing price is less than the last close * 0.96

        if (historicalData.size() >= 2) {

            double lastClose = historicalData[historicalData.size() - 2].close;

            double currentClose = historicalData.back().close;

            double threshold = lastClose * 0.96;

            if (currentClose < threshold) {

                // Exit condition is met, signal to exit the trade

                std::cout << "Exit signal: Sell at timestamp: " << historicalData.back().timestamp << std::endl;

                return true;

            }

        }

        return false;

    }

};

int main() {

    // Step 1: Read historical market data

    std::vector<OHLCData> historicalData = readCSV("historical_data.csv");

    // Step 2: Initialize your trading strategy

    YourStrategyClass strategy;

    // Step 3: Process historical data and execute trades

    for (size_t i = 0; i < historicalData.size(); ++i) {

        // Extract data for the current timestamp

        const OHLCData& currentData = historicalData[i];

        // Feed data to your strategy class

        strategy.processData(currentData);

        // Check if the strategy signals to enter or exit a trade

        if (strategy.shouldEnterTrade()) {

            // Execute buy order or any other actions

            std::cout << "Buy at timestamp: " << currentData.timestamp << std::endl;

            // ... implement buy order execution code ...

        } else if (strategy.shouldExitTrade()) {

            // Execute sell order or any other actions

            std::cout << "Sell at timestamp: " << currentData.timestamp << std::endl;

            // ... implement sell order execution code ...

        }

    }

    // Step 4: Calculate and display performance metrics

    // ... implement performance metrics calculation ...

    return 0;

}

In the preceding code snippet, you can replace YourStrategyClass with the actual name of your C++ class that represents your trading strategy. Your strategy class should have functions to process data (processData()), check entry and exit signals (shouldEnterTrade() and shouldExitTrade()), and handle trade execution.

Conclusion

Backtesting is a vital process in the development and evaluation of trading strategies, allowing traders and investors to assess the historical performance of their algorithms based on past market data. When using C++ for backtesting, traders can benefit from its speed and efficiency, making it a powerful choice for handling large datasets and complex trading strategies.

In this chapter, I introduced the concepts of backtesting and how it can be modeled using the C++ language. By following these steps and utilizing C++ for backtesting, traders can gain valuable insights into the performance and effectiveness of their trading strategies.

Remember that backtesting is just one component of a comprehensive trading strategy development process. A combination of rigorous testing, risk management, and real-world trading experience is essential for successful trading in financial markets.

The next chapter will cover additional library functions and classes that are commonly used to analyze and develop solutions for options and derivatives trading.

© The Author(s), under exclusive license to APress Media, LLC, part of Springer Nature 2023

C. OliveiraOptions and Derivatives Programming in C++23https://doi.org/10.1007/978-1-4842-9827-5_15

15. Using C++ Libraries for Finance

Carlos Oliveira1  

(1)

Seattle, WA, USA

Writing good financial code is a difficult task, one that cannot be done in isolation. As a software engineer, you frequently need to collaborate with others to achieve your development goals. You also need to use code that has been written by other groups. In particular, developers are constantly using libraries created by other companies or open source projects. Integrating these libraries into your own work is a major step to improve productivity.

In the world of quantitative finance, a number of C++ libraries have been used with great success. This chapter reviews some of these libraries and discusses how they can be integrated into your own applications. Some of the topics covered in this chapter include the following:

Boost introduction: The boost repository provides access to many C++ libraries that are based on templates for higher efficiency. You will learn how to install and use boost, as well as integrate particular libraries in the repository to your own applications.

Boost odeint: The odeint library is a well-tested and efficient set of algorithms for the solution of ordinary differential equations (ODEs). You will learn about the different algorithms contained in odeint and the different situations in which they can be employed.

QuantLib: The QuantLib library has been designed as a repository for quantitative algorithms and assorted utilities for financial applications. Many parts of this code can be used to simplify the process of analyzing options and derivatives. You will learn how to use this library and see a few of the most commonly used classes and algorithms that are available in the QuantLib repository.

Boost Libraries

In the last few years, the boost project has become well known for providing high-quality libraries for C++ applications. As a result, the boost project is now the de facto repository for extensions to the STL. In fact, many of the libraries that started as part of the boost repository have been incorporated to the C++ standard, including, for example, std::shared_ptr and std::unique_ptr. A few of the developers working on boost libraries have also become part of the standard C++ committee.

The boost project focuses on using the modern features of the C++ language, including, but not exclusively, the employment of templates for the implementation of high-performance algorithms. Many of the libraries included in boost provide template-based interfaces that make the resulting system much more flexible. For example, different algorithms can be specialized at the template level, so that you can combine different algorithms through the use of templates, when deciding on the optimal techniques to solve a specific problem. This is a much more adaptable strategy, rather than relying on decisions made by library designers.

Note that boost is not a finance library. Instead, it provides a large number of features that are packaged in a few separate libraries. However, many of the components have direct use in the implementation of financial applications. Its components can be used to perform and simplify several tasks, such as the following:

Solving ODEs: Ordinary differential equations appear frequently in the solution of numerical problems in the area of finance. As you have seen, to solve some options analysis models, it is necessary to efficiently compute the value of ODEs. The odeint library gives you access to such functionality, as you see in the next section.

uBLAS: The Basic Linear Algebra Subprograms library provides a C++ interface to an advanced linear algebra library. uBLAS can be used to support more complex matrix-related code, as well as the solution of systems of equations.

Multi-array: Many applications require the use of multidimensional arrays when working in areas such as 3D animation, weather predictions, and so on. The multi-array library provides an easy interface for the creation and manipulation of arrays that can be indexed using multiple indices.

Managing file and directories: The <filesystem> header file contains a set of templates that can be used to manage files and directories. It handles different operating systems so that you don’t need to rely on system-specific libraries for common file-based operations.

Note

The filesystem library has become part of the C++ standard library in the C++17 version. Previously, filesystem was part of the boost library, which was needed to gain access to this functionality. However, you can still access this library using boost, which makes it portable to code written for earlier compilers.

The boost repository contains a large set of useful libraries for C++ development, including the ones listed previously. In its current version, there are 136 libraries that cover all types of tasks needed in modern programing. Table 15-1 shows a list of commonly used libraries contained in the boost project repository, including a quick explanation of their usage.

Table 15-1

List of Commonly Used Boost Libraries

Library

Description

odeint

Implements algorithms to solve ordinary differential equations (ODEs).

filesystem

A set of classes to manipulate files and directories in an OS-independent way.

Multi-array

Provides arrays with multiple dimensions; useful for scientific code.

MPI

Implements the Message Passing Interface, a standard for parallel processing.

Math

A set of mathematical functions not included in the standard library.

Graph

A library that extends the STL and provides containers and algorithms to handle graphs.

Functional

Provides templates that simplify functional programming techniques.

Algorithm

A set of generic algorithms that extends the algorithm header in the STL.

uBLAS

A modern C++ implementation of BLAS (Basic Linear Algebra Subprograms).

Variant

A container that safely stores a union container, capable of storing different data types.

Sort

Implements several sorting strategies using templates for high performance.

Regex

Provides support for regular expressions in C++.

Python

A set of templates and classes that allows interaction between Python and C++ code.

Installing Boost

The first step in using the boost libraries is to install them on your machine. Being an open source repository, boost packages are made available through the Web and mirrored in several websites. The canonical website for the repository is www.boost.org, where you can find instructions for installing boost in several architectures and operating systems.

The most common way to install boost is to download the compressed file containing the headers and source files. Once the files are uncompressed, you can use the main installation script that is provided, bootstrap.sh, to build and install the software on the desired path in the local disk.

Another way to install boost libraries is to use third-party installers or package managers. For example, if you use Linux, it is possible to install boost as a package using the local package manager, such as dpkg on Debian systems. On Windows systems, you can also install Cygwin, which contains a package manager with several common C++ programming packages, including the boost libraries.

Installing from source is also easy. You just need to unzip the source files into a location and use that directory as the include path for the compilation process. An advantage of boost is that most of the libraries are implemented as header files (this is also true for most of the STL). Therefore, there is no need for any compilation. A few libraries, however, require a compilation step that can be performed using the bootstrap script. You will need the build step if you need to use one of the following libraries:

Boost.Filesystem

Boost.IOStreams

Boost.ProgramOptions

Boost.Python

Boost.Regex

Boost.Serialization

Boost.Signals

Boost.Thread

Boost.Wave

Boost libraries are built using a C++ build system called bjam. The build script will try to find bjam in your machine or build it. You can also download bjam from its binary distribution located in boost.org/build.

In the next few sections, you will see how to use a few libraries available from boost. First, you will see how to solve ordinary differential equations with the odeint library.

Solving ODEs with Boost

In the previous chapter, you saw how ordinary differential equations (ODEs) can be implemented directly using C++ code. Due to how options are defined and represented, ODE models arise naturally in the design of financial algorithms. As a result, being able to quickly implement such methods is a great advantage for the quantitative software developer. Moreover, it is much easier to reuse an ODE implementation that has already been reviewed and thoroughly tested, especially considering that numerical errors are hard to catch in many cases.

One of the components of the boost repository, the odeint library, deals specifically with ODEs. With odeint, you can more easily create code to integrate ODEs, choosing from a number of different algorithmic strategies. Figure 15-1 shows a screenshot of the current web page for the odeint website, where its repository is maintained.

A screenshot of the home page of the Odeint website. It consists of descriptive paragraphs. A list of options such as home, performance, examples, highlights, downloads, resources, doc, and feedback are listed on the left.

Figure 15-1

Website of the odeint library, where you can download its latest version

Table 15-2 presents a quick list of the integration techniques available when using odeint. Some of these techniques have been discussed in the previous chapter. Others are variations of the best-known algorithms and can provide performance advantages for use in particular applications.

Table 15-2

List of Integration Techniques Available When Using odeint

Class Name

Description

Euler

Original Euler’s algorithm to solve ODEs.

runge_kutta4

Uses the Runge-Kutta method, with fourth-order approximation.

runge_kutta_cash_karp54

Runge-Kutta method.

runge_kutta_fehlberg78

Variation of Runge-Kutta that uses the Fehlberg algorithm.

adams_moulton

A multistep algorithm for solving ODEs.

dense_output_runge_kutta

An implementation of Runge-Kutta that uses dense output.

bulirsch_stoer

Based on the Bulirsch-Stoer algorithm, provides higher accuracy in the solution of complex ODEs.

implicit_euler

A variation of Euler’s algorithm in which the equation is given in implicit form and requires the use of the associated Jacobian.

The algorithms made available in the odeint library are implemented as separate template classes. Each class corresponds to an algorithm or algorithmic concept. The odeint library contains a set of integration methods that can be parameterized using the provided templates. These templates make it possible to use different strategies through the combination of the given algorithms and concepts.

One of the basic types of strategies classes available in odeint is a stepper. A stepper is used to navigate through the solution space of the given ODE. This is an important concept because ODEs are solved interactively, and the step size and direction determine how a particular solution strategy will behave. Depending on the type of stepping strategy used, the resulting algorithm can perform a calculation that is faster or more accurate. Here are the known stepper types provided by odeint:

runge_kutta4

euler

runge_kutta_cash_karp54

runge_kutta_dopri5

runge_kutta_fehlberg78

modified_midpoint

rosenbrock4

Solving a Simple ODE

In this section, you will see how to use the concepts described previously to solve a simple ODE in the standard form given by

Here, y is a function of x, y' is the first derivative of y, and f(x, y) is a general equation that may depend both on x and y.

To use odeint, the first step is to include the main header file containing this library, with

#include <boost/numeric/odeint.hpp>

To solve any ODE, you need first to determine the f(x, y) part of the system, that is, the right side of the ODE equation. In this example, you will solve for the simple equation

This is done in the following code fragment:

#include "boosttest.hpp"

#include <iostream>

#include <boost/array.hpp>

#include <boost/numeric/odeint.hpp>

//

// This is the equation at the right side of the ODE   y' = f(x,y)

// It is evaluated in the inner steps of the algorithm.

//

auto right_side_equation(double y, double &dydx, double x) -> void

{

   dydx = 3.0/(2.5*x*x) + y/(1.5*x);

}

An optional feature of the odeint algorithm is the use of an observer. The observer is a function that can be used to inspect each step of the algorithm. Using this information, you can record the progression of the solution, or you can perform more complex analysis if necessary. In this example, the observer simply prints the output, which will later be used to plot the convergence of the solution.

// This function simply prints the current value of the interactive

// solution steps.

void write_cout( const double &x , const double t )

{

   cout << t << '\t' << x << endl;

}

Next, you need to define the stepper algorithm. In this case, the runge_kutta_dopri5, a basic stepper based on the Runge-Kutta method, was selected. This can be done with a simple typedef to define the stepper_type.

// A stepper based on Runge-Kutta algorithm.

// The state_type use is 'double'

typedef runge_kutta_dopri5<double> stepper_type;

Finally, the main function is used to integrate the ODE under the given initial conditions. The task is performed by the integrate_adaptive function, which takes as parameters the stepper, the ODE defining equation, state and step parameters, and a function that prints the intermediate results.

// This solves the ODE described earlier with initial condition x(1) = 0.

//

int main()

{

   double x = 0.0;

   auto n = integrate_adaptive(

make_controlled(1E-12, 1E-12, stepper_type()),  // instantiate the stepper

         right_side_equation,            // equation

         x,                              // initial state

         1.0 , 10.0 , 0.1 ,              // start x, end x, and step size

         write_cout );

   cout << " process completed after "  << n << " steps \n";

   return 0;

}

I ran this code and used the output of the observer function to plot the convergence of the results found by the ODE solver. The plot, displayed in Figure 15-2, shows how solution values change as you move from 1.0 to 10.0 in the solution space.

A line graph of the O D E value versus step value. The line follows an increasing trend from (1, 0) to (10, 3.25). Data are estimated.

Figure 15-2

Results of the integrate_adaptive function from the odeint library

Creating Histograms with Boost

Another useful application for boost is the creation of support code such as histograms. A histogram is a useful chart in financial applications that shows the frequency of each particular value in a time sequence. This can be applied, for example, to prices of underlying assets for an options analysis package.

The boost library supports the use of histogram with the histogram.hpp template header file and its declared classes and functions. The histogram class is the main data type provided by boost for this purpose. Let’s consider a sample application of this class:

#include <boost/histogram.hpp>

#include <boost/histogram/ostream.hpp>

#include <cassert>

#include <iostream>

#include <sstream>

#include <string>

int main() {

  using namespace boost::histogram;

  std::ostringstream os;

  auto h1 = make_histogram(axis::regular<>(5, -1.0, 1.0, "axis 1"));

  h1.at(0) = 2;

  h1.at(1) = 4;

  h1.at(2) = 3;

  h1.at(4) = 1;

  // 1D histograms are rendered as an ASCII drawing

  std::cout << h1;

  return 0;

}

In this sample application, the boost/histogram header file is imported to provide the required class and template definitions. The use of this class occurs on function main. The make_histogram function is useful to instantiate a histogram class using default value along with the passed parameters.

The parameters specified in the example determine that the axis for the histogram is regular, with five partitions (bins), starting on value -1.0 and extending to value 1.0. Then, a few values are added to some of the bins maintained by the histogram class.

The output of this sample code can be seen when the main function is executed, as shown in Figure 15-3.

A screenshot of the C plus plus program output displays a histogram. The horizontal chart consists of a list of axes on the left. The horizontal double broken lines represent the trend.

Figure 15-3

Output from histogram example

The QuantLib Library

The second example of a library that is used in quantitative finance and options analysis is the QuantLib library. QuantLib is a well-established repository of quantitative code for C++. The library has been tested and used by many developers, which means that you can take advantage of the hard work that went into creating and testing the algorithms.

Being an open source project, QuantLib is free and can be used by anyone by just downloading and building the source code. The project also accepts contributed code, which means that many people can fix bugs and participate in the improvement of the library.

QuantLib contains a wide assortment of classes that simplify certain tasks that are necessary in quantitative algorithms for finance. A few areas covered by QuantLib are the following:

Date handling: Many algorithms for options and derivatives analysis are based on dates. Therefore, accurate information about trading dates, holidays, and other calendar-specific events is very important for the correct results of such algorithms. QuantLib provides a number of classes that encapsulate the concepts needed for data handling in financial applications.

Design patterns: The QuantLib library puts a lot of effort in following well-established design patterns. Most algorithms use design patterns that make them easier to understand and to maintain. For this reason, QuantLib has a rich implementation of common design patterns, including Singleton, Observer, Composite, and others.

Monte Carlo methods: A few of the classes provided by QuantLib are used to simplify the implementation of Monte Carlo methods. These classes make it easier to create, for example, random paths for financial instruments, as well as similar models based on Brownian motion.

Pricing engines: Another area that is covered by QuantLib is the implementation of efficient pricing engines for options and derivatives. The library provides several techniques for options pricing, which are carefully packaged into C++ classes. These pricing engines include barrier option engines, Asian option engines, basket option engines, and vanilla option engines.

Optimizers: Another utility that is frequently employed in financial applications is an optimization engine. The QuantLib library contains a few classes dedicated to some common optimization strategies. Using such optimization algorithms, it is possible to quickly solve complex problems where the objective is to find the minimum or the maximum of a given function.

In the remaining of this section, you will see a few examples using classes from QuantLib. You will learn how to use some of the main classes available in the library and integrate them to your applications.

Note

On a macOS computer, you can easily install QuantLib using the brew package manager with the following command:

brew install quantlib

Handling Dates

One of the most common tasks in financial algorithms is handling dates correctly. You saw in Chapter 3 that there are several ways to store and transform values stored as dates. The QuantLib library tries to simplify some of these tasks with the introduction of carefully designed date and time classes.

Managing holidays is one of the most difficult problems when using dates in financial applications. Since the number of trading days constitutes part of the calculation, when computing the price of an option, it becomes very important to have precise representations of date intervals, considering which of those days are trading days.

First, let’s consider how to use the Date class provided by QuantLib, along with some of the basic operations defined on that class. The basic way to construct an object of type Date is to pass the desired date in the day-month-year format. Here is an example:

Date date1(10, Month::April, 2010);

This would create a date representing the 10th day of April 2010. Now, using a date created in this way, it is possible to perform operations such as addition or subtraction using the operators that have been overloaded by QuantLib.

void testDates()

{

   Date date(10, Month::April, 2010);

   cout << "original date: " << date << endl;

   date += 2 * Days;

   cout << "after 2 days: " << date << endl;

   date += 3 * Months;

   cout << "after 3 months: " << date << endl;

}

In this code, the operators are used to add a period of two days and three months, respectively, to the original date. The Days and Months identifiers are simple data types that concisely represent a time period and can be used to simplify the handling of intervals.

Another simple operation on dates is incrementing and decrementing. This allows you to quickly find the next or the previous day in a sequence, without needing to check if these dates occur in different months or years. The following code shows an example of how this works:

void nextAndPreviousDay()

{

   Date date(28, Month::February, 2010);

   cout << "original date: " << date << endl;

   date++;

   cout << "next day: " << date << endl;

   date--;

   cout << "previous day: " << date << endl;

}

Additional tools are provided to answer common questions related to dates. For example, member functions of the Date class are used to determine if a particular date occurs in a leap year, if it occurs at the end of the month, or if the date is a weekday. These are exemplified by the code in the following section.

Working with Calendars

Another aspect of dates that causes a lot of confusion is handling local holidays. Each country has nontrading days that are determined by holidays, which also change according to the year in which they occur. To handle these issues, QuantLib provides a set of Calendar objects. These calendars are localized and can be used to determine if a particular date is a holiday.

The following example shows how to use the Calendar class in a typical C++ application:

auto useCalendar() -> void

{

   Calendar cal = UnitedStates(UnitedStates::NYSE);

   cout << " list of holidays " << endl;

   for (auto date : Calendar::holidayList(cal, Date(1, Month::Jan, 2010),

                                               Date(1, Month::Jan, 2012)))

   {

      cout << " " << date;

   }

   cout << " is Jan 1 2010 a business day?  "

        << cal.isBusinessDay(Date(1, Month::Jan, 2010)) << endl;

   cout << " is Jan 1 2010 a holiday?  "

        << cal.isHoliday(Date(1, Month::Jan, 2010))     << endl;

   cout << " is Jan 1 2010 end of month?  "

        << cal.isEndOfMonth(Date(1, Month::Jan, 2010))  << endl;

}

The first line of the useCalendar function shows how to create a new calendar for a particular region. In this case, the calendar corresponds to the United States and in particular to the New York Stock Exchange.

With this calendar loaded, it is possible to answer a number of questions about dates in the United States. For example, the next few lines show how to list all holidays with the holidayList function. The function receives as arguments the calendar and the desired start and end date. The result is a container with all the holidays for the given period.

The next few lines show how to use QuantLib Calendar object to answer a few common questions related to the day of the week and the month. The first call is to isBusinessDay, and it returns true if the given date occurs in a business day (usually Monday to Friday in most markets). The second member function is isHoliday, which returns true only if the given date is a holiday.

Finally, you can see the member function isEndOfMonth example. This function returns true if the given date occurs at the end of a month, which may be an important date in some kinds of financial contracts.

Another interesting feature of the Calendar class is that you can create and manage your own calendars. This is necessary when creating code for countries that are not already covered by the library, or when you’re working on particular institutions or markets that use a distinct calendar.

The main functions to manage calendar holidays are addHoliday and removeHoliday. With these functions, you can create calendars that are specific to your needs. The following example code shows how to use them:

Calendar createNewCalendar()

{

   Calendar newCal = UnitedStates(UnitedStates::NYSE);

   // Remove winter holiday

   newCal.removeHoliday(Date(25, Month::December, 2016));

   // Add international workers' day

   newCal.addHoliday   (Date(1,  Month::May, 2016));

   cout << " list of holidays " << endl;

   for (auto date : Calendar::holidayList(newCa l, Date(1,  Month::Jan, 2016),

                                                  Date(31, Month::Dec, 2016)))

   {

      cout << " " << date;

   }

   return newCal;

}

This function starts with the creation of a new calendar object based on the US calendar, more specifically using the NYSE list of holidays. The function then proceeds to modify the original calendar, adding a common holiday and adding another so the number of holidays remains the same. The code also prints the list of holidays for the year 2016 to the standard output. Finally, the createNewCalendar function returns the newly created calendar as the result.

Another important feature of the Calendar class provided by QuantLib is the ability to determine the number of trading days between two dates. This is done using the businessDaysBetween member function, which returns the number of business days in a particular interval given to the function. A simple example can demonstrate how this function works:

int getNumberOfDays(Date d1, Date d2)

{

   Calendar usCal = UnitedStates(UnitedStates::NYSE);

   int nDays = usCal.businessDaysBetween(d1, d2);

   cout << " the interval size is "  << nDays << endl;

   return nDays;

}

In the beginning, the getNumberOfDays function creates a calendar using the US locale. The next step is to determine the number of business days between two given dates. Then, the function prints the value of this difference and returns that value as the final result.

Computing Solutions for Black-Scholes Equations

The next example of QuantLib is directly related to the problem of pricing options. The main formula for pricing options is derived from the Black-Scholes differential equations. This makes it really important to have a library that can quickly solve Black-Scholes models, at least as an initial step for further analysis.

QuantLib provides classes that are specifically designed to solve Black-Scholes models. Unlike other ODE and PDE packages that can be used to solve general differential equations (as seen in the previous section on boost), the QuantLib classes target efficient techniques to solve a single model in particular. This results in a very specialized algorithm that can be relied on for the efficient solution of Black-Scholes models.

To benefit from options models used by QuantLib, you need to instantiate two classes:

A class representing the option and the associated payoff: QuantLib provides a set of classes for this purpose. An example of such a class is PlainVanillaPayoff, which represents a common (vanilla) option and its associated payoff.

A class representing the pricing method: This class encapsulates the algorithm that is used to compute the option price. This example is interested in the class representing the Black-Scholes algorithm, which is named the BlackScholes calculator.

These classes are exemplified in the following sample code, which includes a function that performs the computation and an associated test function.

First, you need to create a simple storage area, where the necessary information for the algorithm is stored. The BlackScholesParameters structure is used for this purpose. The structure contains the following fields:

The spot price for the underlying instrument

The strike price for the desired option

The current interest rate

The forward interest rate

The volatility of the underlying instrument

The structure can be represented in the sample C++ code as

struct BlackScholesParameters {

   double S0;

   double K;

   double rd;

   double rf;

   double tau;

   double vol;

};

Based on this information, it is possible to describe the use of Black-Scholes pricing method using a C++ function. The function, called callBlackScholes, receives as a parameter a single reference to a structure of type BlackScholesParameters.

void callBlackScholes(BlackScholesParameters  &bp)

{

   // Create a vanilla option (standard option type)

   boost::shared_ptr<PlainVanillaPayoff>

      vanillaPut(new QuantLib::PlainVanillaPayoff(Option::Put,bp.K));

   // Compute discount rates

   double cur_disc = std::exp(-bp.rd  * bp.tau);  // current discount rate

   double for_disc = std::exp(-bp.rf  * bp.tau);  // forward  discount rate

   double stdev    = bp.vol * std::sqrt(bp.tau);  // standard deviation

BlackScholesCalculator putPricer(vanillaPut, bp.S0, for_disc, stdev, cur_disc);

   // Print option Greeks

   cout << "value:" << putPricer.value() << endl;

   cout << "delta:" << putPricer.delta() << endl;

   cout << "gamma:" << putPricer.gamma() << endl;

   cout << "vega:"  << putPricer.vega(bp.tau)  << endl;

   cout << "theta:" << putPricer.theta(bp.tau) << endl;

   cout << "delta Fwd:" << putPricer.deltaForward() << endl;

   cout << "gamma Fwd:" << putPricer.gammaForward() << endl;

}

This code works in the following way. The first instruction is necessary to create a new object describing the required option. This is done with the instantiation of an object of class PlainVanillaPayoff, which indicates that the new option is of plain vanilla type (i.e., it is a standard option). The arguments passed are the type of option (either a call or a put) and the strike. These two parameters determine the type of option that you’re handling, independent of the current characteristics of the market. The object of type PlainVanillaPayoff is stored in a shared_ptr, which automatically manages the lifetime of the object, cleaning up the pointer at the end of the scope of the local variable.

The next part of the callBlackScholes function initializes some of the parameters necessary to use the options pricer. The parameters include the current and the forward discount rate, which are computed from the given interest rate using an exponential transformation. Another important parameter is the standard deviation, which measures the volatility of the underlying instrument.

Once the parameters for the options pricing model are available, you can instantiate the BlackScholesCalculator class, passing as parameters the object that describes the option, the current price, and the other parameters discussed previously.

Using the object of type BlackScholesCalculator, you can retrieve important information about the option price. The most important information is clearly the value of option at a particular date, returned by the member function value. The option Greeks also provide key information that can be used to make decisions about the option. The Greeks calculated by the BlackScholesCalculator include the following:

The delta: Represents the marginal change in value with respect to the price of the underlying

The gamma: Represents the marginal change in delta with respect to the price of the underlying

The vega: Represents the marginal change in value with respect to the change in volatility

The theta: Represents the marginal change in value with respect to the change in remaining time

You can test this code using a function that uses a few common values for each of the parameters and calls the callBlackScholes function. Here is an example of how this can be done:

void testBlackScholes()

{

   BlackScholesParameters bp;

   bp.S0 = 95.0;     // current price

   bp.K  = 100.0;    // strike

   bp.rd = 0.026;    // current rate of return

   bp.rf = 0.017;    // forward rate of return

   bp.tau= 0.62;     // tau (time greek)

   bp.vol= 0.193;    // volatility

   callBlackScholes(bp);

}

Creating a C++ Interface

Based on the previous functions, it is easy to create a generic class that encapsulates a vanilla Black-Scholes pricing strategy. I called this class BlackScholesPricer, and it presents a simple interface that can be called without external references to QuantLib.

The class declaration contains a set of parameters that will be used in the constructor, as shown in the next code listing:

class BlackScholesPricer {

public:

BlackScholesPricer(bool call, double price, double strike, double tau, double r, double fr, double vol);

   BlackScholesPricer(const BlackScholesPricer &p);

   ~BlackScholesPricer();

   BlackScholesPricer &operator=(const BlackScholesPricer &p);

   double value();

   double delta();

   double gamma();

   double theta();

   double vega();

private:

   double m_price;

   double m_strike;

   double m_tau;

   double m_rate;

   double m_frate;

   double m_vol;

   double m_isCall;

   boost::shared_ptr<QuantLib::BlackScholesCalculator> m_calc;

};

The constructor for BlackScholesPricer is responsible for initializing all the parameters with the passed arguments. Inside the constructor, you can see the code that initializes the payoff class. The option payoff can be a put or a call, depending on the value of the first parameter.

Later, you will see these parameters being used to create a new BlackScholesCalculator object. This object is stored in a shared pointer so that it can be used to answer questions about the model.

BlackScholesPricer::BlackScholesPricer(bool call, double price, double strike, double tau, double r, double fr, double vol)

:m_price(price),

m_strike(strike),

m_tau(tau),

m_rate(r),

m_frate(fr),

m_vol(vol),

m_isCall(call)

{

   boost::shared_ptr<QuantLib::PlainVanillaPayoff>

      m_option (new QuantLib::PlainVanillaPayoff(

call ? QuantLib::Option::Call : QuantLib::Option::Put, strike));

   // Compute discount rates

   double cur_disc = std::exp(-m_rate  * m_tau);  // current discount rate

   double for_disc = std::exp(-m_frate * m_tau);  // forward  discount rate

   double stdev    = m_vol * std::sqrt(m_tau);    // standard deviation

m_calc.reset(new QuantLib::BlackScholesCalculator(m_option, m_price, for_disc, stdev, cur_disc));

}

BlackScholesPricer::BlackScholesPricer(const BlackScholesPricer &p)

:m_price(p.m_price),

m_strike(p.m_strike),

m_tau(p.m_tau),

m_rate(p.m_rate),

m_frate(p.m_frate),

m_vol(p.m_vol),

m_isCall(p.m_isCall),

m_calc(p.m_calc)

{}

BlackScholesPricer::~BlackScholesPricer() {}

BlackScholesPricer &BlackScholesPricer::operator=(const BlackScholesPricer &p)

{

   if (this != &p)

   {

      m_price = p.m_price;

      m_strike = p.m_strike;

      m_tau = p.m_tau;

      m_rate = p.m_rate;

      m_frate = p.m_frate;

      m_vol = p.m_vol;

      m_isCall = p.m_isCall;

      m_calc = p.m_calc;

   }

   return *this;

}

Using these definitions, the following member functions can be used to provide access to the results of the pricing algorithm. They rely on the m_calc member variable, which already contains this stored information.

double BlackScholesPricer::value() { return m_calc->value(); }

double BlackScholesPricer::delta() { return m_calc->delta(); }

double BlackScholesPricer::gamma() { return m_calc->gamma(); }

double BlackScholesPricer::theta() { return m_calc->theta(m_tau); }

double BlackScholesPricer::vega()  { return m_calc->vega(m_tau);  }

Complete Code

Listing 15-1 shows the BlackScholesPricer class. It shows an example of how to create an interface for the Black-Scholes component in QuantLib.

#include <ql/quantlib.hpp>

#include <ql/pricingengines/blackcalculator.hpp>

//

// The BlackScholesPricer class provides an interface to the QuantLib

// pricer component

//

class BlackScholesPricer {

public:

BlackScholesPricer(bool call, double price, double strike, double tau, double r, double fr, double vol);

   BlackScholesPricer(const BlackScholesPricer &p);

   ~BlackScholesPricer();

   BlackScholesPricer &operator=(const BlackScholesPricer &p);

   double value();

   double delta();

   double gamma();

   double theta();

   double vega();

private:

   double m_price;

   double m_strike;

   double m_tau;

   double m_rate;

   double m_frate;

   double m_vol;

   double m_isCall;

   boost::shared_ptr<QuantLib::BlackScholesCalculator> m_calc;

};

BlackScholesPricer::BlackScholesPricer(bool call, double price, double strike, double tau, double r, double fr, double vol)

:m_price(price),

m_strike(strike),

m_tau(tau),

m_rate(r),

m_frate(fr),

m_vol(vol),

m_isCall(call)

{

   boost::shared_ptr<QuantLib::PlainVanillaPayoff>

m_option (new QuantLib::PlainVanillaPayoff(call ? QuantLib::Option::Call : QuantLib::Option::Put, strike));

   // Compute discount rates

   double cur_disc = std::exp(-m_rate  * m_tau);  // current discount rate

   double for_disc = std::exp(-m_frate * m_tau);  // forward discount rate

   double stdev    = m_vol * std::sqrt(m_tau);    // standard deviation

m_calc.reset(new QuantLib::BlackScholesCalculator(m_option, m_price, for_disc, stdev, cur_disc));

}

BlackScholesPricer::BlackScholesPricer(const BlackScholesPricer &p)

:m_price(p.m_price),

m_strike(p.m_strike),

m_tau(p.m_tau),

m_rate(p.m_rate),

m_frate(p.m_frate),

m_vol(p.m_vol),

m_isCall(p.m_isCall),

m_calc(p.m_calc)

{}

BlackScholesPricer::~BlackScholesPricer() {}

BlackScholesPricer &BlackScholesPricer::operator=(const BlackScholesPricer &p)

{

   if (this != &p)

   {

      m_price = p.m_price;

      m_strike = p.m_strike;

      m_tau = p.m_tau;

      m_rate = p.m_rate;

      m_frate = p.m_frate;

      m_vol = p.m_vol;

      m_isCall = p.m_isCall;

      m_calc = p.m_calc;

   }

   return *this;

}

double BlackScholesPricer::value()

{

   return m_calc->value();

}

double BlackScholesPricer::delta()

{

   return m_calc->delta();

}

double BlackScholesPricer::gamma()

{

   return m_calc->gamma();

}

double BlackScholesPricer::theta()

{

   return m_calc->theta(m_tau);

}

double BlackScholesPricer::vega()

{

   return m_calc->vega(m_tau);

}

Listing 15-1

Implementation File for BlackScholesPricer.cpp

To compile this code, you need to install the QuantLib library for your platform and add that library to the project. For example, using the gcc compiler, you need to use the –lQuantLib option.

Conclusion

Using good libraries is an important aspect of effective software development. Financial code, especially when options and derivatives are involved, requires the use of efficient and well-tested algorithms. For this reason, it is important that developers be acquainted with high-quality libraries that can be used to simplify the development process.

In this chapter, you learned about some libraries, such as boost and QuantLib, which have been successfully used to create financial applications handling options and other derivatives. The first example was from the boost repository, which contains several special-purpose libraries that use modern C++ features. The odeint library in particular, which is contained in the boost repository, can be used to simplify the computation of solutions to ODEs.

Another important library used in the financial software community is QuantLib. This open source financial library provides many useful algorithms implemented in modern, efficient C++. You saw examples of common utilities provided by QuantLib. The most common classes are for date handling. These utility classes can handle business calendars, date intervals, and sequences in a way that makes it possible to handle financial applications.

You also saw how to use QuantLib to quickly create options and derivative models. The BlackScholesCalculator class encapsulates the solution to the Black-Scholes model. This model is the basis for most techniques that can be used to analyze prices and variations of values for financial derivatives.

© The Author(s), under exclusive license to APress Media, LLC, part of Springer Nature 2023

C. OliveiraOptions and Derivatives Programming in C++23https://doi.org/10.1007/978-1-4842-9827-5_16

16. Credit Derivatives

Carlos Oliveira1  

(1)

Seattle, WA, USA

A credit derivative is a financial contract that aims at reducing credit risk—that is, the risk of default posed by contracts established with a business counterparty. These kinds of derivatives have become increasingly popular in the last decade, because they allow the hedging of complex financial positions even in industries that are not covered by mainstream markets.

As a financial software engineer, you are interested in modeling and analyzing such credit derivative contracts using programming code. Employing some of the methods developed in the previous chapters, it becomes possible to write applications that simplify the pricing and evaluation of such derivative instruments. In particular, credit derivatives can be modeled using some of the same tools that have already been discussed for the analysis of options.

In this chapter, you will learn how to create the C++ code that can be used in the quantitative analysis of credit derivative contracts. Here are some of the topics discussed:

General concepts of credit derivatives: A general exposition of what credit derivatives are and the main types of derivatives commonly used in the marketplace.

Modeling the problem: How to model the problems occurring in the area of credit derivatives. I’ll present examples of how such derivatives can be modeled using tools that have been previously used for standard options.

Barrier options: You will learn about barrier options and how they can be used to compute prices for large classes of credit derivatives. You will also see coding examples of how to handle barrier options in C++.

Using QuantLib for credit derivatives: You will find a complete example of how to use the financial classes contained in QuantLib to implement derivatives-related C++ code. I will present the CDSSolver class, which implements a pricing strategy for derivatives based on barrier options.

Introduction to Credit Derivatives

A credit derivative is a type of financial contract that protects participants from credit risk. Credit risk, in the large majority of the cases, refers to the risk of default (or lack of payment by other means) from a counterpart. For example, consider a company that creates a financial operation that is backed by an insurance contract. If this insurance contract is provided by a third party, this presents a risk of bankruptcy. The participants of this contract want to protect against the possibility of default, so companies can create a credit derivative that will pay a considerable amount of money if the counterpart goes bankrupt. Such contracts are signed with another third party, which makes the payment if the bankruptcy occurs.

Credit derivatives can be classified according to several categories, which consider how the contract is structured and the participants in such a contract. Here are some of the most common types of credit derivatives that are traded in the market:

CDO (collateralized debt obligations): A CDO is a type of credit derivative where the obligations paid are collateralized based on some underlying asset. This process of collateralization creates a tiered system, where the several payers are pooled and graded according to their credit risk. Thus, financial companies can sell different tiers, ranging from the highest credit (AAA) to lower level that represent higher default risk (e.g., B+).

CDS (credit default swap): A CDS allows companies to protect themselves against the default of a major market player. The buyer of a CDS makes one or more payments for a predefined period of time. If a default occurs on the covered asset, the CDS buyer is entitled to receive compensation for this credit event.

Credit default option: A credit default option resembles an option contract, but the underlying corresponds to the credit default against which you are seeking protection.

CDN (credit-linked note): A CDN is a financial instrument that allows a particular type of credit risk to be transferred to other investors. Usually these notes are structured as bonds on lower-risk assets, which are used to pay creditors if the target institution defaults.

CMCDS (constant maturity CDS): A CMCDS works just like a CDS, but it has different rules for the amount of the payoff received in the case of a default. With the CMCDS, payoffs change based on considerations that are determined between the participants of the contract. For example, the payoff may be determined according to a particular interest rate index.

Total return swap: This category of derivative is used to transfer financial results between two institutions according to a predefined contract. The buyer makes one or more payments, while it expects to receive the total return of a particular investment as a payoff. This allows some institutions, such as hedge funds, to receive the return of complex financial investment with the help of a second entity that transfers the financial return at the end of the covered period.

Modeling Credit Derivatives

As you saw in the previous section, credit derivatives encompass a large number of financial products that have in common the mitigation of credit risk from one entity to another. This makes it difficult to come up with general models for such a wide class of financial instruments. In this section, you will see a few examples of C++ models applied to a few common classes of credit derivatives.

The first step in creating effective code for credit derivatives is to have a computer model for this type of security. Given the diversity of CD contracts, having a proper model becomes even more important so that other algorithms can be applied to this type of security without the need to understand the internal complexities of each type of credit derivative.

As a first step, you can define a simple class that can be used to store and manipulate the data corresponding to a credit default swap. The fields in this class represent the characteristic values that define a CDS contract. These values are the following:

Notional: This represents the total value of the position encompassed by the contract. The notional is usually larger than the payments due to leverage that is allowed on derivative contracts.

Spread: The value paid by the buyer of the CDS. It may be paid in a particular schedule, or in a single payment.

Time period: Defines the time period in which the CDS is valid.

Pay at default: A Boolean value that determines if the payoff should be made at the time of credit default.

Is long: A Boolean value that is true if the contract is being bought and false if the contract is being sold.

In the next few sections, you will see how this information can be used to model CDS contracts with standard techniques employed in quantitative finance. In particular, I will discuss how to analyze such derivatives using the concept of barrier options. You will also see how to calculate the price for such barrier options.

Using Barrier Options

In this section, I discuss how to use a technique that is frequently employed for the pricing of derivatives in general, including credit derivatives. To simplify the discussion, I use the most basic structure for a financial derivative so that you don’t need to worry about complex contractual issues. However, the barrier technique described in this section can be expanded to solve a large class of commonly traded derivatives.

The first step in understanding the solution method is to define a barrier option. A barrier option is a special type of derivative where payoff occurs when a particular price level, or barrier, is crossed. This makes it different from a normal option, because common options have a payoff that depends on how much the underlying is above or below some threshold. With a barrier option, however, the payoff is paid only as the barrier is crossed.

Barrier options work well as a simple model for credit derivatives, because the credit event is frequently defined as a particular barrier. For example, if the credit event is the bankruptcy of a company, the barrier to be crossed is given by the difference between assets and liabilities in the corporation. When that barrier is breached, the company becomes insolvent, and the payoff needs to be made.

There are two main types of barrier options, depending on how the barrier is considered as part of the contract:

Knock-in: This is a barrier option where the payoff is given only when the barrier is touched before expiration.

Knock-out: This is a barrier option where the payoff is given only when the barrier is not touched before expiration.

Thus, for example, a barrier option that pays when a company claims bankruptcy is a knock-in option, because the payment happens when the default barrier is reached. You can also classify barrier options according to the current value in relation to the barrier:

Down-option: This is a barrier option where the barrier is below the current value of the underlying asset.

Up-option: This is a barrier option where the barrier is above the current value of the underlying asset.

These two classifications can also be combined so that you can have down-in options or up-out options. Finally, these options can be calls or puts, depending on whether you are buying the right to sell (put) or the right to buy (call) the underlying instrument.

A Solver Class for Barrier Options

To solve the problem, a new class called CDSSolver is defined in this section. This class contains all the elements necessary to define a barrier option, along with the code that solves the pricing problem using functions and classes from the QuantLib repository. The definition of the class contains the member variables needed by the pricing algorithm:

class CDSSolver : boost::noncopyable {

public:

   // constructor

   CDSSolver(double val, double sigma, double divYield,

             double forwardIR, double strike, double barrier, double rebate);

   // solve the model

   std::pair<QuantLib::BarrierOption, QuantLib::BlackScholesMertonProcess>

   solve(QuantLib::Date maturity_date);

   // generate a grid

   void generateGrid(QuantLib::BarrierOption &option,

                     QuantLib::BlackScholesMertonProcess &process,

                     const std::vector<QuantLib::Size> &grid);

private:

   double currentValue;

   double sigma;

   double divYield;

   double forwardInterestRate;

   double strike;

   double barrier;

   double rebate;

};

The first thing to consider when reviewing this class is that the QuantLib code also uses boost libraries for basic functionality, such as smart pointers. In this case, the CDSSolver uses boost::noncopyable as a base class, which indicates that the class cannot be copied. Therefore, no copy constructor or assignment operators are declared in CDSSolver.

Note

Observe that the CDSSolver class uses shared pointers declared in boost. This is necessary because QuantLib has boost as a direct dependency, and many of the internal smart pointers are declared in this way. Remember, however, that C++11 also has its own version of shared_ptr, which is part of the standard namespace. It is important to avoid confusion between shared_ptr declared in boost and in the standard library.

There are two main member functions in the CDSSolver class. The solve function is responsible for performing the main tasks associated with the pricing of barrier options. The generateGrid evaluates the value of the barrier option at particular time points, as defined by the vector of time points passed as a parameter.

The member variables used by the CDSSolver class are the following:

currentValue: Represents the current value of the underlying instrument

sigma: Represents the variance of the financial instrument

divYield: The dividend yield paid annually by the underlying

forwardInterestRate: The forward interest rate, which is used to determine the return of cash that is not invested in the barrier option

strike: The strike of the barrier option, that is, the price that determines the payoff value

barrier: The price barrier that needs to be crossed to trigger the payout of the option contract

rebate: Contractual rebate defined when the barrier option is created

These variables are later used to solve the pricing problem, as you can see in the following description of the associated code. But first, I will provide a short introduction to the classes included in QuantLib that are used to solve this kind of pricing problem.

Barrier Option Classes in QuantLib

QuantLib offers support for pricing credit derivatives and related instruments. In particular, the library contains a set of classes that can be used to price barrier options as defined in the previous section. First, I will review some of these classes, which will later be used in a complete example of how to compute prices for barrier options.

The first class of importance is the Quote class. A quote is defined as one or more values that determine the current price of an instrument. The Quote class is just the base for several classes that represent quotes for different financial instruments. In this example, I will use a SimpleQuote to initialize the quote for the barrier option. The following code shows how this is done:

   Handle<Quote> quote(boost::shared_ptr<Quote>(new SimpleQuote(currentValue)));

This line of code uses a second class that is frequently used in QuantLib: the Handle class. A handle is a simple container that allows objects to be referenced and changed when necessary.

The next class used in the implementation of barrier options is YieldTermStructure. This class allows you to specify the yield curve currently used by the markets. The yield curve is a representation of the effective interest rates in a particular market, such as US Treasury bonds. The curve is formed as you consider the different interest rates for each maturity period, usually measured in years. Figure 16-1 shows an example of the yield term structure for Treasury bonds.

A line graph represents yield in percentage versus borrowing period in years. The line follows an increasing trend from (0, 2.5) to (30, 4.6). Data are estimated.

Figure 16-1

Example of yield structure for a US Treasury bond

Using the YieldTermStructure class in QuantLib, it is possible to store and use this information to compute barrier options. Depending on how the financial instrument is defined, such a yield term structure may be represented by several interest rates, one for each desired time horizon. The YieldTermStructure class is abstract and should be instantiated using one of their subclasses, which include the following:

FlatForward: The simplest cases in which the curve is flat and no variation in interest rate is forecasted.

ForwardCurve: A type of yield curve that can use different rates for each time period. This class can be used for the most common case where the interest rates for different time periods are known.

PiecewiseYieldCurve: A yield curve in which the different segments of the curve are linearized.

FittedBondDiscountCurve: A yield curve where interest rates are given indirectly and the yield can be fitted to represent a set of bonds.

In the example code of the next section, I use the FlatForward class as a way to represent a simple short-term yield structure, with no variation in interest rates. More complex yield term structures can be easily accommodated by using one of the previous classes.

A similar class provided by QuantLib is the BlackVolTermStructure. This class represents the volatility term structure and allows you to determine a particular curve that represents the implied volatility (also known as Black volatility, which is used in the Black-Scholes equation) for the underlying instrument. Similarly to the yield term structure, there are several options for the type of volatility term structure. They differ in the shape of the curve, as well as in the functions that can be used to represent each part of the curve. QuantLib also provides a number of classes that can be used to represent the different types of volatility term structure. Here are some of them:

BlackConstantVol: Used to represent a volatility type that is constant over the whole period.

BlackVarianceCurve: A type of volatility curve where different values of variance are used to determine the volatility.

ImpliedVolTermStructure: A volatility term structure that is defined by the implied volatility associated with a particular instrument.

BlackVarianceSurface: Defines a volatility curve based on a set of data points that define a variance surface. These values are interpolated to generate the desired variance surface.

Using the information stored in these classes, it is possible to describe the Black-Scholes model using the class BlackScholesMertonProcess, which is also part of QuantLib. This class receives as parameters the quote, a risk-free yield term structure, and a yield term representing the asset dividend. The class constructor also receives a volatility term structure as a parameter that describes the process.

The class StrikedTypePayoff is used to build complex payoffs. It also has a few useful derived classes, including the following:

PlainVanillaPayoff: Represents the most common type of payoff, described by a single value and a strike.

PercentageStrikePayoff: A type of payoff where the strike is given as a percentage of the underlying price, instead of as a fixed value.

AssetOrNothingPayoff: A payoff that is structured as a binary decision. The results are either an asset or nothing.

CashOrNothingPayoff: A payoff that is structured as a binary decision. The results are either cash or nothing.

The example code in the next section uses the PlainVanillaPayoff class. The constructor to this class uses as parameters the option type (put or call) and a strike.

BarrierOption is the central class used by QuantLib to model barrier options. This class can be used to calculate the value of a particular barrier option, given a set of parameters that represent that option.

The first parameter to the constructor of BarrierOption is the type of barrier option. As previously described, barrier options can be of four types—UpIn, UpOut, DownIn, and DownOut—depending on the underlying price and the type of barrier used. The next parameters are values that correspond to the barrier, the rebate, the payoff, and the exercise.

Finally, this example also uses a barrier options engine called FdBlackScholesBarrierEngine. This class is used as an implementation for the pricing strategy.

An Example Using QuantLib

Using the classes presented in the previous section, it is possible to explain the implementation of the class CDSSolver. First, consider the first member function, called CDSSolver::solve. This function receives as a parameter a Date object that represents the maturity date of the desired barrier option.

The first step is to create a quote for the option, instantiating the SimpleQuote class and using the current value of the underling as its single argument. Today’s date is also computed with the help of the Date::todaysDate member function.

Next, the code tries to instantiate the two term structure objects: one for the dividend yield and another for free cash interest rates. A volatility term structure object is also instantiated using the given volatility, which is estimated using the parameter sigma.

// solve the valuation problem using the barrier technique, from today to the maturity date

pair<BarrierOption, BlackScholesMertonProcess>

CDSSolver::solve(Date maturity_date)

{

   Handle<Quote> quote(boost::shared_ptr<Quote>(new SimpleQuote(currentValue)));

   Date today = Date::todaysDate();

   auto thirty_360 = Thirty360();

   shared_ptr<YieldTermStructure>   ts1(new FlatForward(today, divYield,  thirty_360));

   shared_ptr<YieldTermStructure>   ts2(new FlatForward(today, forwardInterestRate, thirty_360));

   shared_ptr<BlackVolTermStructure> vs(new BlackConstantVol(today, NullCalendar(),sigma, thirty_360));

The next part of the solve function is responsible for instantiating a process object, which uses QuantLib::BlackScholesMertonProcess. Such a process requires a quote object, yield term structures for interest rates and cash, and a volatility term structure that was previously created.

The function also creates two new objects: a payoff object of type PlainVanillaPayoff that represents the desired call option and a given strike. The exercise is established as a EuropeanExercise type, at the given maturity date.

auto process = BlackScholesMertonProcess(quote,

         Handle<YieldTermStructure>(ts1),

         Handle<YieldTermStructure>(ts2),

         Handle<BlackVolTermStructure>(vs));

   shared_ptr<StrikedTypePayoff> payoff(new PlainVanillaPayoff(Option::Type::Call, strike));

   shared_ptr<Exercise> exercise(new EuropeanExercise(maturity_date));

Finally, you’re ready to create a barrier option object, which is an instance of QuantLib::BarrierOption. It takes as parameters the type of barrier, the barrier value, a rebate (if it is available), and the two objects previously created: payoff and exercise.

The next two steps are to create a generalized Black-Scholes object using the existing process and to set the price engine of the barrier option. The price engine algorithm is responsible for price calculation, and this example uses AnalyticBarrierEngine, which is a common algorithm available from QuantLib. The member function CDSSolver::solve will finally return a pair that contains the option and process objects.

auto option = BarrierOption(Barrier::Type::UpIn,

                               barrier, rebate,

                               payoff,

                               exercise);

   auto proc = shared_ptr<GeneralizedBlackScholesProcess>(&process);

   option.setPricingEngine(shared_ptr<PricingEngine>(new AnalyticBarrierEngine(proc)));

   return std::make_pair(option, process);

}

The next member function implemented in the CDSSolver class is generateGrid. This function is conceptually simple, and it just prints a grid of prices calculated from the given barrier option, using the given BlackScholesMertonProcess and a set of points that determines the option price at a particular date.

Essentially, the function assumes that the grid points are sorted and selects the maximum value. Then, for each element of the grid, a new barrier engine is instantiated and used with the existing barrier option. The price is computed using the resulting combination of option and pricing engines. The code then prints the ratio of increase for that particular point. A backward computation is also performed for comparison purposes.

void CDSSolver::generateGrid(BarrierOption &option, BlackScholesMertonProcess &process, const vector<Size> &grid)

{

   double value = option.NPV();

   Size maxG = grid[grid.size()-1];   // find maximum grid value

   for (auto g : grid)

   {

      FdBlackScholesBarrierEngine be(shared_ptr<GeneralizedBlackScholesProcess>(&process), maxG, g);

      option.setPricingEngine(shared_ptr<PricingEngine>(&be));

      cout << std::abs(option.NPV()/value -1);

      FdBlackScholesBarrierEngine be1(shared_ptr<GeneralizedBlackScholesProcess>(&process), g, maxG);

      option.setPricingEngine(shared_ptr<PricingEngine>(&be1));

      cout << std::abs(option.NPV()/value -1);

   }

}

Complete Code

This section contains the complete listing of the CDSSolver class. It contains a header file (Listing 16-1), which defines the interface for the class, and an implementation file (Listing 16-2), where the methods solve and generateGrid are implemented.

//

//  CDS.hpp

//  CppOptions

#ifndef CDS_hpp

#define CDS_hpp

#include <stdio.h>

#include <utility>

#include <ql/instruments/barrieroption.hpp>

#include <ql/processes/blackscholesprocess.hpp>

//

// CDSSolver class, incorporates the solution to Credit Default

class CDSSolver : boost::noncopyable {

public:

   // constructor

   CDSSolver(double val, double sigma, double divYield,

             double forwardIR, double strike, double barrier, double rebate);

   // solve the model

   std::pair<QuantLib::BarrierOption, QuantLib::BlackScholesMertonProcess>

   solve(QuantLib::Date maturity_date);

   // generate a grid

   void generateGrid(QuantLib::BarrierOption &option,

                     QuantLib::BlackScholesMertonProcess &process,

                     const std::vector<QuantLib::Size> &grid);

private:

   double currentValue;

   double sigma;

   double divYield;

   double forwardInterestRate;

   double strike;

   double barrier;

   double rebate;

};

#endif /* CDS_hpp */

Listing 16-1

Header File for the CDSSolver Class

Listing 16-2 shows the implementation file for class CDSSolver. It also contains a simple test stub called test_CDSSolver, which creates a new instance of CDSSolver using a few test parameters.

//

//  CDS.cpp

#include "CDS.h"

#include <iostream>

// include classes from QuantLib

#include <ql/instruments/creditdefaultswap.hpp>

#include <ql/instruments/barrieroption.hpp>

#include <ql/quotes/SimpleQuote.hpp>

#include <ql/time/daycounters/thirty360.hpp>

#include <ql/exercise.hpp>

#include <ql/termstructures/yield/flatforward.hpp>

#include <ql/termstructures/volatility/equityfx/blackconstantvol.hpp>

#include <ql/processes/blackscholesprocess.hpp>

#include <ql/pricingengines/barrier/analyticbarrierengine.hpp>

#include <ql/pricingengines/barrier/fdblackscholesbarrierengine.hpp>

using namespace QuantLib;

using std::cout;

using std::vector;

using std::pair;

using boost::shared_ptr;

CDSSolver::CDSSolver(double val, double sigma, double divYield, double forwardIR,

                     double strike, double barrier, double rebate)

:

   currentValue(val),

   sigma(sigma),

   divYield(divYield),

   forwardInterestRate(forwardIR),

   strike(strike),

   barrier(barrier),

   rebate(rebate)

{

}

// solve the valuation problem using the barrier technique,

// from today to the maturity date

pair<BarrierOption, BlackScholesMertonProcess>

CDSSolver::solve(Date maturity_date)

{

   Handle<Quote> quote(boost::shared_ptr<Quote>(new SimpleQuote(currentValue)));

   Date today = Date::todaysDate();

   shared_ptr<YieldTermStructure>   ts1(new FlatForward(today, divYield, Thirty360()));

   shared_ptr<YieldTermStructure>   ts2(new FlatForward(today, forwardInterestRate, Thirty360()));

   shared_ptr<BlackVolTermStructure> vs(new BlackConstantVol(today, NullCalendar(),sigma, Thirty360()));

   auto process = BlackScholesMertonProcess(quote,

         Handle<YieldTermStructure>(ts1),

         Handle<YieldTermStructure>(ts2),

         Handle<BlackVolTermStructure>(vs));

   shared_ptr<StrikedTypePayoff> payoff(new PlainVanillaPayoff(Option::Type::Call, strike));

   shared_ptr<Exercise> exercise(new EuropeanExercise(maturity_date));

   auto option = BarrierOption(Barrier::Type::UpIn,

                               barrier, rebate,

                               payoff,

                               exercise);

   auto pproc = shared_ptr<GeneralizedBlackScholesProcess>(&process);

   option.setPricingEngine(shared_ptr<PricingEngine>(new AnalyticBarrierEngine(pproc)));

   return std::make_pair(option, process);

}

void CDSSolver::generateGrid(BarrierOption &option, BlackScholesMertonProcess &process, const vector<Size> &grid)

{

   double value = option.NPV();

   Size maxG = grid[grid.size()-1];   // find maximum grid value

   for (auto g : grid)

   {

      FdBlackScholesBarrierEngine be(shared_ptr<GeneralizedBlackScholesProcess>(&process), maxG, g);

      option.setPricingEngine(shared_ptr<PricingEngine>(&be));

      cout << std::abs(option.NPV()/value -1);

      FdBlackScholesBarrierEngine be1(shared_ptr<GeneralizedBlackScholesProcess>(&process), g, maxG);

      option.setPricingEngine(shared_ptr<PricingEngine>(&be1));

      cout << std::abs(option.NPV()/value -1);

   }

}

void test_CDSSolver()

{

   // use a few test values

   double currentValue = 50.0;

   double sigma = 0.2;

   double divYield = 0.01;

   double forwardIR = 0.05;

   double strike = 104.0;

   double barrier = 85.0;

   double rebate = 0.0;

   CDSSolver solver(currentValue, sigma, divYield, forwardIR, strike, barrier, rebate);

   Date date(10, Month::August, 2016);

   auto result = solver.solve(date);

   std::vector<Size> grid = { 5, 10, 25, 50, 100, 1000, 2000 };

   solver.generateGrid(result.first, result.second, grid);

}

int main()

{

   test_CDSSolver();

   return 0;

}

Listing 16-2

Implementation File for Class CDSSolver

Conclusion

Credit derivatives are one of the most common types of derivatives traded in world markets. In this chapter, you learned a little more about such types of derivatives and how they can be modeled using C++.

I initially discussed the concept of credit derivatives and the different types of financial instruments that take part in this category of derivatives. You saw that such derivatives can be used to mitigate credit risks, such as the bankruptcy of a counterparty or the default of a loan, for example.

You also learned about techniques to model such derivatives. In particular, you saw barrier options as a simplified model that can be used to analyze the behavior of such financial instruments.

This chapter presented a complete example of credit derivatives through the use of barrier options using QuantLib classes. The QuantLib repository contains a number of algorithms that are readily available to analyze credit derivatives. In particular, these classes can be used to determine the fair price of certain types of derivatives.

Another task that is frequently necessary when dealing with options and derivatives is the processing of input and output data in common formats. The most popular format for this type of application is based on XML. However, some other formats offer advantages as well. In the next chapter, you will learn about different strategies to process financial data and the common formats used to transfer such information across applications.

© The Author(s), under exclusive license to APress Media, LLC, part of Springer Nature 2023

C. OliveiraOptions and Derivatives Programming in C++23https://doi.org/10.1007/978-1-4842-9827-5_17

17. Processing Financial Data

Carlos Oliveira1  

(1)

Seattle, WA, USA

Processing financial data in XML involves extracting, parsing, and analyzing information from XML files that contain financial data. XML (eXtensible Markup Language) is a popular format for structuring data, and it’s commonly used in various industries, including finance.

In this chapter, we share an introductory guide on how to process financial data in the XML format.

Introduction to XML in Finance

XML stands for “eXtensible Markup Language.” It is a widely used markup language designed to store and transport structured data. XML does not dictate what the data means but rather describes its structure. It provides a way to define custom tags and elements for organizing and representing data in a human-readable and machine-readable format.

Key features of XML include the following:

Hierarchical structure: XML documents consist of a hierarchical structure with nested elements, similar to HTML or other markup languages. Elements are enclosed within start and end tags.

Use of tags and elements: XML uses tags to define elements and their relationships. Tags are enclosed in angle brackets (e.g., <tag>). Elements can have attributes that provide additional information about the element.

Self-descriptive format: XML documents are self-descriptive, meaning they contain both the data and information about the structure of the data. This makes it easy for humans and machines to understand the data.

Customizable: Unlike predefined languages like HTML, XML allows users to define their own custom tags and elements, making it highly versatile for various applications.

Text based: XML files are human readable because they are text based and can be edited with a simple text editor.

Platform independent: XML is platform independent and can be used on various operating systems and programming languages.

XML is commonly used for data exchange between different systems, applications, and platforms, allowing data to be shared and processed across different environments. Data stored in XML format can be validated against a Document Type Definition (DTD) or XML Schema, ensuring that the data adheres to a predefined structure.

The “eXtensible” in XML indicates its extensibility; new tags and elements can be easily added to accommodate changing data requirements.

Using XML in Finance

XML is used in the finance industry for various purposes due to its ability to represent structured data and facilitate data interchange between different systems, applications, and organizations. Here are some ways XML is used in finance:

Data exchange: Financial institutions often need to exchange data with other organizations, such as regulatory bodies, trading partners, and customers. XML provides a standardized format for representing data, making it easier to share and interpret financial information accurately.

Messaging and protocols: XML-based messaging protocols are used for communication in financial services. For example, The FIX (Financial Information eXchange) protocol uses XML messages to facilitate communication between financial entities like brokers and exchanges for trade execution.

Data integration: Financial organizations deal with a wide range of data from various sources, such as market data feeds, trade execution systems, risk management systems, and more. XML enables data integration by providing a common format for combining and analyzing diverse data types.

Regulatory reporting: Regulatory bodies require financial institutions to report data on various financial activities. XML is used to structure and transmit regulatory reports accurately, ensuring compliance with regulations like MiFID II, Dodd-Frank, and others.

SWIFT messages: SWIFT (Society for Worldwide Interbank Financial Telecommunication) messages, used for international financial transactions, are often represented in XML format. SWIFT messages are standardized and used for various purposes, including funds transfers, securities trading, and documentary credits.

Financial statements: XML can be used to represent financial statements and reports. Financial organizations often need to generate and exchange financial statements with auditors, investors, and other stakeholders.

Data feeds: XML is used in financial data feeds to deliver real-time or historical market data to trading systems, analysis tools, and other applications.

Portfolio and account management: XML can be used to represent and exchange information about investment portfolios, holdings, and accounts between financial institutions and their clients.

Payments and invoicing: XML-based formats like ISO 20022 are used for representing payment messages, invoices, and remittance information. This facilitates secure and standardized payment processing.

Risk management: XML can be used to represent risk assessment and management data, helping financial institutions analyze and mitigate risks effectively.

Understanding the XML Structure

Understanding the XML structure is a crucial step when processing financial data in XML format. XML (eXtensible Markup Language) uses a hierarchical structure to organize data using nested elements, attributes, and text content.

Here’s a more detailed look at understanding the XML structure.

XML Tags and Elements

XML documents consist of elements enclosed within start and end tags. For example:

<Transaction>

                <Date>2023-08-19</Date>

                <Amount>1000.00</Amount>

                <Currency>USD</Currency>

</Transaction>

In this example, <Transaction> is the parent element, and <Date>, <Amount>, and <Currency> are child elements.

Attributes

Elements can also have attributes, which are additional pieces of information associated with the element. Attributes provide metadata or properties about an element. For example:

 <Account id="12345" type="Savings">

                <Balance>5000.00</Balance>

 </Account>

In this case, the <Account> element has attributes id and type. The data for balance, on the other hand, is stored as a new element.

Hierarchy and Nesting

XML elements can be nested within other elements to create a hierarchy. This reflects the relationships between different data points. For instance:

<Portfolio>

                <Account id="98765" type="Investment">

                    <Balance>100000.00</Balance>

                    <Transactions>

                        <Transaction>

                            <!-- Transaction details here -->

                        </Transaction>

                        <!-- More transactions -->

                    </Transactions>

                </Account>

                <!-- More accounts -->

</Portfolio>

Elements can contain text content, which represents the actual data associated with that element. For example, the <Balance> element in the preceding examples contains the monetary value as text content.

XML documents can also use namespaces to avoid naming conflicts and to categorize elements. Namespaces are defined using a namespace declaration, as in the following XML fragment:

<ns:Transaction xmlns:ns="http://example.com/financial">

                <!-- Transaction details here -->

</ns:Transaction>

In this example, the xmlns:ns attribute defines a namespace for the Transaction element.

XML Schema (XSD) and Document Type Definition (DTD)

XML Schema and DTD are used to define the structure and rules that XML documents should adhere to. They specify the valid elements, attributes, data types, and their relationships. Using a schema or DTD can ensure consistency and help validate XML data.

Understanding the XML structure is crucial because it guides how you navigate and extract data from the XML file. When processing financial data, you need to know which elements contain the relevant information, their attributes, and how they are organized hierarchically. This knowledge will be instrumental in writing code to parse, extract, and process the financial data effectively.

XML Parsing in C++

While XML parsing is a complex task, the bulk of XML parsing in modern C++ can be accomplished using various libraries. Popular choices include some open source libraries such as RapidXML and pugixml.

In the examples for this chapter, I will consider using the pugixml library, which provides an efficient and user-friendly interface for XML parsing. Other libraries are also available and work just as well. In the remaining of this section, you’ll see a complete example of XML parsing using pugixml in C++.

Installing the pugixml Library

First, you need to download and install the pugixml library. You can easily find the library and its documentation on its official website: https://pugixml.org/. Follow the steps necessary to install the library in your particular operating system.

The next step is to include the necessary header files in your C++ program. Here are the necessary include lines you will need for the examples:

#include <iostream>

#include "pugixml.hpp"

XML Parsing with pugixml

The next step is to perform the parsing of the given XML data using the library. Assume you have the following sample XML data in a file named financial_data.xml:

<Portfolio>

    <Account id="12345" type="Savings">

        <Balance>5000.00</Balance>

    </Account>

</Portfolio>

This example defines a portfolio data entry, with fields consisting of account id, type, and balance. Now, let’s parse this XML using the pugixml library:

int main() {

    // Load XML file

    pugi::xml_document doc;

    if (!doc.load_file("financial_data.xml")) {

        std::cerr << "Error loading XML file." << std::endl;

        return 1;

    }

    // Access root node (Portfolio)

    pugi::xml_node portfolioNode = doc.child("Portfolio");

    // Loop through Account nodes

    for (pugi::xml_node accountNode = portfolioNode.child("Account"); accountNode; accountNode = accountNode.next_sibling("Account")) {

        // Access attributes

        std::string id = accountNode.attribute("id").as_string();

        std::string type = accountNode.attribute("type").as_string();

        // Access Balance node

        pugi::xml_node balanceNode = accountNode.child("Balance");

        double balance = balanceNode.text().as_double();

        // Print account information

        std::cout << "Account ID: " << id << std::endl;

        std::cout << "Account Type: " << type << std::endl;

        std::cout << "Balance: " << balance << std::endl;

    }

    return 0;

}

In this example, we use the child() function to access the root node (Portfolio) and the next_sibling() function to loop through each Account node. Within the loop, we use the attribute() function to access attributes and the text() function to retrieve the text content of elements.

A few key functions in the library are used to navigate the XML structure:

child(“element_name”): Access the child element with the specified name.

next_sibling(“element_name”): Move to the next sibling element with the specified name.

attribute(“attribute_name”): Access an attribute of the current element.

text(): Retrieve the text content of the current element.

Remember to adjust the code according to your specific XML structure and the data you want to extract.

This processing mechanism can be used with any XML data set. You only need to adjust the C++ code according to your XML structure, so it can access the data you want to extract.

As you see, the pugixml library simplifies XML parsing by providing a clean and intuitive interface. It automatically handles node traversal, attribute access, and text content retrieval. You still need to make sure to handle error cases, such as when the XML file cannot be loaded or when nodes are not found.

Keep in mind that C++ also provides other libraries that you can use to process XML data. Examples include Boost.PropertyTree and TinyXML2 for XML parsing. Before you start a new project, you need to choose an XML library that best suits your needs and the level of complexity required for your financial data processing tasks.

Performing Data Processing and Analysis

After extracting the data, you can perform various types of analysis depending on your goals. This might include calculating totals, generating reports, identifying trends, or performing calculations based on the financial data.

Here’s an example of XML processing using stock trading data. Suppose you have an XML file named trading_data.xml containing information about stock trades:

<Trades>

    <Trade>

        <Symbol>AAPL</Symbol>

        <Price>150.25</Price>

        <Quantity>100</Quantity>

        <Timestamp>2023-08-19T10:15:00</Timestamp>

    </Trade>

    <Trade>

        <Symbol>GOOG</Symbol>

        <Price>2800.50</Price>

        <Quantity>50</Quantity>

        <Timestamp>2023-08-19T11:30:00</Timestamp>

    </Trade>

    <!-- More trade entries -->

</Trades>

Here is a C++ program that demonstrates how to process and analyze this stock trading data using the pugixml library:

#include <iostream>

#include "pugixml.hpp"

int main() {

    pugi::xml_document doc;

    if (!doc.load_file("trading_data.xml")) {

        std::cerr << "Error loading XML file." << std::endl;

        return 1;

    }

    int totalTrades = 0;

    double totalVolumeWeightedPrice = 0.0;

    // Loop through each Trade node and calculate total trades and volume-weighted price

    for (auto tradeNode = doc.child("Trades").child("Trade"); tradeNode; tradeNode = tradeNode.next_sibling("Trade")) {

        std::string symbol = tradeNode.child_value("Symbol");

        double price = std::stod(tradeNode.child_value("Price"));

        int quantity = std::stoi(tradeNode.child_value("Quantity"));

        std::string timestamp = tradeNode.child_value("Timestamp");

        double tradeValue = price * quantity;

        totalVolumeWeightedPrice += tradeValue;

        std::cout << "Symbol: " << symbol << std::endl;

        std::cout << "Price: " << price << std::endl;

        std::cout << "Quantity: " << quantity << std::endl;

        std::cout << "Timestamp: " << timestamp << std::endl;

        std::cout << "Trade Value: " << tradeValue << std::endl;

        std::cout << "----------------------" << std::endl;

        totalTrades++;

    }

    double averageVolumeWeightedPrice

           = totalVolumeWeightedPrice / totalTrades;

    std::cout << "Total Trades: " << totalTrades << std::endl;

    std::cout << "Average Volume-Weighted Price: " << averageVolumeWeightedPrice << std::endl;

    return 0;

}

In this example, the program calculates the total trades, volume-weighted price, and average volume-weighted price for the given stock trading data. This demonstrates how to navigate through the XML structure, extract data, and perform calculations. At the end of the calculation, the results are displayed using the C++ stdout. You can use this code structure for your own data, after adjusting the code based on your actual XML structure and data processing requirements.

Error Handling

Error handling is an essential aspect of XML processing in C++. It helps your program handle unexpected situations, such as malformed XML, missing elements, or invalid data, gracefully.

The pugixml library provides mechanisms to detect and handle errors during XML parsing. Here’s how you can perform error handling for XML processing in C++ using the pugixml library.

Loading XML File and Checking for Errors

When loading an XML file, check if the file is loaded successfully or not. If the file cannot be loaded, it means there’s an error in the XML file itself or the file doesn’t exist.

pugi::xml_document doc;

if (!doc.load_file("financial_data.xml")) {

    std::cerr << "Error loading XML file." << std::endl;

    return 1;

}

Checking for Node Existence

Before accessing specific elements or attributes, it’s a good practice to check if they exist. This is needed to prevent potential crashes due to missing data.

pugi::xml_node accountNode = doc.child("Portfolio").child("Account");

if (!accountNode) {

    std::cerr << "Account node not found." << std::endl;

    return 1;

}

If your XML processing involves data conversions (e.g., converting text content to numbers) or other operations that could throw exceptions, always wrap those operations in try-catch blocks to handle exceptions gracefully.

try {

    double balance = accountNode.child("Balance").text().as_double();

} catch (const pugi::xml_text::convertor::convertor_error& e) {

    std::cerr << "Error converting text content to double: "

              << e.what() << std::endl;

    return 1;

}

Handling Missing Attributes

When accessing attributes, use the attribute() function to check for the existence of an attribute and then extract its value. This helps avoid errors if an attribute is missing.

pugi::xml_attribute idAttribute = accountNode.attribute("id");

if (idAttribute) {

    std::string id = idAttribute.as_string();

} else {

    std::cerr << "Attribute 'id' not found." << std::endl;

    return 1;

}

Other Techniques for Error Checking

If your XML data contains complex structures or follows specific patterns, you might want to perform additional validation and error checks to ensure the data conforms to your expectations.

First, you can use std::cerr or another logging mechanism to output error messages. This helps you identify and diagnose issues during development and debugging. By incorporating these error handling techniques, you can create more robust XML processing code that gracefully handles unexpected situations, provides meaningful error messages, and avoids crashes or data corruption.

Depending on your goals, you might want to output the processed data in a different format (e.g., CSV, Excel) or visualize it using graphs and charts. Utilize relevant libraries or tools for these tasks.

You may also want to test your code with different XML files to ensure it handles various scenarios correctly. This way, you can validate the accuracy of your results against known data.

You can also check the resulting code looking for opportunities for optimization and performance improvement. Depending on the size of your XML files and the complexity of your processing tasks, you might need to optimize your code for performance and memory usage.

Using the HDF5 Format

HDF5 (Hierarchical Data Format version 5) is a data model, file format, and software library designed for efficient storage and management of large and complex datasets. It’s commonly used in scientific computing, engineering, and data-intensive applications where structured data needs to be stored, accessed, and shared. HDF5 is particularly well suited for situations where datasets are too large to fit entirely in memory or when data needs to be organized hierarchically.

Key features of HDF5 include the following:

Hierarchical structure: HDF5 files organize data into a hierarchy similar to a file system. The structure consists of groups (like directories) and datasets (like files), allowing users to create a logical organization for their data.

Support for various data types: HDF5 supports a wide range of data types, including integers, floats, strings, complex numbers, and user-defined types. This flexibility makes it suitable for representing diverse scientific and engineering data.

Compression and chunking: HDF5 provides mechanisms for data compression and chunking, which helps reduce storage requirements and improve access times for large datasets.

Efficient I/O: HDF5 is designed for efficient I/O operations, allowing datasets to be read and written efficiently, even when dealing with large volumes of data.

Parallel I/O: HDF5 supports parallel I/O, which is crucial for high-performance computing and working with data distributed across multiple processes or nodes.

Metadata: Metadata can be associated with groups and datasets, providing additional information about the data, such as units, labels, descriptions, and more.

External links: HDF5 supports external links that allow datasets in one file to reference datasets in another file. This can be useful for organizing data across multiple files.

Cross-platform compatibility: HDF5 files are designed to be platform independent, meaning they can be created on one system and read on another, regardless of the underlying hardware and operating system.

HDF5 is a well-established format, and its core code provides APIs and libraries for various programming languages. Interfaces exist for C, C++, Fortran, Python, Java, and more.

HDF5 finds applications in a wide range of domains, including scientific simulations, image and signal processing, climate modeling, genomics, and more. It’s particularly beneficial for scenarios where datasets are too large to fit in memory, need to be shared between different applications or research groups, or require complex hierarchical organization. These are the features that make this format useful for financial applications in derivatives trading.

To work with HDF5 files, you typically use an HDF5 library, like the one provided by the HDF Group. This library allows you to create, read, write, and manipulate HDF5 files in your programming language of choice.

Using HDF5

Using HDF5 with C++ involves a series of steps, including installing the HDF5 library, creating HDF5 files, writing and reading datasets, and managing attributes and groups. In this section, we’ll provide a basic guide on how to use HDF5 with C++.

Before you start, make sure you have the HDF5 library installed on your system. You can download the library and find installation instructions from the HDF Group’s website: www.hdfgroup.org/downloads/hdf5/.

Next, you need to include the HDF5 C++ header in your C++ code:

#include <iostream>

#include <H5Cpp.h> // HDF5 C++ header

Next, let’s see how to create an HDF5 file using the H5::H5File class provided by the HDF5 library. We consider that you can create datasets and write data to them.

int main() {

H5::H5File file("example.h5", H5F_ACC_TRUNC);

const int data[4][4] = {

    {1, 2, 3, 4},

    {5, 6, 7, 8},

    {9, 10, 11, 12},

    {13, 14, 15, 16}

};

hsize_t dims[2] = {4, 4};

H5::DataSpace dataspace(2, dims);

H5::DataSet dataset = file.createDataSet("my_dataset", H5::PredType::NATIVE_INT, dataspace);

dataset.write(data, H5::PredType::NATIVE_INT);

// Reading Datasets:

//      Use this to read data from a dataset:

int readData[4][4];

dataset.read(readData, H5::PredType::NATIVE_INT);

// Create and Manage Groups:

// You can create groups to organize your data hierarchically:

H5::Group group = file.createGroup("/my_group");

// Attributes:

// You can attach attributes to datasets and groups:

H5::Attribute attr =

    dataset.createAttribute(

     "units",H5::PredType::C_S1, H5::DataSpace(H5S_SCALAR));

const char* units = "pixels";

attr.write(H5::PredType::C_S1, units);

//            Error Handling:

//            Handle errors using try-catch blocks:

try {

    // HDF5 operations

} catch (H5::Exception& e) {

    e.printErrorStack();

    return -1;

}

// Close Resources:

//  Make sure to close the dataset, dataspace, and file objects

// when you're done:

dataset.close();

dataspace.close();

file.close();

}

Compile and Run

You can compile the preceding C++ code using the HDF5 library. For example, with g++, you might use

g++ -o hdf5_example hdf5_example.cpp -lhdf5_cpp -lhdf5

This is just a basic example that shows how to use some of the features of HDF5 with C++. The library offers many more features for advanced use cases, such as compound data types, chunking, compression, parallel I/O, and more.

HDF5 for Trading Data

Here’s a complete example of using HDF5 for a simple trading application in C++. In this example, we’ll create an HDF5 file to store trading data, including stock symbols, prices, quantities, and timestamps.

#include <iostream>

#include <H5Cpp.h>

int main() {

    // Create an HDF5 file

    H5::H5File file("trading_data.h5", H5F_ACC_TRUNC);

    // Create a group for trades

    H5::Group tradesGroup = file.createGroup("/trades");

    // Sample trading data

    const int numTrades = 3;

    std::string symbols[numTrades] = {"AAPL", "GOOG", "MSFT"};

    double prices[numTrades] = {150.25, 2800.50, 300.75};

    int quantities[numTrades] = {100, 50, 75};

    std::string timestamps[numTrades] = {"2023-08-19T10:15:00", "2023-08-19T11:30:00", "2023-08-19T12:45:00"};

    // Create datasets for trading data

    H5::DataSpace dataspace(H5S_SCALAR);

    H5::StrType stringType(H5::PredType::C_S1, H5T_VARIABLE);

    H5::DataSet symbolDataset = tradesGroup.createDataSet("symbols", stringType, dataspace);

    H5::DataSet priceDataset = tradesGroup.createDataSet("prices", H5::PredType::NATIVE_DOUBLE, dataspace);

    H5::DataSet quantityDataset = tradesGroup.createDataSet("quantities", H5::PredType::NATIVE_INT, dataspace);

    H5::DataSet timestampDataset = tradesGroup.createDataSet("timestamps", stringType, dataspace);

    // Write data to datasets

    symbolDataset.write(symbols, stringType);

    priceDataset.write(prices, H5::PredType::NATIVE_DOUBLE);

    quantityDataset.write(quantities, H5::PredType::NATIVE_INT);

    timestampDataset.write(timestamps, stringType);

    // Close resources

    symbolDataset.close();

    priceDataset.close();

    quantityDataset.close();

    timestampDataset.close();

    tradesGroup.close();

    file.close();

    std::cout << "Trading data written to trading_data.h5" << std::endl;

    return 0;

}

In this example, we first create an HDF5 file that is named “trading_data.h5”. This file will store the data created by the application. Next, we create a group named “trades” to store trading data. We also define sample trading data arrays for symbols, prices, quantities, and timestamps.

Next, we create datasets for each type of trading data. We write the trading data arrays to their respective datasets. Then, we close the datasets, group, and file.

To compile and run the program, you can use a command like

g++ -o trading_app trading_app.cpp -lhdf5_cpp -lhdf5

./trading_app

This example provides a basic demonstration of using HDF5 for storing trading data. In a real-world scenario, you would likely expand this example to handle more complex data structures, error handling, and other features of the HDF5 library.

Conclusion

Financial data storage and processing are an important facet of financial software systems. A lot of the data used in derivatives trading is stored in standard formats such as XML and HDF5.

In this chapter, we provided an introduction to data processing using external formats using C++. We discussed examples of how to handle data in XML and HDF5 formats. You have learned how to apply modern libraries to the processing of XML data. I also explained how to use the C++ library provided by the HDF5 project.

While these common formats may be used to store a lot of financial data used by software in the options and derivative space, these formats are just the beginning of a complete data infrastructure. In most financial firms, this also ranges from data stored in simple text files, comma-separated values (CSV), to more traditional database systems such as Oracle and MySQL. In each of these situations, C++ provides you with access to libraries to perform the necessary tasks.

Index

A

addObserver and removeObserver functions

Application Programming Interfaces (APIs)

Arbitrage

Assignment operator

attribute() function

B

Backtesting

C++ code

corporate actions

create engine

data cleaning/processing

developing engine

historical market data

testing a trading algorithm

trading strategy, designing

validation phase

Basic Linear Algebra Subprograms (BLAS)

Binomial model

algorithm

implementation

price American-style option

prices

tree-based structure

visualization

Binomial tree

Black-Scholes-Merton model

Black-Scholes model

input parameters

numerical solution

PDE

Black-Scholes pricing strategy

Boost libraries

components

histogram

Installation

project repository

repository

simple ODE

solving ODEs

C

C++23

availability

coding skills

expressiveness

financial and options-related programming

international standard

performance

C++ language

auto keyword

building/testing

classes

complete code

computing option value, expiration

initialize variables

print values

techniques

callBasePtr function

CDSSolver::solve

child() function

Clearing corporation/clearing agency

Collateralized debt obligation (CDO)

Comma-separated values (CSV)

Compact date implementation

building/testing

class DateCompact

comparison operators

complete code

date objects

Compilation-time polymorphism

Computational techniques

computePriceStep member function

computeRandomStep member function

Constant maturity CDS (CMCDS)

“constexpr” keyword

Copy constructor

Credit default swap (CDS)

Credit derivative

barrier option

definition

QuantLib

solver class

types

categories

complete code

definition

derivative contracts

modeling

QuantLib

risk of default

Cryptocurrencies

Curve-fitting the strategy

D

Data cleaning

Date and time handling

complete code

date operations

day of the week, calculate

member function

operator++

trading dates

financial applications

DateCompact

dayOfTheWeek()

daysToExpiration member function

Default constructor

DEMathFunction

Derivative

Derivative pricing

SeeFinancial derivatives

Derivative-pricing models

Design patterns

algorithms

coding problems

definition

examples

factory method

singleton

Destructor

diffByOne member function

Differential equations (DEs)

applications

definition

mathematical derivatives

ordinary

Document Type Definition

E

Economic models

Encapsulation

Euler’s method

function

implementation

Leonhard Euler

tangent line

target function

Exchange-traded funds (ETFs)

Exchange-traded notes (ETNs)

EXtensible Markup Language (XML)

F

Factory design pattern

Factory method

Fast-paced investment strategies

Finance, C++ libraries

applications

boost project

QuantLib

Financial applications

Financial data

data processing and analysis

error checking

error handling

example

missing attributes

node existence

XML file/checking errors

XML

XML parsing, C++

XML structure

Financial derivative

arbitrage

binomial tree

CDO

CDS

C++ STL

algorithms

building/testing

complete code

containers

generate random walks

templates

equations

example

fair price

FX

mathematical characteristics

numeric model

random walk model

simulation models

standard agreement

Financial Information eXchange (FTX)

Financial software systems

Functional programming

advantages

bind function

complete code

attributes on lambda

Functional.cpp files

lambda functions

lambda trailing return type

definition

functional predicates

function objects

lambda functions, C++23

style

trading options/derivatives

Function call operator

FX derivatives

G

Gaussian distribution

General derivatives

generateWalk()

generic_max function

Generic/template-based programming

getFunctionRoot function

getIntegral member function

get_uniform_int function

Greeks

H

Hierarchical Data Format version 5 (HDF5)

C++

compile/run

definition

features

trading data

Horner’s method

I, J, K

Inheritance

Integer recursive functions

integrate_adaptive function

Integration algorithms

Investment strategy

isTradingDay member function

L

Lambda function

Lattice models

Linear algebra algorithms

Linear algebra (LA) techniques

complete code

financial derivatives

mathematical operations

matrix implementation

multidimensional subscripts, C++23

traditional methods

uBLAS library

vector operations

scalar-to-vector

vector-to-vector

vectors or matrices

Linear algebra package (LAPACK)

linear_congruential_engine

M

Matrices

Mixed models

Modern computer systems

Monte Carlo methods

creating random walks

definition

example

mathematical models and equations

random number generation

common probability distribution

generator instantiations

generators

generic algorithm

probability distribution

pseudorandom number generation

random walks

simulation-based algorithms

Monte Carlo model

N

Network-based algorithm

Networks

building/testing

complete code

dictionary object

IBM/CAT

shortest path

string-production problem

Newton’s method

class

definition

member functions

sequence of values

next_sibling() function

non-virtual member function

Notify

Numerical analysis

complete code

finding roots of equation

Horner’s method

integration

mathematical functions representation

mathematical techniques

O

Object-oriented programming (OOP)

C++

C++23

class hierarchies

elements

encapsulation

features

high-level abstractions

inheritance

object composition

options and derivatives analysis

polymorphism

polymorphism via virtual tables

software programming styles

virtual destructors

virtual functions

Object-oriented techniques

Observer design pattern

abstract interface

abstract observer interface

complete code

scheme

subject

TradingLedger class

Observer pattern

odeint library

Options

ATM option

automated methods

break-even price

C++ use, financial industry

definitions

exercise

exercise styles

expiration

features

IBM stock

intrinsic value

profit chart

selling

settlement

standard contracts

strike price

trading

underlying instrument

Options investing

Options pricing

binomial model

complete code

lattice models

option contracts

options/derivative contracts

Ordinary differential equation (ODE)

P

Partial differential equation (PDE)

PDE-based algorithms

Polymorphism

print_prices function

processNewTrade function

public keyword

pugixml library

Q

Quantitative financial programming

QuantLib library

Black-Scholes equations, creating solutions

calendars, working

C++ interface

complete code

definition

handling dates

quantitative algorithms

R

RandomWalkGenerator

Random walk model

Recursive function

Resource Acquisition Is Initialization (RAII)

Root-finding algorithms

Runge-Kutta (RK) method

complete code

implementation

improve accuracy

solution

strategies

S

sell() member function

Simulation-based algorithms

Simulation models

Simulation techniques

Singleton pattern

cache objects

clearing house

example objects

Smart pointer

Society for Worldwide Interbank Financial Telecommunication (SWIFT)

sort_dates function

Sorting

Standard template library (STL)

algorithms

C++23

C++ libraries

copying container data

finding elements

selecting option data

sorting

algorithms

definition

frequency data

member function

standard comparison operator

template algorithms

std::chrono library

strcmp function

Structured programming

T

Template-based algorithms

Template-based programming

classes

compile-time polymorphism

container objects

containers/smart pointers

lengthy template instantiations, avoid

C++23

constexpr, C++23

library

preinstantiating templates

performance enhancing technique

practices

recursive function

smart pointers

template function

test_poly_function function

text() function

Traditional database systems

transform function

Tree-based methods

triggerNotifications function

Trinomial model

U

uBLAS functions

V

Vector-to-vector operations

virtual keyword

Visitor pattern

W

Word production

X, Y, Z

XML-based messaging protocols

XML Schema