You are on page 1of 269

Einführung in C++ und

Monte Carlo Methoden im


Financial Engineering

Christoph Becker Uwe Wystup


Inhaltsverzeichnis

I. Einführung in C/C++ 1
1. Programmieren in C 3
1.1. Einleitung . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 3
1.2. Aufbau eines C - Programms . . . . . . . . . . . . . . . . . . . . . . . . . . 3
1.3. Lokale und globale Variablen . . . . . . . . . . . . . . . . . . . . . . . . . . 3
1.4. Funktionen und die main - Funktion . . . . . . . . . . . . . . . . . . . . . . 4
1.4.1. Allgemeiner Aufbau von Funktionen . . . . . . . . . . . . . . . . . 4
1.4.2. Die Main - Funktion . . . . . . . . . . . . . . . . . . . . . . . . . . 6
1.5. Anweisungen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 6
1.5.1. Aufruf von Funktionen . . . . . . . . . . . . . . . . . . . . . . . . . 6
1.5.2. Wichtige Sprachelemente . . . . . . . . . . . . . . . . . . . . . . . 7
1.6. Include - Anweisungen für Bibliotheken . . . . . . . . . . . . . . . . . . . . 11
1.6.1. Einführung . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 11
1.6.2. Wichtige Bibliotheksfunktionen . . . . . . . . . . . . . . . . . . . . 12
1.7. Felder und Zeiger . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 14
1.7.1. Felder . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 14
1.7.2. Zeiger (engl. Pointer) . . . . . . . . . . . . . . . . . . . . . . . . . . 15
1.8. Kurioses zum Abschluss . . . . . . . . . . . . . . . . . . . . . . . . . . . . 17
1.9. Übungsaufgaben . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 19

2. Objektorientierte Programmierung 23
2.0. Prolog . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 23
2.1. Begriffe der objektorientierten Programmierung . . . . . . . . . . . . . . . . 24
2.2. Nicht - objektorientierte Erweiterungen
in C++ . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 26
2.2.1. Kommentare . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 26
2.2.2. Referenzen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 27
2.2.3. Const . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 28
2.2.4. inline-Funktionen . . . . . . . . . . . . . . . . . . . . . . . . . . . . 29
2.2.5. Überladen von Funktionen . . . . . . . . . . . . . . . . . . . . . . . 29
2.2.6. Die Operatoren new und delete . . . . . . . . . . . . . . . . . . . . 29
2.3. Klassen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 30
2.3.1. Syntax . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 30
2.3.2. Statische Objektkomponenten . . . . . . . . . . . . . . . . . . . . . 40

iii
Inhaltsverzeichnis

2.3.3. Abgeleitete Klassen, Vererbung . . . . . . . . . . . . . . . . . . . . 43


2.3.4. Vererbung und Zugriffsrechte . . . . . . . . . . . . . . . . . . . . . 44
2.3.5. Auflösung von Mehrdeutigkeiten . . . . . . . . . . . . . . . . . . . . 47
2.3.6. Initialisierung von Basisklassen . . . . . . . . . . . . . . . . . . . . 48
2.3.7. Virtuelle Basisklassen . . . . . . . . . . . . . . . . . . . . . . . . . 50
2.3.8. Virtuelle Funktionen . . . . . . . . . . . . . . . . . . . . . . . . . . 52
2.3.9. Abstrakte Basisklassen . . . . . . . . . . . . . . . . . . . . . . . . . 54
2.4. Übungen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 55
2.5. Templates . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 57
2.5.1. Vorbemerkungen . . . . . . . . . . . . . . . . . . . . . . . . . . . . 57
2.5.2. Funktionstemplates . . . . . . . . . . . . . . . . . . . . . . . . . . . 58
2.5.3. Klassentemplates . . . . . . . . . . . . . . . . . . . . . . . . . . . . 59
2.5.4. Vorteile und Nachteile von Templates, effiziente Solver . . . . . . . . 61
2.5.5. Die Standard-Template-Library - Ein Überblick . . . . . . . . . . . . 65

II. Einführung in Monte-Carlo Methoden 71


3. Monte Carlo-Methoden 73
3.0. Prolog . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 73
3.1. Monte Carlo-Methoden: Einführung . . . . . . . . . . . . . . . . . . . . . . 74
3.1.1. Idee und grundlegende Konvergenzaussage . . . . . . . . . . . . . . 74
3.1.2. Konfidenzintervalle . . . . . . . . . . . . . . . . . . . . . . . . . . . 74
3.1.3. Konvergenzgeschwindigkeit von Monte Carlo . . . . . . . . . . . . . 76
3.1.4. Erzeugung von Pfaden des Underlying . . . . . . . . . . . . . . . . . 77
3.1.5. Einfache Beispiele . . . . . . . . . . . . . . . . . . . . . . . . . . . 82
3.2. Übungen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 83

4. Techniken zur Varianzreduktion 85


4.1. Vorbemerkungen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 85
4.1.1. Hit-or-miss Monte Carlo . . . . . . . . . . . . . . . . . . . . . . . . 85
4.2. Control Variates . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 88
4.2.1. Idee und Konvergenz des Verfahrens . . . . . . . . . . . . . . . . . . 88
4.2.2. Beispiele . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 90
4.2.3. Konfidenzintervall . . . . . . . . . . . . . . . . . . . . . . . . . . . 92
4.3. Antithetic Variates . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 93
4.3.1. Grundlegende Aussage (Antithetic Variates) . . . . . . . . . . . . . . 93
4.3.2. Beurteilung der Effizienz . . . . . . . . . . . . . . . . . . . . . . . . 93
4.3.3. Beispiele . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 94
4.3.4. Faustregeln für die Effizienz . . . . . . . . . . . . . . . . . . . . . . 96
4.3.5. Varianzzerlegung . . . . . . . . . . . . . . . . . . . . . . . . . . . . 96
4.4. Übungen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 97

iv
Inhaltsverzeichnis

5. Griechen in Monte-Carlo 99
5.1. Pfadweises Ableiten des Prozesses . . . . . . . . . . . . . . . . . . . . . . . 99
5.1.1. Motivation . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 99
5.1.2. Beispiele . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 100
5.1.3. Eine hinreichende Bedingung für die Erwartungstreue . . . . . . . . 102
5.2. Likelihood Ratio Method (LRM) . . . . . . . . . . . . . . . . . . . . . . . . 102
5.2.1. Motivation . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 103
5.2.2. Beispiele . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 103
5.2.3. Bias und Varianz . . . . . . . . . . . . . . . . . . . . . . . . . . . . 105
5.2.4. Zweite Ableitungen . . . . . . . . . . . . . . . . . . . . . . . . . . . 105
5.3. Finite Differenzen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 106
5.3.1. Überblick . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 106
5.3.2. Analyse von Bias und Varianz . . . . . . . . . . . . . . . . . . . . . 106
5.3.3. Beispiel . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 107
5.4. Ausblick . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 108
5.4.1. Zweite Ableitungen . . . . . . . . . . . . . . . . . . . . . . . . . . . 108
5.4.2. Fazit . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 108
5.4.3. Übungen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 109

6. Diskretisierung von Stochastischen Differentialgleichungen 111


6.1. Einleitung . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 111
6.1.1. Existenz und Eindeutigkeit . . . . . . . . . . . . . . . . . . . . . . . 111
6.2. Diskretisierungsschemata . . . . . . . . . . . . . . . . . . . . . . . . . . . . 112
6.3. Güte der Approximation . . . . . . . . . . . . . . . . . . . . . . . . . . . . 113
6.4. Pfadabhängige Payoffs und Diskretisierung . . . . . . . . . . . . . . . . . . 113
6.4.1. Erwartungstreue Schätzer für Lookback- und Barrieroptionen . . . . 114
6.4.2. Weitere Verfahren . . . . . . . . . . . . . . . . . . . . . . . . . . . 120
6.5. Effizienz und Vergleich von Monte Carlo-Schätzern . . . . . . . . . . . . . . 123
6.5.1. Vergleichskriterium für unverzerrte Schätzer . . . . . . . . . . . . . 124
6.5.2. Vergleichskriterium für verzerrte Schätzer . . . . . . . . . . . . . . . 125
6.5.3. Aufteilung des Rechenbudgets auf Bias- und Varianzreduktion . . . . 126
6.6. Übungen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 127

III. Programmierung in der Praxis 131


7. Numerische Stabilität und Effizienz, Fehlerbehandlung, Bezeichnungen 133
7.1. Rundungsfehler . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 133
7.1.1. Elementare Begriffe . . . . . . . . . . . . . . . . . . . . . . . . . . 133
7.1.2. Beispiele . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 134
7.2. Effiziente Implementation von Algorithmen . . . . . . . . . . . . . . . . . . 137
7.3. Anwendung des Debuggers . . . . . . . . . . . . . . . . . . . . . . . . . . . 140
7.4. Fehlerbehandlung . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 142
7.5. Überfüllung des Namensraums . . . . . . . . . . . . . . . . . . . . . . . . . 145

v
Inhaltsverzeichnis

7.6. Übungen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 147


7.7. Bezeichnungskonventionen und Layout . . . . . . . . . . . . . . . . . . . . 149
7.-1.1. Einleitung . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 149
7.-1.2. Allgemein . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 149
7.-1.3. spezielle Konventionen . . . . . . . . . . . . . . . . . . . . . . . . . 150
7.-1.4. Konventionen bei Ausdrücken . . . . . . . . . . . . . . . . . . . . . 151

IV. Finanzmathematik in der Praxis 155


8. Ein einfaches Klassenrahmenwerk aus der Praxis 157
8.1. Programm . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 157
8.1.1. Klassen, die man in jedem Projekt braucht . . . . . . . . . . . . . . . 157
8.1.2. Klassen zur Darstellung des Marktes . . . . . . . . . . . . . . . . . . 160
8.1.3. Klassen für die Finanzinstrumente . . . . . . . . . . . . . . . . . . . 161
8.1.4. Klassen zum Berechnen von TV und Greeks . . . . . . . . . . . . . 164
8.1.5. Ein Klassenrahmenwerk zur Optionsbewertung . . . . . . . . . . . . 166
8.2. Übungen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 170

9. Design Patterns und ein verbessertes Rahmenwerk 171


9.1. Flexibilität als zentrales Problem eines Rahmenwerks . . . . . . . . . . . . . 171
9.2. Design Patterns - ein kurzer Überblick . . . . . . . . . . . . . . . . . . . . . 174
9.3. Ein verbessertes Rahmenwerk . . . . . . . . . . . . . . . . . . . . . . . . . 176
9.3.1. Payoffs . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 176
9.3.2. Optionen . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 180
9.3.3. Eine Parameterklasse . . . . . . . . . . . . . . . . . . . . . . . . . . 190
9.3.4. Statistische Auswertung der Daten . . . . . . . . . . . . . . . . . . . 197
9.3.5. Implementation von Zufallsgeneratoren . . . . . . . . . . . . . . . . 203
9.3.6. Kurzer Rückblick . . . . . . . . . . . . . . . . . . . . . . . . . . . . 207
9.4. Ein flexibler Monte-Carlo Option Pricer . . . . . . . . . . . . . . . . . . . . 209
9.4.1. Aufgaben und Klassenstruktur . . . . . . . . . . . . . . . . . . . . . 209
9.4.2. Kommunikation und Zusammenspiel der Klassen . . . . . . . . . . . 211
9.4.3. Implementation . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 212

10.Ausblick: Nützliches für den Alltag 221


10.1. Erstellung von DLLs . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 221
10.1.1. Erstellung einer Dynamic Link Library mit Borland C++Builder . . . 222
10.1.2. Erstellung einer Dynamic Link Library mit Microsoft Visual C++ 6.0 223
10.1.3. Erstellung eines Add-ins für Microsoft Excel . . . . . . . . . . . . . 224
10.2. Interview mit einem Quant . . . . . . . . . . . . . . . . . . . . . . . . . . . 229

V. Anhang 233

vi
Inhaltsverzeichnis

Besprechung der Übungsaufgaben 235


10.3. Übung Nr.1 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 235
10.3.1. Teil a) . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 235
10.3.2. Teil b) . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 236
10.4. Übung Nr.2 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 237
10.5. Übung Nr. 3 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 237
10.6. Übung Nr. 4 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 238
10.7. Übung Nr. 5 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 240
10.7.1. Teil A . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 240
10.7.2. Teil B . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 240
10.8. Übung Nr. 7 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 241
10.9. Übung zu LRM und PM . . . . . . . . . . . . . . . . . . . . . . . . . . . . 242
10.10.Aufgabe zu Diskretisierung . . . . . . . . . . . . . . . . . . . . . . . . . . . 242
10.11.Übungen zu C++ . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 244
10.11.1.Übung Nr. 7 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 244
10.11.2.Übung Nr. 8 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 245
10.11.3.Übung Nr. 9 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 246
10.11.4.Übung Nr. 10 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 247
10.11.5.Übung Nr. 11 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 248
10.11.6.Übung Nr. 12 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 249
10.11.7.Übung Nr.13 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 249
10.11.8.Übung Nr.14 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 251
10.11.9.Übung Nr.15 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 252

Literaturhinweise 253

vii
Inhaltsverzeichnis

viii
Teil I.

Einführung in C/C++

1
1. Programmieren in C
1.1. Einleitung
Der folgende Teil soll lediglich dazu dienen, Ihnen die für den weiteren Verlauf notwendi-
gen Elemente der Sprache C zu vermitteln. Für Details verweisen wir an dieser Stelle auf
die Literaturhinweise bzw die Onlinehilfe ihrer Programmierumgebung. Wir starten mit dem
typischen Aufbau eines C-Programms und besprechen dann die einzelnen Teile in den voran-
gestellten Kapitelnummern.

1.2. Aufbau eines C - Programms


IV # include - Anweisungen für Bibliotheken

IV # include - Anweisungen für eigene Header - files

I Definition globaler Variablen

II void main (){


I lokale Variablen
III Anweisungen
}

II Funktion1 {
I lokale Variablen
III Anweisungen
}

Funktion2 {
s.o.
}

1.3. Lokale und globale Variablen


In C gibt es folgende für uns relevante Datentypen:

i) Integer ( ganzzahlig, (-32000, 32000 ), Bsp.: int a;


Long ( ganzzahlig, (- 2 Mrd, + 2Mrd ), Bsp.: long a;

3
1. Programmieren in C

Bem.: In vielen Programmierumgebungen ist int ≡ long.

ii) Gleitkomma ( 15 signifikante Stellen, bis ±10308 ) , Bsp.: double a;

iii) Buchstaben = einzelne Zeichen, Bsp.: char zeichen = ’m’;

Bemerkung
In C sind Strings kein direkter Bestandteil der Sprache, sondern sind als Felder von char zu
bilden.

1.4. Funktionen und die main - Funktion


1.4.1. Allgemeiner Aufbau von Funktionen

Funktionstyp Funktionsname ( Parameterliste ) {


Lokale Variablen
Anweisungsteil
return Rueckgabewert
}
Bem.:
i) ”Funktionstyp” ∈ { int, long, char, double, void } gibt den Typ der Variable
an, die die Funktion zurückgibt. Handelt es sich um eine Prozedur im Sinne von Turbo
Pascal, d.h. das Unterprogramm liefert keinen Wert zurück, dann wählt man den Funk-
tionstyp ”void”.

ii) Mit dem Befehl ”return variable” liefert die Funktion ihren Rückgabewert und beendet
sich selbst.

iii) Geschweifte Klammern dienen zum Einrichten von ”Befehlsblöcken” und werden sehr
häufig benutzt. Unter anderem macht man damit Anfang und Ende einer Funktion kennt-
lich. Geschweifte Klammern kommen an den unterschiedlichsten Stellen im Programm
vor, etwa zur Abgrenzung des Befehlsblocks einer for - Schleife statt eines ”end”wie in
Basic. Stets kann man in geschweiften Klammern eigene interne Variablen definieren.
Dies wird in Beispielen später sicher verständlicher.

iv) Die Parameterliste sieht so aus: Datentyp variable1, Datentyp variable2,...

v) Lokale Variablen leben im innersten {...} - Block, der sie umgibt und überdecken gleich-
namige Variablen, die ausserhalb dieses {} - Blockes stehen, z.B. globale Variablen, die
ganz am Anfang des Programms ausserhalb von jeder Funktion definiert werden.

Bsp.:

4
1.4. Funktionen und die main - Funktion

int quadriere ( int a ){


int temp ; // Definition einer lokalen Variablen
temp = a * a ; // Wertzuweisung
return temp ;
}
das geht allerdings auch kürzer:

int quadriere( int a){


return a * a;
}

Beachte:

i) Fasst man die beiden Zeilen im ersten Beispiel zu

int temp = a * a;

zusammen, so sagt man auch: Die Variable temp wird mit dem Wert a * a initiali-
siert. So wie es jetzt dasteht, enthält die Variable temp für eine Programmzeile einen
zufälligen Wert.
ii) Der Unterschied ”Definition und spätere Wertzuweisung” bzw. ”Definition und gleich-
zeitige Initialisierung”wird in C++ wichtig werden. Für den Moment ist nur wichtig,
dass der Compiler nach ”int temp;”die Variable temp auch schon auf der rechten Seite
des Gleichheitszeichens akzeptieren würde, was höchst zweifelhafte Ergebnisse zutage
bringt. Manche Compiler liefern einen Fehler, manche eine Warnung, andere gar nichts.
iii) In C sieht ein Kommentar so aus: /* Kommentar */, in C++ so:

// Kommentar für 1 Zeile

Gewöhnen Sie sich besser an die C++ - Variante, der Grund dafür ist eine kleine Spitz-
findigkeit, die auf Dauer in der Praxis aber recht nützlich ist. Stellen Sie sich folgenden
Code in Ihrem Programm vor:

// ...irgend etwas
myfunction1();
/* myfunction2(); */
myfunction3();
// ...irgend etwas

Die Funktion myfunction2 sei aus Gründen der Fehlersuche probehalber auskommen-
tiert worden und nun entschliessen Sie sich, probehalber alle hier dargestellten Funkti-
onsaufrufe auszukommentieren. Mit der C++-Variante // können Sie jeweils nur eine
Zeile auskommentieren, also versuchen Sie es mit

5
1. Programmieren in C

// ...irgend etwas
/* myfunction1();
/* myfunction2(); */
myfunction3(); */
// ...irgend etwas

Das Ergebnis wird sein, dass der Kommentar hinter myfunction2(); */ enden wird,
was nicht der Fall gewesen wäre, wenn Sie zuerst // myfunction2() geschrieben
hätten. Kurz: In der Praxis kommentiert man auch mehrere Zeilen nur zeilenweise mit
// aus, um sich das auskommentieren sehr grosser Blöcke im Zweifelsfall zu erleich-
tern.

1.4.2. Die Main - Funktion


Die Funktion bzw Prozedur mit Namen ”main” ist in C / C++ das Hauptprogramm. Ob man
nun void main() oder int main() schreibt ist letztlich nicht so wichtig, ein Rückgabewert
der main - Funktion kann dem Betriebssystem aber als Hinweis dienen, dass das Programm
korrekt oder eben mit Fehler beendet wurde.
Beachte: void main() ist eine Prozedur im Turbo Pascal / Basic - Sinne, int main() eine
Funktion.

Bsp.:

void main() { int main(){


// tue etwas // tue etwas
} return 0; // alles okay
return -1; // zeigt dem Betriebssystem Fehler an,
// wird aber im Programm nie erreicht
}

Bem.:
Ausserhalb von ”main” definierte Variablen sind in der ganzen Quelltextdatei sichtbar, durch
den Zusatz ”extern Typ Variable” zu Beginn einer zweiten Quelltextdatei wird sie sogar
dort sichtbar.

1.5. Anweisungen
1.5.1. Aufruf von Funktionen
Andere Funktionen ruft man auf, wie man es erwarten würde, also:

void main(){
int ergebnis = quadriere(5);
}

6
1.5. Anweisungen

1.5.2. Wichtige Sprachelemente


1.5.2.1. Die ”if - else”- Kontrollstruktur
Bsp.:

if( a <= b ) Befehl;

Also: Die Bedingung steht hinter dem Schlüsselwort if in Klammern, anschliessend folgt
der Befehl, der ausgeführt werden soll sofern die Bedingung wahr ist. Möchte man mehrere
Befehle in Abhängigkeit vom if - Befehl ausführen, so geschieht das folgendermassen:

if( a <= b ){
Befehl1;
Befehl2;
// ...
}

Hier wird also in geschweiften Klammern ein Befehlsblock ausgeführt.

Warnung: Dieser Unterschied ist ein äusserst beliebter Anfängerfehler und wird
auch für die for- und while-Schleife wichtig sein!
Ein if - else kann man mit Befehlsblöcken folgendermassen formulieren:

if( a <= b){


// Befehlsblock
}
Dies führt direkt zu
if( a > b ){
// Befehlsblock 1
} else if( a < b ){
// Befehlsblock 2
} else {
// Block 3
}

Bem.:
Nach } kommt kein Strichpunkt! Strichpunkte kommen normalerweise am Ende eines Be-
fehls.

1.5.2.2. Die for - Schleife


Beispiel

int i;
for( i = 0; i <= 10; i++) Befehl1;

7
1. Programmieren in C

Diese Schleife würde also 11x den Befehl1 ausführen. Möchte man hingegen mehrere Be-
fehle ausführen, so sieht dies analog zum if - Befehl so aus:

int i;
for(i = 0; i <= 10; i++) {
Befehl1;
Befehl2;
// ...
}

Bemerkung

i) for ( Startwert, Abbruchkriterium, Inkrement )

ii) der Befehl i++; steht kurz für i = i +1 und wird häufig benutzt.

iii) Oft sieht man for( int i = 0; i <= 10; i++) {...}. Dann hat i genau den Schlei-
fendurchlauf als Lebensdauer und man kann den Variablennamen i ansonsten ander-
weitig im Programm verwenden. Existiert vor Schleifendurchlauf bereits eine Variable
beliebigen Typs mit Namen i, so wird diese für den Durchlauf der Schleife überdeckt.
Möchte man im Befehlsblock der for - Schleife dennoch auf die äussere Variable i zu-
greifen, so geschieht dies durch den scope - Operator ::. In folgendem Fragment wird
im letzten Schleifendurchlauf die äussere Variable i auf 36 gesetzt.

int i = 3;
for(int i=0; i < 10; i++){
if( i==9)
::i = 4;
}
// nun i = 36

iv) Mit ”break;”kann eine Schleife vorzeitig abgebrochen werden. Sie sollten diesen Be-
fehl sparsam verwenden, weil er leicht zu unübersichtlichen Programmen führt. Wichtig
ist auch, dass break bei geschachtelten for - Schleifen nur aus der innersten for-Schleife
herausführt. Im Zweifelsfall muss man sich dann mit dem goto - Befehl behelfen, für
den diese Situation die wohl einzig sinnvolle Anwendung darstellt.

v) Möchte man ein anderes Inkrement als plus 1 für die Laufvariable, so schreibt man etwa

for( i = 0; i < 10; i = i + 3 ) Befehl;

vi) Wer von einer Interpreter-Sprache wie etwa Visual Basic oder Matlab gewohnt ist, for-
Schleifen zu vermeiden wegen der schlechten Laufzeiteigenschaft, der sei hier beru-
higt: For-Schleifen sind in C / C++ äusserst effizient und sollten in laufzeitkritischen
Phasen wie etwa einer Monte-Carlo-Simulation nicht vermieden, sondern insbesondere
dort verwendet werden.

8
1.5. Anweisungen

1.5.2.3. Die while - Schleife


Variante1: Kopfgesteuerte Schleife Variante2:Fussgesteuerte Schleife
while( a > 0 ) do{
// tue etwas // tue etwas
a--; }while( Bedingung );
}

Bemerkung
Manche Programmierer finden die fussgesteuerte Schleife unübersichtlich, weil man erst bis
zum Ende der Schleife schauen muss, um das Konstrukt ganz verstehen zu können. Die Verfas-
ser finden sie hingegen manchmal sehr elegant und praktisch, wenn man sowieso mindestens
einen Durchlauf durchführen möchte. Entscheiden Sie selbst!

1.5.2.4. Der switch - Befehl


Dies ist das Analogon zu select - case in Basic:

switch( variable ){ if( variable == wert1){


case wert1: // Anweisungen
// Anweisungen } else if (variable == wert2){
break; // Anweisungen
case wert2: } else {
// Anweisungen // ansonsten
break;
default:
// ansonsten
}

Bemerkung

i) Ohne den Befehl ”break;” am Ende der case -Verzweigung fällt das Programm durch”,
d.h. es geht nicht wie erwartet an das Ende des switch - Blocks sondern führt die nächste
Verzweigung aus. In seltenen Fällen ist das erwünscht und dann sollte man das mit //
FALL THROUGH kommentieren.
ii) Ein Vorteil des switch - Befehls ist die automatische Dokumentation des Programm: Ein
switch zeigt dem Leser unmittelbar an, dass nun eine längere Fallunterscheidung folgt,
während das bei mehreren if-Befehlen weniger klar ist.
iii) Der Hauptvorteil des switch-Befehls liegt aber in folgender Tatsache begründet: Ir-
gendwo in Ihrem Programm sei der Typ ´´Greek´´ und eine Variable diesen Typs defi-
niert:

enum GREEK{ TV, DELTA, GAMMA };


GREEK userChoiceGreek;

9
1. Programmieren in C

Später möchten Sie je nach Benutzerwahl die passende Formel aufrufen. Dies könnte so
aussehen:

switch(userChoiceGreek){
case TV:
// ...
case DELTA:
// ...
case GAMMA:
// ...
default:
// Fehlermeldung oder return -1;
}

Wenn Sie nun Ihr Programm erweitern und im Variablentyp Greek die Auswahl VE-
GA hinzufügen, dann hätten Sie in einem grossen Programm schnell Schwierigkeiten,
alle Stellen zu finden, in denen Sie Ergänzungen vornehmen müssen. Praktisch wird
es wahrscheinlich sogar so sein, dass Sie Fehlermeldungen ernten, deren Ursprung Sie
erst mit einiger Mühe zuordnen müssen, und wenn Sie als default-Fehler return -1;
zurückgeben, was nicht abwegig sein muss, dann wird die Fehlersuche eine kleine Her-
ausforderung. Nicht so in diesem Programm mit switch: Der Compiler wird erkennen,
dass Sie alle Realisierungen der Variablen Greek abfragen und sollte im Normalfall eine
Warnung ausgeben und Sie somit auf diese Stelle hinweisen.

iv) Auf ein kleines praktisches Ärgernis sei noch hingewiesen:


Im obigen Programm könnte

case DELTA:
int i;
// ...

zu einer Fehlermeldung führen, weil hier in einem switch-Zweig eine Variable definiert
wird. Dann hilft es, diesem Zweig einen eigenen Befehlsblock zuzuordnen,d.h.

case DELTA:{
int i;
// ...
}

Nun sollte wieder alles funktionieren.

10
1.6. Include - Anweisungen für Bibliotheken

1.6. Include - Anweisungen für Bibliotheken


1.6.1. Einführung
In C und C++ sind Routinen zur Datenein- / ausgabe nicht in der Sprache integriert, sondern
nachträglich vom Compilerbauer hinzugefügt worden. Dies hat den Nachteil, dass man die
entsprechenden Routinen immer explizit einbinden muss, und den Vorteil, dass nur das im
Programm enthalten ist, was man auch braucht. Dies ist ein Grund für den relativ geringen
Ressourcenverbrauch und die Ausführungsgeschwindigkeit von C-Programmen. Ein weiterer
( vielleicht theoretischer ) Vorteil ist, dass die Sprache C so klein ist, dass man sie realistisch
komplett erlernen kann.
Die Routinen stehen dann im Programm zur Verfügung, wenn man die passende Headerdatei
am Programmanfang einfügt.

Def. Headerdatei / Headerfile

In einem Headerfile werden Funktionen und manchmal auch Variablen deklariert (d.h. dem
Linker wird die Existenz dieser Dinge mitgeteilt), die irgendwo in anderen Quellcodedateien
definiert werden. Ein Headerfile hat normalerweise die Endung .h und wird mit dem include -
Befehl eingefügt.
Bemerkung

i) Der include - Befehl ist ein Befehl für den Präprozessor, der vor dem eigentlichen Über-
setzungsvorgang tätig wird. Deswegen wird diesem Befehl auch das Präprozessor -
typische # vorangestellt. Also: Dort wo ”#include Pfad der Datei” steht, wird automa-
tisch vor der Compilierung der Inhalt der angegebenen Datei eingefügt. Ein ”;” am
Ende der Zeile wäre hier falsch. Oft finden Sie den Pfad der include - Anweisung nicht
in Anführungszeichen, sondern in spitzen Klammern <>. Der Unterschied ist, dass dann
nach der Datei in anderen Unterverzeichnissen gesucht wird, die in den Umgebungs-
optionen des Projekts eingestellt werden können. Praktisch benutzt der Autor stets die
Anführungszeichen und alles funktioniert.

ii) int test( int a ){ ... } ist eine Definition (sprich eine Implementation) der Funk-
tion test. Eine Definition ist gleichzeitig eine Deklaration und kann nur einmal in allen
Quelltextdateien eines Projektes vorkommen. Deklarationen hingegen können beliebig
oft erscheinen und sehen so aus: int test (int a); Dies ist ein Hinweis an den Lin-
ker, dass irgendwo anders eine solche Funktion definiert wird, es diese also gibt.

Bevor wir auf wichtige Bibliotheksfunktionen eingehen, erklären wir kurz, wie man ein
Programm auf mehrere Dateien verteilt:

Im folgenden finden Sie ein Programm das aus einem Hauptprogramm ”Hauptprog.cpp”besteht
und einer Datei ”Arbeitstier.cpp”, in dem die wichtigste Funktion definiert wird. Damit man
diese aus Hauptprog.cpp aufrufen kann, wird ein Headerfile Arbeitstier.h geschrieben und

11
1. Programmieren in C

per include am Anfang von Hauptprog.cpp eingefügt. Das ist alles. In ihrer Programmier-
umgebung müssten Sie wahrscheinlich noch die Datei ”Arbeitstier.cpp” explizit ihrem Pro-
jekt hinzufügen. In einer Entwicklungsumgebung wie Borland C++Builder oder Microsoft
Visual Studio würde man das so realisieren:

1. Unter Datei/Neu erstellen Sie eine Konsolenanwendung, ggf. ohne MFC, VCL oder
welche Zusatzbibliotheken Ihnen auch zunächst angeboten werden. Diese erleichtern
zwar die Programmierung unter Windows, sprengen aber an dieser Stelle den Rahmen
und sind Thema dicker Bücher. Diese Datei speichern Sie unter Hauptprog.cpp.

2. Erstellen Sie unter Datei/Neu eine header-Datei mit dem passenden Namen und Inhalt
wie angegeben. Sollte das nicht zur Auswahl stehen, erzeugen Sie eine leere Textdatei
und speichern Sie mit passendem ab Namen ab.

3. Analog erstellen Sie nun die Datei Arbeitstier.cpp

4. Lassen Sie Ihr Programm laufen. Es könnte sein, dass die Datei Arbeitstier.cpp nicht ge-
funden wird, dann müssen Sie diese Datei explizit Ihrem Projekt hinzufügen unter dem
Menüpunkt Projekt / hinzufügen ( oder ähnlich ). Header-Dateien werden dem Projekt
unter Borland nicht hinzugefügt, diese werden lediglich per include - Befehl eingefügt
und sollten im gleichen Verzeichnis wie die übrigen Projektdateien liegen, damit Sie
gefunden werden. Unter Microsft Visual C++ müssen Headerfiles wie sourcefiles dem
Projekt hinzugefügt werden. Sollten die Projektdateien dann immer noch nicht gefunden
werden, muss noch der Pfad des Projekts in den Projekteinstellungen ggf an mehreren
Stellen hinzugefügt werden.

-----Datei Arbeitstier.cpp------ ------Datei Arbeitstier.h-------

int quadriere ( int zahl ){ int quadriere( int zahl);


return zahl * zahl;
}

-----Datei Hauptprog.cpp-------
#include Arbeitstier.h

void main(){
int ergebnis = quadriere( 5 );
}

1.6.2. Wichtige Bibliotheksfunktionen


1.6.2.1. Datenausgabe
printf("Hallo Welt");
printf("Ergebnis der Rechnung : %lf", meineDoubleVariable);

12
1.6. Include - Anweisungen für Bibliotheken

1.6.2.2. Dateneingabe
double spot;
printf(" Wo steht der Dollar denn heute? \n");
// \n heisst "neue Zeile beginnen"
scanf( " %lf", &spot);
Bemerkung
&spot liefert die Speicheradresse der Variablen spot. Scanf benötigt diese, um in spot etwas
abspeichern zu können. ( Call by reference im Gegensatz zum üblichen Call by value). Das
kryptische Symbol %lf liefert printf und scanf den Datentyp der Variable. Praktisch rele-
vant sind:

%d Integer
%lf Double
%c Char
%s String = Char-array (s. 1.7)

1.6.2.3. Schreiben in eine Datei


Das folgende Programm speichert den Wert von spot in der Datei ”C:\test.txt”.
double spot = 45.10;
File* datei; // Pointer auf Datenstruktur mit Namen File
// Erklärung folgt in Bemerkung iii)
datei = fopen( "C:\\test.txt", "w+");

fprintf( datei, "Der Spot steht heute bei \n");


fprintf( datei, "%lf USD", spot);
fclose( datei );
Bem.
i) Allgemein: fprintf( Dateizeiger, Formatstring, Argument1, Argument2,...)
ii) Alle diese Befehle sind in der Bibliothek StandardInputOutput enthalten, um sie zu
benutzen, fügen Sie am Beginn der Quellcodedatei ( = .cpp - Datei), in der Sie den
Befehl aufrufen, lediglich ”#include <stdio.h>” ein. Das ist alles.
iii) In der C-Bibliothek findet sich in stdio.h insbesondere eine Datenstruktur File, die der
Dateiverwaltung dient. Daran müssen Sie sich allerdings nicht weiter stören, verwenden
Sie den Befehl wie oben angegeben, das ist es, was man praktisch meist braucht. Der
Ausdruck ”w+” im Aufruf der Funktion fopen gibt den Zugriffsmodus an, in diesem
Fall wäre das ”Erzeugen einer Datei für Lese- und Schreiboperationen”, bei Existenz
der Datei wird diese überschrieben. Weitere Modi finden Sie in der Referenz zur Lauf-
zeitbibliothek von C bei der Beschreibung des Befehls fopen. Mathematische Funktio-
nen finden Sie in math.h usw, stöbern Sie ruhig ein wenig in der Onlinehilfe, Abschnitt
”Referenz der C - Laufzeitbibliothek”.

13
1. Programmieren in C

Die aufmerksame Leserin wird sich über den Ausdruck C:\\test.txt im fopen-Befehl wun-
dern. Der Grund ist, dass in einer Zeichenkette der Backslash eine Sonderbedeutung hat (man
denke an das bereits vorgestellte \n, mehr in der C-Referenz) und wenn er nun explizit gemeint
ist, dann geschieht das über einen doppelten Backslash. Die noch aufmerksamere Leserin wird
sich nun wundern, dass ihr hier der Begriff der Zeichenkette bzw des Strings unmerklich un-
tergejubelt wird, obwohl es das in C doch wie am Anfang behauptet nicht gibt. Dies wird nun
im folgenden Kapitel nachgeholt.

1.7. Felder und Zeiger


1.7.1. Felder
Felder werden in C wie erwartet realisiert:

double feld[10];

liefert also 10 Variablen vom Typ double, die über feld[0],... feld[9] angesprochen
werden.

Bemerkung

i) Die Zählung beginnt bei Null. Das ist ein ausgesprochen beliebter Fehler bei Einstei-
gern!! C prüft nicht auf Korrektheit der Feldindizes, d.h. feld[10] wird vom Compiler
akzeptiert. Stehen an dieser Stelle wichtige Daten des Betriebssystems, kann das zu
völlig unregelmässigen Abstürzen des Programms / Computers führen.

ii) Strings werden als Felder vom Typ char realisiert:

char Text[] = "Hallo"

ergibt ein Feld mit 6 (sic!) char - Elementen, nämlich Text[0] = ’H’, . . . , Text[4] = ’o’,
Text[5] = ’\0’. Das Symbol ’\0’ wird von C automatisch angefügt und signalisiert das
Ende des Strings. Möchte man explizit Platz für eine Zeichenkette reservieren, braucht
man also stets einen char - Platz mehr.

iii) Beachten Sie folgenden wichtigen Unterschied:


char myChoice = ’y’; liefert eine char-Variable mit Namen myChoice. Dafür wird
im Speicher genau ein Byte benötigt.
char myChoice[] = "y" liefert ein Feld von char-Variablen, der Speicherverbrauch
beträgt zwei Byte: 1 Byte für das ’y’, ein Byte für die abschliessende ’\0.

14
1.7. Felder und Zeiger

1.7.2. Zeiger (engl. Pointer)


Pointer sind das zentrale Sprachelement von C überhaupt. Pointer sind effizient, flexibel, und
bei erfahrenen C - Programmierern deswegen sehr beliebt. Pointer führen leicht zu Zugrif-
fen auf Speicherbereiche, die man nicht haben wollte, sie sind manchmal kompliziert in der
Schreibweise, sie sind für Anfänger oft verwirrend, Pointer sind bei C - Anfängern deswegen
sehr unbeliebt.

Def.
Ein Zeiger auf eine Variable x enthält die Speicheradresse von x. Durch *Zeiger greift man
auf den Inhalt der Speicherstelle zu, also auf das, was man normalerweise unter der Variable
x versteht.
Dieser Sachverhalt kann gelegentlich zu Verwirrungen führen. Vielleicht hilft Ihnen die fol-
gende Analogie: Ein Haus (unsere Variable) hat eine Adresse, um die kümmert ein Zeiger sich.
Ferner hat es einen Bewohner, und mit dem haben wir uns bisher immer beschäftigt wenn wir
von einer Variable gesprochen haben. Hier ein Beispiel

double x = 10;
double *ptr; // Definition des Zeigers

ptr = &x; // der Zeiger enthält nun die Adresse von x.


// Sprechweise : "ptr zeigt auf x"
*ptr = 8; // d.h. x = 8;

Bemerkungen

i) Mit Zeigern realisiert man Call by reference

void main(){ void quadriere( int* x ){


int x = 10; *x = ( *x ) * ( *x );
quadriere( &x ); // x = 100; }
}

An dem Beispiel auf der rechten Seite sieht man, zu welch unschönen Ausdrücken Poin-
ter gelegentlich führen können.

ii) Mit Zeigern durchläuft man Felder sehr effizient, wenn man sich die Mühe macht, ein
Zeichen am Ende zu speichern, das das Feldende signalisiert.

double aktienkurs[101] = { 1, 1.01, ... , 1.99, -1};


double * zeiger = &aktienkurs[0];
while (*zeiger != -1 ){ //durchlaufe das Feld
if( *zeiger > barrier ) // tue etwas...
zeiger++; // rutsche im Feld eine double - Variable weiter
}

15
1. Programmieren in C

Auch auf Zeiger kann man den Operator ++ anwenden, und hier bedeutet das zunächst
ganz allgemein , dass der Zeiger ”um eins erhöht wird”. Genauer gesagt enthält die
Variable Zeiger eine Speicheradresse und diese wird etwa um 1 Byte erhöht, wenn der
Zeiger vom Typ char* ist, um 4 Byte wenn er vom Typ double* ist usw, je nach dem
worauf er zeigt. Um die exakte Grösse brauchen Sie sich keinerlei Gedanken zu machen,
das macht der Compiler für Sie. Wenn Sie ein Feld durchlaufen, müssen Sie natürlich
sicherstellen, dass Ihnen die Anfangsadresse des Feldes, das Sie bearbeiten möchten,
nicht verloren geht. In obigem Beispiel ergibt sich das ganz natürlich, denn der Feldan-
fang ist vor wie nach der Bearbeitung &aktienkurs[0].

iii) Vielleicht hat Sie die Verwendung des * im letzten Beispiel etwas verwundert, dazu sei
folgendes gesagt: Die genaue Position des Sterns in der zweiten Zeile des Beispiels ist
irrelevant, genauso gut ginge z.B.

double* zeiger = &aktienkurs[0];

Weiterhin könnte Sie verwirrt haben, dass in der zweiten und dritten Zeile auf der linken
Seite des Gleichheitszeichens jeweils ein Ausdruck mit ”*” steht, auf der rechten Seite
hingegen einmal eine Adresse und einmal der Wert der Variablen an der Speicherstelle,
sozusagen der Bewohner um in obiger Analogie zu bleiben.
Erklärung: Steht links vom Gleichheitszeichen ein ”*”vor dem Pointer, so wird der Poin-
ter dereferenziert und damit wird der Bewohner der Adresse angesprochen. Einzige
Ausnahme ist die Definition des Pointers, dort wird ebenfalls der Stern benutzt, und
wenn man den Zeiger mit einem Wert initialisieren will, dann muss dort eine Adresse
stehen, und das kann anfangs etwas seltsam anmuten.

iv) Für C - Neulinge verwirrend ist, dass Felder und Pointer ”das gleiche”sind. Statt

&aktienkurs[0]

könnte man im Programm auch einfach aktienkurs schreiben, d.h. der Name des Fel-
des ist gleichzeitig ein Pointer und enthält die Adresse des ersten Elements im Feld.

v) Dynamische Speicherreservierung erfolgt über Pointer:

double* kurs = (double*) malloc( Anzahl * sizeof(double) );


// arbeite mit kurs...
free( kurs ); //Platz freigeben, wichtig!

In C reserviert man zur Laufzeit Speicher mit dem Befehl malloc. Dieser erwartet die
Anzahl der zu reservierenden Bytes, was hier mit dem Schlüsselwort sizeof gelöst wird.
Dieses Schlüsselwort werden Sie wahrscheinlich auch nur in diesem Zusammenhang
brauchen. Anschliessend liefert malloc einen allgemeinen Zeiger, einen Zeiger auf
void, zurück, der noch in das gewünschte Format konvertiert werden muss, wie man
im Beispiel sieht.

16
1.8. Kurioses zum Abschluss

1.8. Kurioses zum Abschluss


i) Ein Strichpunkt dient nicht zum Trennen von Befehlen wie etwa in Pascal, sondern zum
Abschluss von Befehlen. Die Zeile

ist also eine ’leere’ Anweisung. Ein häufiger Anfängerfehler ist:

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


Befehl;

Dies führt 100 mal eine leere Anweisung aus und einmal Befehl.
ii) i++ bedeutet i = i + 1; i– bedeutet i = i - 1; i *= zahl bedeutet i = i * zahl
Ein guter Compiler sollte diese Ausdrücke natürlich gleichwertig in Maschinensprache
übersetzen, im Zweifelsfall liefert i++ aber die effizientere Variante. Überhaupt wird
dem Compiler durch Verwenden dieser Kurzformen die Optimierung sicherlich erleich-
tert.
iii) Werte für double - Variablen sollte man stets mit Dezimalpunkt schreiben, d.h.

double var = 3.0 / 5.0; // okay, var = 0.6


double var = 3 / 5; // falsch, weil jetzt var = 0

Dies ist ein häufiger Fehler von Einsteigern in C, denn es gilt


Integer / Integer = Integer, und die Nachkommastellen gehen verloren.
iv) Sehr suggestiv sieht auch folgender Code aus:

char* meinErstellterString(){
char meinString[] = "Loesung";
/* ... */
return meinString;
}

Sicher ist gewollt, dass die Funktion den erstellten String in irgendeiner Form zurück-
gibt, so wie man es von anderen in dieser Hinsicht angenehmeren Sprachen vielleicht
kennt. Tatsächlich passiert folgendes:
In der Funktion meinErstellterString wird ein lokales Feld vom Typ char angelegt
und mit einem Wert belegt. Später wird ein Zeiger auf dieses Feld an den Aufrufen-
den geliefert und die Funktion beendet. Mit dem Ende der Funktion endet auch die
Lebensdauer des lokalen Felds und der Speicher wird freigegeben. Der Aufrufende der
Funktion erhält also einen Zeiger auf eine zur Verwendung freigegebene Speicherstelle.
Wann das Programm einen Fehler liefern wird und welchen genau kann von Programm-
durchlauf zu Programmdurchlauf variieren.

17
1. Programmieren in C

v) Die Problematik, dass Computer Zahlen unweigerlich runden müssen, wird später noch-
mals in einem eigenen Kapitel behandelt werden. Hier schon mal ein vielleicht sehr
illustratives Beispiel:

#include sstdio.h"

void main(){

double var1 = 7.0/12.0;


double var2 = 1.0/12.0;
double var3 = 7.0 * var2;

if(var1 == var3)
printf("Vergleich richtig!\n");
else
printf("Vergleich falsch!\n");
}

Die Ausgabe des Programms ist:

Vergleich falsch!

Variablen vom Typ double oder float miteinander zu vergleichen ist selten eine gute
Idee, besser wäre:

#include sstdio.h"
#include "math.h"

void main(){

double var1 = 7.0/12.0;


double var2 = 1.0/12.0;
double var3 = 7.0 * var2;
if( fabs(var1 - var3) <= pow(10,-12) )
printf("Vergleich richtig!\n");
else
printf("Vergleich falsch!\n");
}

Nun funktioniert es. Auf die Potenzfunktion muss hier kurz hingewiesen werden: Ob-
wohl diese sehr bequem daherkommt, sollte man beim Berechnen einfacher Potenzen
lieber auf sie verzichten, also

ergebnis = pow(var,3); // so nicht!


ergebnis = var * var * var; // besser

18
1.9. Übungsaufgaben

Der Grund liegt in der Laufzeiteffizienz: Die pow-Funktion nutzt stets den Zusam-
menhang xa = exp(a log(x)), und das kostet Rechenzeit, denn exp und log sind we-
gen der nötigen Approximation recht aufwendig. In unserem Fall ist der Ausdruck
pow(10,-12) aber kein Problem: Ein guter Compiler wird erkennen, dass es sich hier
um einen bereits zur Übersetzungszeit bestimmbaren Ausdruck handelt und die nume-
rische Konstante im fertigen Programm verwenden.

1.9. Übungsaufgaben
Aufgabe 1. Teil a)
Schreiben Sie eine Funktion zur Berechnung des TV für den Call im Black-Scholes-Modell.
Ein Hauptprogramm soll die benötigten Daten vom Benutzer abfragen, eine geeignete Funk-
tion aufrufen und das Ergebnis ausgeben. Experimentieren Sie beim Funktionsaufruf mit den
Varianten Call by value und Call by reference. Welche ist hier geeigneter?
Auf der CD finden Sie eine Implementation der Verteilungsfunktion der N(0, 1) - Verteilung.
Die Black-Scholes-Formel für den Preis eines Vanilla Call lautet:

C = S0 exp(−r f T )N(d+ ) − Ke−rd T N(d− )

wobei

f = S0 exp (rd − r f )T forward
+
log Kf − 21 σ 2 T
d+ = √
− σ T
Für die Daten

• spot = 1.20

• strike = 1.20

• vol = 0.0935

• rd = 0.02

• rf = 0.02

• tau = 92.0/365

• notional = 1

ergibt sich für den TV (theoretical value) 0.022359.


Teil b)
Bestimmen Sie für die obigen Daten die erste Ableitung nach dem Spot, d.h. das DELTA der
Option. Verwenden Sie dazu die finiten Differenzenquotienten

19
1. Programmieren in C

• Approximation erster Ordnung

f (x + h) − f (x)
f 0 (x) =
h
• Approximation zweiter Ordnung (zentraler Differenzenquotient)

f (x + h) − f (x − h)
f 0 (x) =
2h
Variieren Sie die Schrittweite h zwischen 10−15 und 1, speichern Sie diese mit dem zugehöri-
gen Betrag des Approximationsfehlers. Plotten Sie in Excel log(h) gegen log(Fehler). Inter-
pretieren Sie die Grafik, wie könnte man die beiden Effekte erklären? Die exakte Lösung für
das Delta ist übrigens 0, 506826589408427.
Teil c)
In einem späteren Abschnitt werden wir das Eulerverfahren zur numerischen Lösung stocha-
stischer Differentialgleichungen behandeln, das hier für den deterministischen Fall kurz vor-
gestellt werden soll. Wir beschreiben das explizite und implizite Verfahren und demonstrieren
ein typisches Stabilitätsproblem. Man betrachte im folgenden die gewöhnliche Differential-
gleichung
y0 (x) = f (x, y(x))
mit der Anfangsbedingung
y(a) = ya
Der Einfachheit gelte
y:R→R
wobei alle Aussagen unmittelbar auf den Fall
y : R → Rn
übertragen werden können. Ziel sei die Approximation der exakten Lösung auf einem Intervall
[a, b].
1. explizites Eulerverfahren
Für eine fest gewählte Gittergrösse N und ein Gitter
b−a
x0 = a, xi = x0 + i h i = 1, . . . , N h=
N
bestimmt man die Approximation ηi von y(xi ) durch die Rekursion
ηi+1 = ηi + h f (xi , ηi )
Die Motivation ist wegen
y(x + h) − y(x)
y0 (x) = lim
h
naheliegend.

20
1.9. Übungsaufgaben

2. implizites Eulerverfahren
Mit obigen Bezeichnungen wählt man hier die Rekursion

ηi+1 = ηi + h f (xi , ηi+1 )

Dies kann man im allgemeinen Fall nur mit einem Iterationsverfahren - etwa dem New-
tonverfahren - lösen, bringt aber im Vergleich zu expliziten Verfahren gewisse Vorteile,
siehe unten.

Lösen Sie nun mit beiden Verfahren die lineare gewöhnliche Differentialgleichung

y0 (x) = 2x, y(0) = 1

und erstellen Sie für verschiedene Schrittweiten h Plots der exakten Lösung und ihrer nume-
rischen Approximation auf dem Intervall [0, 1]. Betrachten Sie anschliessend das Problem

y0 (x) = −300x, y(0) = 1


1
mit den Schrittweiten h = 0.01 und h = 301 . Was können Sie beobachten? Stellen Sie (analy-
tisch) Vermutungen an, warum dieser Effekt im konkreten Beispiel auftritt.

Aufgabe 2. Schreiben Sie eine Funktion zur Berechnung der implied vol und verwenden Sie
dazu das Newtonverfahren. Beachten Sie dabei folgende Punkte:

- Wahl des Startwerts

- Wahl des Abbruchkriteriums, denken Sie an mögliche Rundungsfehler

Die folgende Grafik zeigt ein typisches Bild der Funktion Vol → TV. Welches Problem könnte
sich für das Newtonverfahren ergeben? Wie könnte man es lösen?
Zur Bestimmung einer Nullstelle von f ergibt das Newtonverfahren die Iteration

xn+1 = xn − f (xn )/ f 0 (xn )

Aufgabe 3. Die folgende Tabelle enthält den Spot EUR/USD und Marktpreise für den EUR-
Call mit Nominal 1 Mio EUR, Spot 1.20 USD, rd = rUSD = 2.15%, r f = rEUR = 2%, Laufzeit
3 Monate, ATMVol = 9.35%.

Strike 1.10 1.15 1.18 1.20


Marktpreis 101066.02 55763.19 33579.43 22335.87
Strike 1.22 1.25 1.30 1.35
Marktpreis 14194.96 6884.68 2023.63 481.92
Berechnen Sie in einer Schleife die zugehörigen implied-vols, speichern Sie die Ergebnisse
in einer Datei, öffnen Sie diese in Excel und erstellen Sie die zu erwartende Smile-Grafik.

21
1. Programmieren in C

Abbildung 1.1.: Typischer Funktionsverlauf beim Call

22
2. Objektorientierte Programmierung
2.0. Prolog
In allen prozeduralen Programmiersprachen merkt man ab einer gewissen Projektgrösse (>
10.000 Zeilen Code), dass man den grössten Teil der Arbeitszeit nur darum bemüht ist, den
Überblick zu behalten:

• wo wird welche Variable definiert?

• wo steht Funktion xy nochmal?

• welche Funktionen greifen warum und wie auf bestimmte Variablen zu?

• wo werden welche Arten von Benutzereingaben geprüft?

Ein Ausweg wäre die Bündelung logisch zusammengehörender Daten und Funktionen in einer
Struktur, z.B. :

_
struct Option{ | Deklaration
double K, S0, sigma, tau, r; | der Struktur
double TV(); | "Option"
// ...B/S-Formel... |
}; _|

Nun könnte man scheiben:

Option call; call ist eine Instanz


call.S0 = 50: der Struktur "Option"
.
.
.
call.r = 0.02;
double ergebnis = call.TV();

Fazit
Die Übersichtlichtkeit ist wesentlich erhöht, aber: Der Benutzer muss vor Aufruf von TV()
alle Daten richtig belegt haben !
Dies ist sicher sehr fehleranfällig und es wäre besser, wenn die Struktur sensible Daten, wie
S0 , K, σ , etc, vor dem Benutzer ”verstecken” könnte und dieser nur über spezielle Funktionen,

23
2. Objektorientierte Programmierung

etwa setStrike(double K) oder getStrike() darauf zugreifen könnte. Damit wäre sicher-
gestellt, dass nur sinnvolle Eingabewerte zugelassen werden. Ausserdem wäre es sinnvoll, den
Benutzer der Struktur zu zwingen, bei Definition der Instanz call Startwerte anzugeben, da-
mit die Funktion TV sinnvoll arbeiten kann. Denkbar wäre etwa folgendes, was an dieser Stelle
aber rein heuristisch zu verstehen ist:

double spot = 1.20;


double strike = 1.20;
.
.
.
Option call (spot, strike,..)
double result = call.TV();

Die Deklaration einer in dieser Hinsicht verbesserten Struktur nennt man


Klasse / Klassendeklaration, eine Instanz dieser Struktur nennt man Objekt.

Sicherlich wird die geneigte Leserin langsam auf ein erstes ’richtiges’ Beispiel für eine Klasse
warten. Obwohl dieser Wunsch verständlich erscheint, möchten wir an dieser Stelle dennoch
um etwas Geduld bitten: Es erscheint uns hier sinnvoller, zunächst den gesamten Begriffsap-
parat einzuführen samt einigen für den Neuling nicht so wesentlich erscheinenden Erweite-
rungen, um dann schließlich schnell zu praxisnahen Beispielen übergehen zu können. Lassen
Sie die neuen Begriffe nur auf sich wirken, richtig verstehen werden Sie diese bald nach den
ersten Beispielen. Etwas Geduld bitte, nur noch etwas Geduld...

2.1. Begriffe der objektorientierten Programmierung


Def.
Ein Objekt ist eine gekapselte Datenstruktur, die Datenkomponenten enthält und die zu ihrer
Bearbeitung dienenden Funktionen.

Def.
Eine Klasse ist der Typ eines Objekts und beschreibt Typ und Aufbau der Datenkomponenten
und Funktionen.

Bemerkung

i) jedes Objekt hat seinen eigenen Datensatz, auf den allein die Funktionen des Objekts
selbst unbeschränkten Zugriff haben (Kapselung / information hiding), d.h. Zugriff von
aussen geschieht über eine genau definierte Schnittstelle (Methoden-protokoll)

ii) Kapselung als entscheidender Unterschied zu konventionellen Sprachen:

+ Verarbeitung von Daten im Objekt konzentriert und nicht über das Programm verteilt

24
2.1. Begriffe der objektorientierten Programmierung

+ Existenz von Datenschnittstellen und die strikte Trennung von Schnittstelle und Im-
plementation sorgen für Überschaubarkeit
+ Klassen können separat entwickelt werden. Dies erleichtert die Zusammenarbeit von
mehreren Programmieren, die nach dem Baukastenprinzip ein Projekt in zentrale
Teile aufteilen und unabhängig voneinander implementieren können.

Bemerkung
C++ bezeichnet man als Hybridsprache, weil Klassen-und Nichtklassenobjekte (etwa Integer-
variablen) benutzt werden.

Definition
Bei der Vererbung/Ableitung wird eine Klasse aus einer bereits definierten gebildet, indem sie
alle Datenkomponenten+Methoden der Basisklasse übernimmt und eigene hinzufügt oder alte
Komponenten modifiziert.

Klasse 1 = Basisklasse

Klasse 2 = abgeleitete Klasse

Bemerkung

i) Führt man die Vererbung mehrfach durch, einsteht eine Klassenhierarchie deren Spe-
zialisierung i.a. zunimmt:
Option

Plain Vanilla

Single Barrier

Double Barrier

ii) Vorteil: Ersparnis von Entwicklungszeit, denn die Klasse Option könnte man auch für
eine Klasse ”OneTouch” wiederverwenden.

Leitet man eine Klasse aus mehreren Basisklassen ab, dann bezeichnet man dies als multiple
Vererbung.
Klasse 1 Klasse 2 Klasse 3
& ↓ .
Klasse 4

Statt Vererbung kann eine Klasse auch ein Objekt einer anderen Klasse als Komponente haben
(Stichwort: Diskussion über Sein oder haben’). Dies führt in der Praxis oft zu philosophischen
Diskussionen, denn die Grenze ist oft fließend.

25
2. Objektorientierte Programmierung

2.2. Nicht - objektorientierte Erweiterungen


in C++
2.2.1. Kommentare
/* Dies ist ein Kommentar in der Programmiersprache C,
er kann sich ueber viele Zeilen hinziehen
*/

Also kurz: In C beginnt ein beliebig langer Kommentar mit /* und endet mit */. In C++
wurde eine weitere Möglichkeit eingeführt, nämlich

// Dies ist in C++ ein Kommentar fuer genau eine Zeile

In der Praxis werden Sie die C++ - Variante häufiger sehen, sogar wenn mehrere Zeilen
auskommentiert werden müssen und man // mehrfach setzen muß und die C - Variante be-
quemer wäre.
Der Grund: Stellen Sie sich folgende Situation vor:

// irgendwas

int x = 3;
/* ein Kommentar zu
folgender Funktion */
meineFunktion2( x );

Nun möchten Sie zwecks Fehlersuche den Code int x = 3; und den Funktionsaufruf aus-
kommentieren und weil dies mehrere Zeilen sind, verwenden Sie die C-Variante:

// irgendwas

/*int x = 3;
/* ein Kommentar zu
folgender Funktion */
meineFunktion2( x ); */

Was passiert, ist, dass Ihr Kommentar bereits hinter folgender Funktion */ enden wird,
der Funktionsaufruf ist kein Kommentar, und der Ausdruck */ führt zu einem Compilerfeh-
ler. Dies wäre nicht passiert, wenn Sie den Kommentar jeweils am Anfang der Zeile mit //
gekennzeichnet hätten.
Kurz: Die Verwendung von // macht es später leichter, grosse Codeblöcke zwecks Fehlersu-
che kurzerhand auszukommentieren.

26
2.2. Nicht - objektorientierte Erweiterungen in C++

2.2.2. Referenzen
Referenzen sind alternative Namen für ein Objekt.

Bemerkung
i) Verwendung meist zur Übergabe vor Funktionsargumenten und -resultaten
ii) Sprechweise: X& liest man: ’Referenz auf X’
iii) Referenz ≈ Pointer, der automatisch bei jeden Zugriff dereferenziert wird.
Im folgenden Beispiel erhält die Funktion incr die Adresse der Variable a, arbeitet aber ganz
natürlich mit deren Inhalt.
void incr(int &a){a++;} |
| int a;
void main(){ | int &b = a
int x = 1; | int &c = b;
incr(x); // x = 2 | c = 4711; // a = b = c = 4711
} |
Bemerkung
i) Die Lesbarkeit des Programms wird beeinträchtigt (-), denn beim Aufruf der Funkti-
on incr() würde man nicht erwarten, dass der Wert von x sich ändert. Dies kann im
Zweifel verwirrend wirken.
ii) ständiges Dereferenzieren entfällt (+)
iii) call by reference ist effizient: Ein Zeiger benötigt normalerweise 4 Byte Speicher, et-
wa so viel wie ein Integer oder Double. In diesem Fall macht ”Call by reference” also
zumindest aus Effizienzgründen keinen Sinn, möglicherweise aber noch weil eine Funk-
tion mehrere Werte zurückgeben möchte. Wichtig wird ”Call by reference” z.B. bei der
Übergabe eines Objekts (also einer Klasseninstanz).
iv) häufige Fehlerquelle ist die Gültigkeitsdauer der referenzierten Grösse,
Bsp.:

int &f(){
int x = 4711;
return x: // Referenz auf nicht-statische Variable
}

Die Funktion liefert hier eine Referenz (praktisch also einen Pointer) auf eine Variable
zurück, deren Speicher nach Funktionsende freigegeben wird. Das kann nicht gutgehen
und führt zu subtilen Fehlern.

Wie bei Pointern der * ist auch die genaue Position des & bei Definition der Referenz
nicht so wichtig.

27
2. Objektorientierte Programmierung

2.2.3. Const
Das Schlüsselwort const hat in C++ mehrere Bedeutungen, die nun aufgezählt werden sollen.
i) const definiert einen Ausdruck als Konstante und ist somit ein Ersatz für das C-typische
#define. Der Vorteil liegt darin, dass der Compiler nun explizit den Variablentyp mitge-
teilt bekommt, der nun besser prüfen und optimieren kann. Dennoch wird die C-Variante
noch oft verwendet, daher soll sie hier kurz erläutert werden:
#define MAX 1000
Findet der Präprozessor diese Zeile, so wird er bevor die eigentliche Compilierung be-
ginnt, den Programmcode nach dem Ausdruck MAX absuchen und ohne Prüfung auf
Sinnhaftigkeit durch den Ausdruck dahinter, also 1000 ersetzen. Dem Compiler macht
man das Leben allerdings mit dem Befehl
const int max = 1000;
etwas leichter.
ii) Const-Objekte sind nur in der Datei gültig, in der sie definiert wurden. Sie können al-
so ohne Bedenken in header-Dateien eingetragen werden und somit auch an mehreren
Stellen im Programm jeweils maximal einmal pro Datei vorkommen.
iii) Zeiger auf konstante Objekte:
const char *a;
a="Test"; // ok
a="Test2"; // ok

a[0]=’B’; //falsch
Also: Der Zeiger darf überall hin zeigen, aber er darf nichts verändern.
iv) konstanter Zeiger:
int *const a = &variable;
*a = 2; // ok
a = &variable2 // falsch
Also: Der Zeiger darf nur an eine Stelle zeigen, darf diese aber verändern.
Zeiger auf konstante Objekte werden häufig bei Funktionsargumenten verwendet, weil sie
zur Selbstdokumentation des Programms beitragen. Dazu ein Beispiel aus der C-Laufzeitbibliothek:
Die Funktion strcpy kopiert den Inhalt des Strings (genauer: des Felds vom Typ char) src
in den String mit Namen dest (genauer: in den Speicher, der an der Stelle beginnt, den der
Pointer src angibt. Siehe Zusammenhang Felder und Pointer 1.7)
char* strcpy(char* dest, const char* src);
Durch die Deklaration wird unmittelbar klar, dass die Funktion strcpy den Inhalt von src
nicht verändert.

28
2.2. Nicht - objektorientierte Erweiterungen in C++

2.2.4. inline-Funktionen
Beginnt die Funktionsdefinition mit inline, werden die Befehle der Funktion bei jedem Auf-
ruf vom Compiler in den Quelltext eingesetzt.
Dadurch wird der Programmcode länger, andererseits gewinnt man Zeit weil kein Funkti-
onssprung mit Parameterübergabe, Kopieren der Argumente usw stattfindet. Man verwendet
inline also bei sehr kurzen Funktionen, die häufig aufgerufen werden.

2.2.5. Überladen von Funktionen


Darunter versteht man die Deklaration mehrerer Funktionsversionen mit gleichem Namen,
aber unterschiedlichen Parameterlisten. Der Compiler entscheidet dann, welche Funktion auf-
zurufen ist. Wichtig ist dabei, dass der Rückgabetyp dabei gleich bleiben muss:

void swap(int x, int y);


void swap(float x, float y);
int swap(int x, int y); // falsch
int swap(int x, float z); // falsch

// später...

swap(3, 5); // ruft die erste Version auf

2.2.6. Die Operatoren new und delete


new Typ

reserviert Speicher und liefert einen Zeiger Typ* darauf zurück. Sehr wichtig ist, dass der
Programmierer selbst dafür verantwortlich ist, den Speicher wieder freizugeben, ansonsten
kann es sein, dass Windows nach mehrmaligem Aufruf des Programms meldet, dass zu wenig
freier Arbeitsspeicher zur Verfügung steht. Also: delete ptr gibt den Speicher wieder frei.
Felder erzeugt man mit new Typ[Anzahl], man löscht sie mittels delete[] ptr;. Bei Spei-
chermangel wird ein Nullpointer geliefert und deshalb sollte man in der Praxis nach erfolg-
ter Speicherreservierung immer zuerst prüfen, ob die Speicheranfrage auch erfolgreich war.
In einem grossen Programm könnte es dazu kommen, dass man den Speicher aus Versehen
zweimal freigeben möchte, man also zweimal delete anwendet. Dies würde zum Program-
mabsturz führen und wird durch ptr = 0; nach erfolgter Speicherfreigabe vermieden: Die
erneute Anwendung von delete auf den Nullzeiger ist folgenlos. Man gewöhne sich also im
Sinne robusten Programmierens an, nach Speicherfreigabe den Pointer auf Null zu setzen.
Beispiele

i) double *zahl = new double;


if( zahl == 0 )

29
2. Objektorientierte Programmierung

printf("Kein Speicher mehr verfuegbar!\n");


// ...
delete zahl; // sonst Speicherleck
zahl = 0;

ii) int *feld = new int[128];


if( feld == 0 )
printf("Kein Speicher mehr verfuegbar!\n");
// ...
delete[] feld;
feld = 0;

2.3. Klassen
2.3.1. Syntax
Nach langer Vorrede kommen wir schliesslich zum wichtigsten Kapitel überhaupt, dem Auf-
bau und der Erstellung von Klassen. Zunächst abstrakt die Deklaration:

class Name{
public:
// Variablen + Funktionen, von aussen ansprechbar
// Schnittstelle nach aussen für den Anwender
// "Methodenprotokoll"
private:
// Variablen + Funktionen, nur fuer Memberfunktionen
// der Klasse sichtbar
}; // Strichpunkt vergessen fuehrt meist zu
// irrefuehrenden Fehlermeldungen

Es folgt ein ausführliches Beispiel mit einigen erklärenden Bemerkungen.


Beispiel 2.3.1. // Datei CallOption.h
// = Deklaration der Schnittstelle

class CallOption{

public: // Schnittstelle nach aussen


bool setStrike(double strike);
double getStrike();
double tv();

private: // Internes
double strike_, vol_;
};

30
2.3. Klassen

// Datei CallOption.cpp
// = Implementation der Schnittstelle

#include "CallOption.h"

// Definition einer Memberfunktion


bool CallOption::setStrike(double strike){
if(strike <= 0) return false;
else strike_ = strike;
return true;
}

double CallOption::getStrike(){
return strike_;
}

// Definition der Memberfunktion tv() wird


// später nachgeholt

// Datei Hauptprogramm.cpp

#include "CallOption.h"

int main(){

CallOption call; // Instanziierung

call.setStrike(3); // Aufruf einer Memberfunktion der


// Instanz

CallOption* callPtr = &call; // Pointer auf Instanz

printf(sstrike : %lf",callPtr->getStrike() );
// Aufruf einer Memberfunktion von call über Pointer

getch(); // warten auf Benutzereingabe


return 0;
}

Bemerkungen

31
2. Objektorientierte Programmierung

i) Wenn im folgenden vom Anwender der Klasse gesprochen wird, so ist derjenige Pro-
grammierer gemeint, der eine Instanz der Klasse bildet und nur das benutzen kann, was
im public-Teil steht. Die Gesamtheit der Funktionen im public-Teil bezeichnet man
als Methodenprotokoll.

ii) Das einzige, was den Anwender später interessiert, ist die Klassendeklaration, also das,
was üblicherweise im headerfile steht. Deshalb sollte man dort auch keine Funktionen
definieren, auch wenn sie sehr kurz sind. Dies führt direkt zum nächsten Punkt.

iii) In der Deklaration bereits definierte Funktionen sind automatisch inline. Dies geschieht
deshalb, weil man in der Deklaration in der Regel nur sehr kleine Funktionen definiert,
und diese sind gute Kandidaten für die inline-Expansion. Im Sinne der strikten Trennung
von Deklaration und Implementation sollte man dies aber nicht tun, sondern lieber die
Funktion als inline deklarieren und später definieren.

iv) In der Implementation (also der cpp - Datei) wird eine Funktiondefinition begonnen mit
KlassenName::fktName()...

v) Compiler und Linker beschweren sich erst, wenn noch nicht definierte Funktionen tat-
sächlich aufgerufen werden. Man kann eine Klasse daher vollständig deklarieren, Stück
für Stück implementieren und dabei ausgiebig testen, so lange man keine Funktionen
aufruft, die noch nicht definiert wurden.

vi) Es wurde wohl bereits gesagt, aber wegen der grossen Bedeutung wollen wir noch-
mal hervorheben, dass zur sauberen Trennung von Schnittstelle und Implementation die
Klassendeklaration in einer .h-Datei, die Implementation in der .cpp-Datei stehen sollte.

vii) Aufruf der Komponente über Objekt.Komponente oder bei Pointern Objekt→ Kompo-
nente. Ein Beispiel finden im Hauptprogramm des Beispiels (2.3.1).
Wenn Sie an den Prolog zu diesem Kapitel zurückdenken, so haben wir nun einen ersten
wichtigen Punkt erledigt: Sensible Daten können vor dem Anwender im private-Teil der
Klasse versteckt werden, der Zugriff geschieht über Funktionen im public-Teil, siehe Folie
1. Was jetzt noch fehlt, wäre die Möglichkeit den Benutzer zu zwingen, alle Variablen der
Klasse mit sinnvollen Werten zu belegen, wie es im Prolog angedeutet wurde. Dies geschieht
in den Abschnitten über Konstruktoren (2.3.1.3) und Destruktoren (2.3.1.4). Vorher sollen aber
noch einige technische Details erläutert werden, die in der Praxis grosse Bedeutung haben.

2.3.1.1. Der Zeiger this


Memberfunktionen können auf den Zeiger this zugreifen, der auf das aktuelle Objekt zeigt
(d.h. von dem die Funktion ”member” ist).
// in der Klasse "meineKlasse" gebe es eine private-Variable
// mit Namen "test"
meineKlasse::meineFunktion(){

32
2.3. Klassen

test = 3; // ist aequivalent zu


this->test = 3;

}
Wichtig wird dies etwa, wenn man das aktuelle Objekt als Funktionswert (return this / return
*this) zurückgeben will. Dies kommt z.B. vor, wenn man eine Addition von Objekten (Klasse
Matrix etwa) definieren will, d.h. wenn man die Funktion meineKlasse::operator+ schrei-
ben will. Das Objekt, das dort mittels this zurückgegeben wird wäre dann das Resultat, das
auf der linken Seite des Gleichheitszeichens zugewiesen wird.

2.3.1.2. konstante Memberfunktionen


int f() const;

ändert den this-Zeiger von Klasse *const (konstanter Zeiger auf Objekt) zu const Klasse
*const (konstanter Zeiger auf konstantes Objekt).
Bemerkung
i) Der Zusatz const ist sinnvoll bei Funktionen, die nur Datenausgabe leisten. Dadurch
wird nämlich betont, dass diese Funktion das Objekt nicht verändert und man kann sie
bei der Fehlersuche getrost übergehen, wenn man sich z.B. fragt, warum eine Member-
variable plötzlich unerwünschte Werte aufweist.

ii) Eine logische Konsequenz ist, dass über const-Funktionen höchstens weitere const-
Funktionen aufgerufen werden können. Möchte man diesen Mechanismus also in seinen
Programmen verwenden, so muss man ihn von Anfang an konsequent durchhalten, da
man sonst leicht diverse Compilerwarnungen erntet.
Ein Klassenentwurf, der dieses Konzept benutzt, zeigt folgendes Beispiel:

// header - Datei: Deklaration der Schnittstelle

#ifndef FINANCE_MARKET_H
#define FINANCE_MARKET_H

// Praeprozessor - Anweisung, die verhindert, dass diese


// Deklaration zweimal verarbeitet wird beschleunigt
// Compilierung, sonst nichts

class Market{

public:
inline double getSpot() const;
// inline und const sind hier typisch

33
2. Objektorientierte Programmierung

void setSpot( double spot );


inline double getAtmVol() const;
void setAtmVol( double vol );

private:
// Variablen sollten vor Benutzer versteckt sein,
// Zugriff über Funktionen
// interne Variablen erhalten ein ’_’ am Ende per Konvention
double spot_, atmVol_;
// weitere Hilfsfunktionen für den internen Gebrauch,
// ’helper functions’
bool spotIsValid( double spot ) const;
bool volIsValid( double vol ) const;

};

#endif

// cpp - Datei ( Implementation der Schnittstelle )

#include "Market.h"
#include sstdio.h"

bool Market::spotIsValid( double spot ) const {

if( spot > 0 ) return true;


else return false;

double Market::getSpot() const {


return spot_;
}

void Market::setSpot( double spot ) {

if( spotIsValid( spot ) ) spot_ = spot; else


// hier ging etwas schief, nun die ( verbesserungswürdige! )
// Fehlerbehandlung
printf("Please check your spot.\n");

34
2.3. Klassen

// im Hauptprogramm

#include "Market.h"
// Information an den Compiler und Linker : Alles in der
// Datei Market.h wird in weiterer Projektdatei definiert

void main(){

Market market;
// Market ist ein Typ und wird benutzt wie "int" oder "double"

market.setSpot( -50 );
// Ausgabe Fehlertext, Variable sspot_" bleibt undefiniert.
// ggf dauerhaft undefinierte Variablen sind problematisch,
// Frage: Was tun?
market.setSpot(100.0);
double mySpot = market.getSpot();

// Bsp. Für dynamische Allokation einer Instanz der Klasse

Market * dynMarket = new(Market);


dynMarket->setSpot(100);
delete dynMarket;
dynMarket = 0; // sicher ist sicher
}

2.3.1.3. Konstruktoren
Def.:
Ein Konstruktor ist eine spezielle Memberfunktion, deren Name mit der Klasse übereinstimmt.
Nach der Speicherreservierung bei Erzeugung des Objekts wird dieser zur Initialisierung der
Variablen aufgerufen.

Bemerkung
Der Konstruktor hat keinen Rückgabetyp, da er keinen Sinn machen würde. Im Prolog wurde
heuristisch so etwas wie ein Zwang zur Initialisierung der Variablen der Klasse gewünscht,
und dies leistet nun exakt der Konstruktor. Da man dem Anwender der Klasse oft einige Frei-
heiten bei der Initialisierung des Objekts einräumen möchte, wird der Konstruktor sehr häufig
mit diversen Parameterlisten geschrieben, kurz: er wird häufig überladen. Denkbar wäre etwa
ein Konstruktor der Klasse Option, bei dem alle Membervariablen vom Anwender initialisiert
werden, und eine Kurzform, bei der einige Variablen automatisch auf Standardwerte gesetzt
werden. Hier ein Beispiel:

// Deklaration im .h-file

35
2. Objektorientierte Programmierung

class Option{
public:
Option(double strike); // Konstruktor
}

// .cpp-file = Implementation

Option::Option(double strike){
if( strike > 0 )
strike_ = strike;
}

// Im Hauptprogramm

#include <.h-file mit Pfad>


void main{
double meinStrike = 10.0;
Option call(meinStrike);
}

Hier ein ausführliches Beispiel:


// CallOption.h

class CallOption{
public:
bool setStrike(double strike);
inline double getStrike() const;
double tv() const;

// Überladen der Konstruktoren


CallOption(double strike, double vol);
CallOption(double strike);

private:
double strike_, vol_;
};

// Hauptprogramm

#include "CallOption.h"

void main()

36
2.3. Klassen

{
CallOption call(30); // Aufruf des 2.Konstruktors

// Dynamische Allokation und Aufruf des 1. Konstruktors


CallOption* put = new CallOption( 30, 0.2 );

// CallOption.cpp

#include "CallOption.h"
#include sstdio.h"

bool CallOption::setStrike(double strike){


if( strike <= 0) return false;
else strike_ = strike;
return true;
}

double CallOption::getStrike(){
return strike;
}
CallOption::CallOption(double strike, double vol){
if( !SetStrike(strike) ) {
strike_ = 0;
printf("Fehler!\n");
}
vol_ = vol;
}

CallOption::CallOption(double strike){
if( !SetStrike(strike) ) {
strike_ = 0;
printf("Fehler!\n");
}
}
Bemerkung
i) Alle Klassen, die Sie bisher gesehen haben, verfügen über einen vom Compiler kom-
mentarlos hinzugefügten Standardkonstruktor, der keine Argumente erwartet und nichts
tut, so dass der Anwender eine Klasseninstanz ohne Angabe von Parameters erzeugen
kann. Sobald aber ein Konstruktor definiert wurde, muss dieser Standardkonstruktor ex-
plizit definiert werden. Im obigen Beispiel wäre also im Hauptprogramm Option call;
verboten. Dies führt direkt zu einem weiteren wichtigen Punkt:

37
2. Objektorientierte Programmierung

ii) Bei Initialisierung der Instanz mit dem parameterlosen Konstruktur dürfen keine Klam-
men hinter dem Objekt folgen, obwohl das streng genommen eine logische Konsequenz
wäre.

KlasseX instanz; // Instanz ist ein Objekt mit


// parameterlosem default-Konstruktor
KlasseX instanz(); // Instanz ist eine Funktion mit
// Rückgabetyp KlasseX

2.3.1.4. Destruktoren
Wenn ein Konstruktor dynamisch Speicher reserviert, dann muss dieser Speicher irgendwann
wieder freigegeben werden. So etwas passiert gerade im Gegenstück zum Konstruktor: dem
Destruktor. Dieser wird am Ende der Lebensdauer der Instanz vor Freigabe des Speichers
der automatischen Variablen, also alle die Variablen, die nicht dynamisch angelegt wurden,
automatisch aufgerufen. Entsprechend kann man den Destruktor nicht überladen, von ihm darf
es nur höchstens eine Version geben und im Zweifel gibt es wieder den Standarddestruktor,
der nichts tut. Der Destruktor ist typ- und parameterlos, seine Bezeichnung ist

~Klassenname();

Bemerkung
Destruktoren können explizit und damit auch mehrfach aufgerufen werden. Deshalb sollte
man nach Freigabe des Speicherplatzes mittels delete den entsprechenden Pointer auf 0 set-
zen.

2.3.1.5. Besonderheiten
i) Stellen Sie sich folgende Klasse vor:

class KlasseB{
public:
// ...
private:
KlasseA1 klasseA1Instanz;
KlasseA2 klasseA2Instanz;
}

Dabei sollen KlasseA1 und KlasseA2 irgendwo definierte Klassen sein. Offenbar besitzt
KlasseB also Komponenten vom Typ anderer Klassen. Deshalb muss KlasseB dafür sor-
gen, dass die Konstruktoren von klasseA1Instanz und klasseA2Instanz ordnungsgemäss
aufgerufen werden. Dies geschieht in einer Initialisierungsliste des KlasseB - Konstruk-
tors:

38
2.3. Klassen

KlasseB::KlasseB(Parameter) : klasseA1Instanz(param),
klasseA2Instanz(param){
// ...
}

Die Unter-Klassen-Konstruktoren werden in der Deklarationsreihenfolge des private-


Abschnitt aufgerufen, nicht in der Reihenfolge, die in der Initialisierungsliste steht.
Mehr dazu in den Übungen.

Bezeichnung

Konstruktor : Initialisierungsphase{
Zuweisungsphase
}

Konstanten und Referenzen können nur in der Initialisierungsphase mit einem Wert be-
legt werden. Dies wird wohl am besten verständlich, wenn man es selbst einmal pro-
grammiert, daher: siehe Übungen.
ii) Konstruktor für komponentenweise Initialisierung

Ziel:
Ein neu erzeugtes Objekt soll mit dem Wert eines vorhandenen Objekts initialisiert wer-
den. Dies benötigt man z.B. dann, wenn der Rückgabewert einer Funktion eine Klasse
ist und dieser an ein neu angelegtes Objekt zugewiesen wird, ein Beispiel:

class Option{
//...
}

// irgendwo gebe es folgende Funktion


Option computeOption();

// im Hauptprogramm:

Option meineOption = computeOption();

Wenn man die Klasse Option nicht darauf vorbereitet hat, werden einfach die Kompo-
nenten elementweise kopiert, was bei dynamisch angelegten Speicher fatal ist. Deshalb
definiert man den speziellen Konstruktor

Option::Option( const Option &objekt}{


strike_ = objekt.getStrike();
// ...
}

39
2. Objektorientierte Programmierung

Dieser Konstruktor heisst oft copy-constructor, was falsch ist, denn Initialisierung ist
etwas anderes als Wertzuweisung: Bei einer Wertzuweisung wird einem bereits fertig
initialisierten Objekt ein anderes Objekt zugewiesen. Das wäre der Fall in folgender
Situation:

Option option;
option = computeOption();

Auch in diesem Fall würde C++ einfach elementweise kopieren, was oft nicht erwünscht
ist. In diesem Fall definiert man den Zuweisungsoperator ”=”:

Option& Option::operator=( const Option& rechteSeite ){


// ...
return *this;
}

Der Zuweisungsoperator ist wichtig, wenn eine Klasse dynamisch Speicher reserviert
hat oder wenn eine Klasse Konstanten enthält, denn auch die würde C++ versuchen
einander elementweise zuzuweisen, was zu einem Compilerfehler führen würde. Wenn
eine Klasse etwa Strings verwaltet, dann müsste man folgende Schritte durchführen:

1. Den eigenen Speicher freigeben.


2. Die Grösse des benötigten Speichers feststellen, d.h. die Länge des Strings im
Objekt auf der rechten Seite des Gleichheitszeichens.
3. Den String kopieren.

2.3.2. Statische Objektkomponenten


Ziel:
Definiere Komponenten, die allen Klasseninstanzen gemeinsam gehören, d.h. es gibt davon
genau ein Exemplar, das nach Definition der Klasse, vor Instanziierung, bereits existiert.
Realisiert wird dies durch statische Variablen: Komponenten mit statischer Lebensdauer sind
während der gesamten Programmausführung verfügbar.

Beispiel
class Option{
public:
static int TV_Quotierung;
}
Es muss genau eine Definition geben, die an beliebiger Stelle in der Definitionsdatei angefügt
werden kann:
int option::TV_Quotierung = 1;

40
2.3. Klassen

Das Schlüsselwort static wird nur bei der Deklaration angegeben, nicht aber bei der späteren
Definition. Wichtig ist auch, dass die Definition nicht im Konstruktor oder der Deklarations-
datei durchgeführt wird, unter anderem wegen der Gefahr der Mehrfachinitialisierung. Im
übrigen stelle man sich vor, der Benutzer wählt eine TV-Quotierung aus und bei jeder neuen
Instanz der Klasse stellt der Konstruktor den Ausgangszustand wieder her...
Bemerkung
i) Mit enum kann man klassenintern statische Konstanten realisieren. Dabei ist dieser Aus-
druck aber lediglich eine Kurzschreibweise, in dem Sinne dass

class Option{
public:
enum{Name1, Name2};
}

äquivalent ist zu

class Option{
public:
static const int Name1;
static const int Name2;
// ...
}
// im cpp-file

int Option::Name1 = 1;
int Option::Name2 = 2;
// ...

Im folgenden finden Sie ein Beispiel für eine Klasse ”Defaults”, die diverse Programm-
konstanten sauber verwaltet:

// Defaults.h

class Defaults {

public:

static const int foreign;


static const int domestic;
// alternativ: enum{ foreign, domestic }

enum errorCode { // define general error codes


IS_OK, // oder: static const int IS_OK;

41
2. Objektorientierte Programmierung

MEMORY_ALLOCATION_FAILED,
INPUT_INVALID, // An input of a price call was not
// reasonable
UNKNOWN_ERROR // all the rest ;
}; // errorCode ist nun eigener Datentyp

// define parameters for pricing functions

enum PARAMETERS{ VALUE, DELTA, GAMMA};


// VALUE = 0, DELTA = 1 ...

enum LevelType{LOWER_BARRIER, UPPER_BARRIER};


enum KnockStyle{ KNOCK_IN, KNOCK_OUT };
enum Style{AMERICAN, EUROPEAN};

// Defaults.cpp

#include "Defaults.h"

const int Defaults::foreign = 0;


const int Defaults::domestic = 1;

// CallOption.h

#include "Defaults.h"

class CallOption{
// ...
private:
Defaults::Style style_; // in Ordnung, obwohl es keine
// Instanz der Klasse Defaults gibt!!
}

// CallOption.cpp

CallOption::setStyle( int style ){


if( style == Defaults::AMERICAN ||
style == Defaults::EUROPEAN )
style_ = (Defaults::Style) style; // Typkonvertierung,
// sonst Warnung
else
Fehlerbehandlung;
}

42
2.3. Klassen

ii) Auch Funktionen können statisch sein: Diese können dann auch ohne Instanziierung der
Klasse über

MeineKlasse::meineFunktion(params);

aufgerufen werden. Sehr beliebt sind solche Funktionen für helper-functions, die man
in einer geeigneten Klasse sauber verwaltet. Hier ein Beispiel:

// NormalDistribution1D.h

// typisches Beispiel für eine Klasse mit Helperfunctions

class NormalDistribution1D{

public:
static double ndf(double x);
static double normInv(double p);
static double nc(double x);

};

// NormalDistribution1D.cpp

#include "math.h"
#include "NormalDistribution.h"

// bei Definition der Klasse kein static - Zusatz mehr

double NormalDistribution1D::ndf(double x){


return 0.398942280401433 * exp(-x * x * 0.5);
// 15 Stellen Genauigkeit bei der Konstante, mehr geht nicht
}

// usw

2.3.3. Abgeleitete Klassen, Vererbung


Unter Vererbung versteht man die Bildung einer Klasse, indem man auf eine vorhandene Klas-
sendeklaration zurückgreift und diese leicht variiert, d.h. um einige Komponenten erweitert
oder Methoden anders definiert. Die Ausgangsklasse heisst Basisklasse, das Ergebnis abgelei-
tete Klasse. Schreibweise:

class X:public Y, public Z{...};

43
2. Objektorientierte Programmierung

Bei der hier dargestellten public-Ableitung werden die Zugriffsrechte der Basisklasse übernommen,
mehr dazu im nächsten Abschnitt.
Bemerkung

i) Wird eine Klasse aus mehreren Basisklassen gleichzeitig abgeleitet, spricht man von
multipler Vererbung.

ii) Konstruktoren, Destruktoren, operator= werden nicht vererbt

iii) Bei Vererbung stellt sich die Frage: Ist Klasse X ein Y oder hat X ein Y? Neben Verbung
besteht also meist die Alternative

class X{
public:
Y y;
};

iv) Mehrfache Vererbung ist nützlich beim Zusammenführen bereits bestehender, unabhängig
voneinander entwickelter Klassen. Ein schönes Beispiel dafür werden wir später sehen.

2.3.4. Vererbung und Zugriffsrechte


Betrachten Sie folgendes Beispiel, das anschliessend erläutert wird:

// Vanilla.h

class Vanilla{
public:
inline double getStrike() const;
double tv() const; // (*)
private:
double strike_, vol_;
};

// SingleBarrier.h

class SingleBarrier: public Vanilla{


public:
inline double getBarrier();
double tv() const; // alte tv - Funktion wird "ersetzt"
// ...usw
private:
double barrier_;
}

44
2.3. Klassen

// SingleBarrier.cpp

SingleBarrier::tv() const{ // (**)


double result = 3 * strike_; // Fehler! strike_ ist verborgen
double result = 3 * getStrike(); // getStrike ist public in
// Vanilla, also sichtbar für SingleBarrier
}

// Alternativ:

class Vanilla{
// ...
protected:
double strike_, vol_;
}

// SingleBarrier.cpp

SingleBarrier::tv() const{
double result = 3 * strike_; // ok
double result = 3 * getStrike(); // ok
}

// Hauptprogramm.cpp

void main(){
SingleBarrier barrierCall;
Vanilla* vanillaPtr = &barrierCall;
// ueber vanillaPtr ist Basisklassenanteil verfügbar
vanillaPtr->getStrike(); // ok
vanillaPtr->getBarrier(); // Fehler!
// ...
barrierCall.tv(); // ruft Funktion (**) auf
barrierCall.SingleBarrier::tv(); // das gleiche explizit
barrierCall.CallOption::tv() // ruft Funktion (*) auf
// also: Koexistenz der ersetzten Funktion!
}

Zunächst einmal sollte auch eine abgeleitete Klasse keinen Zugriff auf den private - Ab-
schnitt der Basisklasse haben. Dies ist deswegen vernünftig, weil man sonst jeden Zugriffs-
schutz der Klasse durch Vererbung wieder unterlaufen könnte. Andererseits sollte eine abge-
leitete Klasse doch gewisse Privilegien hinsichtlich der Basisklasse geniessen, und dass dies
nicht nur ein philosophisches Problem ist, sondern auch praktisch bedeutsam ist, zeigt obiges

45
2. Objektorientierte Programmierung

für Anwender für abgeleitete Klasse für Basisklasse


public public public
protected protected
private
Tabelle 2.1.: Sichtbare Ebenen innerhalb einer Klasse

Beispiel: Dort möchte die abgeleitete Klasse auf eine Variable der Basisklasse zugreifen, die
sie in ganz natürlicher Weise benötigt. Mit den bisherigen Mitteln müsste man diese Variable
also in den public-Teil setzen, was aber das Konzept des information hiding völlig zunich-
te macht. Kurz: Man hätte gerne einen Abschnitt in der Basisklasse, auf den der Anwender
keinen Zugriff hat, den eine abgeleitete Klasse aber so verwenden kann, als wäre er public.
Dies leistet der dritte und letzte Zugriffsmodus protected. Die Tabelle 2.3.4 soll noch ein-
mal zusammenfassend zeigen, welche Bereiche der Klasse für wen sichtbar sind. Arten der
Vererbung
Den Begriff der public-Ableitung haben Sie bereits am Rande kennengelernt, dies soll nun
präzisiert werden. Im wesentlichen bestimmt die public- / protected- / private - Ableitung,
welche Bereiche der Basisklasse bei der Vererbung in welchen Bereich der abgeleiteten Klasse
rutschen. Aus Anwendersicht wird also festgelegt, wie gross der public-Teil der abgeleiteten
Klasse ist, also wie viel der Anwender von der Basisklasse sieht.

i) public-Ableitung class X : public Y{. . . };


X Y
public ←−
protected ←−
private ←−

1. Bei dieser mit Abstand am häufigsten Art der Vererbung werden die Zugriffsrechte
übernommen.
2. X-Pointer werden gegebenenfalls implizit in Y-Pointer konvertiert. Dies wird im
Hauptprogramm des Beispiels demonstriert: Die Konvertierung erlaubt es, die ab-
geleitete Klasse sozusagen durch die Brille der Basisklasse anzuschauen. Wozu
das gut ist, können wir jetzt noch nicht abschliessend klären, aber immerhin er-
laubt es uns bereits folgendes: Man könnte von der Basisklasse, z.B. der Klasse
Option, diverse spezialisierte Klassen etwa für Vanillas oder Touchoptionen ablei-
ten. Möchte man diese bequem in einem Feld verwalten, so würde sich als Typ
des Feldes die Basisklasse anbieten, wobei man den Preis zahlen müsste, dass man
durch die ’Brille’ der Basisklasse wesentliche Komponenten der spezialisierten
Klassen nicht mehr erreicht. Dieses Problem werden wir später in (2.3.8) genauer
betrachten.

ii) protected-Ableitung class X : protected Y{. . . };

46
2.3. Klassen

X Y
public .
protected .
private ←−
Also: Y - public-Komponenten sind in X protected, Y - protected und Y - private
- Komponenten sind in X privat, d.h. nur für die Basisklasse selbst sichtbar. Diese Art
der Vererbung wird nur selten benutzt, X wäre hier etwa eine Ableitungsbasis für später,
während die Y - Schnittstelle nur intern benutzt wird.

iii) private-Ableitung: class X:private Y {...};

X Y
public ↓
protected .
private ←−

Komponenten, die in Y public oder protected sind, gelten in X als private. Man beachte:

1. Die Basisklasse wird völlig abgeschottet, was sinnvoll ist, wenn diese an wesent-
lichen Stelle neu definiert wird. Eine weitere sehr praktische Anwendung werden
wir später in (2.3.2) sehen.
2. Bei dieser Art der Vererbung kann ein Zeiger der abgeleiteten Klasse nicht in einen
Zeiger der Basisklasse konvertiert werden, weil sonst der Zugriffsschutz unterlau-
fen würde: Schliesslich wird der public - Teil der Basisklasse Y durch X ver-
steckt, könnte man aber das Objekt aber durch die ’Brille’ des Basisklassenpoin-
ters anschauen, dann wäre der public - Teil wieder sichtbar.

2.3.5. Auflösung von Mehrdeutigkeiten


2.3.5.1. 1. Fall:

Die abgeleitete Klasse definiert eine Komponente, die in der Basisklasse bereits existiert.

Lösung: Ist nur der Name der Variable oder Funktion gegeben, sucht der Compiler die Hier-
archie von der am weitesten abgeleiteten Klasse in Richtung der Basisklasse ab, bis die erste
Übereinstimming gefunden wurde. Will man ein davorliegendes Element haben, spricht man
es explizit an:

Objektname.Basisklassenname::Komponente.

Also: Gleichnamige Komponenten existieren parallel und werden nicht ersetzt. Die Unter-
scheidung welche Komponente gemeint ist wird nach der Länge des Ableitungswegs getrof-
fen. (vgl. Übungen)

47
2. Objektorientierte Programmierung

2.3.5.2. 2. Fall:
Multiple Vererbung, wobei in mehreren Basisklassen die gleiche Komponente deklariert ist.

Lösung: In diesem Fall muss stets die vollständige Bezeichnung

Objektname.mizeKlassenname::Komponente

verwendet werden. Hier ein Beispiel:

class X {public: int wert;};


class Y {public: int wert;};

class A:public X, public Y {...};

A a;
a.wert = 3; // Fehler !
a.X::wert = 3; // ok

Namenskollision sollten vermieden werden, sind aber bei Verwendung von grossen kommer-
ziellen Klassen häufig unvermeidbar.

2.3.6. Initialisierung von Basisklassen


Prinzip
Wenn man eine Basisklasse in eine andere Klasse vererbt, wird C++ nicht mehr automatisch
den Basisklassenkonstruktor aufrufen. Die Verantwortung für die Basisklasse liegt nunmehr
bei der abgeleiteten Klasse, deren Konstruktor dafür sorgen muss, dass der Basisklassenkon-
struktor aufgerufen wird. Dies ist wichtig, weil nur dieser Komponenten der Basisklasse in-
itialisieren kann. Man ruft ihn deshalb in der Initialisierungsliste des Konstruktors der abge-
leiteten Klasse auf.

Beispiel

i) abgelKlasse::abgelKlasse(parliste) : basisklasse1(params),
basisklasse2,...{
// ...
};

ii) class X{
protected :
int wert;
X(int a){
wert=a;
}
}

48
2.3. Klassen

class Y : public X{
public:
Y(int a):X(a){}
};

Bemerkung / Beispiel
Basisklassenkomponenten können auch im Anweisungsteil des Konstruktors der abgeleiteten
Klasse mit Werten belegt werden, aber das ist keine Initialisierung und deshalb mindestens
schlechter Stil, gelegentlich kann es sogar wegen vorübergehend uninitialisierter Pointer im
Speicher Probleme geben. Das obige Beispiel könnte man also auch so formulieren, wobei
Sie beachten sollten, dass in diesem folgenden Fall die Klasse X einen Standardkonstruktor
benötigt, der nichts tut:

class X{

public:
int wert;
X(){};
X(int a){
wert = a;
}
};

class Y:public X{
public:
Y(int a){
wert=a;
}
};

In beiden Fällen wird der Variable ”Wert” der Klasse X der korrekte Wert zugewiesen, mit dem
hier belanglosen Unterschied, dass X::wert für einen kurzen Moment einen zufälligen Wert
enthält. Eine Konstante wert könnte man so nicht mehr verändern. Noch ein kurzer Hinweis:
Gewöhnen Sie sich die Schreibweise dieses Beispiels nicht an, sondern definieren Sie alle
Funktionen gesondert im cpp-file. Hier geschieht dies nur aus Platzgründen. Zum Abschluss
folgt eine mögliche Anwendung bei der Darstellung von Optionen:

// Vanilla.h

class Vanilla{
public:
CallOption(double strike, double vol);
inline double getStrike() const;

49
2. Objektorientierte Programmierung

double tv() const; // (*)


private:
double strike_, vol_;
};

// SingleBarrier.h

class SingleBarrier: public Vanilla{


public:
SingleBarrier( double strike, double vol, double barrier);
inline double getBarrier();
double tv() const; // alte tv - Funktion wird "ersetzt"
// ...usw
private:
double barrier_;
}

// SingleBarrier.cpp

SingleBarrier::SingleBarrier(double strike, double vol, double barrier):


CallOption( strike, vol ), barrier_(barrier) {
}

schlechter Stil wäre:

SingleBarrier::SingleBarrier(double strike, double vol, double barrier):


CallOption( strike, vol ){

barrier_ = barrier; // Wertzuweisung, nicht Initialisierung!

2.3.7. Virtuelle Basisklassen


Häufig wird man den Vererbungsmechanismus in sehr grossen Projekten verwenden, in de-
nen gelegentlich (vielleicht auch weniger offensichtlich über einige Umwege) die folgende
Situation eintritt:

Klasse1
Vererbung . & Vererbung
Klasse2 Klasse3
& . multiple Vererbung
Klasse4
In dieser Situation enthält Klasse4 die Klasse1 zweimal. Das kann durch virtuelle Vererbung

50
2.3. Klassen

vermieden werden, denn alle Exemplare einer durch diese Art der Vererbung zu einer virtuel-
len Basisklasse aufgewerteten Klasse werden in einer Vererbungshierarchie zu einem Exem-
plar zusammengefasst, d.h.:
Klasse1
virtuelle Vererbung . & virtuelle Vererbung
Klasse2 Klasse3
& .
Klasse4
Schreibweise:
class Klasse2 : virtual public Klasse1{
// ...
};
Beachte:
Der virtual-Zusatz muss bei jeder Vererbung der Klasse1 stehen, ansonsten bleibt die Klasse
eine ’gewöhnliche’ Klasse.
Es sieht so aus, als wäre das Problem des unbeabsichtigten doppelten Vorhandenseins einer
Klasse gelöst, und doch gibt es noch ein Problem: Wenn eine Instanz der Klasse4 angelegt
wird, werden die Konstruktoren von Klasse2 und Klasse3 aufgerufen werden. Jeder von bei-
den wird nun aber den Konstruktor von Klasse1 aufrufen wollen, was wegen doppelter Initia-
lisierung zur Katastrophe führen kann. Die Aufrufe des Klasse1-Konstruktors aus Klasse2 und
Klasse3 werden von C++ beim Mechanismus der virtuellen Vereerbung deshalb ausgeschal-
tet. Stattdessen muss nun die am weitesten abgeleitete Klasse die Initialisierung von Klasse1
durchführen, ansonsten versucht der Compiler, einen Standardkonstruktor aufzurufen. Wenn
dieser nicht existiert, erhalten Sie entsprechend eine Fehlermeldung.
Dies ist übrigens ein häufiger Fehler, wenn man entweder übersieht oder bei grossen kommer-
ziellen Klassen schlicht nicht weiss, dass irgendwo in der Vererbungshierarchie eine Klasse
virtuell ist: Es wird sich also ergeben, dass Sie eine tadellos arbeitende Klasse in eine eigene
neue hineinvererben, Sie rufen auch korrekt den Konstruktor dieser Klasse in der Initialisie-
rungsliste des Konstruktors Ihrer neuen Klasse wie oben beschrieben auf - und plötzlich erhal-
ten Sie eine Fehlermeldung, dass ein Standardkonstuktor einer Klasse, von der Sie eventuell
noch nie gehört haben, nicht existiert! In diesem Fall sollte ein Blick in die Dokumentation
des kommerziellen Klassenrahmenwerks hoffentlich das Dilemma lösen.

Bemerkung
i) Art der Darstellung

Nicht-virtuell
Klasse2 −→ Klasse1
%
Klasse4
&
Klasse3 −→ Klasse1

51
2. Objektorientierte Programmierung

virtuell
Klasse2 −→ Klasse1-Ptr
%
Klasse4 −→ Klasse3 −→ Klasse1-Ptr
&
Klasse1

ii) Virtuelle Basisklassen werden stets zuerst initialisiert.

2.3.8. Virtuelle Funktionen


Zunächst eine kleine Motivation für dieses anfangs etwas seltsam anmutende Konzept:
Angenommen Sie haben einige Klassen zur Bewertung von Optionen geschrieben, die Sie
gerne praktisch in einem Array verwalten möchten. Als Typ des Arrays bietet sich eine ge-
meinsame Basisklasse an, konkret würde man vielleicht ein Feld von Zeigern vom Typ der
Basisklasse erstellen, und die einzelnen Zeiger werden mit den Instanzen der abgeleiteten
Optionsklassen initialisiert. Diese Basisklasse heisse nun OptionPricer und enthalte zur Ver-
einheitlichung der Arbeitsweise der verschiedenen Pricer eine Funktion TV(), die in den ab-
geleiteten Klassen ersetzt wird. Eine Skizze soll dies verdeutlichen:

VanillaPricer TouchPricer SingleBarrierPricer

↑ ↑ ↑
OptionPricerPointer1 OptionPricerPointer2 OptionPricerPointer3

Nun könnte man bequem aus dem Array heraus die Optionen ansprechen und z.B. die TV()-
Funktionen aufrufen, z.B. durch
OptionPricerPointer1->TV();
Aber - leider funktioniert das nicht. So wie es hier steht, würde die TV()-Funktion aus der
Basisklasse aufgerufen werden, die wahrscheinlich gar nichts tut. Dies ist auch kein Wun-
der, denn der OptionPricerPointer1 sieht nur den Teil der Basisklasse des VanillaPricer.
Man möchte aber nun erreichen, dass trotzdem die überschriebene (genauer: die aktuellste,
bei mehreren Ersetzungen in der Ableitungshierarchie) Version aufgerufen wird, wie es hier
sinnvoll wäre. Dies erreicht man nun gerade dadurch, dass die TV()-Funktion bei der Dekla-
ration den Zusatz virtual erhält, diese also zu einer virtuellen Funktion erklärt wird. Wenn
Sie das verstanden haben, ist der Abschnitt für Sie praktisch fast beendet, lassen Sie uns den
Sachverhalt aber noch einmal etwas abstrakter formulieren:

Allgemein hätte man gerne die Möglichkeit, über einen Basisklassenpointer eine Funktion
einer abgeleiteten Klasse aufrufen zu können.

Beispiel

52
2.3. Klassen

class Klasse1{
public:
voif f(){}
};

class Klasse2 : public Klasse1{


public:
void f(){//...}
}

Klasse2 K2;
Klasse1 *PtrK1 = &K2; // schaue K2 durch die Brille der
// der Basisklasse an

PtrK1->f(); // -> dies ruft Klasse1::f() auf,


// obwohl man gerne Klasse2::f() hätte

Dieses Problem löst man dadurch, dass f als virtuelle Funktion deklariert wird.

class Klasse1{
public:
virtual void f();
}

Wendet man nun einen Klasse1-Pointer zum Zugriff auf eine beliebig spät abgeleitete Klasse
an, so ruft der Compiler automatisch die aktuellste ersetzte Version von f auf.
Bemerkung

i) Das motivierende Beispiel können wir nun so formulieren:


Möchte man spezialisierte Versionen einer Basisklasse in einem gemeinsamen array
verwalten, so bietet sich die Basisklasse als Typ des arrays an. Virtuelle Funktionen
sichern nun den Zugriff auf wichtige Funktionen in den spezialisierten Klassen.

ii) Der Mechanismus wird nur beim Zugriff über Pointer oder Referenzen wirksam (siehe
Übungen).

iii) Faustregel:
Die Funktionen einer Basisklasse, die sich mit der Ableitung ändern können, sollten
virtuell sein.

iv) Die neue Version der virtuellen Funktion muss man deklarieren und definieren, der Zu-
satz virtual wird nicht mehr benötigt (siehe Übungen)

v) Ist eine Klasse für weitere Ableitungen vorgesehen, sollte der Destruktor virtuell sein,
damit bei Anwendung von delete auf einen Basisklassenpointer der richtige Destruktor
aufgerufen wird.

53
2. Objektorientierte Programmierung

2.3.9. Abstrakte Basisklassen


Im obigen (abstrakten) Beispiel wurde eine triviale virtuelle Funktion virtual void f(){}
definiert, die nie aufgerufen wurde. Möchte man die Funktion explizit als Platzhalter definie-
ren und nicht ’irgendwie’, so kann man sie in der Deklaration mit

class Klasse1{
public:
virtual void f()=0;
};

als rein virtuelle Funktion definieren.


Definition / Bemerkung

i) Eine Klasse mit mindestens einer rein virtuellen Funktion heisst abstrakte Klasse.

ii) Abstrakte Klassen können nur als Basisklasse benutzt werden, nicht aber zur Objekter-
zeugung.

iii) Klassen mit ausschliesslich rein virtuellen Funktionen heissen abstrakte Datentypen.

iv) abstrakte Datentypen sind zur Schnittstellendefinition sehr nützlich:


über Ableitung erhält man die ”richtige” Klasse, in der man die Implementation nun
leicht austauschen kann.

Es folgt ein Anwendungsbeispiel für den letzten Punkt der Bemerkung. Insbesondere sehen
Sie dort eine typische Anwendung für die private-Ableitung:
Beispiel 2.3.2. // abstrakte Schnittstelle für Endbenutzer

class Abstrakt{

public: // "user interface"


virtual void f() = 0;

};

// viel später im Projekt...

class Impl1{

public:
void f_impl1();

};

class Impl2{

54
2.4. Übungen

public:
void f_impl2();

};

class EndProdukt: public Abstrakt, private Impl1{

public:
void f();

};

// und in der Implementation steht...

EndProdukt::f(){

f_impl1(); // Redefinition der virtuellen Funktion

2.4. Übungen
Aufgabe 4. Modifizieren Sie das Programm

void main() {

derart, dass es folgenden Text ausgibt und auf eine Benutzereingabe wartet:

Programmstart.
Programmende.

Dabei soll die Funktion main unverändert bleiben.


Aufgabe 8:

Schreiben Sie eine Klasse, die ausgibt, wie viele Instanzen von ihr aktiv sind.

Aufgabe 9:

55
2. Objektorientierte Programmierung

Schreiben Sie eine Klasse Option, die eine für alle Instanzen der Klasse gemeinsam genutz-
te Variable TV Quotation besitzt. Diese soll man auf konstante Werte foreign oder domestic
setzen können.

Aufgabe 10:

Eine Klasse Y enthalte eine Integervariable ’wert’, die vom Konstruktor auf 1 gesetzt wird.
Eine Klasse X erbt Y und enthält eine Funktion print(), die die geerbte Variable ausgibt.

i) Experimentieren Sie beim Vererben und der Deklaration der Klasse X mit den Zugriffs-
modi

ii) Geben Sie im Hauptprogramm mit print() die Variable ’wert’ aus. Greifen Sie dann mit
einem Basisklassenpointer auf die Instanz der Klasse X zu, setzen Sie die Variable ’wert’
auf eine andere Zahl, und lassen Sie diese wieder mit print() ausgeben. Veranschaulichen
Sie sich ggf die Vorgänge nochmals mit einem Schema wie in der Vorlesung.

Aufgabe 11:

Schreiben Sie zwei Klassen X und Y, die beide einen Integer ’wert’ und eine Funktion
print() zur Ausgabe enthalten. Eine Klasse Z soll beide Klassen erben. Experimentieren Sie
ein bisschen, um die Auflösung von Mehrdeutigkeiten zu beobachten. Was passiert, wenn Sie
zur Auflösung der Mehrdeutigkeit einer der beiden print - Funktionen ein Argument spendie-
ren, um sie auf diese Weise in der abgeleiteten Klasse eindeutig ansprechen zu können?

Aufgabe 12:

Schreiben Sie eine Klasse X, die ein const int x enthält. Der Konstruktor soll die Kon-
stante initialisieren. Schreiben Sie eine Klasse Y, die X erbt und die Konstante in X korrekt
initialisiert.

Aufgabe 13:

Schreiben Sie ein Programm analog zum Beispiel in der Vorlesung, Abschnitt ’Virtuelle
Basisklassen’. Vererben Sie zunächst wie gewohnt und fügen Sie in Klasse 1 einen Integer x
ein. Ein Konstruktor soll x initialisieren.

i) Überzeugen Sie sich von der zweifachen Existenz der Klasse 1.

ii) Gehen Sie über zu virtueller Vererbung. Machen Sie sich dabei nochmals klar, was sich
bei den Konstruktoraufrufen ändert.

Aufgabe 14:

56
2.5. Templates

Programmieren Sie das Programm aus dem Abschnitt ’Virtuelle Funktionen’ nach. Greifen
Sie auf f() einmal über Pointer, Referenz und einmal normal zu.

Aufgabe 15:

Betrachten Sie folgendes Programm:

// header-file
class A{
public:
virtual void f(){printf("A\n"); }
};
class A1: virtual public A {
void f(){ printf("A1\n"); }
};
class A2: virtual public A{};
class A12: virtual public A1, virtual public A2{};

// Hauptprogramm
int main(){
A12 a12;
A2* ptr = &a12;
ptr->f();
getch();
return 0;
}

Was wird das Programm Ihrer Meinung nach ausgeben ? Ein kleines Vererbungsschema ist
für diese Aufgabe sicher hilfreich.

2.5. Templates
2.5.1. Vorbemerkungen
Templates (’Masken’) sind Deklarationsschablonen für Funktionen und Klassen und dienen
dazu, die Typbindung in C++ flexibler zu gestalten ohne diese völlig aufzugeben. Man be-
zeichnet Templates manchmal auch als parametrisierte Typen. Die Syntax ist stets

Template-Kopf + gewöhnliche Funktions-/Klassendeklaration

Der Template-Kopf könnte etwa so aussehen:

template <class X, class Y>

57
2. Objektorientierte Programmierung

X und Y bezeichnet man als Template-Parameter und müssen zur Compilierzeit berechenbar
sein. Wichtig ist, dass X nicht notwendig Name einer Klasse sein muss, im Zusammenhang mit
Templates meint das Schlüsselwort class lediglich, dass X ein beliebiger Typ sein kann, also
auch beispielsweise int. Um diese Verwirrung zu vermeiden, gibt es im C++-Standard das
zusätzliche Schlüsselwort typename, so dass der Templatekopf streng genommen so aussehen
müsste:
template <typename X, typename Y>
Da sich diese Konvention in der Praxis nicht durchgesetzt hat, wollen wir im weiteren Verlauf
auch darauf verzichten. Eingesetzt werden Templates für Funktionen oder Klassen, bei denen
man den spezifischen Datentyp erst später separat angeben möchte, sie eignen sich deshalb
sehr gut für Containerklassen, also zur Verwaltung von Arrays eines gewissen (beliebigen)
Datentyps. Im Financial Engineering sind die Anwendungen meist so speziell, dass es den
Aufwand nicht lohnt, Lösungen in einem so allgemeinen Rahmen zu implementieren. Den-
noch werden wir Beispiele kennenlernen, in denen sich Templates als wichtig herausstellen.
Ausserdem gibt es in C++ fertig implementierte und sehr effiziente Klassen zur Verwaltung
von Datenstrukturen, Algorithmen zum Sortieren und vieles weitere mehr, zu deren Verständ-
nis man Templates braucht. Auf diese Sammlung, die Standard Template Library, werden wir
später kurz eingehen.

2.5.2. Funktionstemplates
Nach den einleitenden Bemerkungen möchten wir mit einem konkreten Beispiel beginnen:
1 template < class X > void swap ( X &a , X & b ){
2 X temp = a ;
3 a = b:
4 b = temp ;
5 }
Dieses Codefragment bezeichnet man als Funktionstemplate, wobei zu beachten ist, dass hier
unmittelbar nach dem Templatekopf die komplette Definition der Funktion swap folgt. Der
Templateparameter X wird in dieser Funktion wie ein gewöhnlicher Typ (int, double...)
verwendet. Schreibt man in einem späteren Abschnitt

Option Call, Put;


swap( Call, Put);

wobei die Klasse Option irgendwo definiert sein muss, dann generiert der Compiler beim
Übersetzungsvorgang des Programms intern eine Funktion
void swap( Option &a, Option& b){
Option temp = a;
a = b:
b = temp;
}

58
2.5. Templates

und ruft diese mit den entsprechenden Argumenten auf. Eine so generierte Funktion heisst
Templatefunktion. Man beachte, dass sich mindestens ein Templateparameter auf die Argu-
mentliste der Funktion auswirken muss, damit diese vom Compiler von namensgleichen Funk-
tionen unterschieden und somit erst überladen werden kann. Problematisch an Templates all-
gemein ist nun, dass das Funktionstemplate ohne einen passenden Aufruf der Funktion swap
keinen Effekt auf das Programm hat, denn der Code des Templates wird in diesem Fall weder
mitübersetzt noch auf Fehler hin geprüft! Erst bei Erzeugung einer Templatefunktion können
Fehler in einem geschriebenen Template auffallen, und das erschwert den alltäglichen Ein-
satz erheblich. Das hier angegebene Beispiel funktioniert etwa so lange problemlos wie der
Copy-Konstruktor (wegen Zeile 2) und der Zuweisungsoperator (wegen Zeile 3) für den Typ
X wohldefiniert sind, aber wir möchten diese Diskussion auf einen späteren Zeitpunkt verta-
gen. Hier sei nur noch angemerkt, dass es sinnvoll ist, erst eine Funktion zu schreiben und
vollständig zu testen, und sie dann als Template umzuschreiben.
In der Praxis wird man ein Funktionstemplate zusammen mit der Definition in ein Headerfile
schreiben und überall einfügen, wo es gebraucht wird, obwohl dies die Gefahr von Doppelde-
finitionen mit sich bringt - der Linker ist gewöhnlich so konfigurierbar, dass er diese akzeptiert
und richtig verarbeitet, so dass als Manko lediglich der zusätzliche Speicherplatzbedarf bleibt.
Man kann die Definition des Funktionstemplates zwar in ein eigenes Sourcefile A.cpp packen,
aber wenn man es dann in einem Sourcefile B.cpp aufruft, so wird dies unglücklicherweise
nicht als Anweisung zur Erzeugung der entsprechenden Templatefunktion verstanden, sondern
lediglich als Externreferenz auf eine bereits erzeugte Templatefunktion in A.cpp. Konsequenz:
Möchte man die Definition des Templates sauber auslagern in eine Datei A.cpp, so muss man
dort ebenfalls alle später benötigten ’Realisierungen’ des Templates deklarieren. Dies bedeu-
tet eine massgebliche Einschränkung der Flexibilität und ist insbesondere für kommerzielle
Anwendungen kaum akzeptabel.

2.5.3. Klassentemplates
Wir beginnen auch hier mit einem konkreten Beispiel für ein Klassentemplate, das man sich
auch als eine Klassenschablone vorstellen kann.
template <class X> class Container{
public:
X getData(){
return daten;
}
private:
X daten;
};
Eine Realisierung (Templateklasse) erzeugt man nun durch
Container<int> test;
Völlig analog zu Funktionstemplates benutzt man innerhalb der Klasse die Templateparameter
(hier nur X) als Typen. Zentral ist hier, dass Memberfunktion der Templateklasse automatisch

59
2. Objektorientierte Programmierung

Templatefunktionen sind, was bei der Definition ausserhalb der Klassendeklaration sichtbar
wird. Man hätte also auch schreiben können:
template <class X> class Container{
public:
X getData();
private:
X daten;
};

template <class X> X Container<X>::getData(){


return daten;
}
Die Funktion getData ist also eine Templatefunktion, deshalb benötigt man bei der Defini-
tion vorne einen Template-Kopf, anschliessend den Rückgabetyp und den Namen der Klasse
gefolgt von dem üblichen ::. Bemerkenswert ist, dass der Name der Klasse nicht Container,
sondern Container<X> lautet. Dies haben wir bis jetzt nur deshalb nicht benötigt, weil inner-
halb der Deklaration die Namensergänzung automatisch durch den Compiler vorgenommen
wird. Ein weiterer Punkt bei den Templateparametern ist die Existenz von Defaultparametern
template <class X = int> class Container{
// ...
};
Nun kann man auch schreiben
Container<> test;
Wie schon bei Templatefunktionen ist es in der Praxis üblich die Definition des Klassentem-
plates direkt ins Headerfile aufzunehmen, um den gleichen Schwierigkeiten aus dem Weg zu
gehen. Legt man die Definition getrennt in einem Sourcefile ab, dann würde eine einfache
Deklaration eines konkrekten Typs, der später gebraucht würde, so aussehen:
template class <int> class Container;
und dies ist wegen der eingeschränkten Verwendbarkeit ungünstig. Das folgende Beispiel il-
lustriert die Verwendung des typedef-Befehls, weil bei vielen Templateparametern die Typ-
angabe sehr länglich werden kann.
template <int n, class X> class Container{
private:
X daten[n];
};

Container<10, int> test;


typedef Container<100,char*> Data100Cp;
Data100Cp test2;

60
2.5. Templates

Die Variable test enthält nun also ein Feld von 10 Integervariablen und

Container<10,int>

definiert den zugehörigen neuen Datentyp. Die Definition der Variable test2 zeigt, wie man
die häufige Wiederholung langer Typangaben vermeiden kann.
Die bereits angesprochene Gefahr von Platzverschwendung ist bei Klassen sehr gross, denn
jede erzeugte Klasse besitzt einen eigenen Satz von Funktionen und Variablen. In kritischen
Fällen kann es daher lohnen, gemeinsame Bestandteile von Klassen in eine Basisklasse aus-
zulagern. Insbesondere besitzt jede Klasse einen eigenen Satz an statischen Variablen, deren
praktische Verwendung bzw. Definition kurz dargestellt werden soll:

template<class X> class Container{


private:
static int zahl;
};
template <class X> int Container<X>::zahl = 0;

Abschliessend möchten wir darauf hinweisen, dass die C++-Standardbibliothek sehr umfang-
reichen Gebrauch von Templates macht, wobei der Nachteil sehr deutlich zutage tritt, dass
aus der Deklaration einer Templateklasse nicht unmittelbar hervorgeht, welche Voraussetzun-
gen ein als Parameter benutzter Typ erfüllen muss. Dies wurde in der Diskussion der Feh-
leranfälligkeit von Funktionstemplates bereits angedeutet. Deshalb gibt es passend zur C++-
Standardbibliothek ausführliche Beschreibungen und Tabellen, die verbal alle Anforderungen
an die Typen beschreiben.

2.5.4. Vorteile und Nachteile von Templates, effiziente Solver


In diesem Abschnitt sollen Wege diskutiert werden, wie man numerische Standardverfahren
wie etwa das Newtonverfahren zur Nullstellensuche effizient implementieren kann. Anwen-
dung findet dies im Financial Engineering etwa bei der Berechnung von impliziten Volati-
litäten, d.h. bei der Bestimmung der Volatilität σ in der Gleichung

g(σ ) = BlackScholes(σ , S, K, r, T ) = market price

wobei alle Daten inklusive dem Marktpreis vorgegeben sind. Offensichtlich lässt sich dies als
Nullstellensuche interpretieren. Wir gehen nun schrittweise voran und diskutieren mögliche
Alternativen sowie Vorteile und Nachteile der Verwendung von Templates.
Betrachten wir zunächst das Problem, ein direktes Verfahren zur Nullstellensuche zu imple-
mentieren, das nur die Funktionswerte f (x) benötigt. Eine Möglichkeit wäre nun, eine Basis-
klasse zu erstellen, die das numerische Verfahren vollständig implementiert, aber die Mem-
berfunktion f , auf die das Verfahren angewendet werden soll, noch offen lässt, d.h. f wäre
eine rein virtuelle Funktion. Im konkreten Fall kann man diese Basisklasse dann ableiten, f
überschreiben, und den Algorithmus ausführen. Dies mag funktionieren, hat in der Praxis aber
einige Nachteile:

61
2. Objektorientierte Programmierung

• mangelnde Effizienz
Im Solver wird f vermutlich sehr häufig aufgerufen und deshalb wirken sich Optimie-
rungen sehr deutlich auf die Performance aus. Insbesondere wird bei virtuellen Funk-
tionen vor dem Aufruf immer in einer Tabelle von Funktionspointern nachgeschlagen,
um herauszufinden welche Funktion nun gemeint ist, was vom Laufzeitverhalten aber
weniger kritisch ist.
• mangelnde Flexibilität
Wenn f Memberfunktion einer anderen Klasse ist, wird es sehr schwierig, dieses Kon-
zept zu implementieren. Ein Ausweg wird im Abschnitt über Design patterns vorge-
stellt.
Eine weitere Möglichkeit ist die Anwendung von Funktionspointern, aber dies ist wegen der
Unmöglichkeit der inline-Expansion kaum besser als der Ansatz mit virtuellen Funktionen.
Hier sieht man nun deutlich die Vorzüge von Templates, denn das Einsetzen einer Funktion
geschieht hier zur Compilierzeit, nicht zur Laufzeit, und deshalb sind Optimierungen wie
inline möglich. Ein direktes Verfahren könnte etwa so aussehen:
template< class T> double findRoot( double initialValue, T f ){
// do something
double y = f(initialValue);
// ...
}
Die entscheidende Idee ist, dass die Funktion f in ein ’function object’ gekapselt wird, d.h.
man möchte das Objekt f ganz natürlich wie hier dargestellt verwenden. Dafür ist es nötig,
dass das Objekt f folgende Memberfunktion hat:
double operator()( double x ) const;
Bei der Übertragung auf das Newtonverfahren ergibt sich jedoch ein Problem: Man benötigt
zwei Funktionen, nämlich die Funktion f selbst und ihre Ableitung. Man könnte nun zwei
Namen festlegen, etwa
double f = f.value(x); // oder mit operator() wie oben
double df = f.derivative(x);
aber das sieht nicht sehr elegant aus und schränkt die spätere Anwendung auf Objekte ein, die
unseren einmal definierten Anforderungen entsprechen. Dieses Problem gilt ebenso für unsere
bisherige Idee mit operator(), denn auch das ist ein Name für eine Funktion. Eine bessere
Lösung ist die Übergabe von Pointern auf die richtigen Memberfunktionen:
template <class T, double (T::*val) (double) const,
double (T::*deriv) (double) const>
double Newton(double initialValue, T f){

double y0 = f.*val( initialValue );


// ...
}

62
2.5. Templates

Die Templatefunktion Newton erwartet also ein Objekt f und zwei const-Funktionen, die
je eine double-Variable benötigen und einen double zurückliefern. Eine Anwendung zur
Berechnung impliziter Volatilitäten soll hier kurz skizziert werden: Zunächst deklarieren wir
eine Klasse BlackScholesCall

class BlackScholesCall{
public:
BlackScholesCall( double r, double d, double spot,
double T, double strike);
double tv(double vol) const;
double vega(double vol) const;

private:
double r_, d_, spot_;
double T_, strike_;
};

Die Implementation ist klar und bleibt dem geneigten Leser überlassen. Die Funktion Newton
müsste wohl um den Parameter givenValue ergänzt werden, etwa so:
template <class T, double (T::*val) (double) const,
double (T::*deriv) (double) const>
double Newton(double givenValue,
double initialGuess, T f){

double y0 = f.*val( initialGuess ) - givenValue;


// ...
}
Nachdem die benötigten Daten vom Benutzer abgefragt worden sind, könnte man das New-
tonverfahren folgendermassen aufrufen:
BlackScholesCall( r, d, spot, T, strike);
double impliedVol = Newton<BlackScholesCall, &BlackScholesCall::tv,
&BlackScholesCall::vega>(marketPrice,
initialValue, call);
Wir halten hinsichtlich der Wiederverwendbarkeit und Flexibilität von Programmen folgendes
fest: Wiederverwendbarkeit von einmal geschriebenem Code, hier etwa das Newtonverfahren,
wird einmal durch Einsatz virtueller Funktionen und einmal durch Einsatz von Templates
möglich. Im ersten Fall übergibt man dem Newtonverfahren als Argument einen Basisklas-
senpointer, und der Mechanismus der virtuellen Funktionen sorgt nun dafür, dass man den
relevanten Code (also die zu untersuchende Funktion f ) erreicht. Andererseits kann das rele-
vante Objekt auch als Template-Argument in die Newtonfunktion hineinkommen, und Funkti-
onspointern liefert man bei der Objektübergabe die Adressen der relevanten Funktionen, etwa
f und deriv f. Mit anderen Worten: Templates erlauben eine variable Typbindung wie bei

63
2. Objektorientierte Programmierung

virtuellen Funktionen ohne die Laufzeiteffizienz negativ zu beeinflussen. Formal ist der Un-
terschied zunächst nur, dass der Argumenttyp einmal zur Compilierungszeit (Templates) und
einmal zur Laufzeit (virtuelle Funktionen) bestimmt wird. Als Konsequenz ergibt sich ein
Trade-off zwischen Grösse und Laufzeiteffizienz:
Templates sind schneller, weil
• sie Optimierungen seitens des Compilers ermöglichen
• zur Laufzeit keine Entscheidung mehr nötig ist, welche Funktion nun aufgerufen werden
muss.
Templates können ein Programm unnötig aufblähen, weil
• jeder Aufruf eines Templates mit einem anderen Argument eigenständig kompiliert
wird, d.h. es kommt leicht zur Code-Duplikation oder mindestens zur Existenz von sehr
ähnlichen Codefragmenten im Arbeitsspeicher.
• bei mehreren Template-Parametern die Zahl der zu kompilierenden Versionen leicht ex-
plodieren kann, was sowohl die Grösse des Programms als auch die Compilierungsdauer
negativ beeinflusst.
Templates haben auch diverse praktische Probleme für den Programmierer:
• Templates erschweren die Benutzerauswahl, denn alle Varianten müssen explizit hinge-
schrieben werden. Man stelle sich etwa eine Monte-Carlo-Routine vor, die als Template-
Parameter einen Zufallszahlengenerator und ein Produkt erwartet: Bei fünf Generatoren
und zehn Produkten müsste man bereits eine switch-Anweisung mit 50(!) Abzweigun-
gen erstellen.
• Templates sind schwer zu debuggen: Programmcode wird erst geprüft, wenn er tatsächlich
gebraucht wird, oft erhält man Fehler zu Code, den man nicht explizit sehen kann, und
manchmal erlaubt es die Programmierumgebung nicht, im Template Haltepunkte zu set-
zen. Häufig treten Probleme mit Templates also erst viel später auf.
• es gib in C++ derzeit noch keine Möglichkeit, Anforderungen an einen Template-Datentyp
in für den Compiler verständlicher und zwingender Form vorzugeben. Wir haben dazu
bereits das warnende Beispiel der Funktion swap gesehen, das bei manchen Datentypen
funktionieren wird, bei anderen wiederum nicht, je nachdem, ob ein Copy-Konstruktor
und der operator=() sinnvoll definiert sind. Bei der Anwendung muss man also als
Programmierer besonders wachsam sein.
Praktisch wird man meist eine gewöhnliche Funktion oder Klasse schreiben, diese gründlich
testen, und dann erst in ein Template umschreiben. Gute Kandidaten für Templates sollten also
• kurz sein
• universell wiederverwendbar sein.
Beispiele sind Routinen zur numerischen Integration, Nullstellensuche etc. Generell werden
Templates zunehmend populärer, weil sie die Abstraktionsfähigkeit von C++ mit einer Lauf-
zeiteffizienz verbinden, die sonst low-level-Sprachen wie C vorbehalten ist.

64
2.5. Templates

2.5.5. Die Standard-Template-Library - Ein Überblick


Die Standard-Template-Library - kurz STL - ist ein sehr umfangreicher Teil der C++-Bibliothek
und enthält eine grosse Sammlung von Containern, Algorithmen und Iteratoren. Die Contai-
ner sind in der Regel Templates und realisieren gewisse Datenstrukturen wie Vektoren, Listen
oder Bäume. Für diese Datenstrukturen stehen diverse Algorithmen zur Verfügung, so etwa
zum Sortieren, Suchen usw. Um in den Listen oder Bäumen zu navigieren gibt es zu jeder Da-
tenstruktur eigene Iteratoren, die eine Verallgemeinerung von Zeigern darstellen. Der grosse
Vorteil der STL besteht

• in der Portierbarkeit der verwendeten Datenstrukturen und Algorithmen (einfach weil


die STL Teil des C++-Standards ist)

• in seiner Laufzeiteffizienz, weil virtuelle Funktionen zugunsten von Templates konse-


quent gemieden werden

• in der Vielfalt der vorgefertigten Möglichkeiten, die die Erstellung von Programmen
erleichtert

• in der Vermeidung von Fehlern bei der Speicherverwaltung, weil die Container den
Speicher selbstständig verwalten.

Wir beginnen mit einer Aufzählung ausgewählter Datenstrukturen, illustrieren einige an Bei-
spielen und schliessen das Kapitel mit Anwendungsmöglichkeiten im Financial Engineering.

• vector
Diese Datenstruktur verhält sich wie ein Array in C, d.h. man kann auf beliebige Da-
tenelemente mit dem []-Operator zugreifen und bearbeiten. Elemente können beliebig
eingefügt werden, die Speicherverwaltung erfolgt automatisch. Einfügen von Elementen
am Ende ist effizienter als an anderen Stellen.

• list
Dies ist eine doppelt verkettete Liste ohne []-Operator, navigieren ist hier nur mit Itera-
toren möglich. Suchen von Elementen und einfügen am Anfang oder Ende ist langsamer,
einfügen an beliebiger Stelle schneller als bei vector.

• deques (double ended queue)


Analog zum vector, aber effizientes Einfügen von Datenelementen am Anfang und am
Ende des Vektors möglich.

Möchte man einen Container verwenden, so fügt man die zugehörigen include-Files ein. In
der C++-Standardbibliothek enden die Dateinamen übrigens nicht mehr auf .h, obwohl dies
aus Kompatibilitätsgründen bei den meisten Compilern funktionieren sollte. Beispiel:

#include <vector>

Alle Komponenten der Bibliothek liegen im Namespace std, daher empfiehlt sich der besse-
ren Lesbarkeit wegen die Anweisung

65
2. Objektorientierte Programmierung

using namespace std;

Möchte man einen Algorithmus der STL verwenden, so fügt man zuvor die Zeile

#include <algorithm>

ein. Auch bei den Iteratoren gibt es verschiedene Arten, auf die wir in den Beispielen, etwa
(2.5.1) zurückkommen werden.

2.5.5.1. STL Pair


Eine sehr nützliche Datenstruktur, mit der man kompliziertere Datenstrukturen durch Ver-
schachteln konstruieren kann, ist das pair. Hier ein Anwendungsbeispiel:

pair<double, double> p;

p = make_pair<3.0, 5.0>; // weise einen Wert zu

p.first = 3.0; // Alternative dazu


p.second = 5.0;

// Datenausgabe
cout << "First: " << p.first
<< ssecond: " << p.second << endl;

In den Anwendungen im Financial Engineering (2.5.5.3) werden wir hierauf zurückkommen.

2.5.5.2. STL - Vector


Das folgende Beispiel erstellt einen Vektor von double mit 10 Elementen, die alle mit dem
Wert 0.25 initialisiert werden. Anschliessend wird die Länge des Vektors ausgegeben.

int n = 10;
double val = 0.25;
vector<double> vec(n, val);
cout << ssize of vec: " << vec.size() << endl;

Der []-Operator kann wie gewohnt zum Zugriff verwendet werden, wobei der Programmierer
selbst für die Korrektheit sorgen muss. Die STL prüft intern nicht, ob man über die Grenzen
des Felds hinausgeht. Eine Wertzuweisung erfolgt über

vec[0] = 1.0;

Nun fügen wir am Ende des Felds ein Element ein. Die Speicherverwaltung übernimmt die
STL.

vec.push_back(3.0);

66
2.5. Templates

Eine elegante Möglichkeit, auf das letzte Element zuzugreifen, ist:

int lastElement = vec.size() - 1;


vec[lastElement] = 2.0;

Die Anwendung von Iteratoren demonstriert folgendes Beispiel:


Beispiel 2.5.1. vector<double>::iterator it;
// ++it ist schneller als it++!
for (it = vec.begin(); it<vec.end(); ++it)
*it = 0.25;
Mit einem Iterator kann man nun das gesamte Feld durchlaufen und beliebig auf die Ele-
mente zugreifen. Der Ausdruck

vec.begin()

liefert einen Iterator, der auf das erste Element zeigt,

vec.end()

liefert einen Zeiger, der hinter (!) das letzte Element zeigt. Die Elemente eines Vektors sind
linear angeordnet im Speicher, daher ist der Vergleich

it < vec.end()

zwar in Ordnung, im allgemeinen würde man aber

it != vec.end()

vorziehen. Möchte man lediglich Elemente lesen, so gibt es einen effizienteren Iterator:

vector<double>::const_iterator it_c;
for (it = vec.begin(); it<vec.end(); ++it)
cout << *it;

Nun löschen wir den Vektor explizit:

vec.clear();

Das folgende Beispiel zeigt, wie man die Algorithmen der STL verwenden kann, um einen
Vektor nach benutzerdefinierten Kriterien zu sortieren:

static bool valueLess(const double a, const double b);


bool valueLess(const double a, const double b) {
return a < b;
}

sort(vec.begin(), vec.end(), valueLess);

67
2. Objektorientierte Programmierung

Hierbei ist es wichtig, dass die Funktion valueLess static ist. Das nächste Beispiel zeigt,
dass bei Verwendung von Pointern als Basistyp des Containers Speicherlecks auftreten können.

vector<double*> vec // Vektor von double-Arrays


vec.clear(); // Speicherleck!

Dies gibt zwar explizit den Vektor vec samt den verwalteten Pointern frei, nicht aber den
Speicher, auf den diese Pointer verwiesen haben! Das Problem ist leicht gelöst:

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


delete[] vec[i];

vec.clear();

2.5.5.3. Anwendung im Financial Engineering


Folgendes Konstrukt könnte man zur Darstellung von Zinsstrukturkurven verwenden:

vector<pair<double, double> > curve;

Mit anderen Worten: Eine Zinskurve wird interpretiert als Kollektion von Datenpaaren: ei-
ner Restlaufzeit und dem entsprechenden Zinssatz. Beim Verschachteln von Containern muss
man aufpassen, dass man zwischen zwei > ein Leerzeichen lässt, sonst interpretiert C++ den
Ausdruck >> als Shiftoperator.
Ein weiteres Beispiel sind Implied-Volatility-Tables, in denen für gewisse Restlaufzeiten Da-
tenpaare von Strike und impliziter Volatilität verwaltet werden müssen. Erschwerend kommt
hinzu, dass die Zahl der Strikes zwischen den Restlaufzeiten variiert, und das macht die An-
wendung eines flexiblen Containers der STL besonders lohnend:

vector<pair<double RLZeit, vector<pair<double Strike, double ImplVol> > > > vTab;

In der Praxis würde man an dieser Stelle wohl mit dem struct- oder class-Befehl einen
neuen Datentyp einführen und diesen als Basistyp des Vektors benutzen, weil die Schreibweise
langsam unübersichtlich wird.

2.5.5.4. Ausblick und Probleme der STL


Im Rahmen dieser Einführung können wir auf die STL nicht erschöpfend eingehen, daher
genüge an dieser Stelle der Hinweis, dass die STL auch Klassen zur Verwaltung von Strings
bereitsstellt, Klassen für komplexe Zahlen, Klassen für Input/Output-Ströme usw. Eine ausführ-
liche Darstellung der STL findet man etwa in Josuttis[[7]]. Abschliessend möchten wir einige
Probleme der STL nicht verschweigen. Ein Manko ist, dass die STL zur Effizienzsteigerung
von sich aus keine Bereichsprüfung vornimmt, daher wird ein Ausdruck wie

*vec.end() = 1; // falsch!

68
2.5. Templates

grundsätzlich akzeptiert, mit den gleichen Folgen wie beim Überschreiten der Grenzen ei-
nes Arrays in C: Das Programm wird entweder mit einer Schutzverletzung beendet, der PC
stürzt komplett ab oder es passiert nichts oder das Programm verhält sich an späterer Stelle
merkwürdig.
Ein weiteres mögliches Problem ist der grosszügige Umgang der STL mit Speicher. Meist
wird Speicher in Blöcken zu 1 KB reserviert, was bei grossen Datenmengen möglicherweise
zu erheblicher Speicherverschwendung führen kann.
Grundsätzlich sollte man die STL aber konsequent verwenden, und man gewöhnt sich
tatsächlich sehr schnell an ihre Anwendung. Weniger einfach ist es dann, eigene Templates
zu schreiben, die tadellos mit der übrigen STL zusammenarbeiten, aber das braucht man auch
nur in den seltensten Fällen.

69
2. Objektorientierte Programmierung

70
Teil II.

Einführung in Monte-Carlo Methoden

71
3. Monte Carlo-Methoden

3.0. Prolog
Berechne den theoretical value (im folgenden kurz TV) einer diskreten Barrieroption mit
Payoff :

f (S) := (Stn − K)+ I{maxt }


S <B (3.1)
1 ,...,tn ti

im Black-Scholes-Model.
Es ergibt sich:

Z
−rT −rT
TV = e E[ f (S)] = e f (x)g(x)dxn (3.2)
Rn

mit einer geeigneten Dichte g. Bei der Berechnung von TVs wird man also leicht auf hoch-
dimensionale Integrale geführt. Sei nun konkret n = 6, und wir berechnen das Integral approxi-
−rT 6
R
mativ mittels e [0,1]6 f (x)g(x)dx (o.E. wegen Parameterstranformation auf Einheitswürfel),
indem wir eine numerische Integrationsformel mit O (h2 ) anwenden. Setze h = n1 , dann lie-
fern n6 Funktionsauswertungen einen Fehler von bestenfalls O (h2 ) = O ( n12 ). In Monte-Carlo
wird sich zeigen, dass man bei n6 Simulationen einen Fehler der Größenordnung O ( n13 ) erhält.
Nimmt man nun an, dass eine Funktionsauswertung ungefähr so viel Rechenzeit benötigt wie
eine Simulation in Monte Carlo, so erhält man

Satz 3.0.1 (Faustregel). Bei Integralen ab Ordnung ≥ 6 empfiehlt sich als Integrationsmethode
Monte-Carlo.

Sollte Ihnen die O - Schreibweise (Landau - Symbol) nicht mehr geläufig sein, dann interpre-
tieren Sie den Ausdruck X̄n − E[X] = O( n1x ) einfach als |X̄n − E[X]| ≤ C n1x . Dies soll aussagen,
dass man die genaue Größe des Fehlers nicht bestimmen kann, insbesondere kennt man die
Konstante C nicht, aber dafür kennt man den Zusammenhang zwischen der Größenordnung
des Fehlers und der Anzahl der Simulationen n. Im Fall x = √1n wüßte man z.B., dass man
eine um eine Dezimalstelle höhere Genauigkeit in der Approximation erreicht, wenn man die
Anzahl der Simulationen verhundertfacht. Um es vorwegzunehmen: Dies ist sicher keine sehr
befriedigende Konvergenzrate, aber immerhin hat man diese durch Monte Carlo - Integration
für einen sehr allgemeinen Rahmen garantiert, siehe dazu (3.1.2) und (3.1.3).

73
3. Monte Carlo-Methoden

3.1. Monte Carlo-Methoden: Einführung


3.1.1. Idee und grundlegende Konvergenzaussage
Satz 3.1.1. Sei (Ω, F , P) ein Wahrscheinlichkeitsraum, X eine Zufallsvariable (Payoff), (Xn )n∈N
iid, X1 ∼ X. Zu berechnen sei Z
E[X] = XdP

Man betrachte den arithmetischen Mittelwertschätzer


1 n
X̄n := ∑ Xi, (Xn)n∈N iid, X1 ∼ X
n i=1

dann folgt mit dem starken Gesetz der großen Zahlen

X̄n → E[X] P − f .s.

d.h. X̄n ist ein stark konsistenter Schätzer.

Beweis. Klar.

Diese Aussage liefert bereits das übliche Kochrezept für einen naiven Monte-Carlo-Schätzer:

i) Simuliere n unabhängige Realisierungen Xi der Auszahlung X

ii) Approximiere E[X] durch n1 ∑ni=1 Xi

So wie es nun dasteht wird das Standardverfahren zu Monte Carlo gewöhnlich formuliert. Für
den etwas philosophisch angehauchten Leser könnte es vielleicht hilfreich sein, dass man am
PC eher die Realisierungen Xi (ω) der Zufallsvariablen Xi in dem möglichen Weltzustand ω
simuliert, in dem ”unser PC gerade lebt”. Am PC selbst werden die unabhängigen Realisie-
rungen Xi (ω) letztlich durch das unabhängige Ziehen von Zufallszahlen aus der U [0, 1] - Ver-
teilung gewonnen. Anschließend betrachtet man n1 ∑ni=1 Xi (ω) als Schätzer für die gewünschte
Größe, und dies führt zum Ziel, denn:
Zum einen können die Zufallszahlen am PC unabhängig aus der U [0, 1] - Verteilung ”gezo-
gen” werden, deshalb wird die Unabhängigkeit der Xi , i ∈ N wiedergegeben, zum anderen hat
man für den Mittelwertschätzer für P-fast sicher alle ω die gewünschte Ausage, also auch
P-f.s. für das ω unserer Simulation.

3.1.2. Konfidenzintervalle
Satz 3.1.2. Sei (Ω, F , P) ein Wahrscheinlichkeitsraum, X eine Zufallsvariable (Payoff), (Xn )n∈N
iid, X1 ∼ X. Dann gilt
X̄n − E[X] w
sn −→ N (0, 1)

n

74
3.1. Monte Carlo-Methoden: Einführung

und  
sn sn
X̄n − z δ √ , X̄n + z δ √
2 n 2 n
ist ein asymptotisches Konfidenzintervall für E(X) mit Wahrscheinlichkeit 1 − δ , wobei
s
1 n
sn := ∑ (Xi − X̄n)2
n − 1 i=1
(3.3)

ein Schätzer für die Standardabweichung von X ist und z δ ∈ R das 1 − δ2 -Quantil der Stan-
2
dardnormalverteilung, d.h. 1 − Φ(z δ ) = δ
2 (Φ Verteilungsfunktion von N (0, 1)).
2

Beweis. Der zentrale Grenzwertsatz liefert zunächst


X̄n − E[X] w
σ −→ N (0, 1)

n

mit Var(X) = σ 2 . Für den Schätzer sn gilt


sn → σ (n → ∞) P − f .s.
also auch
σ
−→ 1(n → ∞) P − f .s.
sn
Multiplikation liefert
X̄n − E[X] w
sn −→ N (0, 1)

n
Es folgt:
   
sn sn
P(E[X] ∈ [X̄n − . . . , X̄n + . . . ]) = P E[X] ≤ X̄n + z δ √ − P E[X] < X̄n − z δ √
2 n 2 n
! !
Xn − E[X] Xn − E[X]
= P sn ≥ −z δ − P sn > zδ
√ 2 √ 2
n n
(n→∞
−−−−−−−−→)N (0, 1)([−z δ , ∞)) − N (0, 1)([z δ , ∞))
(Portmanteau) 2 2

= 1 − Φ(−z δ ) − (1 − Φ(z δ )) = 2Φ(z δ ) − 1


2 2 2
δ
= 2(1 − ) − 1 = 1 − δ
2

Die Satzaussage kann man etwas schlampig zusammenfassen als


Var(X)
X̄n − E[X] ∼ N (0, ) für großes n
n
denn wäre X̄n − E[X] exakt so verteilt, dann würde man unmittelbar das bewiesene Vertrau-
ensintervall erhalten, ohne den Zusatz ’asymptotisch’.

75
3. Monte Carlo-Methoden

3.1.3. Konvergenzgeschwindigkeit von Monte Carlo


Monte Carlo-Schätzer werden zur Bestimmung eines Integrals, also einer rellen Zahl, ver-
wendet. In der Statistik heißen solche Schätzfunktionen auch Punktschätzer und ihre Effizi-
enz(Güte) berechnet man meist mit dem Mean Square Error

MSE = E(X̄n − EX)2

Ist der Schätzer erwartungstreu, dann handelt es sich beim MSE gerade um die Varianz des
Schätzers, was auch intuitiv ein plausibles Maß für die Genauigkeit der Schätzung ist, denn X̄n
ist eine Zufallsvariable mit einer unbekannten Verteilung, von der wir uns wünschen, dass sie
ihre Wahrscheinlichkeitsmasse um den gesuchten Wert EX konzentriert. Dies ist z.B. erfüllt,
wenn der Erwartungswert gleich EX und die Varianz der Verteilung klein ist. Im Fall des
naiven Monte Carlo-Schätzers haben wir gesehen, dass

Var(X)
MSE(X̄n ) = Var(X̄n ) =
n
Praktisch sind wir weniger an der Varianz als am durchschnittlichen Fehler interessiert, der im
Fall des naiven Monte Carlo-Schätzers
q
σX
MSE(X̄n ) = √
n

beträgt. In der Literatur wird das gelegentlich 


als der
 Grund angesehen, warum man für die
1
Konvergenzrate von Monte Carlo-Methoden O √n angibt. Die Autoren bevorzugen die In-
terpretation über das Konfidenzintervall, siehe unten. Der entscheidende Vorteil von Mon-
te Carlo-Methoden besteht nun in der Dimensionsunabhängigkeit der Konvergenzrate, denn
egal wie aufwendig und hochdimensional ein Integral auch sein mag, die Konvergenzrate eines
optimalen numerischen Verfahrens wird aufgrund von Monte Carlo nie schlechter als O( √1n )
sein. Diese Konvergenzrate ist zwar nicht besonders gut, aber sie bildet immerhin eine untere
Schranke.
Gelegentlich kann man den MSE eines Monte Carlo-Schätzers nicht mehr ausrechnen und
man zieht sich auf die Interpretation als Bereichsschätzer zurück. Hierbei ist das Kriterium
naheliegend: EX soll mit möglichst hoher Wahrscheinlichkeit in einem möglichst kleinen
Intervall enthalten sein. Kann man beide Varianten wie im Fall des naiven Monte Carlo-
Schätzers explizit ausrechnen, so sind beide Optimalitätskriterien äquivalent, weil in beiden
Fällen Var(X) die entscheidene Größe ist. Manche Varianzreduktionstechniken führen gewis-
se Abhängigkeiten zwischen den Realisierungen der Zufallsvariablen ein, so dass oft nur eine
Variante zur Beurteilung der Effizienz praktikabel ist. Die Konvergenzrate O( √1n ) kann man
sich bei einem Konvergenzintervall noch einmal sehr schön veranschaulichen, wenn man α
sehr klein wählt, so dass man die Tatsache, dass es sich lediglich um ein Konfidenzintervall
handelt, vernachlässigen kann. Dann ergibt sich für den Fehler
sn 1
|X̄n − E(X)| ≤ z δ √ ≈ C √
2 n n

76
3.1. Monte Carlo-Methoden: Einführung

Man sieht insbesondere, dass die Genauigkeit der Approximation über sn auch von Var(X)
abhängt, und dies wird bei den Varianzreduktionsmethoden der Ansatzpunkt sein, um die
Konvergenzgeschwindigkeit oft um ein Vielfaches zu erhöhen. Ziel solcher Methoden ist es,
das Problem geeignet zu stören oder aber die Zufallsvariable X durch eine Zufallsvariable Y
mit sehr viel kleinerer Varianz und E(X) = E(Y ) zu ersetzen.

3.1.4. Erzeugung von Pfaden des Underlying


R
Im Financial Engineering erhält man i.a. Ausdrücke der Form Ω f (St1 , . . . , Stn ) dP, d.h. um
Realisierungen von f (St1 , . . . , Stn ) zu erzeugen benötigt man Realisierungen des Rn -wertigen
Zufallsvektors (St1 , . . . , Stn ). Wir wollen hier nur die Erzeugung des Underlying im Black-
Scholes-Modell besprechen. Hierbei wird sich herausstellen, dass man die zugehörige Stocha-
stische Differentialgleichung

dSt = rSt dt + σ St dWt t ∈ [0, T ]


S0 = x ∈ R

exakt lösen kann, wobei an dieser Stelle noch offen bleiben soll, was das bedeutet. Hier soll
lediglich darauf hingewiesen werden, dass andere Stochastische Differentialgleichungen, etwa
diejenigen, die eine stochastische Volatilität beinhalten, nicht mit dieser Methode bearbeitet
werden können. Wie bei den gewöhnlichen und partiellen Differentialgleichungen existie-
ren zu deren Lösung diverse Verfahren, z.B. das explizite / implizite Eulerverfahren. Obwohl
das explizite Eulerverfahren bei nicht-linearen (stochastischen wie nicht nicht-stochastischen)
Differentialgleichungen häufig Stabilitätsprobleme mit sich bringt, wurde es in der Vergan-
genheit im Financial Engineering bei den dort auftretenden SDEs sehr erfolgreich verwendet.
Eine präzise Darstellung von Lösungsmethoden finden Sie in dem Standardwerk von Kloeden
und Platen[[10]].

3.1.4.1. Erzeugung von U [0, 1]-verteilten Zufallsvariablen


Zitat 1. The generation of random numbers is too important to be left to chance.

Titel einer Arbeit von Robert R. Coveryou,


Oak Ridge National Laboratory

Der Ausgangspunkt für die Simulation von Zufallszahlen irgendeiner Verteilung sind in der
Regel Zufallszahlen der U [0, 1]-Verteilung. Eine entsprechend große Bedeutung kommt die-
sem Fundament der Monte Carlo-Simulation zu. Leider ist diese Thematik wieder ein eigenes
Forschungsgebiet, und wir können darauf an dieser Stelle nur oberflächlich eingehen. Bevor
wir auf die zugrunde liegende Theorie kurz eingehen, möchten wir Sie überzeugen, dass es
notwendig sein kann, sich mit dieser Thematik zu beschäftigen, denn auf den ersten Blick wer-
den Sie sagen, dass doch bei den meisten Compilern ein Zufallsgenerator rand() enthalten
ist. Diesen verwendet man i.a. folgendermaßen:

i) Fügen Sie die Zeile #include <stdlib.h> ein.

77
3. Monte Carlo-Methoden

ii) Initialisieren Sie den Zufallsgenerator mit einer beliebigen positiven Zahl: srand(2.0)

iii) Erzeugen Sie ganzzahlige Zufallszahlen im Bereich [0, RAND MAX] mit rand().
Wenn Sie U (0, 1)-verteilte Zufallszahlen erzeugen möchten, können Sie dies mit

x = rand()/(RAND\_MAX + 1.0);

erreichen.

Es gibt sehr gute, frei erhältliche Zufallsgeneratoren für die U [0, 1]-Verteilung, nur gehört
rand() in aller Regel nicht dazu. Als ersten Hinweis für die mangelnde Qualität eines Gene-
rators können Sie den Rückgabetyp betrachten: Handelt es sich um einen 2-Byte großen Typ
(etwa Integer auf manchen Computern), dann sollten Sie diesen Generator nicht verwenden:
Er könnte dann nämlich höchstens 216 = 65536 bzw 32767 positive Zufallszahlen erzeugen.
Mit anderen Worten, die Zahlen des Generators hätten bestenfalls die Periode 32767, und das
würde eine Monte Carlo-Simulation mit durchaus häufig 1000000 Simulationen völlig torpe-
dieren. Auf Ihrem Rechner sollten Sie sich also die Variable RAND MAX genau anschauen. Im
folgenden soll kurz auf etwas Theorie eingegangen werden.
Die meisten ANSI C - Compiler sind linear congruential generators , d.h. sie erzeugen Zah-
lenfolgen I1 , I2 , . . . zwischen 0 und m − 1 ( z.B: RAND MAX ) gemäß der Rekursion

I j+1 = aI j + c mod m

Diese Technik kann akzeptable Ergebnisse liefern. Bei den gängigen ANSI C Generatoren
ergeben sich aber folgende Probleme:

i) Der Generator kann offensichtlich höchstens eine Periodenlänge m liefern. Bei AN-
SI C ist m aber oft sehr klein. Beispielsweise liefern die Entwicklungsumgebungen
Borland C++ Builder 3.0 und Microsoft Visual C++ 6.0 beide als RAND MAX 32767.

ii) Die maximale Periode wird oft nicht erreicht wegen einer nicht optimalen Wahl von a
und c.

Daher werden wir zunächst einen verbesserten Generator dieser Bauart angeben, der nicht nur
diese, sondern auch grundsätzlichere Probleme dieser Methode umgeht, nämlich:

i) Korrelation im Rk : Verwendet man k Zahlen des Generators zur Beschreibung eines


Punktes im Rk , so wird nicht der ganze Raum ausgefüllt, sondern nur mehr oder weniger
viele Ebenen, je nach geschickter Wahl von a und c.

ii) Die weniger signifikanten Bits der erzeugten Zahlen sind weniger stochastisch als die
mehr signifikanten Bits. Die Zahlen sollten also nicht bitweise auseinander gebrochen
werden, um noch mehr Zufallszahlen zu erhalten.

78
3.1. Monte Carlo-Methoden: Einführung

Im sehr zu empfehlenden Standardwerk Numerical recipes in C++[[2]] werden nun mehre-


re Varianten dieses multiplikativen Kongruenzverfahrens in verbesserter Form angegeben, so
dass diese Probleme je nach gewünschter Periodenlänge nicht auftreten. Dennoch haben auch
diese manchmal die Simulation verfälschende Eigenschaften. In Numerical recipes in C++
wird als Beispiel angegeben, dass bei dieser Konstruktionsweise manchmal (korrekterweise)
sehr kleine Zahlen auftreten, die dann mit a multipliziert immer noch sehr klein sind. Wenn
eine Simulation auf Abfolgen kleiner Zahlen empfindlich reagiert, etwa bei der Analyse selten
auftretender Ereignisse, kann dies das Ergebnis verfälschen.
In der Praxis verwendet man meist den relativ neuen Mersenne twister - Generator, der vor
allem bei höheren Ansprüchen an die Qualität der Zufallszahlen wie bei einer Monte Carlo-
Simulation eindeutig den Vorzug verdient:

i) Die Periode des Generators beträgt die praktisch derzeit unerreichbare Zahl von

219937 − 1 ≈ 4.31 × 106001

ii) Fasst man 623 aufeinanderfolgende Zufallszahlen zu einem Vektor im R623 zusammen,
so wird der gesamte Einheitswürfel ausgefüllt, d.h. der Generator liefert Gleichvertei-
lung bis zum R623 .

iii) Alle Bits der erzeugten Zufallszahlen sind gleich zufällig.

iv) Die Implementation des Algorithmus kommt ohne zeitaufwendige Multiplikationen und
Divisionen aus. Der fertige Generator ist also insbesondere schneller als die meisten
anderen Generatoren.

Den Mersenne-Twister gibt es fertig implementiert in diversen Programmiersprachen (insbe-


sondere C,Lisp, C#, Ada) und auch als Excel-Add-in (www.numtech.com/NtRand).

3.1.4.2. Erzeugung unabhängig. normalverteilter Zufallsvariablen


Wir wollen im folgenden die hoffentlich passablen Zufallszahlen in N (0, 1)-verteilte Zufalls-
zahlen transformieren.
Satz 3.1.3. Sei U ∼ U ([0, 1]), Φ die Verteilungsfunktion von N (0, 1). Dann gilt:

X := Φ−1 (U) ∼ N (0, 1)

Beweis.

P(X ≤ a) = P(Φ−1 (U) ≤ a)


= P(U ≤ Φ(a))
= Φ(a) ∀a ∈ R

Bemerkung 3.1.1.

79
3. Monte Carlo-Methoden

i) X ∼ N (0, 1) =⇒ Z := µ + σ X ∼ N (µ, σ 2 ) (Simulation beliebiger normalverteilter


Zufallsvariablen möglich). Insbesondere gilt der Satz für beliebige Verteilungen mit
Verteilungsfunktion Φ.

ii) Berechnung der Inversen der Verteilungsfunktion von N (0, 1) mit:

1. Approximationsformel von Beasley-Springer-Moro


2. Newtonverfahren (höhere Präzision, aber höherer Redenaufwand)
3. Kombination von beiden: Die Approximationsformel liefert einen gewöhnlich sehr
guten Startwert für das Newtonverfahren, das dann quadratisch konvergiert. Ein bis
zwei Newtonschritte sind dann i.a. völlig ausreichend.

iii) Es gibt weitere Verfahren ohne Auswertung von Φ−1 , etwa Box-Muller, die aber andere
Nachteile haben. Box-Muller etwa erzeugt immer nur zwei Zufallszahlen gleichzeitig,
was das Verfahren in der Praxis oft entweder unhandlich oder ineffizient macht.

3.1.4.3. Erzeugung des Random-walk


Satz 3.1.4. Sei (Wt )t∈[0,T ] Brownsche Bewegung, 0 ≤ t1 < · · · < tn ≤ T ,

(W
bt , . . . , W
1
btn ) := AZ Z ∼ N (0, IdRn )
 √ 
t1 0 ··· ··· 0
 .. √
 . t2 − t1 0 · · · 0


 . . . .. 
 ..
mit A :=  .. .. . 

 . . . ..
 .. .. ..

√ . 
√ √
t1 t2 − t1 · · · · · · tn − tn−1
Dann gilt
(Wt1 , . . . ,Wtn ) ∼ (W
bt , . . . , W
1
btn )

Beweis. Es gilt (Wt1 , . . . ,Wtn ) ∼ N (0,C) mit Ci j = min(ti ,t j ).


Wegen AAT = C (nachrechnen, Cholesky-Zerlegung) folgt

AZ ∼ N (0, AIdAT ) = N (0,C)

Korollar 3.1.1. Sei Xt = µt + σWt BM mit Drift µ und Diffusionskoeffizient σ 2 .


Wegen σ Aσ AT = σ 2C und
 
 t 1 
(Xt1 , . . . , Xtn )T ∼ N µ  ...  , σ 2C
 
tn

80
3.1. Monte Carlo-Methoden: Einführung

folgt  
t1
(X̂t1 , . . . , X̂tn )T := µ  ...  + σ AZ ∼ (Xt1 , . . . , Xtn ) (3.4)
 
tn
Bemerkung 3.1.2. Eine effiziente Implementation von (3.4) ist:

X̂0 = 0

X̂ti+1 = X̂ti + µ(ti+1 − ti ) + σ ti+1 − ti Zi+1
Zi iid ∼ N (0, 1) i = 0, . . . , n − 1

was man intuitiv auch erwarten würde.

3.1.4.4. Erzeugung einer geometrischen Brownschen Bewegung


Satz 3.1.5. Es sei S ∼ GBM(µ, σ 2 ), d.h.

dSt = µSt dt + σ St dWt t ∈ [0, T ]


S0 = x ∈ R

Diese stochastische Differentialgleichung lässt sich explizit lösen mit

σ2
 
St = S0 exp (µ − )t + σWt
2

Dann gilt für den rekursiv erzeugten Prozess Ŝ

σ2 √
  
Ŝti+1 = Ŝti exp µ− (ti+1 − ti ) + σ ti+1 − ti Zi+1 , i = 0...n−1 (3.5)
2

und Ŝ0 = S0 ∈ R die Aussage


P(St1 ,...,Stn ) = P(Ŝt1 ,...,Ŝtn )
d.h. die Methode in (3.5) simuliert exakt die gewünschte Verteilung
 
σ2
Beweis. Definiere Xt := log(St ) = log(S0 ) + µ − 2 t + σWt .
Mit (3.1.2) gilt für den durch

σ2 √
X̂ti+1 = X̂ti + (µ − )(ti+1 − ti ) + σ ti+1 − ti Zi+1 i = 0, . . . , n − 1
2
X̂0 = log(S0 )
Zi ∼ N (0, 1) i = 1, . . . , n

rekursiv erzeugten Prozess X̂:

(X̂t1 , . . . , X̂tn ) ∼ (Xt1 , . . . , Xtn )

81
3. Monte Carlo-Methoden

Definiere jetzt:
f (x1 , . . . , xn ) = (ex1 , . . . , exn )T ∈ Rn
Es folgt
 f
P(St1 ,...,Stn ) = P f (Xt1 ,...,Xtn ) = P(Xt1 ,...,Xtn )
 f
= P(X̂t1 ,...,X̂tn ) = P(Ŝt1 ,...,Ŝtn )

3.1.5. Einfache Beispiele


Beispiel 3.1.1. Plain Vanilla Call im Black-Scholes-Modell
Der Payoff hat die Gestalt
C = (ST − K)+
und deshalb genügt es, nur ST zu simulieren.
Algorithmus 3.1.1.
for i = 1, . . . , n
erzeuge Zi ∼N (0, 1) idd
σ2
√ 
ŜT = S0 exp (r − 2 )T + σ T Zi
Ĉi = (STi − K)+
end
Ĉn = e−rT n1 ∑ni=1 Ĉi

Beispiel 3.1.2. Ganz analog geht das für asiatische Optionen, etwa mit Payoff
m
1
X = (S̄ − K)+ S̄ = ∑ St j
m j=1

Konstruiere also für i = 1, . . . , n

σ2
 
i
p
St j+1 = St j exp (r − )(t j+1 − t j ) + σ t j+1 − t j Z j+1 j = 0, . . . , m − 1
2

wobei (Z1i , . . . , Zmi ), i = 1, . . . , n iid

Die typischen Schritte einer Monte-Carlo-Simulation sind:


Algorithmus 3.1.2.

1. erzeuge (Ui )i∈N idd∼ U ([0, 1])

2. transformiere (Ui )i∈N in die benötigte Verteilung

82
3.2. Übungen

3. erzeuge Pfade

4. erzeuge Payoffs und speichere diese in einem Feld

5. erzeuge Schätzer und Konfidenzintervall

6. erzeuge ggf. Portfoliobewertung

3.2. Übungen
Aufgabe 5.

i) Schreiben Sie ein Programm zur Bewertung eines EUR-Calls mit der Monte Carlo -
Methode. Berechnen Sie dazu in dem Modell

dSt = (rd − r f )St dt + σ St dWt , t ∈ [0, T ] (3.6)

den theoretical value


exp(−rd T )E (St − K)+
Verwenden Sie als Daten Spot = 1.20, T = 92.0/365.0, σ = 9.35%, rd = 2.15% (annu-
alisiert), r f = 2% (annualisiert), Strike K = 1.20.

Geben Sie einen Schätzer mit 99,9% - Konfidenzintervall aus. Beachten Sie dabei, dass
das Black-Scholes - Modell mit stetigen Zinssätzen arbeitet. Sie müssen die angegebe-
nen Jahres - Zinssätze also noch geeignet konvertieren.
Auf der beiliegenden CD finden Sie einen Mersenne - Twister- Zufallszahlengenerator.
Dieser ist quasi Industriestandard und sehr viel besser als der in C++ standardmäßig
integrierte.

ii) Speichern Sie die Schätzer nach 1000, 2000, ... , 50000 Iterationen in einer Textdatei
zusammen mit dem exakten Ergebnis und erzeugen Sie in Excel eine Grafik, um die
Konvergenzgeschwindigkeit zu veranschaulichen.

iii) Wie viele Iterationen müsste man durchführen, um mit einer Wahrscheinlichkeit von
99,9% einen Fehler von höchstens 10−5 zu erhalten?

iv) Welche Operationen in der Monte Carlo Schleife sind in diesem konkreten Beispiel am
zeitaufwendigsten? Um Ihren Verdacht zu bestätigen, stoppen Sie die Zeit bei 10 Mil-
lionen Schleifendurchläufen einmal für das gesamte Programm und dann einmal nur für
den von Ihnen vermuteten Flaschenhals.
Ist dieser Effekt allgemeingültig? Was folgt daraus für die Wahl eines optimalen Dis-
kretisierungsverfahrens für SDEs?

Hinweis
Verwenden Sie folgendes Programm als Rahmengerüst:

83
3. Monte Carlo-Methoden

#include <time.h>

int main()
{
clock_t start, finish;
double duration;
start = clock();

// ...

finish = clock();
duration = (double)(finish - start) / CLOCKS_PER_SEC;

84
4. Techniken zur Varianzreduktion
4.1. Vorbemerkungen
Wir konnten uns bereits im Abschnitt (3.1.2) davon überzeugen, dass die Genauigkeit des
Monte Carlo-Schätzers abhängt von r
Var(X)
n
Das Problem ist, dass man bei großer Varianz von X die Anzahl der Simulationen n sehr groß
wählen muß, um eine vernünftige Genauigkeit zu erhalten, und dies kann leicht dazu führen,
dass der Algorithmus viel zu langsam ist. Das Wesen einer jeden Monte Carlo-Anwendung ist
es deshalb, eine Zufallsvariable Y zu finden mit den Eigenschaften

1. EY = EX

2. Var(Y )  Var(X)

denn dadurch kann man sehr viele Simulationsschritte bei gleicher Genauigkeit einsparen und
viele Probleme überhaupt erst lösen.
Als Monte Carlo-Methoden erstmals in den 1940ern und 1950ern aufkamen legte man sehr
wenig Wert auf die Optimierung von Konvergenzgeschwindigkeiten. So verwendete man da-
mals sehr gerne anstatt des bereits vorgestellten naiven Monte Carlo-Schätzers das sogenann-
te hit-or-miss-Monte Carlo, das kurz vorgestellt werden soll. Dabei werden wir uns von der
schlechten Konvergenz dieses Ansatzes überzeugen können, die maßgeblich für den schlech-
ten Ruf von Monte Carlo in den Folgejahren verantwortlich war. Außerdem werden wir an
diesem Verfahren wichtige Ideen zur Varianzreduktion illustrieren können, die wir dann in
den folgenden Abschnitten genauer beschreiben.

4.1.1. Hit-or-miss Monte Carlo


Wir möchten uns auf einen einfachen aber illustrativen Fall beschränken. Zu berechnen sei das
Integral
Z 1
Θ= f (x) dx, 0 ≤ f (x) ≤ 1 ∀x ∈ [0, 1]
0
f (U) dP, U ∼ U [0, 1]
R
Für den naiven Monte Carlo-Schätzer ergibt sich wegen Θ =

1 n
X̄n = ∑ f (Ui)
n i=1

85
4. Techniken zur Varianzreduktion

Dieser ist offenbar erwartungstreu mit Varianz


Z 1
1 1
Var(X̄n ) = E( f (U) − Θ)2 = ( f (x) − Θ)2 dx
n n 0

Für hit-or-miss-Monte Carlo definieren wir zunächst

g(x, y) = 1, falls y ≤ f (x) und 0 sonst


R1
Wegen f (x) = 0 g(x, y) dy gilt
Z 1Z 1 Z
Θ= g(x, y) dy dx = g(U,V ) dP, U,V ∼ U [0, 1]unabh.
0 0

und für den Schätzer folgt nun

1 n
Ȳn := ∑ g(Ui,Vi), Ui,Vi iid ∼ U [0, 1]
n i=1

Anschaulich erzeugt man n Punkte im Einheitsquadrat [0, 1] × [0, 1] und die Funktion g lie-
fert 1, wenn der Punkt unterhalb des Graphen von f liegt und 0 sonst. Gerade wegen dieser
einfachen Interpretation war hit-or-miss-Monte Carlo ursprünglich so beliebt und wegen

g(U,V ) ∼ Binomial(1, Θ)

ist der Schätzer erwartungstreu mit Varianz


1 1
Var(Ȳn ) = Var(g(U,V )) = Θ(1 − Θ) (4.1)
n n
Ein Vergleich der Varianzen zeigt nun, dass der Schätzer stets schlechter als der naive Monte
Carlo-Schätzer ist:
 Z 1 
1 2
Var(Ȳn ) − Var(X̄n ) = Θ(1 − Θ) − ( f (x) − Θ) dx
n 0
 Z 1 
1 2
= Θ− f (x) dx
n 0
1 1
Z
= f (x)(1 − f (x)) dx ≥ 0
n 0
Die Abschätzung verharmlost das Problem etwas, denn in der Praxis treten leicht Unterschiede
in der Größenordnung von Faktor 3 und mehr auf.
Was lehrt uns dieser kurze Ausflug in die Geschichte der Monte Carlo-Methoden? Schauen
wir uns den Ausdruck für die Varianz des naiven Schätzers noch einmal an:
Z 1
1
Var(X̄n ) = ( f (x) − Θ)2 dx
n 0

86
4.1. Vorbemerkungen

Man könnte die Varianz offenbar verringern, wenn die Funktion f nur noch wenig um Θ
schwanken würde, im Idealfall hätte man f gerne konstant gleich Θ! Es wäre also ratsam,
die Funktion f so zu verändern oder zu ergänzen, dass ihre Schwankungen verringert wer-
den, ohne dass der Schätzer seine Erwartungstreue für Θ verliert. Diese Idee wird bei den
Antithetic-Variates-Methoden verfolgt.
Die schlechte Konvergenz von hit-or-miss- Monte Carlo führt uns auf ein weiteres für Mon-
te Carlo ganz zentrales Prinzip: In einer Monte Carlo-Simulation sollte man Schätzer für Aus-
drücke, die man explizit berechnen kann, stets durch ihren analytischen Ausdruck ersetzen,
denn dadurch wird praktisch immer die Gesamtvarianz der Simulation reduziert. Als Beispiel
dient der Ausdruck Z 1
g(x, y) dy
0
den man durch sein analytisches Ergebnis f (x) wie gesehen ersetzen sollte. Eine Methode, die
dieses Prinzip umsetzt, ist die Control Variates-Methode. Hier wird versucht, einen möglichst
großen Teil von f bereits analytisch auszurechnen, indem man eine Funktion g findet, die f
möglichst ähnlich ist:
Z 1 Z 1 Z 1
f (x) dx = g(x) dx + f (x) − g(x) dx
0 0 0

Der erste Ausdruck wird analytisch bestimmt, der zweite mit einem naiven Monte Carlo-
Schätzer, wobei g den größten Teil der Varianz von f bereits absorbiert.
Nach diesen eher heuristischen Betrachtungen kehren wir nun zum allgemeinen Fall zurück
und präzisieren die genannten Punkte. Es wird sich zeigen, dass man Varianzreduktionsme-
thoden häufig aus verschiedenen Perspektiven betrachten kann und deswegen werden die hier
genannten Interpretationen im folgenden nicht mehr im Zentrum der Betrachtung stehen.

4.1.1.1. Übungen
Aufgabe 6.
Schreiben Sie analog zur Aufgabe 5 ein Programm, das den Standardfehler des naiven Mon-
te Carlo-Schätzers und des hit-or-miss-Schätzers berechnet bei 50000 Simulationen.
Aufgabe 7.
Der Satz vom iterierten Logarithmus besagt, dass für eine stetige Brownsche Bewegung (Wt )t∈[0,∞ ]
gilt:
Wt
lim sup p = 1P − f .s.
t→∞ 2t log (logt)
Wt
lim inf p = −1P − f .s.
t→∞ 2t log (logt)

Veranschaulichen Sie sich diesen Zusammenhang, indem Sie 15 Pfade der Brownschen Bewe-
gung auf einem geeigneten Zeitintervall erzeugen, deren Pfade in einer Datei zwischenspei-
chern und in Excel eine Grafik erzeugen.

87
4. Techniken zur Varianzreduktion

4.2. Control Variates


Wir beginnen mit einem Verfahren, das sehr häufig anwendbar ist und zudem zu den effizien-
testen Varianzreduktionstechniken gehört. Außerdem erlaubt es die Angabe eines Konfidenz-
intervalls, was sowohl die Analyse als auch die Anwendung sehr angenehm macht.

4.2.1. Idee und Konvergenz des Verfahrens

Gegeben seien die Payoffs X und Y , wobei E[X] bekannt und E[Y ] zu bestimmen. Wenn X
und Y sehr ähnlich sind, kann man von Schätzfehlern bzgl. X (bekannt!) auf den Schätzfehler
bzgl. Y (unbekannt!) Rückschlüsse ziehen. Etwas genauer:
Definition 4.2.1. Zu jedem simulierten Pfad des Underlying bestimmen wir Realisierungen
der Payoffs Xi und Yi , so dass (Xi ,Yi ) iid, (X1 ,Y1 ) ∼ (X,Y ). Dann heißt

Ȳ (b) = Ȳ − b(X̄ − E[X]) (4.2)

Control-Variate-Schätzer1 .
Der Schätzer in (4.2)hat gleichen Erwartungswert wie Ȳ , bei geschickter Wahl von b aber eine
kleinere Varianz hat. Der folgende Satz soll diese Idee präzisieren.
Satz 4.2.1. Seien X und Y Payoffs, wobei E[X] bekannt sei und E[Y ] zu bestimmen. Es seien
(Xi ,Yi ), i = 1, . . . , n unabhängig identisch verteilte Replikationen. Dann heißt

Yi (b) = Yi − b(Xi − E[X]) (4.3)

Control Variate und für den Control-Variate-Schätzer

Ȳ (b) = Ȳ − b(X̄ − E[X]) (4.4)

gilt:

i) Der Schätzer Ȳ (b) ist erwartungstreu (engl. unbiased) und stark konsistent

ii) Die Varianz des Schätzer Ȳ (b) wird minimal für

Cov(X,Y )
b∗ =
Var(X)

und es gilt
σb2
2
= 1 − ρX2 Y
σY
wobei Var(Y1 (b)) = σb2 , Var(Y ) = σY2 und ρXY Korrelation von X, Y .
1 engl. variate: Zufallsvariable

88
4.2. Control Variates

Beweis. Zur Erwartungstreue:

E[Ȳ (b)] = E[Ȳ ] − b (E[X̄] − E[X]) = E[Y ] (4.5)

Zur Konsistenz:
!
1 n 1 n
Ȳ (b) = ∑ Yi − b ∑ Xi − E[X] −→ E[Y ] P − f .s. (4.6)
n i=1 n i=1
| {z }
n→∞
−→0 P− f .s.

Für die Varianz zeigt man zunächst

Var(Yi (b)) = Var [Yi − b(Xi − E[X])]


= σY2 − 2 bCov(Yi , Xi ) + b2 σX2
= σY2 − 2 b σX σY ρXY + b2 σX2 =: σb2

und σb2 wird minimal für


σY Cov(X,Y )
b∗ = ρX Y =
σx Var(X)
Einsetzen ergibt nun

2 2Cov(X,Y )2 Cov(X,Y )2 2
σb∗ = σY2 − + σ
Var(X) Var(X)2 X
Cov(X,Y )2
= σY2 − = σY2 − σY2 ρXY
Var(X)

und man sieht direkt, dass


σb2∗ 2
= 1 − ρXY (4.7)
σY2

Bemerkung 4.2.1.

• Die Varianz des Control-Variate Schätzers ist:


!
1 n σb2
Var(Ȳ (b)) = Var ∑ Yi (b) =
n i=1 n

• Die Effizienz der Varianzreduktion wird von der Korrelation ρX Y bestimmt. Gleichung
(4.7) zeigt, dass ρX2 Y den Varianzanteil misst, der durch die Control-Variate-Technik
eliminiert wird.

89
4. Techniken zur Varianzreduktion

• Um durch naive Monte-Carlo Simulation die gleiche Genauigkeit wie beim Control-
σy2 !
Variate Schätzer mit n Pfaden zu erreichen, benötigt man wegen n∗ = Var(Ȳ ) = Var(Ȳ (b)) =
σy2 2
n (1 − ρX Y )
n
n∗ =
1 − ρX2 Y
Replikationen.

Die Technik ist für ρX Y ≈ 1 sehr effizient, fällt dann aber sehr schnell ab wie folgende
Tabelle zeigt:
ρX Y 0.95 0.9 0.7
n∗ 10n 5n 2n
• In der Praxis müssen σY und ρX Y häufig geschätzt werden (ρX Y für b∗ , σY für das
Konfidenzintervall), etwa durch:
∑ni=1 (Xi − X̄)(Yi − Ȳ ) (n→∞)
b̂∗ = −→ b∗ P − f .s. (4.8)
∑ni=1 (Xi − X̄)2

4.2.2. Beispiele
Beispiel 4.2.1. Underlying assets
Underlying Assets sind meist gute Kandidaten, weil die diskontierten Assets Martingale sind
und somit bekannten Erwartungswert haben. Oft weiß man auch noch ein bisschen mehr, etwa
die Varianz, und man muß nur noch die Korrelation schätzen. Folgendes Beispiel zeigt, dass
die Effizienz stark vom konkreten Problem abhängt:
Es gelte im Black-Scholes-Modell:
dSt = rSt dt + σ St dWt t ∈ [0, T ]
S0 = x ∈ R
unter Q, dann ergibt sich für den Erwartungswert
E[e−rT ST ] = E[e−r0 S0 ] = S0
also
E[ST ] = erT S0
Sei nun X := e−rT (ST −K)+ ein Plain Vanilla Call. Dann gilt für den Control-Variate-Schätzer
1 n
X̄(b̂) = ∑ (Xi − b̂[STi − erT S0])
n i=1
Für die Korrelation ρX ST ergibt sich:
K 40 45 50 55 70
ρ̂X, ST 0, 995 0, 968 0, 895 0, 768 0, 286
2
ρ̂X, 0, 99 0, 94 0, 80 0, 59 0, 08
ST

90
4.2. Control Variates

wobei r = 5%, σ = 30%, S0 = 50, T = 41 . Graphisch sieht man nochmal sehr schön, wie
die Effizienz des Verfahrens abnimmt, wenn die Korrelation von Payoff und Kontrollvariable
abnimmt. In diesem Fall ist offensichtlich, dass der Payoff der Option für kleinen Strike K
immer mehr der ersten Winkelhalbierenden ähnelt und somit dem Payoff Y = ST .

Abbildung 4.1.: Korrelation und Effizienz der Kontrollvariable in Abhängigkeit von Kontrakt-
daten

Beispiel 4.2.2. Tractable options


Häufig simuliert man den Erwartungswert einer Auszahlung, obwohl einfachere, sehr ähnliche
Optionen in geschlossener Form bewertbar sind. Diese Optionen bieten sich als Control-
Variate an. Das Standardbeispiel sind asiatische Optionen , bei denen Call und Put auf den
arithmetischen Durchschnitt
1 n
S̄A = ∑ Sti
n i=1

simuliert werden, während Call und Put auf den geometrischen Durchschnitt
 ZT 
1
S̄G := exp ln(St )dt
T 0

in geschlossener Form bewertet werden können. Eine Alternative zur Simulation ist der Zu-
gang über partielle Differentialgleichung, siehe Vecer[[11]].

91
4. Techniken zur Varianzreduktion

4.2.3. Konfidenzintervall
Satz 4.2.2. Mit obigen Bezeichnungen gilt

Ȳ (b̂n ) − E[Y ] w
−→ N (0, 1)
(b̂n )
sn√
n

d.h.
sn (b̂n )
Ȳ (b̂n ) ± z δ √
2 n
p
ist ein asymptotisches Konfidenzintervall, wobei sn (b) den Schätzer für σ (b) = VarY1 (b)
analog zu (3.3) bezeichnet.
Beweis. Mit dem zentralen Grenzwertsatz folgt für festes b ∈ R und σ 2 (b) = Var(Y1 (b))

Ȳ (b) − E[Y ]
−→ N (0, 1)
ω
σ√(b)
n

wobei
1 n
Ȳ (b) = ∑ (Yi − b(Xi − E[X]))
n i=1
Setze nun b̂n aus (4.8) als Schätzer für b∗ ein, d.h.

b̂n → b∗ P − f .s.

dann folgt
√  √
n Ȳ (b̂n ) − Ȳ (b∗ ) = (b∗ − b̂n ) n(X̄ − E[X])
−→ 0 · N (0, σX2 ) = 0 (n → ∞)
ω

Damit folgt:

√ Ȳ (b̂n ) − E[Y ] √ Ȳ (b̂n ) − Ȳ (b∗ ) √ Ȳ (b∗ ) − E[Y ]


n = n + n
σ (b∗ ) σ (b∗ ) σ (b∗ )
0
+ N (0, 1) = N (0, 1) (n → ∞)
ω
−→
σ (b∗ )
sn (b̂n )
Wegen σ (b∗ ) −→ 1 P − f .s. (ohne Beweis) folgt

√ Ȳ (b̂n ) − E[Y ] ω
n −→ N (0, 1)
sn (b̂n )

In der Praxis geht man so vor:

92
4.3. Antithetic Variates

1. Erzeuge (Xi ,Yi ) iid, i = 1, . . . , n

2. Bestimme X̄, Ȳ

3. Bestimme b̂∗

4. Bestimme sn (b̂∗ )

5. Bestimme Ȳ (b̂∗ ) und das Konfidenzintervall

4.3. Antithetic Variates


Antithetic Variates ist ein Sammelbegriff für alle Methoden, die eine Kollektion von geeignet
verteilten Zufallsvariablen einführen und die ihre Varianz aufgrund negativer Korrelation ge-
genseitig ausgleichen sollen: Die Ausreißer der einen Variablen werden ausgeglichen durch
die Ausreißer einer anderen Variablen. Im folgenden werden wir uns auf die gängigste unter
diesen Varianzreduktionsmethoden beschränken, die wir nun in einem Satz vorstellen.

4.3.1. Grundlegende Aussage (Antithetic Variates)


Satz 4.3.1. Sei Y eine Zufallsvariable und E(Y ) zu berechnen. Seien nun (Yi , Ȳi ) iid - verteilte
Zufallsvariablen, Yi , Ȳi ∼ Y, i ∈ N und Cov(Yi , Ȳi ) < 0 ∀ i ∈ N. Dann ist der Antithetic Variates
- Schätzer
!
n n
1 1 n Yi + Ȳi
ŶAV = ∑ ∑Yi + Ȳi = ∑ 2
2n i=1 i=1 n i=1
ein erwartungstreuer Schätzer mit asymptotischem Konfidenzintervall
sAV
ŶAV ± z δ √
2 n
Yi +Ȳi
wobei sAV der erwartungstreue Schätzer der Varianz von 2 ist.
Beweis. Ganz analog wie in Abschnitt(3.1.1) mit dem starken Gesetz der großen Zahlen und
dem zentralen Grenzwertsatz.

4.3.2. Beurteilung der Effizienz


Leider kann man den Effekt dieser Methode in aller Regel nicht präzise quantifizieren, wir
werden uns daher und Faustregeln begnügen müssen. Zunächst beurteilen wir den neuen
Schätzer wie üblich mit dem MSE, für den sich wegen der Erwartungstreue ergibt:
 
Y +Ȳ
Var 2
MSE(ŶAV ) = Var(ŶAV ) =
n

93
4. Techniken zur Varianzreduktion

Wir werden später konkrete Beispiele sehen, die die Annahme rechtfertigen, dass der Zeitbe-
darf für die Erzeugung von Y + Ȳ in etwa dem doppelten Zeitbedarf für die Erzeugung von Y
entspricht. Deshalb ist es plausibel, den Antithetic Variates-Schätzer
1 n Yi + Ȳi
∑ 2
n i=1

mit dem naiven Monte Carlo-Schätzer


1 2n
∑ Yi
2n i=1

zu vergleichen. Der Antithetic Variates-Schätzer ist nun effizienter, wenn gilt

1 2n 1 1
Var(ŶAV ) < Var( ∑ Yi ) ⇐⇒ Var(Y + Ȳ ) < 2 2n Var(Y )
2n 1 4n 4n
⇐⇒ Var(Y + Ȳ ) < 2Var(Y )
⇐⇒ Var(Y ) + Var(Ȳ ) + 2Cov(Y, Ȳ ) < 2Var(Y )
⇐⇒ Cov(Y, Ȳ ) < 0

Die Effizienz des Schätzers hängtnur davon ab, wie stark die negative Korrelation zwischen Y
und Ȳ ist, also ist dies die einzige Eigenschaft, die wir im konkreten Fall untersuchen müssen.
Es sei an dieser Stelle nochmals betont, dass die Antithetic Variates-Methoden die Varianz
durch Einführung negativer Abhängigkeit zwischen den Replikationen zu reduzieren versu-
chen, was anschaulich den gegenseitigen Ausgleich von Ausreißern bedeutet.

4.3.3. Beispiele
Beispiel 4.3.1. Ein für unsere Zwecke sehr wichtiges Beispiel ist ein Payoff, der aus [0, 1]-
gleichverteilten Zufallsvariablen erzeugt wird, d.h. wir betrachten Payoffs der Form

Y = f (U1 , . . . ,Un ) U1 , . . . ,Un ∼ U [0, 1] iid

Wegen
1 −U ∼ U ∼ U[0, 1]
bietet sich die Definition
Ȳ = f (1 −U1 , . . . , 1 −Un )
an.
Beispiel 4.3.2. Bei der Simulation beliebiger Verteilungen mit Verteilungsfunktion F haben
wir in (3.1.4.2) gezeigt, dass F −1 (U) , U ∼ U [0, 1] bereits die gewünschte Verteilung hat. Gilt
also
Y = F −1 (U)
so bietet sich die Definition
Ȳ = F −1 (1 −U)

94
4.3. Antithetic Variates

an, und im Fall einer symmetrischen Verteilung gilt sogar

F −1 (1 −U) = −F −1 (U)

Wenn wir also einen Payoff


Y = f (N) , N ∼ N(0, 1)
simulieren, so ergibt sich
Ȳ = f (−N)
und hier sei darauf hingewiesen, dass bei der Erzeugung von Y und Ȳ immer die gleichen
Zufallszahlen benutzt werden müssen (klar). Das zentrale Beispiel ist hier der Fall eines von
einer geometrischen Brownschen Bewegung mit stetiger Dividende abhängenden Payoffs, d.h.

dSt = (rd − r f )St dt + σ St dWt t ∈ [0, T ]


S0 = x ∈ R

und
Y = f (St0 , . . . , Stn ), 0 = t0 < · · · < tn = T
Hier erhält man

σ2 √
 
Sti+1 = Sti exp (rd − r f − )(ti+1 − ti ) + σ ti+1 − ti Z i = 0, . . . , n − 1
2
σ2 √
 
S̄ti+1 = S̄ti exp (rd − r f − )(ti+1 − ti ) + σ ti+1 − ti (−Z) i = 0, . . . , n − 1
2
S0 = S̄0 = x ∈ R

Man kann die Methode anhand dieser Beispiele wie in (4.1.1) auch so interpretieren, dass
man die Varianz des Schätzers, im wesentlichen also Var(Y ), dadurch reduziert, dass Y ohne
den Erwartungswert zu verändern in eine möglichst konstante Abbildung transfomiert wird:
Ausschläge in eine Richtung durch einen extremen Wert der Zufallszahl N werden ausgegli-
chen durch eine weitere Simulation mit 1 − N. Es gibt wesentlich komplizierte Beispiele in
denen etwa die Inputvariablen U1 , . . . ,Un mittels geeigneter Matrizen transformiert und mehr
als zwei Variablen pro Iteration erzeugt werden, aber die genannten Beispiele sind für die
Praxis zunächst einmal die wichtigsten und die Beurteilung weiterführender Verfahren wird
schnell sehr aufwendig.
In (4.3.2) haben wir angenommen, dass der Zeitbedarf zur Erzeugung einer Replikation von
Y + Ȳ dem doppelten Zeitbedarf der Erzeugung von Y entspricht. Dies wird nun plausibel,
wenn die Zeitersparnis durch Wiederverwendung einer Zufallszahl sehr klein ist im Verhältnis
zur gesamten Rechenzeit, die man zur Erzeugung eines komplizierten Payoffs benötigt (was
aber in der Praxis häufig nicht stimmt).
Abschließend möchten wir auf die Effizienz der Schätzer eingehen, die sich aus den Beispielen
ergeben.

95
4. Techniken zur Varianzreduktion

4.3.4. Faustregeln für die Effizienz


Der Antithetic Variate-Schätzer ist effizient, wenn Y und Ȳ stark negativ korreliert sind. Damit
folgt leicht:

a) Im Fall
Y = f (U) , Ȳ = f (1 −U)
sollte f die negative Korrelation der Inputvariablen erhalten, was möglich ist wenn f
monoton.

b) Im Fall
Y = f (U)
oder
Y = f (N)
und f linear gilt
Var(Y + Ȳ ) = 0
Mit anderen Worten: je besser sich f linear approximieren lässt, desto besser der Schätzer.

Die genannten Bedingungen sind so wie wir sie eingeführt haben lediglich hinreichend für
einen effizienten Schätzer. Dass sie in gewisser Weise auch notwendig sind, zeigt der folgende
Abschnitt.

4.3.5. Varianzzerlegung
Betrachte Y = f (Z) , Z ∼ N(0, 1) und

f (z) + f (−z)
f0 (z) =
2
bezeichne den symmetrischen Anteil von f ,

f (z) − f (−z)
f1 (z) =
2
den schief-symmetrischen Anteil, der im hier gezeigten eindimensionalen Fall also punkt-
symmetrisch zum Ursprung (z.B. linear) ist. Dann gilt

1
Cov( f0 (Z), f1 (Z)) = E( f0 (Z) f1 (Z)) − E( f0 (Z))E( f1 (Z)) = E( f 2 (Z) − f 2 (−Z)) − 0 = 0
4
und damit folgt die Varianzzerlegung

Var( f (Z)) = Var( f0 (Z)) + Var( f1 (Z)) (4.9)

96
4.4. Übungen

In Worten: Die Varianz des naiven Monte Carlo-Schätzers Var( f (Z)) ist die Summe der Va-
rianz des Antithetic Variates-Schätzer Var( f0 (Z)) und der Varianz des schief-symmetrischen
Anteils Var( f1 (Z)). Ebenso leicht folgt
 
Y + Ȳ 1
ŶAV effizient ⇐⇒ Var < Var(Y )
2 2
1
⇐⇒ Var(Y ) − Var( f1 (Z)) < Var(Y )
2
1
⇐⇒ Var( f1 (Z)) > Var(Y )
2
⇐⇒ f ist ausreichend schief-symmetrisch

Fazit
Es gibt in der Praxis nur sehr selten Fälle, in denen f nicht die genannten Bedingungen erfüllt,
und deshalb trägt die Antithetic-Variates-Methode im allgemeinen zur Varianzreduktion bei.
Sie ist leicht zu implementieren, man benötigt kein spezielles Wissen über das benutzte Mo-
dell, und entsprechend wird sie in der Praxis so gut wie immer als eine erste Stufe der Vari-
anzreduktion, basierend auf dem linearen Anteil des Payoffs, benutzt. Wegen der universellen
Anwendbarkeit und ihrer Einfachheit sollte man hinsichtlich der Effizienzsteigerung aber i.a.
keine allzu großen Verbesserungen erwarten. Nichtsdestotrotz sei nochmals betont, dass man
mit dieser Methode auch einen etwas schlechteren Schätzer erhalten kann, der aber immerhin
noch erwartungstreu bleibt.

4.4. Übungen
Aufgabe 8.
Schreiben Sie ein Programm, das einen Plain Vanilla Call mit einem naiven Monte Carlo-
Schätzer und zusätzlich mit Antithetic Variates berechnet. Verwenden Sie die gleichen Daten
wie in Aufgabe 5. Variieren Sie den Strike, was können Sie beobachten? Begründung?

97
4. Techniken zur Varianzreduktion

98
5. Griechen in Monte-Carlo
Die Bedeutung der Greeks kann an dieser Stelle nicht dargestellt werden, aber zumindest
möchten wir auf den Begriff des Delta - Hedgings hinweisen. Neben diesem ”Delta” sind
weitere Ableitungen etwa für das Risikomanagement von Optionen relevant, so z.B. Vega,
Vanna und Volga. Diese Dinge sind an sich eigene Forschungsgebiete und wir verweisen auf
Jürgen Hakala und Uwe Wystup[[8]].
Zur Bestimmung dieser Ableitungen gibt es drei Ansätze:

1. Pfadweises Ableiten des Prozesses

2. Ableitung einer geeigneten Wahrscheinlichkeitsdichte

3. Finite Differenzen

Im folgenden bezeichnet der Ausdruck Y (x) den Payoff Y , der von der Größe x abhängt und
dessen Ableitung nach dieser Größe bestimmt werden soll.

5.1. Pfadweises Ableiten des Prozesses


5.1.1. Motivation
Wir betrachten die Funktion f (x) := E[Y (x)]. Setzt man die Definition der Ableitung von f an
der Stelle x ein, so ergibt sich
 
0 E[Y (x + h)] − E[Y (x)] Y (x + h) −Y (x)
f (x) = lim = lim E (5.1)
h→0 h h→0 h

Könnte man nun Grenzwert und Integral vertauschen, und könnte man den Prozess Y pfad-
weise differenzieren, dann würde folgen
 
0 Hoffnung Y (x + h) −Y (x)
f (x) = E lim (5.2)
h→0 h

Wegen  
Y (x + h) −Y (x) Y (x + h)(ω) −Y (x)(ω)
lim (ω) := lim
h→0 h h→0 h
spricht man von der pfadweisen Ableitung des Prozesses. Das Vertauschen von Limes und
Integral muß im Einzelfall geprüft werden.

99
5. Griechen in Monte-Carlo

5.1.2. Beispiele
Beispiel 5.1.1. Black-Scholes-Delta
Wir gehen erneut vom Modell mit stetigen Dividenden aus, d.h.

dSt = (rd − r f )St dt + σ St dWt t ∈ [0, T ]


S0 = x ∈ R

und betrachten den Payoff


Y (S0 ) := e−rd T (ST (S0 ) − K)+
Für festes ω ∈ Ω definiere gω : R+ → R+
0 mit

gω (S0 ) := e−rd T (ST (S0 )(ω) −K)+ ∈ R+


0
| {z }
∈R+

Später werden wir sehen, dass man Limes und Integral vertauschen darf und es soll nun der
Ausdruck (5.2) ausführlich berechnet werden. Wir wenden dazu die Kettenregel an:

gω (S0 ) =: u(v(S0 ))

mit
v(S0 ) := ST (S0 )(ω) und u(X) := (X − K)+ e−rd T
Die Kettenregel ist anwendbar, denn

v differenzierbar ∀ S0 ∈ R +

und
u differenzierbar in ST (ω)(S0 )∀ ω ∈ {ST (S0 ) 6= K}
Wegen P(ST (S0 ) 6= K) = 1 folgt

ST (ω)
g0ω (S0 ) = e−rd T I{ST (ω)(S0 )>K} für fast alle ω ∈ Ω
S0

Es ergibt sich

Y (S0 + h) −Y (S0 )
Z
0
E[Y (S0 )] = lim dP
h→0 h
Y (S0 + h) −Y (S0 )
Z
= lim dP
h→0 h
ST
Z
= e−rd T I{ST (S0 )>K} dP
Ω S0
0
= E[Y (S0 )]

100
5.1. Pfadweises Ableiten des Prozesses

Beispiel 5.1.2. Für das Black-Scholes-Vega zeigt man


∂σ g(ST ) = e−rd T I{ST (σ )>K} ∂σ ST (σ ) (5.3)
   2

ST (σ )
log S0 − rd − r f + σ2 T
−rd T
= e ST (σ )I{ST (σ )>K} (5.4)
σ
denn wegen
 
1
ST = S0 exp (rd − r f − σ 2 )T + σ WT ⇐⇒
2
 
ST 1
σWT = log − (rd − r f − σ 2 )T
S0 2
folgt
ST
∂σ ST = ST (−σ T +WT ) = (−σ 2 T + σWT )
σ
σ2
 
ST ST
= log( ) − (rd − r f + )T
σ S0 2
und Einsetzen in (5.3) liefert die Behauptung.
Beispiel 5.1.3. Digital-Optionen
Betrachte den Payoff
Y = e−rd T I{ST >K}
mit der Dynamik
dSt = rSt dt + σ St dWt unter dem risikoneutralen Maß Q
Y ist stückweise konstant in St bzw. S0 . Auf {ω ∈ Ω : ST (ω) = K} ist die Ableitung nicht
definiert, aber das ist wegen P(ST = K) = 0 harmlos. Es folgt Y 0 (S0 ) = 0 [P]. Wegen 0 =
(S0 )]
E[Y 0 (S0 )]6= ∂ E[Y
∂ S0 funktioniert die Methode hier also nicht, denn sie berücksichtig nur, dass
S0 lokal ohne Einfluß auf den Payoff ist. Die höhere Wahrscheinlichkeit des Überschreitens
der Barriere wird übersehen.
Bemerkung 5.1.1.
• analog ist die Methode bei Barrier-Optionen nicht anwendbar, weil ein einzelner Pfad
lokal nicht sensitiv bzgl. S0 ist.
• zweite Ableitungen sind mit dieser Methode i.a. nicht bestimmbar, wie das Beispiel des
Plain Vanilla Call zeigt:
Y = e−rT (ST − K)+
dY ST
= e−rT I{ST >K}
dS0 S0
2
d Y
= 0 [P]
d 2 S0
wie beim Digital Call.

101
5. Griechen in Monte-Carlo

5.1.3. Eine hinreichende Bedingung für die Erwartungstreue


Satz 5.1.1. Es sei Y (θ ) = f (X1 (θ ), . . . , Xm (θ )), mit f : Rm → R.
Es gilt
dE(Y (θ )
E[Y 0 (θ )] =

unter den folgenden Voraussetzungen:
1. ∃ Xi0 (θ )[P], ∀ θ , i =, 1 . . . , m

2. P(X(θ ) ∈ D f ) = 1, ∀ θ , mit D f := {x ∈ Rm , f diffbar in x}

3. f ist global Lipschitz-stetig, d.h. ∃ k f > 0, ∀ x, y ∈ Rm :


| f (x) − f (y)| ≤ k f ||x − y||

4. es gibt Zufallsvariablen X̃i ∈ L 1 , i = 1, . . . , m, mit |Xi (θ2 ) − Xi (θ1 )| ≤ X̃i |θ2 − θ1 |


Beweis.
Y (θ + h) −Y (θ )
1) + 2) =⇒ ∃Y 0 (θ ) = lim [P], ∀ θ
h→0 h
m
∂f
mit Y 0 (θ ) = ∑ (X(θ ))Xi0 (θ )
i=1 ∂ xi
2) + 3) =⇒ |Y (θ2 ) −Y (θ1 )| ≤ k f ||X(θ2 ) − X(θ1 )||
o.E.||.||=||.||1 m
≤ kf ∑ X̃i |θ1 − θ2|, ∀ θ1, θ2 ∈ I.
i=1
| {z }
∈L1

Der Satz von der dominierten Konvergenz liefert die Behauptung.


Bemerkung 5.1.2.
• Im Black-Scholes-Modell sind beim Delta
1)ok, 2)ok
4)ok weil ST linear in S0 , 3) auch erfüllt

• Bedingung 3) ist wesentliche Einschränkung für die Praxis.

Wir schließen diesen Abschnitt mit einer


Satz 5.1.2. Faustregel für die Praxis
Die Pathwise-method ist anwendbar, wenn der Payoff im abzuleitenden Parameter stetig ist.

5.2. Likelihood Ratio Method (LRM)


Das Problem bei der Pathwise-Method ist die Glattheit des Integranden. Im folgenden versucht
man, nicht mehr Payoffs, sondern die i.a. glatten Wahrscheinlichkeitsdichten abzuleiten.

102
5.2. Likelihood Ratio Method (LRM)

5.2.1. Motivation
Der Payoff
Y (θ ) = f (X(θ )) = f (X1 (θ ), . . . , Xm (θ ))
besitze eine Wahrscheinlichkeitsdichte gθ , d.h. es gelte
Z
h(θ ) = E[Y (θ )] = f (u)gθ (u)du
Rm

Kann man nun unter dem Integral differenzieren, dann erhält man

dgθ (u)
Z
0
h (θ ) = f (u) du
Rm dθ
ġθ (u)
Z
= f (u) g(u)du
R m g(u)
 
ġθ (X)
= E f (X)
gθ (X)

Definition 5.2.1.
ġθ (X)
f (X)
gθ (X)
heißt Likelihood Ratio Method (LRM)-estimatior.
Wahrscheinlichkeitsdichten sind meistens sehr glatt, deshalb ist das Vertauschen von Limes
und Integral hier weniger einschränkend als bei der pathwise method. In der Statistik wird ein
Ausdruck der Form
d log(gθ ) ġθ
=
dθ gθ
ġθ
als score-function bezeichnet und im folgenden heißt gθ score. LRM wird manchmal auch als
score-function-method bezeichnet.

5.2.2. Beispiele
Beispiel 5.2.1. Black-Scholes-Delta
Die Dichte von ST ist
 
σ2

x
1 log S0 − (rd − r f − 2 )T
g(x) = √ φ √ 
xσ T σ T

Dieses Ergebnis erhält man, indem man sich die Verteilungsfunktion x → P(ST ≤ x) auf-
schreibt und nach x ableitet. Für den score ergibt sich
  2
dg(x) log Sx0 − (rd − r f − σ2 )T
dS0
=
g(x) S0 σ 2 T

103
5. Griechen in Monte-Carlo

Die Zufallsvariable  
ST 2
log S0 − (rd − r f − σ2 )T
e−rd T (ST − K)+
S0 σ 2 T
ist ein erwartungstreuer Schätzer. Wenn ST gemäß

σ2 √
 
ST = S0 exp (r − )T + σ T Z Z ∼ N (0, 1)
2

erzeugt wird, läßt sich dies vereinfachen zu

(ST − K)+ Z
e−rT √
S0 σ T

Bemerkung 5.2.1. Die genaue Darstellung des Payoffs ist hier irrelevant. Ein Schätzer für das
Delta einer Digital-Option ist z.B.

Z
e−rT I{ST >K} √
S0 σ T

Die score-function zur Bestimmung von Vega ist


dg(X) √
dσ Z2 − 1
= ··· = −Z T
g(X) σ

was dem Leser als Übung überlassen wird.


Beispiel 5.2.2. Pfadabhängige Deltas
Wir betrachte jetzt eine Option, deren Payoff abhängig ist von St1 , . . . , Stm .
Einige Rechnungen ergeben für den score

Z1
√ Z1 ∼ N (0, 1)
S0 σ t1

wobei Z1 die Zufallsvariable zur Erzeugung von St1 aus S0 ist. Zum Beispiel erhält man als
LRM-Schätzer für das Delta einer asiatischen Option

Z1
e−rT (S̄ − K)+ √
S0 σ t1

Bemerkung 5.2.2. Für das pfadabhängige Vega erhält man den score
!
m Z 2j − 1 p
∑ σ − Z j t j − t j−1
j=1

Diskrete Barrier-Optionen werden analog behandelt.

104
5.2. Likelihood Ratio Method (LRM)

5.2.3. Bias und Varianz


Zunächst zum Begriff des Bias: Zur Berechnung von E[X] wird die Zufallsvariable X simuliert
bzw im allgemeinen lediglich in Verteilung durch ein XMC approximiert. Daraus folgt also,
dass
E[X] 6= E[XMC ]
In diesem Fall, in dem X nicht exakt simuliert werden kann, hat man also das Problem, dass
man gemäß dem starken Gesetz der großen Zahlen einen falschen Erwartungswert approxi-
miert, der dazu noch mit einem Fehler behaftet ist, der über die Varianz eingegrenzt werden
kann. Diese Situation gilt es also wenn möglich zu vermeiden. LRM-Schätzer sind meistens
erwartungstreu, wie bereits in (5.2.1) intuitiv begründet wurde. Problematisch sind hingegen
die folgenden Punkte:

a) Existenz und Bestimmung einer Dichte

b) große Varianz des Schätzers. Dies soll hier nur heuristisch motiviert werden:

i) Bei festem T und 0 ≤ t1 < · · · < tm ≤ T gelte m → ∞, d.h. die Partition werde beliebig
fein. Für die Varianz des scores für das Delta einer asiatischen Option folgt
 
Z1 1
Var √ = 2 2 →∞ (t1 → 0)
S0 σ t1 S0 σ t1

ii) Bei äquidistanten t1 , . . . ,tm und T → ∞ wächst die Varianz des Vega-Schätzers

Der score hat bekannten Erwartungswert 0 und kann deshalb als control variate benutzt wer-
den, un die Varianz etwas zu reduzieren.

5.2.4. Zweite Ableitungen


5.2.4.1. LRM
Mit der Likelihood Ratio Method können auch höhere Ableitungen ausgerechnet werden. Für
das Gamma im Black-Scholes-Modell erhält man den score:
d 2 g(St )
d 2 S0 Z2 − 1 Z
= ··· = 2 2
− 2 √ , Z ∼ N (0, 1)
g(ST ) S0 σ T S0 σ T

Wie in (5.2.1) motiviert ist


g̈θ (X)
f (X)
| {z } gθ (X)
(Payoff)

normalerweise ein erwartungstreuer Schätzer, die Varianz wächst aber oft nochmals an.
Bemerkung 5.2.3. Die Erweiterung auf pfadabhängige Optionen ist leicht möglich, indem man
wieder T durch t1 und Z durch Z1 ersetzt.

105
5. Griechen in Monte-Carlo

5.3. Finite Differenzen


5.3.1. Überblick
Eine in der Praxis häufig benutzte Möglichkeit zur Bestimmung der Ableitung von

f (θ ) = E(X(θ ))

ist die Verwendung finiter Differenzen


 
0 f (θ + h) − f (θ )
f (θ ) ≈
h
Die Varianz ist manchmal sehr günstig, wobei die Wahl von h aber entscheidend ist. Weite-
re Argumente gegen diesen intuitiv naheliegenden Ansatz sind der erhöhte Rechenaufwand,
schließlich müssen hier zwei Größen geschätzt werden, und man schätzt die falsche Größe.
Wir können dies auch so ausdrücken: der finite Differenzen-Ansatz kann zwar eine kleinere
Varianz haben als andere Verfahren wenn h gut gewählt wird, andererseits wird dies durch
einen bias und höheren Rechenaufwand erkauft.

5.3.2. Analyse von Bias und Varianz


Der finite Differenzen-Schätzer sieht folgendermaßen aus:
X̂n (S0 + h) − X̂n (S0 )
∆ˆ n =
h
Offensichtlich ist dieser Schätzer verzerrt mit
E(X(S0 + h)) − E(S0 ) ∂ E(X(S0 ))
E(∆ˆn ) = = + O(h)
h ∂S0
sofern die Abbildung S0 → E(X(S0 )) genügend glatt ist. Man kann diese Fehlerordnung ver-
bessern, indem man den zentralen Differenzenquotienten der Form
f (x + h) − f (x − h)
f 0 (x) = + O(h2 )
2h
verwendet, aber bei der Bestimmung von Ableitungen mit Monte Carlo Methoden ist dies das
kleinere Problem. Viel schlimmer ist die Varianz
Var(X̂n (S0 + h) − X̂n (S0 ))
Var(∆ˆ n ) =
h2
die für h → 0 und festes n ∈ N explodieren kann, wenn der Zähler nicht schnell genug ver-
schwindet. Es ergeben sich nun drei Fälle:
• Var(X̂n (S0 + h) − X̂n (S0 )) = O(1)
d.h. der Zähler konvergiert gegen eine Konstante c ∈ R, die Varianz wird somit asym-
ptotisch wie h → h12 verlaufen.

106
5.3. Finite Differenzen

Abbildung 5.1.: Finite Differenzen-Approximation für das Delta beim Call und beim eu-
ropäischen Up and Out Call für verschiedene Schrittweiten h

• Var(X̂n (S0 + h) − X̂n (S0 )) = O(h)


dies erhält man typischerweise bei Verwendung gleicher Zufallszahlen bei den Schätzern.
Asymptotisch wird die Varianz des ∆-Schätzers wie h → 1h explodieren.

• Var(X̂n (S0 + h) − X̂n (S0 )) = O(h2 )


dies erhält man i.a. dann, wenn die Bedingungen aus (5.1.3) erfüllt sind, so dass man
als wesentliche praktische Einschränkung die Stetigkeit der Option im abzuleitenden
Parameter erhält. Für eine genaue Diskussion siehe das Buch von Glasserman[[4]].

5.3.3. Beispiel
Für den Call sollte also der Fehler für h → 0 beliebig klein werden, von Rundungsfehlern
einmal abgesehen, die man im deterministischen Fall ebenfalls erhält. Der Barrier-Call hin-
gegen verletzt die Stetigkeitsbedingung und wird wie O( 1h ) explodieren. Die Abbildung 5.1
bestätigt das. Man erkennt sehr schön, wie der relative Fehler beim Call immer kleiner wird
und dann bei sehr kleinem h wieder instabiler wird wegen der Rundungsfehler. Ferner sieht
man, dass der Fehler beim Barrier-Call für kleines h schnell groß wird und der optimale Wert
für h überraschend groß ist, nämlich zwischen 2 und 4. Man erkennt ganz allgemein, dass die
Varianz beim Call erheblich geringer ist, weil die Linie viel ruhiger verläuft, beim Barrier-Call
hingegen wird sie je näher man h = 0 kommt immer weiter aufgerauht.

107
5. Griechen in Monte-Carlo

5.4. Ausblick
5.4.1. Zweite Ableitungen
Man kann zur Schätzung zweiter Ableitungen auch gemischte Schätzer verwenden. Dies führt
oft zu sehr viel kleineren Varianzen.
Beispiel 5.4.1. Black-Scholes-Gamma

i) LR-PW-Schätzer:

e−rT Z K I{ST >K}


 
d −rT + Z
e (ST − K) √ = √
dS0 S0 σ T S02 σ T

ii) PW-LR-Schätzer: (nach Rechnung)


 2  
−rT ST Z
e I{ST >K} √ −1
S0 σ T

5.4.2. Fazit
Die Pathwise method liefert die besten Ergebnisse, sofern sie anwendbar ist:

- kleinere Varianz als LRM

- weniger Rechenaufwand und Bias als bei finiten Differenzen

Die Likelihood-ratio-method ist wichtig, wenn die Pathwise method nicht anwendbar ist, aber:

- Die Wahrscheinlichkeitsdichte muss gegeben sein

- häufig große Varianz

Finite Differenzen :

- einfach zu implementieren

- um Bias und Varianz klein zu halten, muss die Schrittweite h geschickt gewählt werden.
Obwohl dies in Glasserman[[4]] analysiert wird, ist der praktische Nutzen dieser theo-
retischen optimalen Schrittweiten h∗ begrenzt, weil deren Berechnung Größen benötigt
werden, die man wiederum nicht kennt, bei der Bestimmung erster Ableitungen wie
f 0 (θ ) in (5.3.1) etwa der Wert der zweiten Ableitung f 00 (θ ).

108
5.4. Ausblick

5.4.3. Übungen
Aufgabe

Berechnen Sie das Delta des Call im Black-Scholes-Modell mit

a) der Pathwise - method

b) der Likelihood - ratio - method

c) finiten Differenzen

Verwenden Sie für a) und b) die Daten Spot = 50, T = 0.25, K = 50, rd = 3%, r f = 0%,
σ = 30%, Nominal = 1.
Hinweis: Im Black - Scholes - Modell ergibt sich als Referenzwert 0.549447.

Teil c) dient dazu, das Beispiel (5.3.3) am Rechner selbst nachzuvollziehen. Verwenden Sie
bitte die Daten Spot = 120, Strike = 120, T = 1, σ = 9.35%, rd = 0.02 (annualisiert), r f =
0.02 (annualisiert), und 10000 Iterationen.
Für das Call-Delta ergibt sich analytisch 0.50847427. Bestimmen Sie zusätzlich das Delta
eines europäischen Up-and-out Call mit Barrier 130. Hier ergibt sich analytisch 0.07149855.

109
5. Griechen in Monte-Carlo

110
6. Diskretisierung von Stochastischen
Differentialgleichungen
6.1. Einleitung
In der Praxis wird man meistens mit komplizierteren stochastischen Differentialgleichungen
arbeiten als mit der einer geometrischen Brownschen Bewegung. Im folgenden werden auto-
nome SDEs der Form

dXt = a(Xt ) dt + b(Xt ) dWt t ∈ [0, T ]


X0 = x ∈ R

betrachtet, wobei wir der Einfachheit halber Xt ∈ R, also den eindimensionalen Fall, unterstel-
len. Analoge Aussagen gelten für den mehrdimensionalen Fall. Bevor wir beginnen, möchten
wir aus dem Buch von Kloeden und Platen[[10]] ein allgemeines Resultat über Existenz und
Eindeutigkeit der Lösung einer Stochastischen Differentialgleichung angeben.

6.1.1. Existenz und Eindeutigkeit


Satz 6.1.1. Betrachte die nicht-autonome SDE

dXt = a(t, Xt )dt + b(t, Xt )dWt t ∈ [t0 , T ]

mit den folgenden Bedingungen:

A a(t, x) und b(t, x) sind messbare Abbildungen in (t, x) ∈ [t0 , T ] × R

B a und b sind Lipschitz-stetig, d.h.

∃K ∈ R : |a(t, x) − a(t, y)| + |b(t, x) − b(t, y)| ≤ K|x − y| ∀t ∈ [t0 , T ], x, y ∈ R

C Lineares Wachstum von a und b, d.h.

|a(t, x)|2 + |b(t, x)|2 ≤ K(1 + x2 ) ∀t ∈ [t0 , T ], x, y ∈ R

D Der Anfangswert Xt0 ist messbar bzgl. der Sigma-Algebra F0 = σ (Wt0 ) und

E(|Xt0 |2 ) < ∞

111
6. Diskretisierung von Stochastischen Differentialgleichungen

Dann existiert eine Lösung der SDE mit stetigen Pfaden, die pfadweise eindeutig ist, d.h. es
gilt
P( sup |Xt − X̄t | > 0) = 0
t0 ≤t≤T

für zwei Lösungen X und X̄.


Bemerkung 6.1.1.
Insbesondere existiert eine Lösung für jede gegebene Brownsche Bewegung: man spricht in
diesem Fall von einer starken Lösung der SDE, im Gegensatz zu einer schwachen, wenn man
die Brownsche Bewegung zur Lösung austauschen muß.

6.2. Diskretisierungsschemata
Im Fall deterministischer Differentialgleichungen wird die Taylorregel extensiv benutzt, um
geeignete Diskretisierungsschemata zu finden. Dies wird nun auf den stochastischen Fall
übertragen, indem man die Ito-Formel rekursiv anwendet und so stochastische Taylorent-
wicklungen erhält, was an dieser Stelle aber nicht ausgeführt werden soll, siehe Kloeden und
Platen[[10]]. Praktisch bedeutsame Verfahren zur numerischen Approximation sind

i) Das Euler - Maruyama - Schema


Sei X̂ die Approximation auf dem Zeitgitter T0 = 0 < t1 < · · · < tN = T . Dann gilt für
diese Art der Approximation die Vorschrift

X̂0 = X0

X̂ti+1 = X̂ti + a(X̂ti ) (ti+1 − ti ) + b(X̂ti ) ti+1 − ti Zi+1

wobei Z1 , . . . , ZN iid, Z1 ∼ N (0, 1).

ii) Das Milstein-Schema


Sei X̂ die Approximation auf dem Zeitgitter T0 = 0 < t1 < · · · < tN = T . Dann gilt für
diese Art der Approximation die Vorschrift

X̂0 = X0

X̂ti+1 = X̂ti + a(X̂ti ) (ti+1 − ti ) + b(X̂ti ) ti+1 − ti Zi+1 +
1 0 2
b (X̂ti )b(X̂ti )(ti+1 − ti )(Zi+1 − 1)
2

wobei Z1 , . . . , ZN iid, Z1 ∼ N (0, 1).

In den Formeln wurden Ausdrücke wie Wti+1 − Wti , die durch die Taylorentwicklungen ent-
stehen, bereits durch verteilungsgleiche Ausdrücke ersetzt, die man am PC leicht simulieren
kann.

112
6.3. Güte der Approximation

6.3. Güte der Approximation


Definition 6.3.1. Zur Messung der Approximationsqualität gibt es zwei wichtige Kategorien:

i) Starke Konvergenz der Ordnung β


Diese liegt vor, wenn
E(|X̂tN − XT |) ≤ chβ (6.1)
für eine Konstante c ∈ R und h ∈ R klein genug, wobei h die Schrittweite des Diskreti-
sierungsschemas bezeichnet, d.h. es gelte 0 = t0 < · · · < tn äquidistant mit n = Th . Dies
entspricht der globalen Konvergenz im deterministischen Fall, d.h. wenn in der SDE gilt
b ≡ 0.

ii) Schwache Konvergenz der Ordnung β


Diese liegt vor, wenn
E(| f (X̂tN ) − f (XT )|) ≤ chβ (6.2)
für eine Konstante c ∈ R und h ∈ R klein genug und alle Funktionen f : R → R für
deren Ableitung gilt:

| f (i) (x)| ≤ c(1 + |x|q ) ∀x ∈ R, i = 0, . . . , 2β + 2

für gewisse Konstanten c, q ∈ R. Diese Eigenschaft bezeichnet man auch als polynomial
wachstumsbeschränkte Ableitungen bis zum Grad 2β + 2.

Es gibt nun diverse Sätze, die Aussagen über die Qualität der Approximationen machen und
dabei im wesentlichen Voraussetzungen an die Funktionen a und b der SDE stellen. Für ge-
naue Aussagen sei auf Kloeden und Platen[[10]] verwiesen. Dennoch sollte man sich gewisse
Faustregeln für β merken: Das Euler-Schema hat bei hinreichend gutmütigen SDEs eine starke
Konvergenzordnung von 21 und eine schwache Konvergenzordnung von 1, was überraschend
gut ist. Das Milstein-Schema liefert sowohl starke wie schwache Konvergenzordnung 1. We-
gen der überraschend guten schwachen Konvergenzordnung des Euler-Schemas wird es in der
Praxis sehr häufig verwendet, wobei man aber wie im deterministischen Fall Probleme mit der
Stabilität der Lösung bekommen kann (siehe Übungen). Da die derzeit in der Finanzmathe-
matik benutzten SDEs in der Regel nur schwach nicht-linear sind lohnt es den Aufwand aber
nicht, auf kompliziertere Verfahren zurückzugreifen.

6.4. Pfadabhängige Payoffs und Diskretisierung


Die Kriterien zur Beurteilung von Diskretisierungsschemata lassen zunächst keinen Rück-
schluss auf die Approximation ganzer Pfade zu, obwohl gerade das häufig benötigt wird.
Im folgenden Abschnitt leiten wir erwartungstreue Schätzer für Payoffs her, die von zeit-
stetigen Extrema einer geometrischen Brownschen Bewegung abhängen, wobei wir speziell
Lookback- und Barrieroptionen betrachten. In einem weiteren Abschnitt geben wir ohne Be-
weis einige weitere Varianten für allgemeine Diffusionsprozesse an, die in der Praxis wichtig
sind.

113
6. Diskretisierung von Stochastischen Differentialgleichungen

6.4.1. Erwartungstreue Schätzer für Lookback- und Barrieroptionen


Wir folgen in diesem Abschnitt der Darstellung von Andersen[[1]]. In einem ersten Schritt
zeigen wir, dass das naive Schätzen zeitstetiger Extrema sogar im Fall exakter Simulierbarkeit
der Verteilung des Underlying nicht optimal ist:
Satz 6.4.1. Sei (St )t≥0 eine geometrische Brownsche Bewegung, d.h. der Prozess erfülle

dSt = µSt dt + σ St dWt , t ∈ [0, T ] (6.3)


S0 = x ∈ R (6.4)

und zu schätzen sei


M[0,T ] = max St m[0,T ] = min St (6.5)
0≤t≤T 0≤t≤T

Wir betrachten als naiven Schätzer


N
M̂[0,T ] = max Sti
i=0,...,N
(6.6)

bzw.
m̂N
[0,T ] = min Sti (6.7)
i=0,...,N

wobei 0 = t0 < · · · < tN = T , und die Verteilung von (St0 , . . . , StN ) wird exakt simuliert durch
die Rekursion

σ2 √
 
Sti+1 = Sti exp (µ − )(ti+1 − ti ) + σ ti+1 − ti ñi+1 , i = 0, . . . , N − 1 (6.8)
2
S0 = x ∈ R (6.9)

mit ñi ∼ N(0, 1) iid, i = 1, . . . , N. Dann gilt

a) Die Schätzer in (6.6) und (6.7) unterschätzen f.s. das Maximum bzw überschätzen f.s.
das Minimum
N d
b) M̂[0,T ] → M[0,T ]

Beweis. Aussage a) ist klar. Zu b): Wir beweisen nur die Aussage für das Maximum. Sei o.E.
T = 1. Mit Portmanteau genügt es zu zeigen, dass gilt
Z Z
N
f ◦ M̂[0,T ] dP → f ◦ M[0,T ] dP (N → ∞)
n
∀ f : R → R Lipschitz-stetig und beschränkt

Klar ist für g((St )t ) := max0≤t≤T St die Abschätzung

|g(S) − g(S̃)| ≤ ||S − S̃||∞,[0,T ] P − f .s. (6.10)

114
6.4. Pfadabhängige Payoffs und Diskretisierung

Bezeichnet ŜN nun den Prozess, der durch pfadweise lineare Interpolation von (St0 , . . . , StN )
entsteht und ÂN den Prozess, der durch pfadweise lineare Interpolation des mit dem einfachen
Eulerschema gebildeten Prozesses (S̃t0 , . . . , S̃tN ) entsteht, dann folgt
 
N N

|E f ◦ M̂[0,T ] − E f ◦ M[0,T ] | ≤ E| f ◦ M̂[0,T ] − f ◦ M[0,T ] |
 
N
≤ E L f |M̂[0,T ] − M [0,T ] | (6.11)

≤ E L f ||ŜN − S||∞

≤ L f E||ŜN − ÂN ||∞ + E||ÂN − S||∞
r !
1 log (N − 1)
≤ Lf max Ci √ +D (6.12)
i=0,...,N N −1 N −1
→ 0 (N → ∞)
wobei L f Lipschitzkonstante von f und maxi=0,...,N Ci beschränkt.
Der Beweis suggeriert bereits die langsame Konvergenz, die im Beispiel 6.4.1 bestätigt wird.
Um einen erwartungstreuen Schätzer zu konstruieren, benötigen wir einige Verteilungsresul-
tate, die wir nun bereitstellen.
Satz 6.4.2. Sei 0 = t0 < · · · < tN = T, N ∈ N, S eine geometrische Brownsche Bewegung aus
(6.3) und s > 0 eine Barriere. Dann gilt für die bedingten Verteilungen des running maximum
und running minimum mit t < ti+1
(
 0, s ≤ Sti
P M[ti ,t] ≤ s|Sti , Sti+1 = (6.13)
N(−xi ) − ξi (s) N(yi ), s > Sti
(
 1, s ≥ Sti
P m[ti ,t] ≤ s|Sti , Sti+1 = (6.14)
N(xi ) + ξi (s) N(yi ), 0 < s < Sti
wobei
S   
t s
(t − ti ) log Si+1
ti
− δ log Sti
xi := p (6.15)
σ δ (t − ti )(ti+1 − t)
 2   
(t − ti ) log St s St − δ log Sst
yi := p i+1 i i
(6.16)
σ δ (t − ti )(ti+1 − t)
   S 
t
2 log Sst log i+1 s
i
ξi (s) := exp  2
 (6.17)
σ δ

Beweis. Der Beweis ist rein technisch, deshalb begnügen wir uns mit dem Hinweis, dass man
zunächst mit der Ito-Formel übergeht zum Prozess
 
Sr
Zr := log r ∈ [ti , T ]
Sti

115
6. Diskretisierung von Stochastischen Differentialgleichungen

Anschließend bestimmt man die Dichte der Verteilung von (M[tZi ,t] , Zti+1 ), mit der man die
Verteilungsfunktion
z 7→ P(M[tZi ,t] ≤ z|Zti+1 = zti+1 ) ≡ P(M[ti ,t] ≤ s|Sti , Sti+1 )
berechnen kann.
Nun folgern wir das zentrale Hilfsmittel (time to first passage), mit dem wir später erwar-
tungstreue Schätzer konstruieren können:
Satz 6.4.3. Für
τtsi := inf{t ≥ ti : St = s}, s>0
und t ∈ (ti ,ti+1 ), i ∈ {0, 1, . . . , N} folgt
P(τtsi ≤ t|Sti , Sti+1 ) = N(φi xi ) + ξi (s) N(φi yi ) (6.18)
wobei
φi = sign(s − Sti )
und xi , yi , ξi (s) wie in (6.15).

Beweis. Für s = Sti gilt trivialerweise


P(τtsi ≤ t|Sti , Sti+1 ) = 1
Für s > Sti folgt
P(τtsi ≤ t|Sti , Sti+1 ) = 1 − P(τtsi > t|Sti , Sti+1 ) = 1 − P(M[ti ,t] < s|Sti , Sti+1 )
und für 0 < s < Sti gilt
P(τtsi ≤ t|Sti , Sti+1 ) = P(m[t,ti ] ≤ s|Sti , Sti+1 )
Einsetzen liefert die Behauptung.

Korollar 6.4.1. Durch Grenzübergang t ↑ ti+1 erhalten wir die Wahrscheinlichkeit für den Bar-
rieredurchbruch (
ξi (s), φi (Sti+1 − s) < 0
P(τtsi ≤ ti+1 |Sti , Sti+1 ) = (6.19)
1, sonst
d.h. wenn z.B. Sti und Sti+1 beide oberhalb oder unterhalb der Barriere s liegen, dann ist ξi (s)
die Wahrscheinlichkeit, dass S die Barriere in [ti ,ti+1 ] erreicht.
Wir betrachten im folgenden einige Beispiele, wobei wir als zugrundeliegendes Modell bei
der Black-Scholes-Welt bleiben, so dass für den fairen Preis eines claims VT gilt
V0 = exp(−rT ) EQ (VT ) Q risikoneutrales Maß (6.20)
Als allgemeinen Schätzer verwenden wir
1 N
V̂0 = exp(−rT ) ∑ V̂T (Sti0 , . . . , Stin )
N i=1
(6.21)

wobei die (Sti0 , . . . , Stin ) ∼ (St0 , . . . , Stn ), i = 1, . . . , N unabhängig sind.

116
6.4. Pfadabhängige Payoffs und Diskretisierung

Beispiel 6.4.1. Wir betrachten eine Up-and-out Digital Option mit Payoff

VT = x I{max0≤t≤T St <H} (6.22)

Für den naiven Schätzer


V̂T = x I{St <H, i=0,...,N} (6.23)
i

mit (St0 , . . . , StN ) erzeugt wie in (6.8) erhält man als Ergebnis Abbildung 6.1 (siehe Übungen).
Man erkennt, dass (6.23) asymptotisch erwartungstreu ist, für die Praxis ist der Rechenauf-

Abbildung 6.1.: Ergebnis des naiven Schätzers für die Up and out Digital Option

wand allerdings zu hoch. Um einen erwartungstreuen Schätzer zu konstruieren, führt man


zusätzliche Zufallsvariablen

Ui ∼ U [0, 1], i = 0, . . . , N − 1

ein und setzt


N
V̂T,ad j = x I{St <H, i=0,...,N, ui−1 ≥ξi−1 (H), i=1,...,N}
i
(6.24)
Die Idee dabei ist, dass die Barriere H zwischen ti und ti+1 mit bekannter Wahrscheinlichkeit
ξi (H) erreicht wird. Als Ergebnis erhält man die Tabelle (6.4.1) (siehe Übungen). Wie man
sieht sind die Schätzer unabhängig von N gleich gut und der analytische Wert 82.222 liegt
stets im 2σ -Intervall, was darauf hindeutet, dass der Bias eliminiert wurde.
Man kann diese Technik auf alle Optionen anwenden mit Payoff ausschließlich in t = T , um
einen erwartungstreuen Schätzer zu konstruieren, problematisch wird es hingegen wenn auch
der Zeitpunkt des Überschreitens der Barriere benötigt wird, wie im folgenden Fall.

117
6. Diskretisierung von Stochastischen Differentialgleichungen

N Schätzer Standardabweichung
1 82.149117 0.060367
2 82.177462 0.060301
4 82.160679 0.060340
8 82.178208 0.060299
16 82.139047 0.060391
32 82.149863 0.060366
64 82.334104 0.059929
128 82.203942 0.060238
256 82.196110 0.060257
512 82.146879 0.060373
Tabelle 6.1.: Ergebnisse für den verbesserten Schätzer (6.24)

Beispiel 6.4.2. Wir betrachten eine Up-and-in Cash-at-Hit-Option mit Payoff


VT = exp(r(T − τ0H )) R I{τ H ≤T } (6.25)
0

mit τ0H := inf{t ≥ 0 : St = H}


Man beachte hier den Ausdruck exp(r(T − τ0H )), der nötig ist um den Auszahlungsbetrag R
auf den einheitlichen Zeitpunkt T zu diskontieren. In einem naiven Schätzer würde man
τ̂0H = min{ti : Ŝti φ0 ≥ φ0 H ∀i = 0, . . . , N}, φ0 = sign(H − S0 )
verwenden, was zu einer systematischen Unterschätzung des Preises führt. Stattdessen erinnert
man sich an den Schätzer (6.24), in dem der Treffer mit einer U [0, 1]-verteilten Zufallsvariable
U simuliert wurde. Wegen der analytisch bekannten Verteilungsfunktion
t 7→ Fi (t) = P(τtHi ≤ t | Sti , Sti+1 )
H
kann man für U < ξi (H) den Trefferzeitpunkt τ̂0,ad j bestimmen mit

H −1
τ̂0,ad j = Fi (U) (6.26)
Wir lösen diese Gleichung (6.26) explizit für den Fall einer upper barrier und erhalten einen
erwartungstreuen Schätzer für alle N ∈ N:

Fi (t) = P(τtHi ≤ t|Sti Sti+1 )


(
ξi (t, H) Sti , Sti+1 < H
=
1 sonst
     St  
 2 log SHt log i+1 H
 i
 exp  2 (t−t )
 Sti , Sti+1 < H
= σ i


1 sonst

118
6.4. Pfadabhängige Payoffs und Diskretisierung

Dabei nehmen wir an, dass   


H Sti+1
log log <0
Sti H
so dass Fi (t) ↑ 1 (t → ∞) wie es sein soll. Insbesondere beachte man, dass ξi hier als Funktion
in Abhängigkeit der Zeit t und nicht der barrier H betrachtet wird, wie es im späteren Beispiel
des Lookback-Put sein wird. Die Gleichung
U = Fi (τ)
kann man nun durch einige elementare Umformungen lösen und man erhält
Sti+1
2 log( SHt ) log( 2
H ) + ti σ logU
i
τ=
σ 2 logU
Wegen U ≤ ξi (H) ∈ (0, 1) erhält man die Abschätzung
Sti +1
2 log( SHt ) log( 2
H ) + ti σ logU
i
τ≤
σ 2 log (ξi (H,ti+1 ))
was darauf hindeutet, dass die Auswertung von τ numerisch stabil bleibt.
Zum Abschluß betrachten wir das
Beispiel 6.4.3. Lookback-Put
Diese Option hat Payoff
VT = M[0,T ] − ST
Wegen (
1 − ξi (s) s ≥ max(Sti , Sti+1 )
Fi (s) = P(M[ti ,ti+1 ] ≤ s | Sti , Sti+1 ) = (6.27)
0 sonst
kann man die Verteilung
M[ti ,ti+1 ] | Sti =si , Sti+1 =si+1
P
simulieren durch
Fi−1 (U), U ∼ U (0, 1)
bzw. äquivalent durch
ξi−1 (U), U ∼ U (0, 1)
Als erwartungstreuer Schätzer für alle N ∈ N erhält man nun
V̂T,ad j = max{ξi−1 (U), i = 0, . . . , N − 1} − ST (6.28)
d.h. man simuliert zunächst (St0 , . . . , StN ) und damit wie angegeben die bedingten Verteilungen
von M[ti ,ti+1 ] . Zur analytischen Lösung:
M̂[ti ,ti+1 ] = ξi−1 (U) ⇐⇒ ξi (M̂[ti ,ti+1 ] ) = U
! !
M̂[ti ,ti+1 ] Sti+1 σ2
⇐⇒ log log = (ti+1 − ti ) logU
Sti M̂[ti ,ti+1 ] 2
  σ2
⇐⇒ log M̂[ti ,ti+1 ] − log Sti log Sti+1 − log M̂[ti ,ti+1 ] = (ti+1 − ti ) logU
2

119
6. Diskretisierung von Stochastischen Differentialgleichungen

Einfache Umformungen, bei denen man nur die Lösung der quadratischen Gleichung mit dem
positiven Wurzelzweig verfolgt, ergeben schließlich
r  2
 Sti+1
log Sti+1 Sti + log St − 2σ 2 (ti+1 − ti ) logU
i
log(M̂[ti ,ti+1 ] ) = (6.29)
2

und daraus erhält man M̂[ti ,ti+1 ] .

6.4.2. Weitere Verfahren


An dieser Stelle sollen kurz und ohne Beweis weitere Lösungsmöglichkeiten vorgestellt wer-
den.

6.4.2.1. Einführung einer Statusvariablen zur Beseitigung der zeitlichen


Abhängigkeit
Wir betrachten
T
  Z 
E exp − rt dt
0

mit

drt = µ(rt )dt + σ (rt )dWt t ∈ [0, T ]


r0 = x ∈ R

Die offensichtliche Lösung ist wie immer:

1. Simuliere (r̂ti , i = 1, . . . , n) mit äquidistanten ti

2. berechne exp (−h ∑ni=1 r̂ti ) und bilde das arithmetische Mittel

Viel besser ist hingegen die Einführung einer Statusvariablen, in der die Vergangenheit des
Pfades abgebildet wird. Dazu definieren wir
 Zt 
Dt = exp − ru du t ∈ [0, T ]
0

und wenden ein Diskretisierungsschema zweiter Ordnung an auf den augmentierten Prozess

drt = µ(rt )dt + σ (rt )dWt (6.30)


dDt = −rt Dt dt (6.31)

Die Brownsche Bewegung bleibt eindimensional, das Problem wurde also nicht unnötig auf-
gebläht, und in der Regel bleibt wegen der Glattheit der Koeffizienten die Konvergenzordnung
des Verfahrens, das man auf die erste Komponente (6.30) anwendet, bei Diskretisierung des

120
6.4. Pfadabhängige Payoffs und Diskretisierung

zweidimensionalen Systems erhalten. Mit anderen Worten kann man die schwache Konver-
genzordnung des verwendeten Diskretisierungsverfahrens, die nur in einem festen Zeitpunkt
definiert ist, auf D̂T übertragen. Folgendes Schema liefert nun zweite Ordnung:

1
r̂ti+1 = r̂ti + µh + σ ∆W + σ σ 0 ∆W 2 − h

 2 
1 0 0 1 2 00
+ σ µ + µσ + σ σ ∆W h +
2 2
 
1 0 1 2 00 2
+ µµ + σ µ h
2 2
 
1 2  2 1
D̂ti+1 = D̂ti 1 − r̂ti h + r̂ti − µ(r̂ti ) h − σ (r̂ti )∆W h
2 2
wobei alle Funktionen an der Stelle r̂ti auszuwerten sind.

6.4.2.2. Approximation mit der Brownschen Brücke


In diesem Abschnitt möchten wir unser Wissen über die Verteilung des Maximums einfacher
Prozesse wie der geometrischen Brownschen Bewegung auf allgemeinere Diffusionsprozesse
übertragen. Dazu widmen wir uns erneut der Simulation des running maximum

Mt := max Su
0≤u≤T

und S sei ein Diffusionsprozess der Gestalt

dSt = a(St )dt + b(St ) dWt t ∈ [0, T ] (6.32)

Die mehrfach vorgestellte naive Lösung ist wie immer

M̂T = max(Ŝt0 , . . . , Ŝtn )

mit zu äquidistanten Zeitpunkten 0 = t0 < · · · < tn = T unter Verwendung eines beliebigen


Diskretisierungsverfahrens erzeugten Realisierungen Ŝt0 , Ŝt1 , . . . , Ŝtn Dies entspricht der linea-
ren Interpolation der Pfade zwischen den Zeitpunkten ti , i = 0, . . . , n. Besser wäre hingegen
auf jedem Intervall [ti ,ti+1 ] die Interpolation mit einer Brownschen Bewegung, deren Anfangs-
bzw. Endpunkt durch Sti und Sti+1 vorgegeben ist - was per Definition eine Brownsche Brücke
ist. Wir haben die Verteilung des Maximums einer geometrischen Brownschen Bewegung mit
bedingtem Anfangs- und Endpunkt bereits hergeleitet und man kann die Ergebnisse durch
Anwendung der Itô-Formel auf den einfacheren Fall der Brownschen Brücke übertragen, was
wir aber nicht durchführen möchten. Speziell bei unserem Problem kann man S auf jedem
Teilintervall [ti ,ti+1 ] mit einer Brownschen Bewegung mit konstanten Koeffzienten ai = a(Ŝti )
und bi = b(Ŝti ) interpolieren, und man simuliert deren Maximum M̂i mittels
q
Ŝti + Ŝti+1 + (Ŝti+1 − Ŝti )2 − 2b2i (ti+1 − ti ) log(Ui )
M̂i = (6.33)
2

121
6. Diskretisierung von Stochastischen Differentialgleichungen

wobei Ui ∼ U [0, 1] iid. Nun setzt man

max Su = max{M̂0 , . . . , M̂n−1 }


0≤u≤T

Ganz analog kann man den Prozess S auf jedem Intervall [ti ,ti+1 ] mit einer geometrischen
Brownschen Bewegung und vorgegebenen Randwerten approximieren. Für die Simulation
des Maximums M̂i ergibt sich die bereits hergeleitete Formel
s   2
Ŝti+1
log(Ŝti+1 Ŝti ) + log Ŝti
− 2 b2i (ti+1 − ti ) log(Ui )
log(M̂i ) = (6.34)
2

6.4.2.3. Zeitstetige Barrieren und Varianzreduktion


Wir betrachten exemplarisch einen Knock-out-Put mit Payoff

S = (K − ST )+ I{τ>T }
τ = inf{t ≥ 0 : St > B}

und S sei ein Prozess der Form (6.32). Anwendung der Brownschen Brücke aus (6.33) oder
(6.34) liefert den Ausdruck
n−1
I{τ>T } = ∏ I{M̂i ≤B} (6.35)
j=0

in dem man sukzessive die Mi simuliert und unterwegs abbricht, falls die Barriere B über-
schritten wird. Man kann diesen Ausdruck auch schreiben als
n−1
I{τ>T } = ∏ I{Ui ≤ p̂i } Ui ∼ U [0, 1] iid (6.36)
i=0

mit !
 2(B − Ŝti )(B − Ŝti+1 )
p̂i := P I{M̂i ≤B} = 1 Sti Sti+1 ) = 1 − exp − (6.37)

b(Ŝti )2 h
Wenn S ein Markovprozess ist, kann man den Schätzer in (6.36) durch seine bedingte Erwar-
tung ersetzen. Diese besitzt den gleichen Erwartungswert und damit den gleichen Bias, hat
aber wegen der Jensenungleichung ein kleineres zweites Moment, was zu einer geringeren
Varianz führt. Es ergibt sich
!
n−1 n−1  
+ +
E (K − Ŝtn ) ∏ I{M̂i ≤B} |Ŝt0 , . . . , Ŝtn = (K − Ŝtn ) ∏ E I{M̂i ≤B} |Ŝti , Ŝti+1
i=0 i=0
n−1
= (K − Ŝtn )+ ∏ p̂i
i=0

122
6.5. Effizienz und Vergleich von Monte Carlo-Schätzern

und für die Varianz folgt


! !2
n−1 n−1
Var (K − Ŝtn )+ ∏ p̂i = E E((K − Ŝtn )+ ∏ I{M̂i ≤B} |Ŝt0 , . . . , Ŝtn ) −
i=0 i=0
!!2
n−1
E E((K − Ŝtn )+ ∏ I{M̂i ≤B} |Ŝt0 , . . . , Ŝtn )
i=0
!!2
n−1
≤ E E((K − Ŝtn )+ ∏ I{M̂i ≤B} |Ŝt0 , . . . , Ŝtn ) −
i=0
!2
n−1
E((K − Ŝtn )+ ∏ I{M̂i ≤B} )
i=0
!
n−1
= Var (K − Ŝtn )+ ∏ I{M̂i ≤B}
i=0

6.5. Effizienz und Vergleich von Monte Carlo-Schätzern


Nach den vorangehenden Kapiteln sind folgende Kriterien zur Beurteilung der Qualität eines
Schätzers naheliegend

• Varianz des Schätzers

• Bias / Verzerrung des Schätzers

• Anzahl benötigter Rechenoperationen

Wir haben bereits Techniken zur Reduktion von Varianz und Bias vorgestellt. Hierbei ist es
wichtig zu bemerken, dass diese Methoden orthogonal aufeinander stehen: Man entscheide
sich für eine Methode zur Reduktion des Bias, anschließend wende man davon unabhängig
Verfahren zur Varianzreduktion auf den Schätzer an. Es stellt sich nun die Frage, wie man
ein gegebenes Rechenbudget optimal aufteilt. Wir beginnen mit einem Vergleichskriterium
für unverzerrte Schätzer, das erstmals wohl 1964 von Hammersley und Handscomb[[5]] vor-
geschlagen wurde. Anschließend betrachten wir den Fall verzerrter Schätzer und beschäftigen
uns mit der optimalen Aufteilung eines gegebenen Rechenbudgets auf Bias- und Varianzre-
duktion. Bei der gesamten Betrachtung beschränken wir uns auf den praktisch relevanten Fall
des arithmetischen Mittelwertschätzers

1 n
X̂n := ∑ Xi Xi iid, i = 1, . . . , n (6.38)
n i=1
und es gelte
σ 2 := Var (X1 )

123
6. Diskretisierung von Stochastischen Differentialgleichungen

6.5.1. Vergleichskriterium für unverzerrte Schätzer


Satz 6.5.1. Wir betrachten zwei konkurrierende Schätzer
1 n
X̂n := ∑ Xi Xi iid, i = 1, . . . , n (6.39)
n i=1

und
1 n
Ŷn := ∑ Yi Yi iid, i = 1, . . . , n (6.40)
n i=1
mit
σ12 = Var(X1 ) σ22 = Var(Y1 )
Wir führen folgende Bezeichnungen ein:
s: Anzahl verfügbarer Rechenoperationen (”computational budget”)

τ1 : Anzahl benötigter Rechenoperationen pro Replikation von X1 , analog τ2 für Y1


Man wählt nun den Schätzer mit dem kleineren Produkt σ 2 τ, d.h. man betrachtet die Relation

σ12 τ1
(6.41)
σ22 τ2

Bemerkung 6.5.1. Die variance ratio


σ12
σ22
ist hier vor allem vom konkreten Problem abhängig und kann wie bereits mehrfach gesehen
geschätzt werden. Die labour ratio
τ1
τ2
hängt stark von der gewählten Monte Carlo-Methode ab. Bereits Hammersley und Handscomb[[5]]
bemerken, dass diese Größe nur sehr grob abgeschätzt werden sollte, etwa im Sinne der An-
zahl benötigter Multiplikationen und Divisionen.

Beweis. Man betrachte den Schätzer


1 n
X̂n = ∑ Xi Xi iid, i ∈ N
n i=1

Mit dem zentralen Grenzwertsatz folgt direkt


√  w
n X̂n − E (X) −→ N (0, σ 2 )

Nun folgt rj k 
s 
w
X̂b s c − E (X) −→ N (0, σ 2 )(s → ∞)
τ τ

124
6.5. Effizienz und Vergleich von Monte Carlo-Schätzern

und wegen
jsk1 1
→ (s → ∞)
τ s τ
ergibt sich
√  
w
s X̂b s c − E (X) −→ N (0, σ 2 τ)
τ

Fasst man mehrere Replikationen X1 , . . . , Xk zu einem Durchschnitt zusammen, so ändert sich


wegen !
1 k Var(X1 )
Var ∑ Xi τneu = kτ = σ 2 τ
k i=1 k
die Größe in (6.41) nicht. Eine reine Bezeichnungsänderung, was denn unter einer Replika-
tion zu verstehen sei, ändert die Effizienz des Verfahrens gemäß dieser Kennzahl also nicht,
was sehr plausibel ist. Ist der Rechenaufwand pro Replikation nicht konstant, wie etwa im
Fall einer Barrier-Option oder einer amerikanischen Option, so betrachtet man in (6.41) den
Ausdruck
σ 2 E (τ)

6.5.2. Vergleichskriterium für verzerrte Schätzer


Zur weiteren Analyse betrachten wir nun einen Schätzer

1 n
Ẑn := ∑ Zi
n i=1

Zu schätzen sei die Größe E(X) und es gelte

E(Z1 ) 6= E(X)

so dass der Schätzer Ẑn gegen den falschen Wert konvergiert. Im Financial Engineering ergibt
sich dieses Problem typischerweise in zwei Situationen:

• Diskretisierung des Modells


Die zugrundeliegende Stochastische Differentialgleichung kann nur approximativ gelöst
werden, im allgemeinen kann man den Bias durch Verkleinern der Schrittweite h im
Diskretisierungsschema beliebig reduzieren auf Kosten des Rechenaufwand pro Repli-
kation.

• Diskretisierung des Payoffs


Payoffs wie
X = I{max0≤t≤T St <B} (ST − K)+
müssen im allgemeinen mit St1 , . . . , Stn approximiert werden. Mit steigendem Aufwand
pro Replikation wird der Bias entsprechend klein.

125
6. Diskretisierung von Stochastischen Differentialgleichungen

Die Beurteilung eines verzerrten Schätzers erfolgt nun sehr leicht mit dem Mean Square Error
(MSE), für dessen Berechnung wir die allgemeine Beziehung

EY 2 = (EY )2 + Var(Y )

für jede Zufallsvariable Y ausnutzen. Es gilt:


!2
1 n 2
MSE = E ∑ Zi − E(X) = E(Ẑn ) − E(X) + Var(Ẑn − EX) = Bias2 + Var(Ẑn )
n i=1

Insbesondere folgt, dass die Qualität eines Schätzers von der Aufteilung des Rechenbudgets
auf Varianz- und Biasreduktion abhängig ist. Dies soll nun genauer untersucht werden.

6.5.3. Aufteilung des Rechenbudgets auf Bias- und


Varianzreduktion
Das im vorangehenden Kapitel aufgeworfene Problem soll nun analysiert werden. Dabei wer-
den wir uns hinsichtlich der Ergebnisse mangels besserer Alternativen mit einer asymptoti-
schen Analyse für s → ∞ begnügen müssen. Wir betrachten nun den Schätzer

1 n
X̂n,δ := ∑ Xi,δ
n i=1

Dabei sei X ein von ST abhängiger Payoff und für jede Realisierung Xi sei die zugehörige SDE
diskretisiert worden mit äquidistanter Schrittweite δ . Daher liegt die Annahme nahe, dass gilt

E Xi,δ =: αδ → E(X) =: α(δ → 0)

Ferner bezeichne τδ die Anzahl der Rechenoperationen pro Replikation bei Schrittweite δ , s
sei das zur Verfügung stehende Rechenbudget. Folgende Annahmen sind bei den in der Praxis
verwendeten Diskretisierungsschemata plausibel:
 
αδ − α = bδ + O δ β
β

Dies reflektiert die Qualität des Diskretisierungsverfahrens, in der Praxis gilt meist β ∈ { 12 , 1, 2}
und b ist eine nicht weiter interessante, weil nicht bestimmbare, Konstante.

τδ = cδ −η + o δ −η


Dies reflektiert die Kosten der Biasreduktion, bei der Diskretisierung von SDEs gilt meist
η = 1. Ziel unserer Bemühungen ist nun die Herleitung einer Budgetallokationsregel, spezi-
ell möchten wir einen geeigneten Wert für den Parameter γ in folgender Schrittweitenregel
bestimmen:
δ (s) = as−γ + o(s−γ )

126
6.6. Übungen

Je größer γ gewählt wird, desto kleiner die Schrittweite δ und desto größer die Reduktion des
Bias im Vergleich zur Varianz. Der Schätzer ist mit Angabe der Diskretisierungsschrittweite
vollständig bestimmt. Man kann nun durch Einsetzen und ausrechnen zeigen, dass
 
  2 2β − 2β2β+η − 2β2β+η

−η 2
MSE X̂n(s),δ (s) = b a + ca σ s +o s (6.42)

und  
 − 2ββ+η
RMSE X̂n(s),δ (s) = O s (6.43)

gilt. Diese optimalen Resultate erreicht man durch


1
γ∗ = (6.44)
2β + η
 1
ηcσ 2 2β +η

a∗ = (6.45)
2β b2
Eine Herleitung findet man in Glasserman[[4]]. Die Konstanten in (6.42) und (6.45) können
für ein Finetuning bei besonders laufzeitkritischen Anwendungen (also für den Fall s → ∞)
nützlich sein, zentral hingegen ist Gleichung (6.44). Typische Werte bei der Diskretisierung
von SDEs sind β = η = 1, so dass man als Allokationsregel erhält
1
δ (s) ≈ s 3
Mit anderen Worten: In typischen Anwendungen halbiert man die Schrittweite δ , wenn man
das Rechenbudget verachtfacht. Man verwendet also den weitaus größeren Teil des Rechen-
budgets für die Varianzreduktion, meist gilt sogar
 
2 1
Bias = O
δ2
 
1
Varianz = O
δ
und dies impliziert, dass es meist sehr viel leichter ist, den Biaszu verkleinern als die Varianz.
1
Vergleicht man den RMSE in (6.43) mit dem RMSE O s− 2 für den Fall des unverzerrten
Schätzers, so sieht man, dass die Diskretisierung einer SDE die Konvergenz des Schätzers
gewöhnlich verschlechtert. Selbst für den günstigeren Fall β = 2 und η = 1 erhält man
 2
O s− 5

6.6. Übungen
Aufgabe 9.
Die Faustregeln aus Abschnitt (6.3) sollen Sie nun selbstständig erarbeiten. Gehen Sie dazu
folgendermaßen vor:

127
6. Diskretisierung von Stochastischen Differentialgleichungen

1. Die Fehlerungleichungen (6.1) und (6.2) werden als exakt, d.h. mit einem ”=” interpre-
tiert.

2. Für verschiedene Schrittweiten h1 , . . . , hn werden die Größen

εs (h) := E(|X̂thN − XtN |) εw (h) := |E(X̂thN ) − E(XtN )|

berechnet, d.h. als Testfunktion für die schwache Konvergenz wählen wir hier die Iden-
tität f (x) ≡ x. In den Formeln wurde statt Wti+1 −Wti direkt die verteilungsgleiche Dar-
stellung (ti+1 − ti ) Xi+1 , Xi+1 ∼ N (0, 1) gewählt. Dies ist hier ausnahmsweise etwas
hinderlich und man braucht zur Simulation von XtN

N N √
WT −W0 = ∑ Wti −Wti−1 = ∑ ti − ti−1 Xi
i=1 i=1

3. Wegen log(ε(h)) = log(c) + β log(h) kann man nun die Paare

(log(hi ), log(c) + β log(hi ))

in Excel als Graph darstellen lassen und die Steigung der Regressionsgeraden lässt
Rückschlüsse auf β zu.

Bestimmen Sie nun das jeweilige β für die starke und schwache Konvergenz für beide Dis-
kretisierungsschemata. Als SDE verwenden Sie bitte

dXt = aXt dt + bXtdWt

mit a = 1.5, b = 1.0 für die Analyse der schwachen Konvergenz, a = 0.05, b = 1.0 für die
starke Konvergenz.
Welche theoretischen Schwierigkeiten können Payoff-Funktionen wie der Call oder Barrier-
optionen bereiten? Wie könnte man dies behandeln?
Hinweis:
Es empfiehlt sich für h etwa h = 2−i , i = 1, . . . , 8 und als Logarithmus den Zweierlogarithmus
zu wählen.

Aufgabe 10.

a) Schreiben Sie eine Simulation zur Bewertung der asiatischen Option mit dem Payoff
 ZT +
1
X= St dt − K
T 0

Simulieren Sie dazu in jeder Iteration den Pfad durch

(S0 , St1 , . . . , StN )

128
6.6. Übungen

und approximieren Sie das Integral mit der wiederholt angewendeten Simpsonregel, d.h.
für N gerade erhält man
N
Z T 2 −1
h
f (t) dt = ∑ ( f (x2i ) + 4 f (x2i+1 ) + f (x2i+2 )) + O(h4 )
0 i=0 3
Variieren Sie die Zahl der Diskretisierungspunkte unter der Nebenbedingung
Pfaditerationen ∗ Diskretisierungspunkte ≡ const
und bestimmen Sie eine Kombination mit möglichst geringem MSE. Verwenden Sie als
Eingangsdaten

Spot = 1.20
Strike = 1.20
T = 1.0
vol = 0.10
rd 1year = 0.0215
rf 1year = 0.02
Pfaditerationen = 100000
N = 50

Eine Simulation mit 1 Mio Iterationen und N = 500 lieferte einen Wert von 0.027440,
den Sie für die Berechnung des MSE als Referenzwert benutzen können.
b) Wenn Sie eine gute Aufteilung des Rechenbudgets gefunden haben, nutzen Sie zusätz-
lich die Option mit Payoff
Z T 
X = exp log(St ) dt
0

als Kontrollvariable, die aufgrund ihrer Log-Normalverteilung eine geschlossene Lösungs-


formel besitzt.

Aufgabe 11.
Implementieren Sie die beiden Schätzer (6.23) und (6.24) mit folgenden Daten:
Spot 100, Laufzeit T = 1.0, σ = 0.25, stetiges µ = 0.07, stetige Dividendenrate 0.02, Barriere
150, Barriere x = 100.
Hinweis: Verwenden Sie als Referenwert für den Preis 82.222.

Aufgabe 12.
Vollziehen Sie die Ergebnisse (6.1) und (6.4.1) zur Up-and-out Digital Option mit Payoff
(6.22) nach. Verwenden Sie als Daten Spot 100, Laufzeit T = 1.0, Volatilität 25%, stetige
Verzinsung µ = 7%, stetige Dividende 2%, Barriere bei 150, x = 100. Die Beispielzahlen
wurden mit 250000 Simulationen erzeugt, das analytische Ergebnis ist 82.222.

129
6. Diskretisierung von Stochastischen Differentialgleichungen

130
Teil III.

Programmierung in der Praxis

131
7. Numerische Stabilität und Effizienz,
Fehlerbehandlung, Bezeichnungen
Wir behandeln in diesem Abschnitt einige in der Praxis auftretende Probleme, die wir in loser
Reihenfolge vorstellen.

7.1. Rundungsfehler
Implementiert man einen Algorithmus am Computer, etwa zur numerischen Differentiation,
so wird man typischerweise mit zwei Fehlerarten konfrontiert werden:
1. Das, was man in der Numerik oft lapider als ”der Fehler” bezeichnet, also etwa Diskre-
tisierungsfehler, Fehler durch das Berechnen einer Reihe aus nur endlich vielen Sum-
manden usw. Dies bezeichnen wir im folgenden als truncation error.
2. Der Fehler, der durch die spezifische Art der Darstellung von Zahlen im Computer ent-
steht. Diesen bezeichnen wir als roundoff error.
Im allgemeinen kann man sich eine Berechnung am Computer vorstellen als die exakte Um-
setzung der numerischen Approximationsformel plus einem Rundungsfehler, denn gewöhn-
lich treten Rundungsfehler unabhängig von dem numerischen Verfahren auf. Rundungsfehler
können praktisch nicht vermieden werden, aber es gibt Situationen, in denen diese begünstigt
bzw durch die Art des auszuwertenden Ausdrucks stark vergrößert werden. In diesem Ab-
schnitt sollen einige zentrale Begriffe vorgestellt und exemplarisch an zwei Situationen ver-
tieft werden.

7.1.1. Elementare Begriffe


Definition 7.1.1. Zahlen werden im Computer dargestellt als
Vorzeichen × Mantisse × 2Exponent
Abhängig vom reservierten Speicherplatz für Mantisse und Exponent lassen sich exakte Zah-
len also mehr oder weniger gut approximieren, wobei die Speichergröße für die Mantis-
se angibt, wie viel Stellen Genauigkeit für eine beliebig aber fest gewählte reelle Zahl zur
Verfügung stehen. Die Größe des Exponenten gibt an, in welcher Größenordnung Bereiche
auf dem Zahlenstrahl abgedeckt werden.

133
7. Numerische Stabilität und Effizienz, Fehlerbehandlung, Bezeichnungen

Definition 7.1.2. Die kleinste Zahl, die man zur Zahl 1 dazu addieren kann, so dass eine von 1
verschiedene Zahl als Ergebnis geliefert wird, bezeichnet man als Maschinengenauigkeit εm .

Bemerkung 7.1.1. Auf einem Standard-Laptop der Autoren gilt εm ≈ 0.2 × 10−15 . An dieser
Stelle soll betont werden, dass diese Zahl εm nichts mit der kleinsten auf dem Computer dar-
stellbaren Zahl zu tun hat, denn diese hängt von der maximalen Größe des Exponenten, nicht
von der Größe der Mantisse ab. Auf dem besagten Laptop ist die kleinste darstellbare positive
Zahl ≈ 10−308 .

7.1.2. Beispiele
7.1.2.1. Addition/Subtraktion von Zahlen unterschiedlicher Größenordnung
Ganz allgemein sollte man davon ausgehen, dass jede arithmetische Operation einen Run-
dungsfehler von εm mit sich bringt, weil auch das schönste Ergebnis abgeschnitten werden
muß mit einem Fehler ≤ εm . Wenn alles gut geht, dann tendieren die Rundungsfehler abwech-
selnd nach √ oben oder unten und von N Operationen würde gemäß einem random-walk ein
Fehler von Nεm resultieren. Daran lässt sich nichts ändern. Sehr problematisch wird es nun,
wenn eine sehr große und eine sehr kleine Zahl addiert werden sollen: Intern würde der Com-
puter die Mantisse der kleineren Zahl so lange nach rechts verschieben, bis die Exponenten
gleich sind, und dann würde er die beiden Zahlen addieren. Unvermeidlich gehen dabei also
Informationen über die kleinere Zahl verloren, im schlimmsten Fall wird sie in der Addition
gar nicht mehr berücksichtigt. Oft hilft es schon, zuerst die (möglicherweise vielen) kleinen
Zahlen aufzuaddieren und dann erst diese Zahl auf die große zu addieren.
Ein weiteres großes Problem ergibt sich, wenn zwei Zahlen voneinander subtrahiert werden
sollen, die nahe beieinander liegen. Um das zu sehen, genügt folgende einfache Vorstellung:
Vorangegangene Operationen haben bereits Fehler der Größenordnung εm eingeführt, d.h. die
letzte oder auch die letzten Stellen der Mantisse sind nicht mehr ganz korrekt. Subtrahiert man
die Zahlen nun, dann verschwinden die exakten Stellen vorne in der Mantisse und die wenigen
Stellen am Ende der Mantisse allein, die nun übrig bleiben, können im schlimmsten Fall nur
noch Unsinn angeben. Ein klassisches Beispiel dafür wäre der Ausdruck

−b + b2 − 4ac
x=
2a
für ac  b2 .

7.1.2.2. Numerische Differenziation


Dies tritt in der Praxis sehr häufig auf und soll hier einmal recht genau abgehandelt werden.
Dazu schweifen wir kurz auf einige Numerik-Resultate ab, bevor wir uns an die Analyse der
Rundungsfehler machen.
Die erste Idee, die man hat, wäre der Ausdruck

f (x + h) − f (x)
f 0 (x) = (7.1)
h

134
7.1. Rundungsfehler

mit einem ’irgendwie geeignet klein’ gewählten h. Dies wäre aber nur vorteilhaft, wenn f sehr
aufwendig zu berechnen ist und man f (x) bereits berechnet hat. Von Rundungsfehlerüberle-
gungen abgesehen wäre dieser Ausdruck nämlich sehr viel schlechter als
f (x + h) − f (x − h)
f 0 (x) = (7.2)
2h
denn während (7.1) einen truncation error der Ordnung 1 hat, d.h. auf einem idealen PC oh-
ne Rundungsprobleme würde man eine Fehlerabschätzung gemäß |Schätzer − wahrerWert| ≤
C1 × h erhalten, liefert (7.2) eine Abschätzung |Schätzer − wahrerWert| ≤ C2 × h2 , was al-
so für h  1 einen deutlich niedrigeren Fehler garantiert. Die Konstanten C1 und C2 sind in
der Praxis meist unbekannt und zum Glück auch bedeutungslos, denn aus dieser Abschätzung
ersieht man vor allem eines: dass man zum Gewinn von sagen wir zusätzlich 2 Dezimalstel-
h
len Genauigkeit in der Approximation den Parameter h einmal auf h1 = 100 , und einmal nur
h
auf h2 = 10 setzen muß. Da wir h in der Praxis nicht beliebig klein wählen können, liefert
(7.2) unter gleichen Bedingungen also deutlich bessere Resultate. Nun zu den Rundungsfeh-
lern: Die später im Abschnitt folgenden Daumenregeln basieren alle auf folgender einfacher
Überlegung. Es bezeichne durchgehend f¯(x) die Auswertung von f (x) am Rechner, so dass
gilt
| f¯(x) − f (x)| ≤ C εm (7.3)
bzw.
f¯(x) = f (x) + O(εm ) (7.4)
Für die am PC berechnete Approximation
1 ¯
f (x + h) − f¯(x)

(7.5)
h
erhält man damit
1 ¯ 1
f (x + h) − f¯(x) =

( f (x + h) − f (x) + O(εm ))
h h
1 1
= ( f (x + h) − f (x)) + O(εm )
h h
1 0 1
f (x) h + O(h2 ) + O(εm )

=
h h
1
= f 0 (x) + O(h) + O(εm )
h
und dies ist äquivalent zu
1 εm
| f¯(x + h) − f¯(x) − f 0 (x)| ≤ c1 h + c2

(7.6)
h h
mit unbekannten (und deshalb uninteressanten) positiven Konstanten c1 , c2 . Man ersieht hieraus,
dass die Approximation in (7.5) bestenfalls eine Genauigkeit in der Größenordnung der Wur-
zel der Maschinengenauigkeit erreichen kann. Leitet man den Ausdruck rechter Hand in (7.6)
nach h ab, so erhält man als optimale Wahl für h
r
c2
hopt = εm
c1

135
7. Numerische Stabilität und Effizienz, Fehlerbehandlung, Bezeichnungen

und dies wird sehr schön in der Übungsaufgabe 1 mit der Grafik 10.3.2 demonstriert. In [[2]]
wird als Schrittweite für (7.5) s !
ε f f (x)
h=O (7.7)
f 00 (x)
vorgeschlagen, wobei ε f die Genauigkeit ist, mit der f ausgerechnet werden kann. Meist
nimmt an, dass ε f ≡ εm , was zumindest für einfache Funktionen gerechtfertigt erscheint. Nor-
malerweise wird man f 00 (x) nicht kennen und deshalb ist (7.7) zunächst von geringem Wert,
weshalb in [[2]] die Approximation
f (x)
≈x
f 00 (x)
verwendet wird, sofern x nicht zu nahe bei 0 liegt. Für diese Schrittweite ergibt sich ein Dif-
ferenziationsfehler von
f (x + h) − f (x)
− f 0 (x) = O ε f | f 0 (x)|
p 
h
Dies bestätigt erneut unser Ergebnis, dass man mit der Approximation in (7.1) bestenfalls
eine Genauigkeit in der Größenordnung der Wurzel der Maschinengenauigkeit erreichen kann.
Anders hingehen beim zentralen Differenzenquotienten in (7.2): Mit der Schrittweite

ε f f (x)  31
 
h=O
f 000 (x)
wobei man dies gemäß [[2]] für x nicht zu nahe bei 0 approximieren kann durch
 1

h = O (ε f ) 3 x

ergibt sich für den Fehler


f (x + h) − f (x − h)  2

− f 0 (x) = O | f 0 (x)|(ε f ) 3
2h
und dies liefert in aller Regel zwei Dezimalstellen mehr Genauigkeit als der einfache Differen-
zenquotient in (7.1), was ebenfalls in der Grafik 10.3.2 sehr schön zu sehen ist. Die Ableitung
δ2 f
δ xδ y kann man approximieren durch

( f (x + h, y + h) − f (x + h, y − h)) − ( f (x − h, y + h) − f (x − h, y − h))
(7.8)
4h2
Dies ist ein Verfahren zweiter Ordnung, d.h. es gilt

|Approximation − exakter Wert| ≤ Ch2

wobei man die Schrittweise gemäß


 1

h = O (ε f ) 4 x

136
7.2. Effiziente Implementation von Algorithmen

wählt für x nicht zu nahe bei 0.

Betrachten wir abschließend noch die Variablen x, h, und x +h. Angenommen x und die Varia-
ble x + h können beide nicht exakt im Arbeitsspeicher abgebildet werden, d.h. beide besitzen
von vorneherein einen Rundungsfehler der Ordnung ≤ εm . Dann würde das h im Nenner, das
im Computer gespeichert ist, sicher nicht genau zur Differenz (x + h) − h im Computer pas-
sen. Um diesen unnötigen Fehler von leicht ein bis zwei Dezimalstellen nicht in die Rechnung
hereinzutragen, bietet sich folgender Trick an:

double x = 2.5; // wir moechten f’(2.5) berechnen


double h = 0.001;
volatile double temp = x + h;
h = temp - x;
return ( f(temp) - f(x) ) / h;
// ...

Das Schlüsselwort volatile verhindert in diesem Fall, dass ein optimierender Compiler den
Trick zerstört.

Fazit:
Wie wir gesehen haben liefern einfache Verfahren wie (7.1), (7.2) und (7.8) eine schlechtere
Approximation als εm . Zum Glück gibt es Alternativen mit besserer Approximationsgüte, aber
diese erfordern in der Regel mehr Funktionsauswertungen und einige Voraussetzungen an die
Glattheit der Funktion: Eine Idee wäre analog zur Romberg-Integration die Differentiation mit
mehreren Schrittweiten h1 , . . . , hn , um die Ergebnisse zur Interpolation h → 0 zu nutzen. Dies
finden Sie fertig implementiert in der Funktion dfridr. Weitere Möglichkeiten, insbesondere
wenn man eine Funktion häufig numerisch differenzieren möchte, sind die Approximation der
Funktion durch Chebychev-Polynome oder durch ein mittels Kleinste-Quadrate-Schätzung
gewonnenes Polynom, um dann diese Funktion als Approximation leicht differenzieren zu
können. Stichwort wäre hier Savitzky-Golay. Für genauere Informationen und effiziente Im-
plementationen verweisen wir auf [[2]].

7.2. Effiziente Implementation von Algorithmen


Ist man mit dem Laufzeitverhalten eines Programms unzufrieden, so sollte man als erstes
prüfen, ob man einen geeigneten Algorithmus implementiert hat - oder ob es da nicht deutlich
bessere gibt. Hat man dies getan, sollte man prüfen, ob man einige Grundregeln effizienter Pro-
grammierung berücksichtigt hat, die im folgenden in der gegebenen Kürze aufgezählt werden.
Betrachten Sie folgendes Programm:

#include <iostream>
#include <math.h>
#include <time.h>
#include <stdio.h>

137
7. Numerische Stabilität und Effizienz, Fehlerbehandlung, Bezeichnungen

using namespace std;

int main() {
clock_t start, finish;
double a, b, duration;
double * mem;
start = clock();
for (int i=1; i<10000000; ++i) {
b = (double)i;
// a = 0;
a = b + b;
// a = b - b;
// a = b * b;
// a = b / b;
// a = exp(-b);
// a = log(b);
// a = sin(b);
// a = cos(b);
// a = pow(b, 2.0);
// mem = new double[100];
// delete[] mem;
// mem = (double*) malloc( 100 * sizeof(double) );
// free(mem);
}
finish = clock();
duration = (double)(finish - start) / CLOCKS_PER_SEC;
cout << duration << " seconds" << endl;
}
Dieses Programm ließen wir sowohl unter Borland C++ als auch unter Microsoft Visual C++
im Debug- und im Runtime-Modus laufen. Leider wird unter Visual C++ im Runtime-Modus
die gesamte Schleife dadurch optimiert, dass sie gar nicht ausgeführt wird, so dass für diesen
Compiler von den Beispielen für new und malloc abgesehen nur die Ergebnisse im Debug-
Modus vorliegen.

Offenbar haben beide Compiler ihre Stärken und Schwächen, auf die wir hier aber nicht weiter
eingehen möchten. Für unsere Zwecke wichtig sind folgende Beobachtungen:
- Addition, Subtraktion und Multiplikation sind ”billig” und können extensiv verwendet
werden.
- Teuer“hingegen sind die Division und wie man erwarten würde die Funktionen exp,

log, sin, cos, pow.

138
7.2. Effiziente Implementation von Algorithmen

Borland Visual C++


Codebeispiel DebugModus ReleaseModus DebugModus
nur Schleife 3,045 2,554 4,407
plus Konversion 6,138 2,33 3,765
mit Addition 23,14 5,418 6,59
mit Subtraktion 25,447 5,528 6,6
mit Multiplikation 25,467 5,528 6,539
mit Division 41,79 17,305 15,542
exp 78,012 75,959 417,7
log 146,52 144,558 131,149
sin 226,045 227,367 138,579
cos 228,168 227,477 139,151
pow(a,2) 127,553 85,052 220,667
new 198,245 192 360 (release)
malloc 171 170 371 (release)

Tabelle 7.1.: Illustration des Rechenaufwands unterschiedlicher Operationen

- Operater new und malloc sind sehr langsam.

Daraus ergeben sich einige Konsequenzen:

- Es ist klar, dass man teure Operationen in zeitkritischem Code, also etwa in Schleifen,
vermeiden sollte. Es empfiehlt sich etwa, solche rechenintensiven Operationen aus einer
Schleife auszulagern und innerhalb der Schleife nur noch auf die gespeicherten Werte
zurückzugreifen.

- Möchte man häufig durch die gleiche Konstante a dividieren, so empfiehlt es sich, 1a
einmal auszurechnen und dann statt durch a zu dividieren lieber mit der gespeicherten
Zahl a1 zu multiplizieren.

- Programmiert man eine Klasse, so könnte der Konstruktor häufig gebrauchte aber teure
Rechnungen bereits bei der Konstruktion des Objekts durchführen und die Ergebnisse
in private-Variablen speichern.

- Um die Potenz einer Variablen auszurechnen (etwa x2 ) sollte man nicht auf die Potenz-
funktion pow der Standardbibliothek zurückgreifen, da diese die teuren Funktionen exp
und log aufruft und entsprechend langsam ist. Da es in C++ nichts Besseres gibt, sollte
man die Multiplikation explizit hinschreiben (also x * x).

- Der Operator new und die Funktion malloc sind so langsam, weil sie mit dem Betriebs-
system kommunizieren müssen, um zur Laufzeit Speicherplatz reservieren zu können.
Deshalb sollte man diese sparsam verwenden, schon gar nicht in zeitkritischen Schlei-
fen. Oft werden diese Befehle implizit durch Klassen verwendet, die man als Rückga-
beparameter einer Funktion benutzt, oder man verwendet Container der STL innerhalb

139
7. Numerische Stabilität und Effizienz, Fehlerbehandlung, Bezeichnungen

einer Schleife, und bei jedem Durchlauf wird die Größe des Containers neu bestimmt
und somit Speicher alloziiert. Dies vermeidet man zum einen dadurch, dass man den
benötigten Speicher in ausreichendem Umfang ein Mal außerhalb der Schleife sozu-
sagen als ”working space” reserviert und anschließend wieder freigibt, zum anderen
empfiehlt es sich, größere Objekte (und das sind typischerweise solche die new irgend-
wann aufrufen) als Referenz an andere Funktionen weiterzureichen, um teures Kopieren
und die Anzahl von Konstruktoraufrufen zu minimieren.
Die gerade genannte Idee lautet kurz: Call by reference statt Kopieren der Argumente.

Dies sind die wichtigsten Punkte, aber es gibt natürlich noch viele weitere, subtilere Dinge:
Wenn möglich sollte man in Klassen const-Funktionen verwenden, denn dies zwingt nicht
nur zur klareren Programmkonzeption wie bereits bemerkt, sondern erlaubt dem Compiler
zusätzliche Optimierungen. Echten Laufzeitpuristen sind virtuelle Funktionen ein Dorn im
Auge, weil bei jedem Aufruf einer virtuellen Funktion zur Laufzeit des Programms zuerst in
einer Tabelle von Funktionspointern der richtige herausgesucht werden muß, um dann diese
Funktion aufzurufen. Dies lässt sich mit Templates in gravierenden Fällen kontrollieren, etwa
bei Solvern, die eine virtuelle Funktion sehr oft aufrufen müssen. Siehe dazu (2.5.4). Im all-
gemeinen steht der Effizienzgewinn durch Verzicht auf virtuelle Funktionen aber in keinem
Verhältnis zu deren Nutzen. Dies führt uns zu einer abschließenden

Warnung:
Die angegebenen Hinweise können Programme hinsichtlich ihrer Laufzeiteffizienz um ein
Vielfaches verbessern. Dennoch sollte man diesen Gewinn an Geschwindigkeit gegen die
Nachteile eines weniger klaren Programms abwägen. Ein Programm, das einen Bruchteil ei-
ner Sekunde schneller ist, dafür aber unübersichtlich und kaum noch zu warten, kann nicht
gemeint sein!

7.3. Anwendung des Debuggers


Wenn Sie ein Programm geschrieben haben und laufen lassen, werden Sie in aller Regel fest-
stellen, dass es nicht das tut, was es soll. Um den / die Fehler aufzuspüren, kann man folgende
einfache Tricks anwenden:

i) Kurze Textausgaben am Bildschirm verdeutlichen den Weg, den das Programm durch
die Kontrollstrukturen nimmt.

ii) Zusätzliche Hilfsvariablen zählen, wie oft gewisse Schleifen tatsächlich durchlaufen
werden.

iii) Zusätzliche Informationen können in log-files gespeichert werden.

Sie sehen bereits, dass all diese Dinge wenn auch nicht völlig unpraktisch, so aber doch
mühsam sind, denn sie bringen stets einen gewissen zusätzlichen Programmieraufwand mit
sich. Eine große Erleichterung stellt in diesem Zusammenhang der Debugger dar, der in der

140
7.3. Anwendung des Debuggers

folgenden Form in praktisch jeder Programmierumgebung zu jeder Programmiersprache exi-


stiert. Insbesondere also auch in Visual Basic, Delphi usw. Die zentralen Elemente, die Sie
gewöhnlich im Menü Debugger finden, sind:

i) Programm zeilenweise ausführen


In Microsoft Visual C++ geht das meist mit der Taste F10, im Borland C++ Builder
meist mit F8. Während Sie nun das Programm in Ruhe beim Durchlauf beobachten
können, haben Sie die Möglichkeit, im Fenster ”überwachte Variablen” den Inhalt der
aktuellen Variablen des Programmspeichers zu verfolgen.

ii) Zeilenweise in einen Aufruf springen


Wenn Sie in einer Zeile angelangt sind, in der eine Funktion aufgerufen wird, so wird
beim zeilenweisen Ausführen die komplette Funktion ausgeführt und Sie können nur
das Ergebnis beobachten. Wenn man weiß, dass der Fehler woanders liegt, ist das sehr
praktisch. Möchte man hingegen in den Funktionsaufruf hineinspringen“, so geschieht

das in Visual C++ meist mit F11, im C++Builder meist mit F7. Beachten Sie bitte, dass
man nur in eine Funktion springen kann, deren Quellcode auch vorliegt, was bei einer
Funktion der Laufzeitbibliothek nicht der Fall sein muß. Stattdessen werden Sie direkt
im Assembler-Code landen, was für die meisten von uns sicher von zweifelhaftem Wert
sein wird. Immerhin sollten Sie mit ein- oder mehrmaligem Ausführen bis Rücksprung
wieder in gewohnte Fahrwasser zurückkehren.

iii) Ausführen bis Cursor


Haben Sie schließlich die fehlerträchtige Stelle einigermaßen eingegrenzt, so wäre es
mühselig, sich Zeile für Zeile dorthin vorzuarbeiten. Stattdessen kann man den Cursor
an die betreffende Stelle setzen und mit ST RG+F10 (Visual C++) bzw F11 (C++ Builder)
das Programm bis zur betreffenden Stelle ausführen. Oft ist diese Technik auch sehr
praktisch, wenn man in einer (langen) Schleife einen Durchlauf weiter gehen möchte:
Sitzt man mitten in der Schleife und wird diese noch mindestens einmal ausgeführt,
dann kommt man mittels Ausführen bis Cursor in den nächsten Durchlauf.

iv) Haltepunkte
Manchmal ist das Setzen von Haltepunkten ganz praktisch: Das Programm kann dann
ganz normal gestartet werden und wird beim Durchlauf an der gewünschten Stelle
anhalten. Nun kann man die Variablen untersuchen und das Programm entweder nor-
mal weiterlaufen lassen oder zeilenweise vorangehen. Wie das Setzen des Haltepunktes
funktioniert, ist unterschiedlich:
In Visual C++ wird man das rechte Maustaste-Menü öffnen und Haltepunkt setzen“oder

die Schaltfläche mit dem Handsymbol auswählen. Im C++ Builder wählt man den ent-
sprechenden Menüpunkt oder klickt in der Codezeile an den linken Zeilenrand. Un-
abhängig davon kann man sich z.B. im C++ Builder im Menü Ansicht“eine Übersicht

der gesetzten Haltepunkte anzeigen lassen und Ihnen sogar Bedingungen für das Anhal-
ten des Programms zuweisen (Bedingte Haltepunkte).

Zum Schluß einige Anmerkungen:

141
7. Numerische Stabilität und Effizienz, Fehlerbehandlung, Bezeichnungen

Wenn Sie den Debugger zum ersten Mal verwenden wollen, wird Ihre Entwicklungsumge-
bung möglicherweise einfach all die genannten Befehle ignorieren. Das liegt daran, dass der
Compiler zwei verschiedene Versionen Ihres Programms erzeugt: Eine Debug- bzw. Entwick-
lungsversion mit vielen an und für sich überflüssigen Informationen im Programm, und einer
kompakteren auf Geschwindigkeit optimierten Release-Version. Natürlich funktionieren die
Debugbefehle nur in der Debugversion und die ist meist nicht voreingestellt. In Visual C++
geschieht dies im Menüpunkt ”Aktive Konfiguration festlegen”, im C++ Builder kann man im
Menü Optionen / Compiler das Gewünschte wählen.
Moderne Entwicklungsumgebungen enthalten oft noch sehr viel weitergehende Debug-Möglich-
keiten, unter anderem zur genaueren Analyse der Laufzeitperformance. Ob ein Blick ins Hand-
buch für Sie lohnt sei dahingestellt, die vorgestellten Dinge werden wahrscheinlich auch die
wichtigsten für Sie bleiben, aber zumindest darauf hinweisen möchten wir doch.

7.4. Fehlerbehandlung
In einer prozeduralen Programmiersprache, also insbesondere in C, gibt es für eine Funktion
folgende Wege, einen Fehler an den Aufrufer zu melden:

i) Rückgabe eines unsinningen Werts

ii) Belegung einer zusätzlichen Fehlervariablen

iii) Fehlermeldung und Programmabbruch

i), ii) hoffen auf die Sorgfalt des Aufrufenden, iii) ist oft zu drastisch. In C++ gibt es die
Möglichkeit, dass die Funktion eine Klasse ( Fehlerklasse“) auswirft“, die der Aufrufende
” ”
auffängt“. In dieser Klasse, die durch Vererbung beliebig ausgebaut werden kann, werden

dann alle nötigen Informationen zur Fehlerverarbeitung eingefügt.

Im folgenden finden Sie ein praktisches Beispiel für den hier lediglich skizzierten Weg der
Fehlerbehandlung:

// Exception.h

#ifndef EXCEPTION_H
#define EXCEPTION_H

// Diese Klasse stellt eine einfache Exception-Klasse dar,


// die einen maximal 127 Byte langen Fehlertext sowie einen
// numerischen Fehlercode übergeben kann.

class Exception {

public:
Exception(int errnum, const char *msg);

142
7.4. Fehlerbehandlung

const char *getMessage() const;


const int getErrno() const;

private:
// Speicher fuer die Fehlernachricht
char msg_[128];
// Speicher fuer den Fehlercode
int errno_;
};

#endif

// Exception.cpp ist klar

// SingleBarrier.cpp

#include "Exception.h"
#include "Defaults.h"

SingleBarrier::SingleBarrier(double strike, double tau,


double barrier) {

if( strike < 0 ) // Fehler!


throw Exception( "Negativer Strike.",
Defaults::INPUT_INVALID);
else
// ...
}

// Hauptprogramm.cpp

void main(){

double strike = -50;


double tau = 0.3;
double barrier = 60;
try{
SingleBarrier barrierOption( strike, tau, barrier );
} catch( Exception& exception ){
// Fehlerbehandlung:
if( exception.getErrno() == Defaults::INPUT_INVALID )
// ...usw
} // Ende catch - Block
}

143
7. Numerische Stabilität und Effizienz, Fehlerbehandlung, Bezeichnungen

Bemerkung 7.4.1.

i) Häufig benutzt man in eigenen Programmen Funktionen, über deren Implementation


man nichts weiß. Insbesondere weiß man oft nicht, welche Ausnahmeklassen eine Funk-
tion auswirft. Um dieses Problem zu beheben, gibt es folgende Schreibweise:

catch(...){
// irgend etwas ging schief
}

Dies hat den Vorteil, dass alle möglichen Fehlerklassen tatsächlich aufgefangen wer-
den, andererseits verliert man die Möglichkeit, eine Instanz einer Ausnahmeklasse auf
genauere Fehlerinformationen hin auswerten zu können. Um dieses Ärgernis zu besei-
tigen, folgt direkt die nächste Bemerkung:

ii) Um die Benutzerfreundlichkeit zu erhöhen, sollte man die Exception - auswerfende


Funktion in der Header-Datei so deklarieren:

SingleBarrier(...) throw (Exception);

Dies ist ein Versprechen der Funktion, nur Ausnahmeklassen vom Typ Exception oder
davon abgeleitete Klassen auswerfen. Beachten Sie, dass eine ”Ausnahmeklasse” sich
durch nichts von einer normalen Klasse unterscheidet, außer dass sie zum Zwecke der
Übermittlung von Fehlerinformationen über den throw-catch-Mechanismus geschrie-
ben wurde.

Die Bedeutung der ersten beiden Bemerkungen wird in der nächten vielleicht etwas
klarer.

iii) Wird eine ausgeworfene Ausnahme nicht im folgenden catch-Block abgefangen, wird
sie an die nächst höherliegende catch-Struktur weitergereicht. Manchmal werden Aus-
nahmen von catch abgefangen und wieder ausworfen, weil sie auf diese Ebene nicht
behandelt werden können. Niemals aufgefangene Ausnahmen führen zum Programmab-
bruch.
Dem Verfasser dieses Skripts war das Verhalten von unbehandelten Ausnahmen viel zu
drastisch. Ein typischer Gedanke war etwa: Eine Ausnahme macht mir im Zweifel das

ganze Programm kaputt, will ich nicht.“Man lernt dazu, und heute schätzt ebendieser
den Exception-Mechanismus sehr, und zwar aus folgenden Gründen:

a) Oft weiß man bei Erstellung eines Programms noch nicht, in welchem Umfeld
es später eingesetzt werden wird. Dies macht die Ausgabe von Fehlermeldungen
praktisch unmöglich, wenn man keine späteren Anwender ärgern möchte. Das
Auswerfen einer Ausnahme hingegen übergibt die Verantwortung über das, was
nun geschehen soll, an den Programmierer, der Ihre Klassenbibliothek anwenden
möchte, und der sollte doch wissen, was im jeweiligen Fall zu tun ist.

144
7.5. Überfüllung des Namensraums

b) Die Rückgabe falscher“Werte als Fehlermeldung ist manchmal fast unmöglich.



c) Die Rückgabe falscher“Werte oder das Belegen einer Fehlervariablen fordert das

Entstehen schwer nachvollziehbarer Fehler geradezu heraus. Eine ausgeworfene
Klasse hingegen erlaubt kein Verstecken des Fehlers, und auch kein Verschieben
der Fehlerbehandlung auf irgendwann später. Es zwingt den Programmierer, sich
Gedanken über eine angemessene Fehlerbehandlung zu machen, es erzieht sozu-
sagen zu einem saubereren Programmierstil.

iv) Vererbung bei Fehlerklassen hat folgenden Sinn:

class Fehlerbasis{
// ...
};

class Fehlerabl : public Fehlerbasis{


// ...
};

Sei nun f eine Funktion, die die Fehlerklasse Fehlerabl auswirft. Dann wäre in:

try{
f();
}
catch(Fehlerabl& err){
// spezieller Fehler
} _
catch(Fehlerbasis& err){ |
// allg. Fehler | (*)
} _|

der Abschnitt (*) auf den ersten Blick überflüssig, er dient aber als default“-Zweig

und Sicherheitsnetz, falls man später neue Ableitungen von Fehlerbasis einführt, aber
nicht explizit abfängt.

7.5. Überfüllung des Namensraums


Wenn man eine große Bibliothek benutzt und der Hersteller der Bibliothek nicht aufgepasst
hat, kann es schnell zu Namenskollisionen kommen von Variablen / Funktionen des Anwen-
derprogramms und der Bibliothek. Um dies zu vermeiden, gibt es in C++ die Möglichtkeit,
eigene Namensräume zu erstellen.

Beispiel

145
7. Numerische Stabilität und Effizienz, Fehlerbehandlung, Bezeichnungen

namespace MathFinance{
void f(){
//...
}
}

Würde man dieses Codefragment über eine header-Datei test.h in ein Programm einfügen,
das selbst eine Funktion f definiert, so käme es nicht zu Konflikten:

#include "test.h"

void f(){
// ...
}

void main(){
f(); // Funktion des Hauptprogramms selbst
MathFinance::f(); // Bibliotheksfunktion
}

Deklaration und Definition können auch getrennt erledigt werden:

namespace MathFinance{
void f();
}

// später...

namespace MathFinance{ oder void MathFinance::f(){


void f(){ //...
// ... }
}
} // end of namespace MathFinance

Bemerkung

i) namespace-Deklarationen sind kumulativ, man kann also den Namensraum über viele
Dateien ausdehnen und die gesamte Bibliothek darin erstellen.

ii) Bindet man die Bibliothek ins Benutzerprogramm ein, so kann man durch

using namespace MathFinance;

zu Beginn der Datei im weiteren Verlauf alle Bibliothekselemente ohne MathFinance::


ansprechen. Dies dient sehr der Bequemlichkeit, andererseits wird der Sinn der Namens-
räume, nämlich der Schutz vor Namenskollisionen, dadurch wieder aufgehoben.

146
7.6. Übungen

iii) Verwendet man mehrere Namensräume, so dürfen deren Namen natürlich nicht selbst
kollidieren. Hersteller verwenden deshalb oft lange Namen wie
IntroductionToCppAndMonteCarlo. Die Anwendung der Bibliothek wird dann durch
eine Alias-Bildung erleichtert:

namespace kurzerName = langerName;


kurzerName::f();

7.6. Übungen
Aufgabe 13. Modifizieren Sie das Programm

void main() {

derart, dass es folgenden Text ausgibt und auf eine Benutzereingabe wartet:

Programmstart.
Programmende.

Dabei soll die Funktion main unverändert bleiben.

Aufgabe 14. Schreiben Sie eine Klasse, die ausgibt, wie viele Instanzen von ihr aktiv sind.

Aufgabe 15. Schreiben Sie eine Klasse Option, die eine für alle Instanzen der Klasse gemein-
sam genutzte Variable TV Quotation besitzt. Diese soll man auf konstante Werte foreign oder
domestic setzen können.

Aufgabe 16. Eine Klasse Y enthalte eine Integervariable wert“, die vom Konstruktor auf 1

gesetzt wird. Eine Klasse X erbt Y und enthält eine Funktion print(), die die geerbte Variable
ausgibt.

i) Experimentieren Sie beim Vererben und der Deklaration der Klasse X mit den Zugriffs-
modi

ii) Geben Sie im Hauptprogramm mit print() die Variable wert“aus. Greifen Sie dann mit

einem Basisklassenpointer auf die Instanz der Klasse X zu, setzen Sie die Variable
wert“auf eine andere Zahl, und lassen Sie diese wieder mit print() ausgeben. Veran-

schaulichen Sie sich die Vorgänge nochmals mit einem Schema.

147
7. Numerische Stabilität und Effizienz, Fehlerbehandlung, Bezeichnungen

Aufgabe 17. Schreiben Sie zwei Klassen X und Y, die beide einen Integer wert“und eine

Funktion print() zur Ausgabe enthalten. Eine Klasse Z soll beide Klassen erben. Experimen-
tieren Sie ein bisschen, um die Auflösung von Mehrdeutigkeiten zu beobachten. Was passiert,
wenn Sie zur Auflösung der Mehrdeutigkeit einer der beiden print - Funktionen ein Argu-
ment spendieren, um sie auf diese Weise in der abgeleiteten Klasse eindeutig ansprechen zu
können?

Aufgabe 18. Schreiben Sie eine Klasse X, die ein


const int x;
enthält. Der Konstruktor soll die Konstante initialisieren. Schreiben Sie eine Klasse Y, die X
erbt und die Konstante in X korrekt initialisiert.

Aufgabe 19. Schreiben Sie ein Programm analog zum Beispiel in (2.3.7). Vererben Sie zunächst
wie gewohnt und fügen Sie in Klasse 1 einen Integer x ein. Ein Konstruktor soll x initialisieren.
i) Überzeugen Sie sich von der zweifachen Existenz der Klasse 1.
ii) Gehen Sie über zu virtueller Vererbung. Machen Sie sich dabei nochmals klar, was sich
bei den Konstruktoraufrufen ändert.

Aufgabe 20. Programmieren Sie das Programm aus dem Abschnitt (2.3.8) nach. Greifen Sie
auf f() einmal über Pointer, Referenz und einmal normal zu.

Aufgabe 21. Betrachten Sie folgendes Programm:


// header-file
class A{
public:
virtual void f(){printf("A\n"); }
};
class A1: virtual public A {
void f(){ printf("A1\n"); }
};
class A2: virtual public A{};
class A12: virtual public A1, virtual public A2{};

// Hauptprogramm
int main(){
A12 a12;
A2* ptr = &a12;
ptr->f();
getch();
return 0;
}

148
7.7. Bezeichnungskonventionen und Layout

Was wird das Programm Ihrer Meinung nach ausgeben? Ein kleines Vererbungsschema ist für
diese Aufgabe sicher hilfreich.

7.7. Bezeichnungskonventionen und Layout


7.-1.1. Einleitung
Die folgenden Hinweise sorgen dafür, dass Programmcode auch bei mehreren Autoren für alle
Beteiligten leicht lesbar bleibt. Ferner wird sich bei großen Projekten zeigen, dass eine klar
vorgeschriebene Groß- / Kleinschreibung das Leben einfacher macht: Stellen Sie sich vor, Sie
möchten auf eine Variable xyz zugreifen, müssen aber immer wieder nachschauen, ob Sie
diese xyz, Xyz, XYZ oder dergleichen genannt haben. Die folgenden Hinweise sparen Ihnen
also auch bei einem größeren Ein - Mann - Projekt Zeit und Nerven.

7.-1.2. Allgemein
i) Typnamen beginnen mit einem Großbuchstaben, dann gemischte Schreibweise:

class Exception;
class SavingsAccount;

ii) Variablennamen beginnen mit einem Kleinbuchstaben, dann gemischte Schreibweise:

Exception exception;
SavingsAccount savingsAccount;

iii) Konstanten ausschließlich in Großbuchstaben

iv) Funktionen: gemischt, beginnend mit einem Kleinbuchstabe. Da Sie vermutlich alle
Bezeichnungen in englischer Sprache wählen, sollten Sie gemäß englischen Gepflo-
genheiten keine komplizierten deutschen Nominalkonstruktionen sondern Verbformen
verwenden:

getName();
computeTV();

v) Variablen im Privatteil der Klasse enden mit “



class Klasse{
private:
int length_;
}

149
7. Numerische Stabilität und Effizienz, Fehlerbehandlung, Bezeichnungen

Dadurch können zum einen wichtigere“Klassenvariablen von lokalen Hilfsvariablen in



Klassenfunktionen unterschieden werden, andererseits werden dadurch lästige Bezei-
chungsprobleme bei setter methods auf sehr natürliche Art und Weise behoben:

void setLength(int length ){


length_ = length;
}

vi) Variablen haben den gleichen Namen wir ihr Typ, wenn sie keine tiefere Aufgabe haben
( generic variable“), etwa

void setTopic( Topic* topic);

Nicht aber:

void setTopic( Topic* value);

Variablen mit einer richtigen“Aufgabe ( non-generic variable“) haben einen zusam-


” ”
mengesetzten Namen Aufgabe + Typ“, etwa

Point startingPoint;
Name loginName;

Dadurch wird die Vielfalt der Ausdrücke und somit die Komplexität des Programms
etwas reduziert.
vii) Alle Bezeichner in englischer Sprache.

7.-1.3. spezielle Konventionen


i) Bei Zugriff auf eine Klassenvariable über eine Funktion verwende get / set im Name,
z.B.

option.setStrike(1.30);

ii) Zeitintensive Operationen tragen ein compute“im Namen, dies ist zwar kein internatio-

naler Standard, erhöht aber die Lesbarkeit des Programms für den Anwender:

matrix.computeInverse();

iii) Iterationszähler in for - Schleifen heißen i, j, k


iv) Bool - Variablen und bool - Funktionen beginnen mit ”is”, ”has”, ”should”, denn dies
zwingt den Programmierer zur sinnvollen Variablenbezeichung, z.B. ist bool isFinished;
aussagekräftiger als bool operationStatus;
Warnung: Negierte bool - Variablen wie

150
7.7. Bezeichnungskonventionen und Layout

bool isNoError; // eine entsetzliche Bezeichnung

stiften leicht Verwirrung und sollten vermieden werden( !isNoError ist stets ein Stol-
perstein beim Lesen des Programms)

v) Ausnahmeklassen enden mit Exception“, dadurch wird beim Lesen klar, dass diese

Klasse nicht zum Kernprojekt gehört sondern der Fehlerbehandlung dient, z.B. class
AccessException;

vi) Im .h - file steht ausschließlich die Deklaration einer Klasse, im gleichnamigen .cpp -
file dann die Definition.

vii) Um Probleme mit dem Zeilenumbruch bei Verwendung mehrerer Editoren bei der Pro-
grammerstellung zu vermeiden, gilt: Zeilen enthalten max 80 Zeichen, kein Tabulator,
kein Zeilenumbruch.

viii) Im .h - file verhindert folgendes, am Anfang befindliches und immer einzufügendes,


Konstrukt ein mehrfaches Einfügen der header - Datei, somit auch Compilierungsfeh-
ler. Außerdem wird die Compilierungszeit reduziert.

#ifndef BEZEICHNER_H
#define BEZEICHNER_H

//... Inhalt der header - Datei

#endif

ix) Include - Befehle stehen am Anfang der Datei, gruppiert nach Bedeutung, mit aufstei-
gender Abstraktionsebene, d.h. low - level - files wie stdio.h zuerst.

7.-1.4. Konventionen bei Ausdrücken


7.-1.4.1. Typen

i) Eine Klassendeklaration wird sortiert nach public, protected, private.

ii) Nicht-triviale Typumwandlungen stets explizit durchführen, damit der Compiler weder
einen Fehler noch eine Warnung ausgibt, d.h.

int value = (int) doubleValue;

151
7. Numerische Stabilität und Effizienz, Fehlerbehandlung, Bezeichnungen

7.-1.4.2. Variablen
i) Variablen sollten nach der Deklaration bald mit einem sinnvollen Wert belegt werden,
da sie sonst einen zufälligen Wert enthalten, aber bei Zuweisungen durchaus auf der
rechten Seite des Gleichheitszeichens stehen dürfen. (Fehlerquelle!)
ii) Globale Variablen sind in C++ überflüssig, stiften leicht Verwirrung, erschweren die
Wiederverwendbarkeit von Code und sollten nicht benutzt werden.
iii) Klassenvariablen sollten des Prinzips des information hiding“ wegen nie public“sein.
” ”
Man setze sie in den Private - oder Protected - Teil der Klasse und erlaube den Zugriff
nur über Funktionen, die im public - Teil stehen.

7.-1.4.3. Schleifen
Endlosschleifen werden formuliert als
while(true){
}

oder

for( ; ; ){
}
wobei die zweite Variante für den Neuling weniger klar ist.

7.-1.4.4. Bedingte Ausdrücke


i) if( Riesenbedingung )
// irgendwas
sollte vermieden und ersetzt werden durch

bool sinnvollerName = Riesenbedingung;


if( sinnvollerName )
//...

Dadurch wird das Programm leichter lesbar und gleichzeitig automatisch dokumentiert.
ii) Bei if - Abzweigungen sollte der reguläre Weg des Programms immer der im Hauptzweig,
nicht der im else - Zweig sein. Dadurch wird man beim Lesen nicht so schnell von Feh-
lerbehandlungen abgelenkt:

if( !isError ){
// Normalfall des Programms
} else{
// Fehler abarbeiten
}

152
7.7. Bezeichnungskonventionen und Layout

7.-1.4.5. Layout
i) Einrücken mit zwei oder drei Leerzeichen, nicht per Tabulator.

ii) Verwende als Blocklayout:

while( !done ){
doSomething();
}

bzw. für eine Funktion

void someMethod(){
int x;
}

bzw. für ein if - else - Konstrukt

if( Bedingung ){
//...
} else if( Bedingung ) {
//...
} else {
//..
}

iii) Leerzeichen sollten nach Bezeichnern, Operatoren wie +, -,*,/ eingesetzt werden, um
die Lesbarkeit zu erhöhen.

iv) Zwei Funktionsdefinitionen werden durch drei Leerzeilen getrennt.

7.-1.4.6. Kommentare
i) Klarer Code sollte Kommentare weitgehend überflüssig machen.
tricky code should not be commented but rewritten!

ii) Kommentare stets in Englisch

iii) Auch bei mehrzeiligen Kommentaren immer // benutzen, dann kann man mit /* ... */
problemlos ganze Blöcke auskommentieren.

iv) // Kommentar
Das soll heißen: Nach dem Kommentarzeichen ein Leerzeichen lassen, dann mit einem
Großbuchstaben beginnen.

153
7. Numerische Stabilität und Effizienz, Fehlerbehandlung, Bezeichnungen

7.-1.4.7. Diverses
i) Alle im Code verwendeten Zahlen außer 0 und 1 sollten als benannte Konstanten ein-
geführt werden. Dazu führt man oft eine eigene Klasse Defaults“ein mit static -

const - Variablen, damit der Anwender später bei der Namenswahl seiner eigenen Va-
riablen keine Einschränkungen hat. Man greift dann auf die Konstante mittels Defaults::Konstante
zu.

ii) goto ist nur beim Beenden tief verschachtelter Schleifen sinnvoll, sonst sollte man es
wegen Unübersichtlichkeit unbedingt vermeiden. Kernighan und Ritchie, die Erfinder
der Programmiersprache C, schreiben in Ihrem sehr zu empfehlenden Klassiker und
Standardwerk ”Programmieren in C”[[9]] folgendes:

C verfügt auch über eine beliebig zu missbrauchende goto-Anweisung



und Marken, zu denen gesprungen werden kann. Formal ist goto niemals
notwendig, und man kann fast immer leicht ohne goto-Anweisungen aus-
kommen. Wir haben in diesem Buch goto nicht verwendet. [..] Wir wollen
zwar nicht dogmatisch sein, aber es scheint doch, dass goto-Anweisungen
wenn überhaupt, dann selten benutzt werden sollten. “

154
Teil IV.

Finanzmathematik in der Praxis

155
8. Ein einfaches Klassenrahmenwerk
aus der Praxis
Im folgenden möchten wir als abschließendes Ergebnis dieser Einführung in C++ ein exem-
plarisches Rahmengerüst für einen ”OptionPricer” skizzieren, das Sie in dieser oder in leicht
abgewandelter Form bereits für Projekte im Financial Engineering verwenden können. Bei
der Implementation werden Sie in sehr natürlicher Weise wieder auf die zentralen Konzepte
der objektorientierten Programmierung geführt werden, die wir vorgestellt haben. Eine Imple-
mentation im Quellcode befindet sich auf der CD.

8.1. Programm
1. Klassen, die man in jedem Projekt braucht

2. Klassen zur Darstellung des Marktes

3. Klassen für die Finanzinstrumente, hier Optionen

4. Klassen zum Berechnen von TV und Greeks ( PricingEngines“)



5. Zusammenführung in einer Klasse Pricer“, die für den Anwender gedacht ist

Beachte: Alles wird im Namensraum MathFinance Project“erstellt.

8.1.1. Klassen, die man in jedem Projekt braucht


i) Meistens benötigt man im Projekt diverse Konstanten, um z.B. eine benutzerfreundli-
che Schnittstelle zu erstellen. Um dadurch nicht den Projektnamensraum unbrauchbar
zu machen, bündelt man diese Konstanten als static - const - Variablen in einer oder
mehreren Klassen, etwa in einer Klasse ”Defaults”.

Die konkreten Implementationen sind etwas länger und befinden sich auf der beiliegen-
den CD, hier sollen lediglich einige Fragmente folgen, die das wesentliche zeigen. In
der Praxis würde man mit Leerzeichen und Leerzeilen sicher großzügiger umgehen als
wir es hier tun. Zunächst das Header-file:

157
8. Ein einfaches Klassenrahmenwerk aus der Praxis

namespace MathFinance_Project {

class Defaults {

public:

// Enum with error codes that are delivered


// by an exception
enum errorCode {
IS_OK, // Everything was o.k.
MEMORY_ALLOCATION_FAILED,
// Memory allocation not possible
INPUT_INVALID,
// An input of a price call was not reasonable
DIVISION_BY_ZERO,
UNKNOWN_ERROR // all the rest
};

enum PARAMETERS{ VALUE, DELTA, GAMMA, MARKET_PRICE};


enum LevelType{ LOWER_BARRIER, UPPER_BARRIER};
enum KnockStyle{ KNOCK_IN, KNOCK_OUT };
enum Style{AMERICAN, EUROPEAN};
enum Underlying{EURUSD, USDGBP};
static bool PROTOCOL;

static const double MIN_MATURITY;


static const double MAX_MATURITY;

};

} // end of namespace

Nützlich ist die Konstante Protocol, die während der Entwicklungszeit auf true ge-
setzt werden kann, um dadurch die Ausgabe zusätzlicher Informationen zu ermöglichen.
Im final release setzt man sie dann auf false.
Im cpp-file sieht das ganze so aus:

#include "Defaults.h"

namespace MathFinance_Project {

bool Defaults::PROTOCOL = true; // test version

158
8.1. Programm

const double Defaults::MIN_MATURITY = 1.0 / 365.0 ;


const double Defaults::MAX_MATURITY = 5.0;

ii) Man erstelle sich Fehlerklassen und definiere passende Fehlercodes. Dies geschieht in
der Klasse Exception:

namespace MathFinance_Project {

class Exception {

public:
Exception(int errnum, const char *msg);
~Exception();
const char *getMessage() const;
const int getErrno() const;

private:
// memory for the error text
char msg_[128];
// memory for the numerical error code
int errno_;
};

} // end of namespace

Für die Implementation in der cpp-Datei verweisen wir auf die beiliegende CD, da dort
nichts Besonderes passiert. Die angegebene Klasse lässt sich natürlich stark erweitern,
die Angabe eines Fehlertextes und eines Fehlercodes stellen wohl das Minimum dar.
Beachten Sie, dass die Klasse Exception die vordefinierten Fehlercodes aus der Klasse
Defaults benutzen soll, was hier aber nicht erzwungen wird.

iii) Oft benötigt man noch diverse Hilfsfunktionen, etwa Verteilungsfunktionen, Wahrschein-
lichkeitsdichten, Zufallsgeneratoren usw. Diese Helper - functions werden in sinnvollen
Klassen zur Verfügung gestellt. Als Beispiel folgt die Klasse NormalProbability:

namespace MathFinance_Project{

class NormalDistribution1D{

159
8. Ein einfaches Klassenrahmenwerk aus der Praxis

public:
static double ndf(double x);
static double normInv(double p);
static double nc(double x);

};

// cpp-file...

namespace MathFinance_Project{

double NormalDistribution1D::ndf(double x){


return 0.398942280401433 * exp(-x * x * 0.5);
}
// usw
}

Für die Implementation der übrigen Funktionen, insbesondere die Inverse der Verteilungs-
funktion der Standardnormalverteilung haben wir den Cody-Algorithmus samt einer ferti-
gen Implementation verwendet. Dies ist als als CDFLIB“frei erhätlich unter der Adresse

http://biostatistics.mdanderson.org/SoftwareDownload/. Für Details verweisen wir auf die
Begleit-CD.

8.1.2. Klassen zur Darstellung des Marktes


Hier gehen wir in zwei Schritten vor:

i) Eine Klasse mit allgemeinen Marktdaten, also Spot, At-the-money - Vol oder ATM-Vol-
Surface.

#include "Exception.h"

namespace MathFinance_Project {

class Market{

public:
Market(double spot, double atmVol);
inline double getSpot() const;
void setSpot( double spot ) throw (Exception);
inline double getAtmVol() const;
void setAtmVol( double vol ) throw (Exception);

private:

160
8.1. Programm

double spot_, atmVol_;


bool spotIsValid( double spot ) const;
bool volIsValid( double vol ) const;

};

Die setter-methods greifen hier zur Prüfung der eingegeben Werte auf die Funktion
spotIsValid und volIsValid zurück. Diese dienen rein mechanisch der Prüfung von
Eingabewerten und sollen das Objekt nicht verändern, deshalb erhalten sie hier den Zu-
satz const. Die Implementation der Klasse ist offensichtlich, siehe CD.

ii) Für unser Beispiel benötigen wir noch eine Spezialisierung für den Devisenmarkt, die
zusätzlich Inlandszins rd, Auslandszins rf und eventuell das betrachtete Devisenpaar
enthält.

#include "Market.h"
#include "Exception.h"

namespace MathFinance_Project{

class FX_Market: public Market{

public:
FX_Market(double spot, double atmVol,
double rd, double rf) throw(Exception);
inline double get_rd() const;
void set_rd( double rd ) throw( Exception );
inline double get_rf() const;
void set_rf( double rf ) throw(Exception);

private:
double rd_, rf_;
bool rdIsValid( double rd ) const;
bool rfIsValid( double rf ) const;
};

8.1.3. Klassen für die Finanzinstrumente


Hier gehen wir in 3 Schritten vor:

161
8. Ein einfaches Klassenrahmenwerk aus der Praxis

i) Vorsichtigerweise starten wir mit einer ( leeren ) Basisklasse FinancialProduct:

namespace MathFinance_Project{

class FinancialProduct{

};

ii) Dann leiten wir eine Klasse Option ab, die z.B. die Laufzeit enthält.

namespace MathFinance_Project {

class Option: public FinancialProduct{

public:
Option(double maturity,int underlying,
int fd ) throw(Exception);
inline double getMaturity() const;
inline int getUnderlying() const;
inline virtual int get_fd() const;

virtual void set_fd( double fd ) throw(Exception);


void setMaturity( double maturity ) throw ( Exception );
void setUnderlying( int underlying ) throw( Exception );

protected:

bool maturityIsOkay( double maturity ) const;


bool underlyingIsOkay( int underlying ) const;

private:
double maturity_; // time in years
Defaults::Underlying underlying_;
int fd_; // notional in foreign or domestic currency

};

Die Hilfsfunktion maturityIsOkay und underlyingIsOkay liegen hier im Abschnitt


protected, weil abgeleitete Klassen vielleicht darauf zugreifen möchten. Im ganz stren-
gen Sinn sollte das natürlich nicht nötig sein, aber es schadet sicher auch nicht, weil es

162
8.1. Programm

const- Funktionen sind und somit das Prinzip des information-hiding nicht verletzt
wird. Die Implementation ist wieder klar, repräsentativ ein Beispiel:

namespace MathFinance_Project {

bool Option::maturityIsOkay( double maturity ) const{

if( maturity >= Defaults::MIN_MATURITY &&


maturity <= Defaults::MAX_MATURITY )
return true;
else
return false;

iii) Dann leiten wir Klassen für konkrete Optionen ab, z.B. class Vanilla oder class
TouchOption. Diese Klassen enthalten die konkreten Kontraktdaten. Beispiel:

#include "Option.h"
#include "Exception.h"

namespace MathFinance_Project{

class Vanilla: public Option{

public:

Vanilla( double strike,


int phi,
double maturity,
double notional,
int underlying,
int fd) throw(Exception);

inline double getStrike() const;


inline double getNotional() const;
inline int getPhi() const;

void setStrike( double strike ) throw(Exception);


void setPhi( int phi ) throw (Exception);
void setNotional( double notional ) throw (Exception);

163
8. Ein einfaches Klassenrahmenwerk aus der Praxis

private:

double strike_;
int phi_;
double notional_;

bool strikeIsValid( double strike) const;


bool notionalIsValid( double notional) const;
bool phiIsValid( int phi ) const;

};

Auch hier ist die Implementation klar (siehe CD). Beim Konstruktor der Klasse ha-
ben wir eine etwas ungwöhnlich aussehende Formatierung benutzt, die aber sehr häufig
verwendet wird, da sie sehr übersichtlich ist. Beachten Sie bei der Entscheidung, wel-
che Daten in die Klasse Market- bzw Option oder deren Ableitung gehören, folgendes:
Marktdaten gehören sämtlich in die Klasse Market (was auch sonst), alle Informatio-
nen, die auf dem Term-sheet der Option auftauchen, also alle Kontraktdaten, gehören in
die Klasse Option bzw eine davon abgeleitete Klasse.

8.1.4. Klassen zum Berechnen von TV und Greeks


Das Verhalten der fertigen PricingEngines für die einzelnen Optionen soll nach außen hin
möglichst einheitlich sein, deshalb definieren wir zuerst eine

i) abstrakte Basisklasse PricingEngine, die das Interface zum Benutzer und etwa die
möglicherweise ausgeworfenen Fehlerklassen festlegt.

#include "FX_Market.h"
#include "Defaults.h"
#include "Exception.h"

namespace MathFinance_Project{

class PricingEngine{

public:

PricingEngine( const FX_Market* const fxmarket );


virtual double computeTV() const = 0;
virtual double computeGreek(int greekID) const= 0;

protected:

164
8.1. Programm

const FX_Market* const fxmarket_;

};

Fast alle Funktionen sind rein virtuelle Funktionen, entsprechend ist das cpp-file sehr
kurz:

PricingEngine::PricingEngine( const FX_Market*


const fxmarket ): fxmarket_( fxmarket ){

}
}

Zum Pointer fxmarket kommen wir gleich zurück.

ii) Nun leitet man konkrete PricingEngines ab, in denen z.B. die analytischen Formeln
implementiert werden. Zu unterscheiden ist dabei nach Optionstyp und PricingMethode
( analytisch, Monte Carlo, PDE ). Es entstehen also Klassen wie:

#include "PricingEngine.h"
#include "Vanilla.h"

namespace MathFinance_Project{

class VanillaPricingEngineAnalytic: public PricingEngine{

public:

VanillaPricingEngineAnalytic(
const Vanilla* const financialProduct,
const FX_Market* const fxmarket);

virtual double computeTV() const throw(Exception);


virtual double computeDelta() const throw(Exception);
virtual double computeGamma() const throw(Exception);
virtual double computeVega() const throw(Exception);
virtual double computeVanna() const throw(Exception);
virtual double computeVolga() const throw(Exception);
double computeTheta() const throw(Exception);
double computeRhod() const throw(Exception);
double computeRhof() const throw(Exception);

private:
const Vanilla* const finProd_; // financial product

165
8. Ein einfaches Klassenrahmenwerk aus der Praxis

void compute_d12(double* d1, double* d2) const;


void prepareVariables(double*, int*, double*, double*,
double*, double*,double*, double*) const;

};

Im private-Teil finden sich einige helper-functions, etwa prepareVariables, die von


den Funktionen computeTV und computeGreek aufgerufen werden um die benötigten
Variablen aus den Datenklassen finProd und fxmarket auszulesen. Wie man sieht,
benötigt der Compiler bei der Deklaration gar nicht die Namen der Variablen, sondern
nur deren Typ, wovon hier der Demonstration halber ausnahmsweise Gebrauch gemacht
wird. Generell ist dies nicht zu empfehlen, da Variablennamen ein Stück Selbstdoku-
mentation des Programms sind. Die Funktion compute d12 dient dazu, die bekannten
Hilfsgrößen d1 und d2 in den Black-Scholes-Formeln auszurechnen.

Diese Programmiertechnik könnte man bereits als ein erstes Design-pattern ansehen, obwohl
wir diese erst später besprechen möchten. Hier nur so viel: Die Technik, dass man eine Ba-
sisklasse definiert, die das grundsätzliche Verhalten der Klasse festlegt, die Implementation
aber offen lässt und auf eine abgeleitete Klasse verschiebt, bezeichnet man als das Template-
pattern, was natürlich zunächst einmal nichts mit templates im Sinne von C++ zu tun hat.
Warnung
Die konkreten PricingEngines benötigen zwar die Informationen aus den Klassen FX Market
und konkrete Option, sollten diese Klassen aber nicht erben. Dies hat mehrere Gründe: Zum
einen würde diese Vererbung dem Gedanken des Sein oder Haben“widersprechen, denn ei-

ne PricingEngine ist keine Weiterentwicklung einer Markt- oder Kontraktdatenklasse, zum
anderen würde der Vererbungsmechanismus dann dazu führen, dass am unteren Ende der
Hierarchie ein bunt zusammengewürfeltes Datenreservoir in Form einiger Klassen entsteht,
das den Vorteil der klaren Struktur wieder zunichte macht. Stattdessen sollten die Markt-
und Kontraktdatenklassen selbstständig existieren und ein Pointer auf diese an den Konstruk-
tor der Klasse PricingEngine übergeben werden. Auf diesem Weg wird die Versorgung mit
den benötigten Informationen elegant hergestellt. Da ein Zeiger auf eine Instanz der Klasse
fxmarket in jeder Instanz von einer abgeleiteten Klasse von PricingEngine benötigt wird,
wird deren Verwaltung bereits in die Basisklasse vorgezogen, um Schreibarbeit zu sparen.

8.1.5. Ein Klassenrahmenwerk zur Optionsbewertung


An dieser Stelle sind wir theoretisch bereits fertig und könnten ein Hauptprogramm schreiben,
das folgendes tut:

i) Frage alle benötigten Variablen vom Benutzer ab.

ii) Erzeuge Instanzen von FX MARKET und der konkreten Option, etwa Vanilla. Deren
Konstruktoren erwarten dabei all die abgefragten Daten.

166
8.1. Programm

iii) Nun erzeugt man eine Instanz der passenden PricingEngine, etwa
VanillaPricingEngineAnalytic. Der Konstruktor erwartet nun Zeiger auf die In-
stanzen von FX MARKET und Vanilla.

iv) Rufe nach Bedarf die Funktionen der PricingEngine auf, etwa computeTV(). Gib das
Ergebnis aus.

Um ein solches Programm schreiben zu können, muß der Anwender also über die Struktur
unseres Klassenrahmenwerks Kenntnis haben, was vielleicht von diesem nicht erwünscht ist.
Deshalb fassen wir die genannten Schritte in einer abschließenden Klasse Pricer zusam-
men, die alle benötigten Daten im Konstruktor erwartet und dem Anwender die Funktionen
computeTV und computeGreek zur Verfügung stellt:

i) Auch hier sollten Sie großen Wert auf eine einheitliche Benutzerschnittstelle legen. Man
beginne daher mit einer abstrakten Basisklasse Pricer, die alle Funktionen bereits
enthält, die der Anwender später braucht.

#include "PricingEngine.h"
#include "Exception.h"

namespace MathFinance_Project{

class Pricer{
// abstract base class to define a common interface
public:
Pricer(FX_Market fxmarket) throw(Exception);
virtual double computeTV() const throw(Exception) = 0;
virtual double computeGreek() const throw(Exception) = 0;

inline double get_rd() const;


void set_rd( double rd ) throw( Exception );
inline double get_rf() const;
void set_rf( double rf ) throw(Exception);

protected:
FX_Market fxmarket_;
};

Da eine Instanz von FX MARKET in jedem Pricer vorkommen wird, wird deren Verwal-
tung in die abstrakte Basisklasse vorgezogen. Es folgt ein kurzer Auszug aus der Imple-
mentation:

#include "Pricer.h"
#include "Exception.h"
#include "Defaults.h"

167
8. Ein einfaches Klassenrahmenwerk aus der Praxis

namespace MathFinance_Project{

Pricer::Pricer(FX_MARKET fxmarket)
throw(Exception): fxmarket_(fxmarket){

inline double Pricer::get_rd() const{

return fxmarket_.get_rd();

ii) Nun leitet man konkrete Klassen ab, etwa VanillaPricer, in die die bereits besproche-
nen Bausteine Market bzw FX Market, VanillaOption und VanillaPricingEngineAnalytic
als Membervariablen, d.h. als Instanz, eingebunden werden. Die Befehle der Benutzer-
schnittstelle werden dann mit den internen Klassen verbunden:

#include "VanPricEngAnaly.h"
#include "Vanilla.h"
#include "FX_Market.h"
#include "Exception.h"

namespace MathFinance_Project{

class VanillaPricer: public Pricer{

public:

VanillaPricer(double spot,
double atmVol,
double rd,
double rf,
double strike,
int phi,
double maturity,
double notional,
int underlying,
int fd);

double computeTV() const throw(Exception);


double computeGreek() const throw(Exception);

168
8.1. Programm

inline double getMaturity() const;


inline int getUnderlying() const;
inline int get_fd() const;
inline double getStrike() const;
inline double getNotional() const;
inline int getPhi() const;
virtual void set_fd( double fd ) throw(Exception);

void setNotional( double notional ) throw( Exception );


void setPhi( int phi ) throw (Exception);
void setMaturity( double maturity ) throw ( Exception );
void setStrike( double strike ) throw (Exception) ;
void setUnderlying( int underlying ) throw( Exception );

private:

Vanilla option_;
VanillaPricingEngineAnalytic pricingEngine_;

};

Stellvertretend wieder ein Auszug aus der Implementation:

VanillaPricer::VanillaPricer(double spot, double atmVol,


double rd, double rf,
double strike, int phi,
double maturity, double notional,
int underlying, int fd)
:
Pricer(spot, atmVol, rd, rf),
option_(strike, phi, maturity,notional,underlying, fd),
pricingEngine_(&option_, &fxmarket_)
{

double VanillaPricer::computeTV() const throw(Exception){

return pricingEngine_.computeTV();

169
8. Ein einfaches Klassenrahmenwerk aus der Praxis

Bemerkung 8.1.1. i) Möchte man aus dem AnalyticPricer einen MonteCarloPricer ma-
chen, so genügt es die spezialisierte PricingEngine auszutauschen. Da das Methoden-
protokoll durch eine abstrakte Basisklasse für die PricingEngine klar definiert ist, funk-
tionieren die Einzelteile der Klasse MonteCarloPricer reibungslos miteinander.

ii) Vielleicht wundern Sie sich über den extensiven Einsatz des Schlüsselworts virtual
(siehe 2.3.8) in diesem Rahmenwerk, obwohl es doch scheinbar nicht benötigt wird.
Recht haben Sie, es wäre wohl an den meisten Stellen, so wie der Einsatz hier vorge-
schlagen wird, überflüssig. Da hier aber häufig vererbt wird und man an die Zukunft
inklusive möglicher Erweiterungen denken sollte, erhält dieses Stück defensives Pro-

grammieren“zumindest ein bisschen Rechtfertigung.
Wie bereits gesagt, können Sie dieses Rahmenwerk bereits in realistischen Projekten anwen-
den. Leider müssen wir einräumen, dass es auch ein paar Mängel hat, was etwa die einfache
Erweiterbarkeit angeht: Wenn Sie statt konstanten Zinssätzen eine Zinsstrukturkurve verwen-
den möchten, müssten Sie im Moment ziemlich weit unten im Gerüst anfangen umzubauen.
Ferner ist der Einsatz im Zusammenhang mit Monte Carlo nicht ganz glücklich, denn über das
elegante Zusammenspiel von Klassen zur Pfaderzeugung, von Zufallsgeneratoren und so wei-
ter finden Sie hier keine abschließenden Hinweise. Ein anderes Rahmenwerk, das sich auch
für den Einsatz von Monte Carlo eignet, wird im nächsten Kapitel besprochen.

8.2. Übungen
Aufgabe 22. Erstellen Sie ein Klassenrahmenwerk zum Bewerten von Devisenoptionen.
Implementieren Sie dazu das vorgestellte Klassenrahmenwerk so weit, dass Sie eine Klasse
VanillaPricer mit TV- und Delta - Bestimmung für den praktischen Anwender anbieten
können. Dabei gibt es zwar recht klare Vorgaben, dennoch haben Sie bei den Details der
Schnittstellen viel Freiraum.

Hinweis
Diese Aufgabe eignet sich sehr schön für eine Gruppenarbeit. Jede Gruppe kann ihren Teil
unabhängig von den anderen recht weit bearbeiten, sofern die Deklarationen der Klassen, die
von anderen Gruppen erstellt werden, vorhanden sind. Am Ende können die .cpp - Dateien
dann ausgetauscht werden und das Programm getestet werden. Man sieht also sehr schön
die Vorzüge einer klaren Benutzerschnittstelle, des Versteckens komplizierter Details, kurz:
Sie zeigt, wie C++ eine produktive Zusammenarbeit vieler Programmierer an einem Projekt
ermöglicht.

170
9. Design Patterns und ein
verbessertes Rahmenwerk
In diesem Kapitel soll ein völlig neues, stark verbessertes Rahmenwerk entwickelt und der Le-
ser in einige Feinheiten eleganter Programmierung eingeführt werden. Das gesamte Kapitel
ist sehr eng an das Buch C++ Design Patterns and Derivatives Pricing von Mark Joshi[[6]]1
angelehnt, das wir deshalb als Referenz empfehlen möchten.

9.1. Flexibilität als zentrales Problem eines


Rahmenwerks
Um die Bedeutung eines flexiblen Rahmenwerks aufzuzeigen, möchten wir mit einem ein-
fachen Programm beginnen, einige praktische Forderungen hinsichtlich der Erweiterbarkeit
formulieren und diese dann nach und nach berücksichtigen. In diesem Programm sind bereits
Hinweise hinsichtlich effizienter Programmierung berücksichtigt, so wurden etwa rechenin-
tensive Operationen aus zeitkritischen Schleifen ausgelagert, aber es wurde auf objektorien-
tierte Programmierung verzichtet. Mit anderen Worten: Es handelt sich um ein ad-hoc Pro-
gramm, wie man es in der Praxis zur Bestimmung eines Preises vielleicht schreiben würde.
Die Datei ’helperFunctions.h’ samt Implementation ist in den Lösungen zu Übungsaufgabe 5.
in diesem Buch enthalten.

#include "helperFunctions.h"
#include "stdio.h"
#include "math.h"

double SimpleMonteCarlo1(double expiry,


double strike,
double spot,
double vol,
double r,
1 Fürpraktisch alle Programme dieses Kapitels gilt: Copyright (c) 2002 Mark Joshi, Permission to use, copy,
modify, distribute and sell this software for any purpose is hereby granted without fee, provided that the above
copyright notice appear in all copies and that both that copyright notice and this permission notice appear in
supporting documentation. Mark Joshi makes no representations about the suitability of this software for any
purpose. It is provided ”as is”without express or implied warranty.

171
9. Design Patterns und ein verbessertes Rahmenwerk

unsigned long numberOfPaths){

double variance = vol*vol*expiry;


double rootVariance = sqrt(variance);
double itoCorrection = -0.5*variance;

double movedSpot = spot*exp(r*expiry +itoCorrection);


double thisSpot;
double runningSum=0;

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

double thisGaussian = double N01();


thisSpot = movedSpot*exp( rootVariance*thisGaussian);
double thisPayoff = thisSpot - strike;
if (thisPayoff < 0)
thisPayoff = 0;
runningSum += thisPayoff;
}

double mean = runningSum / numberOfPaths;


mean *= exp(-r*expiry);
return mean;
}
Und hier das Hauptprogramm:
void main()
{

double expiry;
double strike;
double spot;
double vol;
double r;
unsigned long numberOfPaths;

printf("\nGeben Sie bitte die Laufzeit ein\n");


scanf("%lf",&expiry);

printf("\nGeben Sie bitte den Strike ein\n");


scanf("%lf",&strike);

printf("\nGeben Sie bitte den Spot ein\n");


scanf("%lf",&spot);

172
9.1. Flexibilität als zentrales Problem eines Rahmenwerks

printf("\nGeben Sie bitte die Volatilit"at ein\n");


scanf("%lf",&vol);

printf("\nGeben Sie bitte den risikoneutralen Zins ein\n");


scanf("%lf",&r);

printf("\nGeben Sie bitte die Anzahl der Simulationen ein\n");


scanf("%lf",&numberOfPaths);

double result = SimpleMonteCarlo1(expiry, strike, spot,


vol, r, numberOfPaths);

printf("Preis: %lf\n", result);


}

Dieses effiziente Programm liefert einen Monte Carlo-Schätzer für den Plain Vanilla Call,
aber sonst leider nichts. Was würde nun in der Praxis passieren? Selbstverständlich würde
man das Programm erweitern wollen, hier einige mögliche Änderungswünsche, die jeweils
einen Umbau des bisherigen Programms erfordern würden: 2

• Erweiterung auf Puts

• Ausgabe des Standardfehlers

• Das Programm ist nicht schnell genug, daher Anwendung von antithetic sampling

• Einsatz eines neuen Zufallszahlengenerator

• Bewertung asiatischer Optionen

• Bestimmung des besten Preises bis mittags um 14 Uhr

• Bestimmung des besten Preis bei Rechenzeit von 3 Stunden

• Darstellung der Konvergenz in einer Tabelle

Nach wenigen Wochen und ständigem Ändern des bestehendes Programms wird die Freude
an der Arbeit nachlassen, und um genau das zu vermeiden, dafür ist dieses Kapitel gedacht!
Unsere Aufgabe ist es nun, ein Programm zu schreiben, das all diesen Eventualitäten Rech-
nung trägt. Dabei orientieren wir uns an dem Paradigma eleganter Programmierung, dem
2 zitiert und frei übersetzt nach Joshi[[6]], Seite 9

173
9. Design Patterns und ein verbessertes Rahmenwerk

9.1.0.1. Open-Closed-Principle
Dieses Prinzip steht für open for extension, closed for modification und meint, dass Programm-
code so geschrieben werden sollte, dass er einerseits leicht erweiterbar ist, andererseits aber
nicht mehr umgeschrieben werden muß.
So seltsam sich dieses Prinzip zunächst anhört, so werden wir es doch im Rest des Kapitels
strikt befolgen, denn praktisch bedeutet das, dass jeder der obigen Punkte nur durch neuen,
zusätzlichen Programmcode lösbar wird, alles Andere bleibt erhalten. Dabei werden uns de-
sign patterns sehr nützlich sein.

Für den weiteren Ablauf ergibt sich: In einem ersten Schritt werden wir die Darstellung von
Optionen hinsichtlich ihrer Erweiterbarkeit und Wiederverwendbarkeit verbessern, anschlie-
ßend schreiben wir neue Klassen zur Verwaltung von Marktdaten, so dass auch diese sich
leicht erweitern lassen, etwa von konstanten auf zeitlich variable Zinssätze. Dann schreiben
wir eine Klasse zur statistischen Auswertung der Daten, die sich leicht auf Eventualitäten wie
eine Konvergenztabelle erweitern lässt. Ein weiterer Punkt wird sein, wie man einen Zufalls-
generator so flexibel schreibt, dass man auch antithetic sampling nachträglich einbauen kann.
Die genannten Bausteine sollen schließlich in einem Pricer zusammengefasst werden. Bevor
es losgeht wollen wir noch kurz auf Design patterns zu sprechen kommen, da wir diese im
folgenden oft benötigen.

9.2. Design Patterns - ein kurzer Überblick


Bei der Entwicklung objektorientierter Software treten oft wiederkehrende Typen von Proble-
men auf. Die Kenntnis dieser Problemmuster und ihrer zugehörigen Lösungsstrategien ist ein
entsprechend großer Vorteil bei der täglichen Arbeit, denn sie erleichtert nicht nur das Auffin-
den adäquater Lösungen, sondern auch die Kommunikation von Programmierern untereinan-
der. Schließlich erleichtern diese Musterstrategien die Wiederverwendbarkeit von Software,
weil sie eine schnellere Einarbeitung in ein bestehendes Projekt erlauben.
Ein design pattern (Entwurfsmuster) beschreibt nun einen solchen Problemtypus auf abstrak-
ter Ebene und schlägt eine Lösung vor in Form von gewissen Klassen, die in angegebener
Weise miteinander arbeiten sollen. Design Patterns lassen sich in verschiedene Kategorien
einteilen, wobei wir exemplarisch einige Entwurfsmuster nennen, die wir im Laufe des Kapi-
tels verwenden werden. Die Erklärungen sind lediglich Andeutungen und erst im Laufe des
Kapitels ganz verständlich. Sie sollen vor allem als Gedächtnisstütze dienen.

a) Creational Patterns / Erzeugungsmuster


Erzeugungsmuster abstrahieren Objekterzeugungsprozesse. Sie behandeln also Proble-
me im Zusammenhang mit der Erzeugung von Objekten, etwa dem Delegieren der Ob-
jekterzeugung an andere Objekte.

i) Virtual copy constructor


erzeugt eine Kopie eines Objekts ohne Kenntnis dessen Typs

174
9.2. Design Patterns - ein kurzer Überblick

ii) Singleton pattern / Einzelstück


sorgt dafür, dass von einer Klasse höchstens eine Instanz erzeugt werden kann und
ermöglicht einen globalen Zugriff auf dieses Objekt. guter Anwendungskandidat:
Marktdatenklasse

b) Structural patterns / Strukturmuster


Strukturmuster fassen Klassen und Objekte zu größeren Strukturen zusammen und op-
timieren deren Zusammenarbeit.

i) Adapter pattern (auch: wrapper), bridge pattern (Brücke)


übersetzt eine Schnittstelle in eine andere, d.h. ein geeignetes Interface soll zwei
Programmteilen, die sich nicht verstehen, die Zusammenarbeit ermöglichen.
bridge pattern: wird direkt bei Programmentwurf eingeplant zur gezielten Tren-
nung von Schnittstelle und Implementation;
adapter pattern: wird nachträglich eingefügt, etwa wegen der Einbindung einer ex-
tern vorhandenen Bibliothek.
ii) Decorator pattern / Dekorierer
erweitert oder modifiziert die Funktionalität einer Klasse, ohne dass man diese
selbst verändert oder ableitet, indem eine Klasse mit gleichem Interface zwischen
Anwender und ursprüngliche Klasse geschaltet wird. Daduch kann der Anwender
die neue Klasse genau wie die ursprüngliche Klasse anwenden - er merkt es gar
nicht.

c) Behavioural patterns / Verhaltensmuster


Verhaltensmuster beschreiben die Interaktion von Objekten und Kontrollstrukturen.

i) Strategy pattern / Strategie


Teile eines Algorithmus werden in Objekte ausgelagert, die bei Aufruf des Al-
gorithmus (bzw der den Algorithmus implementierenden Funktion) als Argument
übergeben werden.
ii) Template pattern / Schablonenmethode
Definiere den groben Ablauf eines Algorithmus in einer Basisklasse, konkrete Im-
plementationen können dann flexibel in abgeleiteten Klassen vorgenommen wer-
den.

Bevor wir den Programmcode mit diesem Rüstzeug ausgestattet optimieren, formulieren wir
zwei zentrale Voraussetzungen wiederverwendbaren Codes:

a) Klarheit (als soziale Komponente)


Wenn ein Programmierer zum Verstehen eines Programms allzu lange braucht, dann
schreibt er es lieber gleich neu. Entwurfsmuster sind hier sehr hilfreich.

b) Elegante Datenstrukturen und Algorithmen (als technische Komponente)


Wenn ein Programm nicht leicht anpassbar oder erweiterbar ist, dann schreibt man es
besser neu. Das open-closed-principle dient hier als Orientierung.

175
9. Design Patterns und ein verbessertes Rahmenwerk

Es gibt natürlich viele weitere Beispiele, auf die wir hier nicht eingehen können. Das Ge-
biet der Design patterns wurde im wesentlichen bekannt durch das Buch Design Patterns -
Elements of Reusable Object-Oriented Software[[3]], auf das wir den interessierten Leser aus-
drücklich hinweisen möchten.

9.3. Ein verbessertes Rahmenwerk


Wir möchten nun von dem einleitenden Beispiel ausgehen und SimpleMonteCarlo1 sukzes-
sive ausbauen.

9.3.1. Payoffs
Zuerst möchten wir uns von der Einschränkung befreien, dass SimpleMonteCarlo1 nur einen
Plain Vanilla Call bewerten kann. Eine Möglichkeit dies zu beheben wäre die Verwendung
eines Funktionspointers, ganz im Sinne von C:

double SimpleMonteCarlo1(...double (*payoffPtr)(double, double) ){


// ...
}

Hier erwartet SimpleMonteCarlo1 also einen Zeiger auf eine Funktion mit zwei Argumenten,
nämlich Spot und Strike, wobei diese Idee problematisch wird, wenn wir eine Option wie den
DoubleOneTouch bewerten wollen, die mehr als nur zwei Argumente benötigt. Man könnte
diesen Ansatz retten, indem die Funktion nicht eine fixe Anzahl von Argumenten, sondern ein
Feld von double erwartet, aber dann müsste man je nach geforderter Option den Programmco-
de von SimpleMonteCarlo1 anpassen, was dem open-closed-principle widerspricht. Im fol-
genden wird eine objektorientierte Lösung im Sinne des Strategy-Pattern vorgeschlagen Wir
definieren eine Basisklasse Payoff mit einer virtuellen Funktion thisPayOff, die lediglich
die Variable Spot erwartet. Konkrete Optionen entstehen nun durch Ableiten, wobei sich der
private-Teil dieser abgeleiteten Klassen zum Unterbringen zusätzlich benötigter Daten wie
Strike oder Barrier eignet. Unsere Pricing-Funktion SimpleMonteCarlo1 erwartet nun eine
Referenz Payoff& auf die Basisklasse und verwendet lediglich die Funktion thisPayOff.
Dadurch ist sie unabhängig von konkreten Optionen, denn sie muß deren genauen Typ ja
überhaupt nicht kennen. Entsprechend muß sie beim Einfügen neuer Optionen auch nicht um-
geschrieben werden, solange diese nicht pfadabhängig sind, aber das wollen wir erst sehr viel
später zulassen. Wir wollen dies in Grafik (9.1) veranschaulichen:
Bevor wir den Programmcode anschauen, möchten wir noch eine Kleinigkeit abändern. So
wie wir unsere Basisklasse nun beschrieben haben, müsste SimpleMonteCarlo1 in etwa so
aussehen:
double SimpleMonteCarlo1(...,PayOff& payoff){

// ...
for (int i=0; i < numberOfPaths; i++){

176
9.3. Ein verbessertes Rahmenwerk

Abbildung 9.1.: Schema zum Zusammenspiel von Pricingfunktion und (abgeleiteten) Optio-
nen

177
9. Design Patterns und ein verbessertes Rahmenwerk

// ...
double simulatedPayoff = payoff.thisPayOff(thisSpot);
// ...

Viel eleganter wäre

double SimpleMonteCarlo1(...,PayOff& payoff){

// ...
for (int i=0; i < numberOfPaths; i++){
// ...
double simulatedPayoff = payoff(thisSpot);
// ...

Hier benutzt man das Objekt payoff wie eine Funktion, was sehr natürlich aussieht. Dies
erreichen wir, indem wir den operator() überladen. Solche Objekte heißen in der englisch-
sprachigen Literatur function-objects oder functors. Schauen wir nun die Deklaration der
Basisklasse Payoff samt einer abgeleiteten Klasse für einen Call an:

class PayOff
{
public:

PayOff(){};
virtual double operator()(double Spot) const=0;
virtual ~PayOff(){}

private:
};

class PayOffCall : public PayOff


{
public:

PayOffCall(double strike);
virtual double operator()(double spot) const;
virtual ~PayOffCall(){}

private:

178
9.3. Ein verbessertes Rahmenwerk

double strike_;
};

Bemerkenswert ist hier zunächst einmal das Überladen von operator(), da wir das bis jetzt
noch nicht behandelt haben, wobei diese Funktion hier natürlich virtuell (siehe (2.3.8)) sein
muß. Der Destruktor (siehe (2.3.1.4)) einer Klasse, die für weitere Ableitungen bestimmt ist,
sollte immer virtuell sein. Die Deklaration der Klasse PayoffCall und die folgende Imple-
mentation ist naheliegend.

#include <PayOff2.h>
#include "Hilfsfunktionen.h"

PayOffCall::PayOffCall(double strike) : strike_(strike)


{
}

double PayOffCall::operator () (double spot) const


{
return max(spot-strike_,0.0);
}

Die Funktion SimpleMonteCarlo sieht nun so aus:

double SimpleMonteCarlo2(const PayOff& thePayOff,


double expiry,
double spot,
double vol,
double r,
int numberOfPaths)
{
// ...
for (int i=0; i < numberOfPaths; i++){
// ...
double thisPayOff = thePayOff(thisSpot);
// ..
}

//...
}

Wir möchten an dieser Stelle auf den selbstdokumentierenden Effekt von const hinweisen,
denn die Tatsache, dass SimpleMonteCarlo2 ein const PayOff& erwartet statt einem einfa-
chen PayOff& bringt zum Ausdruck, dass SimpleMonteCarlo2 das Objekt nicht verändern
möchte. Ferner wird deutlich, warum diese Technik als strategy pattern bezeichnet wird:
Der Monte Carlo-Algorithmus besteht im wesentlichen aus den Teilen Erzeugung von Pfaden

179
9. Design Patterns und ein verbessertes Rahmenwerk

und Erzeugung von Payoffs. Die Erzeugung von Payoffs wird hier in ein externes Objekt aus-
gelagert.

Möchte man nun eine weitere Option hinzufügen, so erstellt man lediglich ein neues Hea-
derfile und eine neue cpp-Datei mit der passenden abgeleiteten Klasse. Von den bestehenden
Dateien wird lediglich die Datei mit dem Hauptprogramm verändert, aber das lässt sich oh-
nehin nie vermeiden. Mit dieser Technik haben wir das open-closed-principle also erstmals
realisiert: Der bestehende Code ist hinsichtlich pfadunabhängiger Optionen erweiterbar, muß
dafür aber nicht mehr umgeschrieben werden. Wenn das Programm erweitert wird, sollten le-
diglich die neu hinzugekommenen Dateien compiliert werden, ansonsten ist das ein Hinweis
auf noch vorhandene Abhängigkeiten. Abschließend noch das Hauptprogramm:
void main()
{
double expiry, strike, spot, vol, r;
unsigned long numberOfPaths;

printf("\nGeben Sie bitte die Laufzeit ein\n");


scanf("%lf",&expiry);

// usw

PayOffCall callPayOff(Strike);

double resultCall =
SimpleMonteCarlo2(callPayOff, expiry, spot, vol,
r, numberOfPaths);

printf("Preis: %lf\n", resultCall);

9.3.2. Optionen
Gegeben seien N Payoffklassen Payoff1 ,. . . , PayoffN , die wir nun zu einer Klasse Option mit
allen benötigten Kontraktdaten (im Gegensatz zu den Marktdaten) erweitern möchten. Dabei
möchten wir der Einfachheit halber nur pfadunabhängige Optionen modellieren und es gelte

Optioni = Payoffi + Laufzeiti

d.h. jeder Payoff wird um das Kontraktdatum Laufzeit “erweitert und das Ergebnis bezeich-

nen wir als Klasse Option. Dies kann man wie üblich mit Vererbung lösen, aber das würde
viel monotone Schreibarbeit wie die N-malige Implementation der Funktion getExpiry mit
sich bringen, was unserem Ideal wiederverwendbaren Codes nicht gerecht wird. Viel elegan-
ter ist es, eine Klasse fig:OptionPathIndependent zu schreiben, die einen Zeiger bzw. eine

180
9.3. Ein verbessertes Rahmenwerk

Abbildung 9.2.: Erweiterung der Klasse Payoff zu einer Klasse Option mit Hilfe einer Klasse
PathIndependent

181
9. Design Patterns und ein verbessertes Rahmenwerk

Referenz auf die Instanz einer Payoffklasse erwartet. Diese Idee funktioniert bereits, läßt aber
ein Problem offen: Die Klasse PathIndependent arbeitet nun mit Payoff 1(siehe (??)) und
geht von dessen Konstanz aus, was aber nicht der Fall sein muß. Von außen könnte Payoff 1
modifiziert oder gar gelöscht werden ohne dass die Klasse PathIndependent dies bemerkt!
Deshalb ist es zweckmäßig, dass sich PathIndependent eine eigene Kopie des Payoff 1-
Objekts anlegt und dazu benötigen wir ein neues Entwurfsmuster, den virtual copy construc-
tor.

9.3.2.1. Virtual copy constructor


Das Problem ist nämlich, dass die Klasse PathIndependent gar keine Kopie anlegen kann,
weil sie den genauen Typ des Payoff-Objekts nicht kennt: Sie verfügt lediglich über einen Ba-
sisklassenpointer (hier eine Referenz) auf eine Instanz irgendeiner von der Basisklasse Payoff
abgeleiteten Klasse! Unser neues Entwurfsmuster löst das Problem dadurch, dass zumindest
das Objekt selbst seinen Typ kennt und man es daher bitten kann, eine Kopie von sich selbst
zu erzeugen. Das sieht dann etwa so aus:

In der Basisklasse Payoff erstellen wir eine öffentliche, rein virtuelle Funktion clone:
virtual PayOff Clone() const = 0;
In jeder abgeleiteten Klasse überschreibt man diese Funktion nun durch
PayOff* PayOffCall::Clone() const{
return new PayOffCall(*this);
}
Man beachte, dass mit PayOffCall(*this) implizit ein copy-constructor erzeugt wird, was
an dieser Stelle aber unproblematisch ist, weil PayOffCall keine Zeiger oder Konstanten
verwaltet.
Dieser Punkt ist gelöst, doch haben wir jetzt das Problem, dass die Klasse PathIndependent
selbst einen Pointer verwaltet, was irgendwann zu ungewollten Effekten führen kann, weil
C++ bei der Objektzuweisung nur den Zeiger, nicht aber das damit referenzierte Objekt ko-
piert. Um das zu verhindern, müssen wir noch einen operator= schreiben, was uns auf eine
sehr wichtige praktische Faustregel führt, die nun vorgestellt wird.

9.3.2.2. rule-of-three
Die rule of three besagt, dass eine Klasse entweder keine oder alle der folgenden Funktionen
besitzt:
- Destruktor
- Zuweisungsoperator operator=()
- copy constructor
Es gibt natürlich Ausnahmen von dieser Regel, aber generell sollte man sich daran halten. Ein
typisches Beispiel ist eine Klasse, die dynamisch erzeugten Speicher über Pointer verwaltet.

182
9.3. Ein verbessertes Rahmenwerk

9.3.2.3. Die Klasse PathIndependent


Das fertige Programm für das Beispiel einer Payoffklasse für den Call und einer Option
PathIndependent sieht so aus:

class PayOff
{
public:

PayOff(){};

virtual double operator()(double spot) const=0;


virtual ~PayOff(){}
virtual PayOff* clone() const=0;

private:

};

class PayOffCall : public PayOff


{
public:

PayOffCall(double strike);

virtual double operator()(double spot) const;


virtual ~PayOffCall(){}
virtual PayOff* clone() const;

private:

double strike_;

};

Die Implementation ist nach dem Gesagten leicht und soll hier unterbleiben. Als nächstes
betrachten wir das Headerfile der Klasse PathIndependent:

class PathIndependent
{
public:

PathIndependent(const PayOff& thePayOff, double expiry);


PathIndependent(const PathIndependent& original);

183
9. Design Patterns und ein verbessertes Rahmenwerk

PathIndependent& operator=(const PathIndependent& original);


~PathIndependent();

double getExpiry() const;


double optionPayOff(double spot) const;

private:
double expiry_;
PayOff* thePayOffPtr_;
};

#endif
Die Implementation ist bis auf den operator= sehr naheliegend:
#include <PathIndependent.h>

PathIndependent::PathIndependent(const PayOff& thePayOff, double expiry)


: expiry_(expiry)
{
thePayOffPtr_ = ThePayOff_.clone();
}

double PathIndependent::getExpiry() const


{
return expiry_;
}

double PathIndependent::OptionPayOff(double spot) const


{
return (*thePayOffPtr_)(spot);
}

PathIndependent::PathIndependent(const PathIndependent& original)


{
expiry_ = original.getExpiry();
thePayOffPtr_ = original.thePayOffPtr->clone();
}

PathIndependent& PathIndependent::operator=(const PathIndependent& original)


{
if (this != &original){
Expiry = original.expiry_;
delete thePayOffPtr_;
thePayOffPtr_ = original.thePayOffPtr_->clone();

184
9.3. Ein verbessertes Rahmenwerk

}
return *this;
}

PathIndependent::~PathIndependent()
{
delete thePayOffPtr_;
}
Eine Besonderheit beim operator= ist, dass eine Zuweigung der Art a = a abgefangen wer-
den muß, weil sie das Programm zum Absturz bringen würde. Anschließend passiert das Übli-
che: Kopiere die Variable expiry, lösche die bisherige eigene Payoff-Instanz und erzeuge eine
neue mit Hilfe des Objekts auf der rechten Seite des Gleichheitszeichens.

9.3.2.4. Anwendung und Hauptprogramm


Unsere Funktion SimpleMonteCarlo hat nun folgende Parameter:
double SimpleMonteCarlo3(const PathIndependent& theOption,
double spot,
double vol,
double r,
unsigned long numberOfPaths){
//...
}
In der Funktion ergeben sich einige einfache Änderungen. Zu Beginn wird die Laufzeit abge-
fragt
double expiry = theOption.getExpiry();
und in der Schleife erzeugt man die Realisierung des Payoffs mit
double thisPayOff = theOption.optionPayOff(thisSpot);
Im Hauptprogramm fragt man nun wie üblich alle Daten vom Benutzer ab und erzeugt zunächst
das passende Payoff-Objekt und eine Instanz von PathIndependent.
int main(){

// frage Laufzeit, Strike, Spot, Zins, Vol, Zahl der Pfade ab

PayOffCall thePayoff(strike);
PathIndependent option(thePayoff, expiry);

double result = SimpleMonteCarlo3(option, spot,


vol, r, numberOfPaths);

185
9. Design Patterns und ein verbessertes Rahmenwerk

// Ausgabe
}

Im Ergebnis haben wir nun folgende Struktur realisiert: Die Klasse PathIndependent kann

Abbildung 9.3.: Verbessertes Zusammenspiel mit virtual copy constructor

nun also mit beliebigen pfadunabhängigen Payoffs arbeiten, die wir vorher erstellt haben.
Vorhandener Code kann somit verwendet werden.

9.3.2.5. Das Bridge-Pattern


Obwohl dieses Ergebnis bereits praktisch verwendbar ist, gibt es hinsichtlich der Wieder-
verwendbarkeit von Code noch ein Problem: Wenn wir wieder eine Klasse schreiben, die
einen Pointer oder eine Referenz verwaltet, dann müssen wir wieder gemäß der rule of three
einen copy-constructor, einen Destruktor und einen operator= schreiben. Dies wäre nicht nur
langweilig, sondern auch im Sinne der Wiederverwendbarkeit von Code nicht optimal. Im fol-
genden schreiben wir also eine wrapper-Klasse PayoffBridge, die die Speicherverwaltung
für uns übernimmt, so dass sich die Klasse PathIndependent wieder so verhält, als hätte sie

186
9.3. Ein verbessertes Rahmenwerk

Abbildung 9.4.: Schema zum Entwurfsmuster der Brücke für den Payoff

nur gewöhnliche automatische private-Variablen. Entsprechend benötigt PathIndependent


auch keine besondere Beachtung im Sinne der rule-of-three mehr. Dieses Entwurfsmuster be-
zeichnet man auch als die Brücke: Zwei Bauteile des Programms arbeiten nicht optimal zu-
sammen, deswegen schalten wir ein drittes Bauteil dazwischen, eben die Brücke. In Abbildung
9.4 sieht das etwa so aus: Die Implementation einer solchen wrapper-Klasse ist leicht, denn
im wesentlichen handelt es sich um die Klasse PathIndependent ohne die Member-Variable
expiry. Hier das headerfile:
class PayOffBridge
{
public:

PayOffBridge(const PayOffBridge& original);


PayOffBridge(const PayOff& innerPayOff);

inline double operator()(double spot) const;


~PayOffBridge();
PayOffBridge& operator=(const PayOffBridge& original);

187
9. Design Patterns und ein verbessertes Rahmenwerk

private:
PayOff* thePayOffPtr_;
};

inline double PayOffBridge::operator()(double spot) const


{
return thePayOffPtr_->operator ()(spot);
}

Und hier die Implementation:

#include<PayOffBridge.h>

PayOffBridge::PayOffBridge(const PayOffBridge& original)


{
thePayOffPtr_ = original.thePayOffPtr_->clone();
}

PayOffBridge::PayOffBridge(const PayOff& innerPayOff)


{
thePayOffPtr_ = innerPayOff.clone();
}

PayOffBridge::~PayOffBridge()
{
delete thePayOffPtr_;
}

PayOffBridge& PayOffBridge::operator=(const PayOffBridge& original)


{
if (this != &original)
{
delete thePayOffPtr_;
thePayOffPtr_ = original.thePayOffPtr_->clone();
}

return *this;
}

Die neue Klasse PathIndependent ist ebenfalls naheliegend, wegen einer kleinen Besonder-
heit möchten wir aber noch darauf eingehen:

#include <PayOffBridge.h>

188
9.3. Ein verbessertes Rahmenwerk

class PathIndependent
{
public:

PathIndependent(const PayOffBridge& thePayOff, double expiry);

double optionPayOff(double Spot) const;


double getExpiry() const;

private:
double expiry_;
PayOffBridge thePayOff_;
};

// nun das cpp-file

PathIndependent::PathIndependent(const PayOffBridge& thePayOff, double expiry)


: thePayOff_(thePayOff), expiry_(expiry)
{
}

double PathIndependent::getExpiry() const


{
return expiry_;
}

double PathIndependent::optionPayOff(double spot) const


{
return thePayOff_(spot);
}

Sehr angenehm ist nun, dass man in der Anwendung SimpleMonteCarlo die Zeilen

PayOffCall payoff(strike);
PathIndependent theOption(payoff, expiry);

nicht ändern muß, obwohl der Konstruktor von PathIndependent eine Referenz vom Typ
PayOffBridge und nicht PayOffCall erwartet: Der Konstruktor der Klasse PayOffBridge
erwartet nämlich ein Objekt vom Typ PayOff, also ist auch ein Objekt einer davon abge-
leiteten Klasse in Ordnung. Der Konstruktor erzeugt nun ein temporäres Objekt vom Typ
PayOffBridge und liefert es an den Konstruktor der Klasse PathIndependent. Dieser ar-
beitet dann völlig normal weiter. Zur Illustration empfiehlt es sich, diese Schritte im Debugger
nachzuvollziehen.

189
9. Design Patterns und ein verbessertes Rahmenwerk

9.3.2.6. Eine Warnung zum Schluß


Im Abschnitt über effiziente Implementation haben wir gesagt, dass der Operator new sehr
langsam ist, weil er intern mit dem Betriebssystem verhandelt. Deswegen sollte man ihn nicht
zu oft und schon gar nicht innerhalb von zeitkritischen Schleifen verwenden. Hier haben wir
nun ein Beispiel für das implizite Auftreten von new: Immer wenn eine Klasse kopiert wird,
die das bridge-pattern nutzt, wird implizit auch der Operator new aufgerufen. Entsprechend
sollte man die Instanzen der Klasse PathIndependent nicht unnötig kopieren.

9.3.3. Eine Parameterklasse


Im folgenden möchten wir das Programm auf zeitlich variable Parameter vorbereiten. Ziel
ist wie immer, die Klassen so zu konstruieren, dass man bei Erweiterung von z.B. konstan-
ten Volatilitäten zu zeitlich variablen nur zusätzlichen Code schreiben und bestehenden Code
nicht verändern muß. Gute Kandidaten für solche Erweiterungen sind Zinssätze, Volatilitäten
oder auch Sprungintensitäten. Normalerweise benötigt man nicht die Parameter selbst, son-
dern Ausdrücke der Form Z t2
param(t)dt
t1

oder Z t2
param2 (t)dt
t1

was das Interface unserer Klasse festlegt. Zur internen Beschreibung der Parameter könnte
man Konstanten, stückweise konstante Funktionen oder geeignete Polynome verwenden, wo-
bei wir der Einfachheit halber lediglich eine Klasse für einen konstanten Parameter schreiben.
Bei komplexeren Darstellungen könnte das Speichermanagement Schwierigkeiten bereiten,
wenn man die Klasse als Parameter an andere Klassen als Referenz weiterrecht. Deswegen
verwenden wir von Anfang an das Bridge-Design. Man betrachte nun Abbildung (9.5).
Die Klasse ParametersInner ist eine rein virtuelle Klasse, die das Interface der abgelei-
teten Klassen festlegen soll. Die Klasse Parameters ist eine wrapper-Klasse und übernimmt
das Speichermanagement, d.h. die Umsetzung der rule of three. Die Klasse Application ar-
beitet mit den Daten und muß sich über Speicherverwaltung keine Gedanken machen. Dies
kann nun so geschickt implementiert werden, dass man im späteren Hauptprogramm die Exi-
stenz dieser wrapper-Klasse überhaupt nicht bemerkt! Das headerfile und die Implementati-
on der Klassen sind selbsterklärend und mögen an dieser Stelle folgen. Zunächst die Datei
Parameters.h:

class ParametersInner{

public:
ParametersInner(){}

virtual ParametersInner* clone() const=0;


virtual double integral(double time1, double time2) const=0;

190
9.3. Ein verbessertes Rahmenwerk

Abbildung 9.5.: Klassenentwurf für im Zeitablauf variable Parameter

191
9. Design Patterns und ein verbessertes Rahmenwerk

virtual double integralSquare(double time1, double time2) const=0;

private:
};

class Parameters{
public:

Parameters(const ParametersInner& innerObject);


Parameters(const Parameters& original);
Parameters& operator=(const Parameters& original);
virtual ~Parameters();

inline double integral(double time1, double time2) const;


inline double integralSquare(double time1, double time2) const;

private:
ParametersInner* innerObjectPtr_;
};

inline double Parameters::integral(double time1, double time2) const


{
return innerObjectPtr_->integral(time1,time2);
}

inline double Parameters::integralSquare(double time1,


double time2) const
{
return innerObjectPtr_->integralSquare(time1,time2);
}

class ParametersConstant : public ParametersInner{


public:
ParametersConstant(double constant);

virtual ParametersInner* clone() const;


virtual double integral(double time1, double time2) const;
virtual double integralSquare(double time1, double time2) const;

private:
double constant_;
double constantSquare_;

192
9.3. Ein verbessertes Rahmenwerk

};
Und hier die Datei Parameters.cpp:
#include <Parameters.h>

Parameters::Parameters(const ParametersInner& innerObject)


{
innerObjectPtr_ = innerObject.clone();
}

Parameters::Parameters(const Parameters& original)


{
innerObjectPtr_ = original.innerObjectPtr_->clone();
}

Parameters& Parameters::operator=(const Parameters& original)


{
if (this != &original){
delete innerObjectPtr_;
innerObjectPtr_ = original.innerObjectPtr_->clone();
}
return *this;
}

Parameters::~Parameters(){
delete innerObjectPtr_;
}

double Parameters::mean(double time1, double time2) const


{
double total = integral(time1,time2);
return total/(time2-time1);
}

double Parameters::rootMeanSquare(double time1, double time2) const


{
double total = integralSquare(time1,time2);
return total/(time2-time1);
}

ParametersConstant::ParametersConstant(double constant)
{
constant_ = constant;
constantSquare_ = constant*constant;

193
9. Design Patterns und ein verbessertes Rahmenwerk

ParametersInner* ParametersConstant::clone() const


{
return new ParametersConstant(*this);
}

double ParametersConstant::integral(double time1, double time2) const


{
return (time2-time1)*constant;
}

double ParametersConstant::integralSquare(double time1, double time2) const


{
return (time2-time1)*constantSquare_;
}

Die Deklaration der Anwendung SimpleMonteCarlo sieht nun so aus:

double SimpleMonteCarlo4(const VanillaOption& theOption,


double spot,
const Parameters& vol,
const Parameters& r,
unsigned long numberOfPaths);

Die Änderungen in der Implementation sind offensichtlich, wobei in dieser Funktion ein Auf-
ruf wie

double variance = vol.integralSquare(0,expiry);

den zusätzlichen Vorteil einer automatischen Dokumentation mit sich bringt. Im Hauptpro-
gramm wird man die Existenz einer wrapper-Klasse nicht bemerken, von der Tatsache abge-
sehen, dass sie in der Deklaration von SimpleMonteCarlo steht. Wir wollen das Programm
nur andeuten:

int main(){

// Abfrage der Daten

ParametersConstant volParam(vol);
ParametersConstant rParam(r);

double result = SimpleMonteCarlo4(theOption,


spot,
volParam,

194
9.3. Ein verbessertes Rahmenwerk

rParam,
numberOfPaths);
// Ausgabe des Ergebnisses
}
Wie bereits an anderer Stelle gesagt erkennt der Compiler, dass die wrapper-Klasse einen
Konstruktor besitzt, der ein Argument vom Typ ParamtersConstant akzeptiert (genauer: ein
Argument vom Typ der zugehörigen Basisklasse). Dieser Konstruktor wird aufgerufen, ein
Objekt vom Typ Parameters wird im Speicher angelegt, und dann als Referenz an die Funk-
tion SimpleMonteCarlo4 übergeben.

9.3.3.1. Eine Template-Version der wrapper-Klasse


Wie die obigen Beispiele zeigen, wird man in der Praxis sehr häufig das Bridge-Pattern benöti-
gen. Dazu mussten wir bisher eine stets gleich aufgebaute wrapper-Klasse schreiben, was
wieder einmal den Prinzipien wiederverwendbaren Codes zuwiderläuft. Im folgenden geben
wir ein Template an für eine solche wrapper-Klasse, die sich nach außen hin wie ein Poin-
ter verhält, tatsächlich aber die Speicherverwaltung im Sinne der rule-of-three implementiert.
Von einer solchen Klasse würde man sich etwa wünschen, dass
*wrapperObjekt
das Objekt zurückliefert, das im wrapper verwaltet wird, und mit
(*wrapperObjekt).funktion();
könnte man jede beliebige Funktion aufrufen. Im folgenden Beispiel wird dies eleganter gelöst,
indem man nicht nur den operator* als const und nicht-const-Versionen schreibt, sondern
analog auch den operator->. Dies erlaubt den natürlichen Aufruf von Funktionen gemäß
wrapperObjekt->funktion();
Es ist wichtig sowohl eine const als auch eine nicht-const Version zu schreiben, damit man
die jeweilige Operation nach Bedarf sowohl auf ein const- als auch auf ein gewöhnliches
Objekt anwenden kann. Ansonsten gelten die üblichen Vor- wie Nachteile für Templates, der
Vorteil der Geschwindigkeit (hier insbesondere durch inline-Funktionen) ist abzuwägen gegen
eine größere ausführbare Datei.
template<class T> class Wrapper
{
public:

Wrapper()
{ dataPtr_ =0;}

Wrapper(const T& inner)


{

195
9. Design Patterns und ein verbessertes Rahmenwerk

dataPtr_ = inner.clone();
}

~Wrapper()
{
if (dataPtr_ !=0)
delete dataPtr_;
}

Wrapper(const Wrapper<T>& original)


{
if (original.dataPtr_ !=0)
dataPtr_ = original.dataPtr_->clone();
else
dataPtr_=0;
}

Wrapper& operator=(const Wrapper<T>& original)


{
if (this != &original)
{
if (dataPtr_!=0)
delete dataPtr_;

dataPtr_ = (original.dataPtr_ !=0) ? original.dataPtr_->clone() : 0;


}

return *this;
}

T& operator*()
{
return *dataPtr_;
}

const T& operator*() const


{
return *dataPtr_;
}

const T* const operator->() const


{
return dataPtr_;

196
9.3. Ein verbessertes Rahmenwerk

T* operator->()
{
return dataPtr_;
}

private:
T* dataPtr_;

};

Eine Anwendung sehen wir in Abschnitt (9.3.4.2).

9.3.4. Statistische Auswertung der Daten


Bis jetzt ist die statistische Analyse der Monte Carlo-Simulation sehr einfach: Die Funktion
SimpleMonteCarlo berechnet den arithmetischen Mittelwert, der anschließend im Haupt-
programm ausgegeben wird. Dies möchten wir flexibler gestalten, was uns auf ein bereits
angesprochenes wichtiges Entwurfsmuster führt.

9.3.4.1. Das strategy-pattern


Die Auslagerung von Teilen eines Algorithmus in eine neue Klasse heißt strategy-pattern. In
diesem Fall möchten wir die Bestimmung von Statistiken auslagern, wobei wir zum Zeitpunkt
des Klassenentwurfs noch nicht absehen können, welche Statistiken wir einmal benötigen
werden. Deshalb beginnen wir gemäß dem Templatization-pattern mit der Definition einer
abstrakten Basisklasse, die die Benutzerschnittstelle aller späteren Statistik-Klassen festlegt:

#include <vector>

class Statistics
{
public:

Statistics(){}

virtual void dumpOneResult(double result)=0;


virtual std::vector<std::vector<double> > getResultsSoFar() const=0;
virtual Statistics* clone() const=0;
virtual ~Statistics(){}

};

197
9. Design Patterns und ein verbessertes Rahmenwerk

Wichtige Funktionen sind dumpOneResult zur Übergabe eines Simultionsergebnisses und


getResultsSoFar, dessen Rückgabewert mit Hilfe der Standard Template Library als Tabelle
realisiert wird. Dies wird wichtig werden, wenn wir das Konvergenzverhalten in Abhängigkeit
von der Anzahl der Simulationen darstellen möchten, aber darauf gehen wir später ein. Ferner
ist eine Funktion clone für den virtual copy constructor vorgesehen, den wir später brauchen
werden. Von dieser abstrakten Basisklasse können wir diverse Statistik-Klassen ableiten, etwa
eine Klasse Mean oder Variance. Dies implementieren wir so:

class StatisticsMean : public Statistics


{
public:

StatisticsMean();
virtual void dumpOneResult(double result);
virtual std::vector<std::vector<double> > getResultsSoFar() const;
virtual Statistics* clone() const;

private:

double runningSum_;
unsigned long pathsDone_;
};

Und das cpp-file:


#include<Statistics.h>
using namespace std;

StatisticsMean::StatisticsMean()
:
runningSum_(0.0), pathsDone_(0)
{
}

void StatisticsMean::dumpOneResult(double result)


{
pathsDone_++;
runningSum_ += result;
}

vector<vector<double> > StatisticsMean::getResultsSoFar() const


{
vector<vector<double> > results(1);

results[0].resize(1);

198
9.3. Ein verbessertes Rahmenwerk

results[0][0] = runningSum_ / pathsDone_;

return results;
}

Statistics* StatisticsMean::clone() const


{
return new StatisticsMean(*this);
}

Die Anwendung in SimpleMonteCarlo ist trivial, wir geben daher nur die neue Deklaration
an:

void SimpleMonteCarlo5(const VanillaOption& TheOption,


double Spot,
const Parameters& Vol,
const Parameters& r,
unsigned long NumberOfPaths,
Statistics& gatherer);

Im Hauptprogramm ergeben sich zwei Neuerungen:

int main(){
// Abfrage der Daten
// ...
StatisticsMean statistics;
SimpleMonteCarlo5( TheOption,
spot,
vol,
r,
numberOfPaths,
statistics );

vector<vector<double> > results = gatherer.getResultsSoFar();

printf("\nFor the call price the results are \n");

for (unsigned long i=0; i < results.size(); i++)


{
for (unsigned long j=0; j < results[i].size(); j++)
printf("%lf ", results[i][j]);
printf("\n");
}

199
9. Design Patterns und ein verbessertes Rahmenwerk

Man beachte bereits an dieser Stelle, dass die Funktion getResultsSoFar indirekt über die
STL den Operator new aufruft und entsprechend langsam ist, was aber nicht wichtig ist,
weil die Funktion nur selten aufgerufen wird. Auf der anderen Seite wird man die Funktion
dumpOneResult sehr häufig aufrufen, deshalb ist Effizienz an dieser Stelle sehr viel wichtiger.

9.3.4.2. Das decorator-pattern


Unter dem decorator-pattern versteht man das Schachteln einer bestehenden Klasse durch
eine andere Klasse, die das gleiche Interface bereitstellt, die alte Klasse aber um zusätzliche
Funktionalitäten erweitert.
Dieses Entwurfsmuster möchten wir nun anwenden, um die zeitliche Entwicklung einer Sta-
tistik darzustellen. Genauer möchten wir nach jeder 2n -ten Simulation den aktuellen Wert der
Statistik abfragen und in einer Tabelle abspeichern, die später im Hauptprogramm ausgege-
ben wird. Der besondere Reiz der folgenden Implementation liegt darin, dass sie ohne wei-
tere Anpassungen für jede Statistik-Klasse angewendet werden kann, die von der Basisklasse
StatisticsMC abgeleitet ist. Zunächst eine Übersicht in Abbildung 9.6.

Abbildung 9.6.: Schema zum StatisticsGatherer

Von der bereits bestehenden Klasse Statistics wird eine neue Klasse StatisticsGatherer
bzw. konkret in unserem Beispiel ConvergenceTable abgeleitet, die das gleiche Interface
bereitstellt und intern eine Instanz etwa von der Klasse Mean oder Variance verwaltet. Die
Implementation ist nicht schwierig:

class ConvergenceTable : public Statistics


{
public:

200
9.3. Ein verbessertes Rahmenwerk

ConvergenceTable(const Wrapper<Statistics>& inner);


virtual Statistics* clone() const;
virtual void dumpOneResult(double result);
virtual std::vector<std::vector<double> > getResultsSoFar() const;

private:

Wrapper<Statistics> inner_;
std::vector<std::vector<double> > resultsSoFar_;
unsigned long stoppingPoint_;
unsigned long pathsDone_;
};

Die Klasse ConvergenceTable reicht in der Funktion dumpOneResult lediglich den Wert an
die Statistikklasse inner weiter und erhöht pathsDone um eins. Wenn diese Variable eine
gewisse Grenze stoppingPoint erreicht, wird das bisherige Ergebnis von inner abgefragt
und in resultsSoFar gespeichert. Hier das Programm:

ConvergenceTable::ConvergenceTable(const Wrapper<Statistics>& inner)


: inner_(inner){
stoppingPoint_=2;
pathsDone_=0;
}

Statistics* ConvergenceTable::clone() const{


return new ConvergenceTable(*this);
}

void ConvergenceTable::dumpOneResult(double result){


inner_->dumpOneResult(result);
++pathsDone_;

if (pathsDone_ == stoppingPoint_){
stoppingPoint_ *= 2;
std::vector<std::vector<double> >
thisResult(inner_->getResultsSoFar());

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


thisResult[i].push_back(pathsDone_);
resultsSoFar_.push_back(thisResult[i]);
}
}
}

201
9. Design Patterns und ein verbessertes Rahmenwerk

std::vector<std::vector<double> >
ConvergenceTable::getResultsSoFar() const
{
std::vector<std::vector<double> > tmp(resultsSoFar_);

if (pathsDone_*2 != stoppingPoint_){
std::vector<std::vector<double> >
thisResult(inner_->getResultsSoFar());

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


thisResult[i].push_back(pathsDone_);
tmp.push_back(thisResult[i]);
}
}
return tmp;
}
Wie bereits bemerkt muß man sich den impliziten Aufruf von new in dumpOneResult gut
überlegen, weil sie sehr häufig aufgerufen wird. Hier ist das wegen des seltenen Eintretens der
if-Bedingung aber unproblematisch. Die Anwendung der neuen Klassen im Hauptprogramm
ist sehr einfach, die Änderungen sind lediglich
StatisticsMean gatherer;
ConvergenceTable gathererTwo(gatherer);

SimpleMonteCarlo5(theOption, Spot, VolParam,


rParam, NumberOfPaths, gathererTwo);
Das Objekt gatherer wird vom Konstruktor akzeptiert, weil es implizit in ein Objekt der zu-
gehörigen Wrapper-Klasse konvertiert wird. Die Funktion SimpleMonteCarlo muß gar nicht
umgeschrieben werden, weil ConvergenceTable von der Klasse Statistics abgeleitet wur-
de und im Interface nichts verändert wurde.

9.3.4.3. Bemerkung zum Decorator- und Strategy-pattern


Wir haben nun zwei weitere sehr nützliche Entwurfsmuster kennengelernt:

i) das Strategy-Pattern zur Auslagerung von Teilen eines Algorithmus

ii) das Decorator-Pattern zum Hinzufügen neuer Funktionalitäten zu einer Klasse, ohne
dass der Anwender der dekorierten Klasse den Unterschied bemerkt.

Für diese beiden Muster gibt es noch viele weitere Anwendungsmöglichkeiten: Das Strategy-
Pattern könnte man nutzen, um eine Klasse Terminate zu schreiben, die das Ende der for-
Schleife in SimpleMonteCarlo angibt, das Decorator-Pattern könnte man zur Realisierung
von Antithetic-Sampling verwenden (was wir gleich tun werden). Bemerkenswert ist, dass

202
9.3. Ein verbessertes Rahmenwerk

man eine dekorierte Klasse immer wieder dekorieren kann: Man könnte z.B. eine Klasse
StatisticsCollector schreiben, die ein array von Objekten vom Typ ConvergenceTable
besitzt und mehrere Statistiken sehr effizient verwaltet.

9.3.5. Implementation von Zufallsgeneratoren


Die wichtigsten Ideen wurden bereits vorgestellt, daher möchten wir uns hier kurz fassen.
Warum macht es Sinn, den Zufallsgenerator - also im einfachsten Fall den Aufruf der Funk-
tion rand() - in eine eigene Klasse zu packen? Einige Punkte wurden bereits in vorangehen-
den Kapiteln angesprochen, so z.B. die zweifelhafte Qualität des Standardgenerators rand().
Wichtig ist dabei aber auch, dass dieser Zufallsgenerator implementationsabhängig ist und
somit das Testen auf verschiedenen Maschinen erschwert. Ein weiterer Punkt ist, dass rand
nur eine einzige globale Variable seed verwaltet, so dass sich unterschiedliche Programmtei-
le gegenseitig beeinflussen können, wenn sie abwechselnd auf rand zugreifen. Mit anderen
Worten: Die Reproduzierbarkeit der Zufallszahlen ist bei Aufruf von rand nicht unbedingt
garantiert.

9.3.5.1. Eine Basisklasse RandomBase


Eine Klasse für einen Zufallsgenerator sollte Funktionen besitzen, die der geforderten Dimen-
sion entsprechend Zufallszahlen einer gewünschten Verteilung liefern, wobei wir uns auf die
U [0, 1]- und N(0, 1)-Verteilung beschränken möchten. Wie man diese Zahlen erzeugt wur-
de bereits besprochen, deswegen sollen diese Programmteile hier nicht weiter kommentiert
werden. Zunächst die Deklaration einer geeigneten Basisklasse:

#include <Arrays.h>

class RandomBase
{
public:

RandomBase(unsigned long dimensionality);

inline unsigned long getDimensionality() const;

virtual RandomBase* clone() const=0;


virtual void getUniforms(MJArray& variates)=0;
virtual void skip(unsigned long numberOfPaths)=0;
virtual void setSeed(unsigned long seed) =0;
virtual void reset()=0;

virtual void getGaussians(MJArray& variates);


virtual void resetDimensionality(unsigned long dimensionality);

203
9. Design Patterns und ein verbessertes Rahmenwerk

private:
unsigned long dimensionality_;

};
Im allgemeinen sollte man die Arbeit mit bloßen Zeigern vermeiden, weil sie schwer zu
handhaben und fehleranfällig sind. Deswegen wird hier eine Klasse MJArray verwendet,
auf die wir an dieser Stelle nicht weiter eingehen möchten (siehe Anhang). Wichtig ist, dass
MJArray implizit den Operator new verwendet, so dass man mit der Definition neuer Objekte
sparsam umgehen muß. Insbesondere deswegen erwarten die Funktionen getUniforms und
getGaussians Zeiger auf bereits vordefinierte Felder, so dass diese in der Anwendung nur
einmal alloziiert werden müssen. Die Implementation ist naheliegend, wobei wir die Funktion
getGaussians und somit das Verfahren, wie aus uniform verteilten Zufallsvariablen normal-
verteilte gewonnen werden, bereits in der Basisklasse definieren. Dabei wird eine Funktion
inverseCumulativeNormal benötigt, die wir unter anderem Namen bereits benutzt haben.
Außerdem deklarieren wir eine Funktion skip, die das Auslassen von Simulationsschritten
erlaubt, um etwa ein Verfahren an gewissen Zufallszahlen zu kalibrieren und an anderen zu
testen.

unsigned long RandomBase::getDimensionality() const{


return dimensionality_;
}

void RandomBase::getGaussians(MJArray& variates){


getUniforms(variates);

for (unsigned long i=0; i < dimensionality_; i++)


{
double x=variates[i];
variates[i] = inverseCumulativeNormal(x);
}
}

void RandomBase::resetDimensionality(long dimensionality){


dimensionality_ = dimensionality;
}

RandomBase::RandomBase(long dimensionality)
: dimensionality_(dimensionality){
}

Von dieser Klasse RandomBase kann man nun diverse Varianten ableiten, zweckmäßig wäre
etwa eine Klasse RandomMT, in der der Mersenne Twister- Algorithmus für die Erzeugung uni-
form verteilter Zufallszahlen verwendet wird. Dies wollen wir aber der interessierten Leserin
überlassen.

204
9.3. Ein verbessertes Rahmenwerk

9.3.5.2. Antithetic Sampling


Die Idee des Antithetic sampling wurde bereits vorgestellt. Nun möchten wir diese Technik
einmalig implementieren, so dass sie für beliebige vorhandene Zufallsgeneratoren, also Ablei-
tungen von RandomBase, verwendet werden kann. Dazu verwenden wir erneut das decorator-
pattern. Zunächst die Deklaration:

class AntiThetic : public RandomBase{

public:
AntiThetic(const Wrapper<RandomBase>& innerGenerator );
virtual RandomBase* clone() const;
virtual void getUniforms(MJArray& variates);
virtual void skip(unsigned long numberOfPaths);
virtual void setSeed(unsigned long seed);
virtual void resetDimensionality(unsigned long dimensionality);
virtual void reset();
private:
Wrapper<RandomBase> innerGenerator_;
bool oddEven_;
MJArray nextVariates_;
};

Die Klasse besitzt also das gleiche Interface wie RandomBase und verwaltet intern ein Zufallsgenerator-
Objekt. Dabei werden erzeugte Zufallszahlen ggf. zwischengespeichert und mit der Variablen
oddEven entschieden, ob neue Zufallszahlen erzeugt werden müssen oder die bisherigen ge-
eignet transformiert werden. Wichtig ist, dass diese Klasse sich durch Verwendung eines wrap-
pers eine eigene Kopie des Zufallsgenerators erzeugt, so dass der ursprünglich an die Klasse
Antithetic übergebene Zufallsgenerator unverändert bleibt. Insbesondere werden von dem
übergebenen Generator also keine Zufallszahlen erzeugt. Ein weiterer wichtiger Punkt ist, dass
im private-Abschnitt ein Feld nextVariates definiert wird, das die passende Größe be-
sitzt. Es dient den übrigen Funktionen als ’working space’, so dass man häufiges Anlegen und
Löschen von Feldern und den damit verbundenen Overhead vermeidet. Die Implementation
ist an einer Stelle nicht ganz offensichtlich, daher wollen wir sie hier anfügen:

AntiThetic::AntiThetic(const Wrapper<RandomBase>& innerGenerator )


: RandomBase(*innerGenerator),
innerGenerator_(innerGenerator)
{
innerGenerator_->Reset();
oddEven_ =true;
nextVariates_.resize(getDimensionality());
}

RandomBase* AntiThetic::clone() const

205
9. Design Patterns und ein verbessertes Rahmenwerk

{
return new AntiThetic(*this);
}

void AntiThetic::getUniforms(MJArray& variates)


{
if (oddEven_)
{
innerGenerator_->getUniforms(variates);
for (unsigned long i =0; i < getDimensionality(); i++)
nextVariates_[i] = 1.0-variates[i];
oddEven_ = false;
}
else
{
variates = NextVariates;
oddEven_ = true;
}
}

void AntiThetic::setSeed(unsigned long seed)


{
innerGenerator_->setSeed(seed);
oddEven_ = true;
}

void AntiThetic::skip(unsigned long numberOfPaths)


{
if (numberOfPaths ==0)
return;

if (oddEven_){
oddEven_ = false;
numberOfPaths--;
}

innerGenerator_->skip(numberOfPaths / 2);

if (numberOfPaths % 2){
MJArray tmp(getDimensionality());
getUniforms(tmp);
}
}

206
9.3. Ein verbessertes Rahmenwerk

void AntiThetic::resetDimensionality(unsigned long dimensionality){


RandomBase::resetDimensionality(dimensionality);
nextVariates_.resize(dimensionality);
innerGenerator_->resetDimensionality(dimensionality);
}

void AntiThetic::reset(){
innerGenerator_->reset();
oddEven_ =true;
}

Im Konstruktor liefert *innerGenerator das vom Wrapper geschachtelte Objekt zurück, das
vom Konstruktor von RandomBase als Objekt einer abgeleiteten Klasse von RandomBase er-
kannt und somit akzeptiert wird. Der Basisklassenkonstruktor sieht nur den für ihn relevanten
Teil der abgeleiteten Klasse und kann sich korrekt initialisieren.

9.3.6. Kurzer Rückblick


Die einzelnen Entwurfsmuster sind nun vorgestellt worden. Abschließend möchten wir die
optimierte“Funktion SimpleMonteCarlo samt Hauptprogramm nochmals aufführen. An-

schließend gehen wir dazu über, ein vollständiges Klassenrahmenwerk für die Praxis zu er-
stellen, das alle bisherigen Elemente benutzt.

void SimpleMonteCarlo6(const VanillaOption& theOption,


double spot,
const Parameters& vol,
const Parameters& r,
unsigned long numberOfPaths,
Statistics& gatherer,
RandomBase& generator){

generator.resetDimensionality(1);
double expiry = theOption.getExpiry();
double variance = vol.integralSquare(0,expiry);
double rootVariance = sqrt(variance);
double itoCorrection = -0.5*variance;
double movedSpot = spot*exp(r.integral(0,expiry) +itoCorrection);

double thisSpot;
double discounting = exp(-r.iIntegral(0,expiry));

MJArray variateArray(1);

for (unsigned long i=0; i < numberOfPaths; i++){

207
9. Design Patterns und ein verbessertes Rahmenwerk

generator.getGaussians(variateArray);
thisSpot = movedSpot*exp( rootVariance*variateArray[0]);
double thisPayOff = theOption.optionPayOff(thisSpot);
gatherer.dumpOneResult(thisPayOff*discounting);
}

return;
}

An dieser Stelle kann man darüber diskutieren, ob das Objekt generator als Referenz oder
als echte Kopie übergeben werden soll - je nach dem, ob man den Generator im Hauptpro-
gramm unverändert lassen möchte oder nicht. Auf jeden Fall darf man generator nicht als
const-Referenz übergeben, da man sonst nicht-const- Funktionen wie getGaussians nicht
aufrufen könnte. Nun das Hauptprogramm:

int main()
{
double expiry, strike, spot, vol, r;
unsigned long numberOfPaths;

cout << "\nEnter expiry\n"; cin >> expiry;


cout << "\nStrike\n"; cin >> strike;
cout << "\nEnter spot\n"; cin >> spot;
cout << "\nEnter vol\n"; cin >> vol;
cout << "\nr\n"; cin >> r;
cout << "\nNumber of paths\n"; cin >> numberOfPaths;

PayOffCall thePayOff(Strike);

VanillaOption theOption(thePayOff, expiry);

ParametersConstant VolParam(vol);
ParametersConstant rParam(r);

StatisticsMean gatherer;
ConvergenceTable gathererTwo(gatherer);

RandomMT generator(1);

AntiThetic GenTwo(generator);

SimpleMonteCarlo6(theOption, Spot, VolParam,


rParam, NumberOfPaths, gathererTwo,

208
9.4. Ein flexibler Monte-Carlo Option Pricer

GenTwo);

vector<vector<double> > results =


gathererTwo.GetResultsSoFar();

cout <<"\nFuer den Callpreis ergibt sich \n";

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

for (unsigned long j=0; j < results[i].size(); j++)


cout << results[i][j] << " ";

cout << "\n";


}
double tmp;
cin >> tmp;

return 0;
}

9.4. Ein flexibler Monte-Carlo Option Pricer


In diesem Abschnitt sollen nun alle vorgestellten Klassen in eine Klasse ExoticsPricer zu-
sammenfließen. Dabei wollen wir uns auf Optionen beschränken, die von endlich vielen Rea-
lisierungen (St1 , . . . , Stn ) des Underlying abhängen und einen Payoff an bekannten Zeitpunkten
t¯1 , . . . , t¯m liefern.

9.4.1. Aufgaben und Klassenstruktur


Die grundsätzlich zu erledigenden Aufgaben in unserer PricingEngine sind nun:

• erzeuge Pfadrealisierungen (St1 , . . . , Stn )

• erzeuge zugehörige Cashflows (CFt¯1 , . . . ,CFt¯m )

• diskontiere die Cashflows

• bestimme den theoretischen Wert der Option

Es ist nicht sinnvoll, dass die Klasse ExoticsPricer all diese Aufgaben selbst übernimmt
und deshalb werden wir die meisten Dinge auslagern: Für die Erzeugung der Cashflows bie-
tet sich die Klasse Option an, die wir dem Konstruktor von ExoticsPricer per Referenz
übergeben. Die Bestimmung des theoretischen Werts und alle weiteren denkbaren Auswer-
tungen übernimmt die Klasse StatisticsGatherer, die bereits existiert. Die Diskontierung

209
9. Design Patterns und ein verbessertes Rahmenwerk

der Cashflows ist zumindest bei deterministischen Zinssätzen immer gleich und es ist sinnvoll,
diese Funktion direkt im ExoticsPricer zu implementieren. Dafür benötigt man natürlich
Informationen über die Marktzinssätze, die ebenfalls an den Konstruktor übergeben werden
als Parameters-Klasse, so dass wir hinsichtlich der internen Repräsentation der Zinssätze
flexibel bleiben. Alle bisher erfolgten Auslagerungen von Operationen erfolgten im Sinne
des Strategy-Pattern, d.h. benötigte Funktionalitäten bzw. Teile eines Algorithmus werden als
Input-Objekt zur Verfügung gestellt. Genauso könnte man nun die Pfaderzeugung behandeln,
wir verwenden aber an dieser Stelle das Template-Pattern, d.h. wir deklarieren eine rein vir-
tuelle Funktion zur Pfaderzeugung und verlagern ihre Definition und somit die Wahl eines
konkreten Modells oder eines gewissen numerischen Verfahrens in eine abgeleitete Klasse
aus. Insbesondere benötigt die abgeleitete Klasse nun einen Zufallszahlengenerator, den wir
als Input-Objekt zur Verfügung stellen. Im folgenden verwenden wir eine Erweiterung des
Black/Scholes-Modells mit zeitvariablen aber deterministischen Zinssätzen r, Dividenden d
und Volatilitäten vol. Bevor wir Details besprechen soll die Abbildung 9.7 als Versuch dienen,
das Gesagte graphisch zu veranschaulichen:

Abbildung 9.7.: Übersicht zur PricingEngine

210
9.4. Ein flexibler Monte-Carlo Option Pricer

9.4.2. Kommunikation und Zusammenspiel der Klassen


Der grobe Ablauf ist klar:

Pfadgenerierung → (St1 , . . . , Stn ) → Option → (CFt¯1 , . . . ,CFtm¯ ) → Diskontierung → Statistics-


Gatherer → Ergebnis

Dazu benötigen die Klassen voneinander diverse Informationen, die zu Beginn in den Kon-
struktoren ausgetauscht werden. Als Leitmotiv kann dabei gelten, dass man nach Möglich-
keit workspace in Form von private- Variablen anlegt, um bei späteren Rechenoperationen
nicht durch häufiges dynamisches Anlegen von Speicher Zeit zu verlieren wegen der sehr
flexiblen Struktur des Optionspayoffs. Außerdem zieht sich das Motiv durch die gesamte Im-
plementation, immer wieder gebrauchte Resultate zwischenzuspeichern. Beginnen wir mit
einer Übersicht in Abbildung 9.8: In einem ersten Schritt liefert die Option mit einer Funk-

Abbildung 9.8.: Übersicht zum Zusammenspiel der Klassen

tion getLookAtTimes die Zeitpunkte (t1 , . . . ,tn ) an den Pfadgenerator, der diese Information
für die spätere Pfadsimulation benötigt. Außerdem liefert dieser die Anzahl der Zeitpunkte an
den Zufallsgenerator weiter, der hier nicht im Bild erscheint. Anschließend liefert die Option

211
9. Design Patterns und ein verbessertes Rahmenwerk

mit den Funktionen possibleCashFlowTimes und maxNumberOfCashFlows Informationen


an den Diskontierer, der nun einmalig ein Array für die erzeugten Cashflows erzeugt (Ef-
fizienzsteigerung!) und einmalig die für diese Zeiten benötigten Diskontfaktoren berechnet
und speichert. Zu beachten ist dabei, dass der Diskontierer in der Basisklasse ExoticEngine
und der Pfadgenerator in der abgeleiteten Klasse ExoticBlackScholesEngine implemen-
tiert werden. Die übrigen Dinge werden zusammen mit dem Programmcode vorgestellt.

9.4.3. Implementation
Bis jetzt haben wir das Aussehen der Cashflow-Objekte noch nicht besprochen. Hierfür gibt
es wieder diverse Möglichkeiten und aus Effizienzgründen entscheiden wir uns für ein Array
von Objekten, die je zwei Elemente enthalten: den Zahlungsbetrag selbst und einen Zeitindex.
Dieser Zeitindex soll es dem Diskontierer erlauben, möglichst leicht den benötigten Diskont-
faktor in seiner Liste nachschlagen zu können. Hier der Code:
class CashFlow
{
public:
double amount;
unsigned long timeIndex;

cashFlow(unsigned long timeIndex_=0, double amount_=0.0)


: timeIndex(timeIndex_),
amount(amount_){};
};
Ein weiterer noch offener Punkt ist die Darstellung der Klasse Option. Wir verwenden das
gleiche Konzept wie bisher, d.h. ein fertiges Payoff-Objekt wird per Bridge-Pattern in eine
Optionsklasse eingebettet, aber zu den bisherigen Funktionen kommen einige neue hinzu:

class PathDependent
{
public:

PathDependent(const MJArray& lookAtTimes);

const MJArray& getlookAtTimes() const;

virtual unsigned long maxNumberOfCashFlows() const=0;


virtual MJArray possibleCashFlowTimes() const=0;
virtual unsigned long cashFlows(const MJArray& spotValues,
std::vector<CashFlow>& generatedFlows) const=0;
virtual PathDependent* clone() const=0;

virtual ~PathDependent(){}

212
9.4. Ein flexibler Monte-Carlo Option Pricer

private:

MJArray lookAtTimes_;

};

PathDependent::PathDependent(const MJArray& lookAtTimes)


: lookAtTimes(lookAtTimes)
{}

const MJArray& PathDependent::getlookAtTimes() const


{
return lookAtTimes_;
}
Die Implementation ist bis hierhin sehr einfach. Später werden wir den Preis einer asiatischen
Option mit dem Payoff ! +
12
1
12 ∑ St j − K
j=1
berechnen und wir möchten an dieser Stelle etwas vorgreifen und den Code für diese Option
bereits angeben:
class PathDependentAsian : public PathDependent
{
public:

PathDependentAsian(const MJArray& lookAtTimes,


double deliveryTime,
const PayOffBridge& thePayOff);

virtual unsigned long maxNumberOfcashFlows() const;


virtual MJArray possibleCashFlowTimes() const;
virtual unsigned long cashFlows(const MJArray& spotValues,
std::vector<CashFlow>& generatedFlows) const;
virtual ~PathDependentAsian(){}
virtual PathDependent* clone() const;

private:

double deliveryTime_;
PayOffBridge thePayOff_;
unsigned long numberOfTimes_;
};

213
9. Design Patterns und ein verbessertes Rahmenwerk

Offensichtlich werden die bisher rein virtuellen Funktionen ersetzt. Die auffälligste Neuerung
ist hier die Variable deliveryTime , die es erlaubt, dass die Zahlung nicht notwendig am
letzten für den Underlying relevanten Zeitpunkt tn erfolgt. Die Option liefert genau einen
Payoff zum Zeitpunkt deliveryTime, entsprechend ist die Implementation naheliegend:

PathDependentAsian::PathDependentAsian(const MJArray& lookAtTimes,


double deliveryTime,
const PayOffBridge& thePayOff)
:
PathDependent(lookAtTimes),
deliveryTime_(deliveryTime),
thePayOff_(thePayOff),
numberOfTimes_(lookAtTimes.size())
{
}

unsigned long PathDependentAsian::maxNumberOfcashFlows() const


{
return 1;
}

MJArray PathDependentAsian::possibleCashFlowTimes() const


{
MJArray tmp(1);
tmp[0] = deliveryTime_;
return tmp;
}

unsigned long PathDependentAsian::cashFlows(const MJArray& spotValues,


std::vector<CashFlow>& generatedFlows) const
{
double sum = spotValues_.sum();
double mean = sum/numberOfTimes_;

generatedFlows[0].timeIndex = 0UL;
generatedFlows[0].amount = thePayOff_(mean);

return 1UL;
}

PathDependent* PathDependentAsian::clone() const


{
return new PathDependentAsian(*this);
}

214
9.4. Ein flexibler Monte-Carlo Option Pricer

Nun folgt die Deklaration der Basisklasse ExoticEngine, die noch ein paar Kommentare
verdient:

class ExoticEngine
{
public:

ExoticEngine(const Wrapper<PathDependent>& theProduct,


const Parameters& r);

virtual void getOnePath(MJArray& spotValues)=0;

void doSimulation(StatisticsMC& theGatherer, unsigned long numberOfPaths);


virtual ~ExoticEngine(){}
double doOnePath(const MJArray& spotValues) const;

private:

Wrapper<PathDependent> theProduct_;
Parameters r_;
MJArray discounts_;
mutable std::vector<CashFlow> thesecashFlows_;
};

In der Basisklasse sind mehrere der in der Grafik gezeigten Elemente enthalten. Die hier noch
rein virtuelle Funktion GetOnePath wird in einer abgeleiteten Klasse definiert und stellt den
Pfadgenerator dar, entsprechend wird der vierte Zwischenschritt auch erst dort implementiert.
Die Funktion DoOnePath ist der Diskontierer, entsprechend findet man bei den private-
Variablen bereits die Klasse für die Zinssätze und den angesprochenen workspace Discounts
für die Zwischenspeicherung der Diskontfaktoren, sowie den workspace ThesecashFlows
für die effiziente Zwischenspeicherung des Payoff, den die ebenfalls hier gespeicherte Option
TheProduct liefert. Die Diskontierung von cashFlows ist eine das Objekt nicht verändernde,
rein funktionale und deshalb passende Tätigkeit für eine const-Funktion, entsprechend muß
man ihren workspace im private-Teil der Klasse so einrichten, dass sie diesen trotz ihres
const-Status verändern darf: genau das geschieht mit dem neuen Schlüsselwort mutable.
Man beachte, dass alle Funktionen das Kopieren von Arrays vermeiden und lediglich bereits
bestehende per Referenz erwarten: Hätte etwa die Funktion GetOnePath als Rückgabewert
MJArray, so wäre bei jedem Aufruf implizit die Anwendung von new nötig, was für die Lauf-
zeiteffizienz sehr problematisch wäre. Hier nun die Implementation:

ExoticEngine::ExoticEngine(const Wrapper<PathDependent>& theProduct,


const Parameters& r)
:
theProduct_(theProduct),

215
9. Design Patterns und ein verbessertes Rahmenwerk

r_(r),
discounts_(theProduct_->possibleCashFlowTimes())
{
for (unsigned long i=0; i < discounts_.size(); i++)
discounts_[i] = exp(-r.integral(0.0, discounts_[i]));

thesecashFlows.resize(theProduct->maxNumberOfcashFlows());
}

void ExoticEngine::doSimulation(Statistics& theGatherer,


unsigned long numberOfPaths)
{
MJArray spotValues(theProduct_->getlookAtTimes().size());

thesecashFlows.resize(theProduct_->maxNumberOfcashFlows());
double thisValue;

for (unsigned long i =0; i < numberOfPaths; ++i)


{
getOnePath(spotValues);
thisValue = doOnePath(spotValues);
theGatherer.dumpOneResult(thisValue);
}

return;
}

double ExoticEngine::doOnePath(const MJArray& spotValues) const


{
unsigned long numberFlows = theProduct_->cashFlows(spotValues,
thesecashFlows_);
double Value=0.0;

for (unsigned i =0; i < numberFlows; ++i)


Value += thesecashFlows_[i].amount *
discounts_[thesecashFlows_[i].timeIndex];

return Value;
}

Nun schauen wir uns die abgeleitete Klasse ExoticBSEngine an, in der im wesentlichen die
Implementation von getOnePath durchgeführt wird. Als Modell für den Underlying wählen
wir
dSt = (rt − dt )St dt + σt St dWt

216
9.4. Ein flexibler Monte-Carlo Option Pricer

so dass wir die Sti berechnen können gemäß


Z t0 rZ
1 t0
log(St0 ) = log(S0 ) + rs − ds − σs2ds + σs2 dsWt0
0 2 0

und sZ
Z tj tj
1
log(St j ) = log(St j−1 ) + rs − ds − σs2 ds + σs2dsWt j
t j−1 2 t j−1

Benötigt wird dabei ein Zufallszahlengenerator, den die abgeleitete Klasse als wrapper-Objekt
verwaltet.
class ExoticBSEngine : public ExoticEngine
{
public:

ExoticBSEngine(const Wrapper<PathDependent>& theProduct,


const Parameters& r,
const Parameters& d,
const Parameters& vol,
const Wrapper<RandomBase>& theGenerator,
double spot);

virtual void getOnePath(MJArray& spotValues);


virtual ~ExoticBSEngine(){}

private:

Wrapper<RandomBase> theGenerator_;
MJArray drifts_;
MJArray standardDeviations_;
double logSpot_;
unsigned long numberOfTimes_;
MJArray variates_;
};
In der Implementation ist bemerkenswert, dass diverse Größen wie die Inkremente der Drift
und der Volatilitäten bei jedem Pfad gleich sind und deshalb im Konstruktor einmal berechnet
und gespeichert werden. Ferner wird der Zufallszahlengenerator initialisiert und einmal ein
Feld variates erstellt, in dem die Zahlen zwischengespeichert werden können. Zur Berech-
nung der (St0 , . . . , Stn ) bietet sich die angegebene Darstellung an, weil dadurch die Anzahl der
Aufrufe von exp und log minimiert wird.

void ExoticBSEngine::getOnePath(MJArray& spotValues)


{

217
9. Design Patterns und ein verbessertes Rahmenwerk

theGenerator_->getGaussians(variates_);

double currentLogSpot = logSpot_;

for (unsigned long j=0; j < numberOfTimes_; j++)


{
currentLogSpot += drifts_[j];
currentLogSpot += standardDeviations_[j]*variates_[j];
spotValues[j] = exp(currentLogSpot);
}

return;
}

ExoticBSEngine::ExoticBSEngine(
const Wrapper<PathDependent>& theProduct,
const Parameters& r,
const Parameters& d,
const Parameters& vol,
onst Wrapper<RandomBase>& theGenerator,
double spot)
:
ExoticEngine(theProduct,r),
theGenerator_(theGenerator)
{
MJArray times(theProduct->getlookAtTimes());
NumberOfTimes = times.size();

theGenerator_->resetDimensionality(numberOfTimes_);

drifts_.resize(numberOfTimes_);
standardDeviations.resize(numberOfTimes_);

double variance = vol.integralSquare(0,times[0]);

drifts_[0] = r.integral(0.0,times[0]) -
d.integral(0.0,times[0]) - 0.5 * variance;
standardDeviations[0] = sqrt(variance);

for (unsigned long j=1; j < numberOfTimes; ++j)


{
double thisVariance = vol.integralSquare(times[j-1],times[j]);
drifts_[j] = r.integral(times[j-1],times[j]) -
d.integral(times[j-1],times[j]) -

218
9.4. Ein flexibler Monte-Carlo Option Pricer

0.5 * thisVariance;
standardDeviations[j] = sqrt(thisVariance);
}

logSpot = log(spot);
variates.resize(numberOfTimes);
}

Schließlich stellen wir ein einfaches Anwendungsprogramm vor, das der geneigte Leser zum
Anlaß nehmen kann, um eine noch einfachere Anwenderschnittstelle in Form einer Klasse
Pricer wie im ersten Klassenrahmenwerk zu erstellen.

int main()
{

double expiry, strike, spot, vol, r, d;


unsigned long numberOfPaths, numberOfDates;

cout << "\nEnter expiry\n"; cin >> expiry;


cout << "\nStrike\n"; cin >> strike;
cout << "\nEnter spot\n"; cin >> spot;
cout << "\nEnter vol\n"; cin >> vol;
cout << "\nr\n"; cin >> r;
cout << "\nd\n"; cin >> d;
cout << "Number of dates\n"; cin >> numberOfDates;
cout << "\nNumber of paths\n"; cin >> numberOfPaths;

PayOffCall thePayOff(strike);
MJArray times(numberOfDates);

for (unsigned long i=0; i < numberOfDates; i++)


times[i] = (i+1.0)*expiry/numberOfDates;

ParametersConstant volParam(vol);
ParametersConstant rParam(r);
ParametersConstant dParam(d);

PathDependentAsian theOption(times, expiry, thePayOff);

StatisticsMean gatherer;
ConvergenceTable gathererTwo(gatherer);

RandomMT generator(numberOfDates);
AntiThetic GenTwo(generator);

219
9. Design Patterns und ein verbessertes Rahmenwerk

ExoticBSEngine theEngine(theOption, rParam, dParam,


volParam, genTwo, Spot);

theEngine.doSimulation(gathererTwo, numberOfPaths);

vector<vector<double> > results =gathererTwo.getResultsSoFar();

cout <<"\nFuer den Asian Call ergibt sich \n";

{
for (unsigned long i=0; i < results.size(); i++)
{
for (unsigned long j=0; j < results[i].size(); j++)
cout << results[i][j] << " ";

cout << "\n";


}}

double tmp;
cin >> tmp;

return 0;

220
10. Ausblick: Nützliches für den Alltag
Sie können an dieser Stelle aufhören zu lesen. Legen Sie das Skript weg. Lassen Sie die Dinge
eine Weile auf sich wirken - und dann drängt sich irgendwann die Frage auf, wie man das
Gelernte in der Praxis wirklich umsetzt. Im folgenden sollen einige sehr praktische Fragestel-
lungen aufgegriffen werden.

10.1. Erstellung von DLLs


In aller Regel wird man in C++ ein Programm schreiben, das von einem anderen Programm
aufgerufen wird. Man könnte etwa ein bestehendes System um zusätzliche Funktionen erwei-
tern wollen, und ein häufig praktizierter Weg wäre die Erstellung einer Dynamic Link Library
(DLL) die die neuen Funktionalitäten zur Verfügung stellt. Wie man das bewerkstelligt, soll
in einem eigenen Abschnitt behandelt werden. Man könnte aber auch lediglich das Problem
einer geeigneten Oberfläche auf ein anderes Programm abwälzen wollen. Im C++Builder kann
man sehr leicht ordentliche Oberflächen erstellen, im Microsoft Developer Studio kann man
in Visual C++ eine DLL erstellen und diese aus Visual Basic aufrufen, mit dem man ebenso
leicht Oberflächen erstellen kann. Beide Varianten haben aber auch gewisse Nachteile:
i) Verfügbarkeit der Daten
Eine übersichtlich gestaltete Oberfläche ist das eine, leider wird man in der Praxis die
produzierten Datenkolonnen aber auch gerne weiterverarbeiten wollen, z.B. die Greeks
im Rahmen des Risikocontrollings. Um diese zusätzlichen Funktionalitäten der Ober-
fläche müsste man sich also besonders bemühen.

ii) Einheitlichkeit der im Umlauf befindlichen Oberflächen


So geeignet Ihre Oberfläche auch sein mag, so wenig angenehm wird es für den User
in der Praxis sicher sein, sich mit unzähligen Oberflächen vertraut machen zu müssen.
Auch dies schränkt den Wert einer selbst erstellten Oberfläche ein.
All diese Nachteile werden nun sehr elegant dadurch gelöst, dass man Microsoft Excel die
neuen Funktionalitäten per DLL zur Verfügung stellt:

i) Excel macht die Datenverarbeitung sehr leicht und ermöglicht die Zusammenarbeit und
Weiterverarbeitung mit MS Word und MS Access, was zusätzliche Vorteile bringt.

ii) Auch wenn C++Builder und Visual Basic eine hinsichtlich der benötigten Entwick-
lungszeit schnelle Erstellung von Oberflächen erlauben, so dürften sie von MS Excel
doch um Längen geschlagen werden. Außerdem wird der spätere User in aller Regel

221
10. Ausblick: Nützliches für den Alltag

mit Microsoft Excel gut vertraut sein und die Einarbeitung wird auf das Nötigste redu-
ziert.
Natürlich können Sie MS Excel Ihre Funktionen genauso zur Verfügung stellen, wie jedem
anderen Programm auch, allerdings wird der Weg über Excel in der Praxis sicher der mit
Abstand häufigste sein und um der größeren Eleganz willen möchten wir Ihnen noch ein open-
source-Tool vorstellen, das Ihnen die Erstellung von speziellen Excel-Addins sehr einfach
macht. Genug der Vorrede, fangen wir an.

10.1.1. Erstellung einer Dynamic Link Library mit Borland


C++Builder
Im folgenden soll eine Funktion meineFunktion(double x) “aus einer selbst erstellten DLL in

Excel aufgerufen werden. Dies lässt sich in analoger Weise auf jedes andere Programm übert-
ragen. Sie könnten also auch aus einem selbst erstellten Programm auf Funktionen aus einer
von Ihnen erstellten DLL zugreifen. Das geht ganz leicht, siehe C++Builder - Dokumentation.
Gehen Sie also im Menü Datei/Neu“auf DLL. Fügen Sie dann in die cpp-Datei folgendes ein:

double __declspec(dllexport) __stdcall meineFunktion( double x ){
return x + 3;
}
Die Schlüsselwörter
declspec(dllexport) stdcall
verwendet man unabhängig vom Programm, das die DLL aufrufen wird. An dieser Stelle soll
auf technische Details und Hintergrund verzichtet werden. Anschließend deklarieren Sie die
Funktion in der zugehörigen Headerdatei:
extern "C" double __declspec(dllexport) __stdcall
meineFunktion(double x);
Erzeugen Sie nun die DLL. An dieser Stelle sind Sie bereits fertig! Lassen Sie sich über das
Menü der rechten Maustaste die Schnellansicht der DLL anzeigen (sofern Sie diese installiert
haben), dann sollte ”meineFunktion” im Exporttable erscheinen. Das ist auch ein erster Test
um zu sehen, ob alles gut gegangen ist. Alternativ kann man unter MS-DOS
dumpbin meineDLL.dll /Exports
verwenden. Wichtig ist, dass man im header-file zusätzlich extern "C" hinzufügt. Möchten
Sie nun die DLL in Excel einbinden, so gehen Sie im Menü Extras/Makro“auf VisualBasic-
” ”
Editor“. Erzeugen Sie ein Modul und geben Sie unter Deklarationen/Allgemein“folgendes

ein:
Declare Function meineFunktion Lib _
"C:\meinVerzeichnis\meineDLL.dll" (ByVal x As Double) As Double
Der Unterstrich “wurde hier nur eingefügt um zu demonstrieren, wie man die Deklaration

auf mehrere Zeilen verteilt. Beachten Sie bitte noch, dass C++ - Integer in Visual Basic als
long deklarariert werden müssen. Nun können Sie in einer Zelle in Excel mit =meineFunktion(3)
auf Ihre Funktion zugreifen.

222
10.1. Erstellung von DLLs

10.1.2. Erstellung einer Dynamic Link Library mit Microsoft


Visual C++ 6.0
Wir erstellen eine DLL mit Namen myDLL.dll“, die eine Funktion myfunction“enthält. Die-
” ”
se erhält einen double x und liefert 2*x zurück. Die folgende knappe Beschreibung sollte
nach dem vorangehenden Abschnitt für Borland ausreichen. Man beachte, dass sich die De-
tails bei Microsoft von Version zu Version leider immer wieder ändern.

1. Erstellen Sie ein neues Projekt Win32-Dynamic-Link-Library“, nennen Sie das Projekt

myDll“.

2. Erstellen Sie eine .cpp-Datei in diesem Projekt mit folgendem Inhalt:

#include <windows.h>
#include <oleauto.h>
#include "xlcall.h"

double WINAPI myFunction(double x){

return x * 2;

Die Datei xlcall.hßollte sich in Ihrem Projektordner befinden. Sie können sie z.B. im

Internet unter der bekannten Kursadresse downloaden.

3. Erstellen Sie eine neue Textdatei mit dem Inhalt

LIBRARY myDLL

DESCRIPTION ’tutorial’
EXETYPE WINDOWS
HEAPSIZE 8192

EXPORTS
myFunction

und speichern Sie diese als myDll.def“. Fügen Sie diese dem Projekt hinzu.

4. Erstellen Sie nun die DLL. Nun können Sie die Funktion analog zur Beschreibung für
den Borland-Compiler in Excel einbinden.

223
10. Ausblick: Nützliches für den Alltag

10.1.3. Erstellung eines Add-ins für Microsoft Excel


Speziell für Microsoft Excel geschriebene DLLs erhalten das besondere Kürzel xll, ansonsten
handelt es sich im Prinzip um gewöhnliche DLLs. Die folgenden Ausführungen beziehen sich
auf das Microsoft Visual Studio, was folgende Gründe hat:
1. Die Erklärungen sind offensichtlich recht ausführlich, allein aus Platzgründen ist eine
Beschränkung notwendig
2. Microsoft Visual C++ ist der de facto Industriestandard, was nicht notwendig heißen
soll, dass es besser wäre als etwa Borland C++. Beispielsweise sind bei Borland die Feh-
lermeldungen des Compilers im allgemeinen sehr viel sinnvoller. Vor allem Program-
mierneulinge werden sich mit den Fehlermeldungen des Microsoft-Compilers häufig
sehr schwer tun.
3. Das vorzustellende open-source-Tool wurde bisher vor allem mit Visual C++ verwen-
det, in Zusammenarbeit mit diesem sollte es also recht stabil laufen.
Dem open-source Tool liegt auch eine Projektdatei .mak für Borland C++ bei. Diese können
Sie allerdings nicht mit C++Builder öffnen, was zugegebenermaßen unbefriedigend ist. Im-
merhin wird offenbar an einer richtigen”Version für Borland gearbeitet.

Details zum vorgestellten Tool entnehmen Sie der recht spartanischen Beschreibung oder bes-
ser den HeaderFiles der jeweiligen Klassen. Das Vorgehen gestaltet sich wie folgt:
1. In Sourceforge finden Sie das Open-Source - Tool XLW, das die ziemlich kryptische C
API (mehr dazu siehe unten), wie sie im Microsoft Excel 97 Developer’s Kit beschrie-
ben wird, durch ein Klassenrahmenwerk versteckt und in recht gut benutzbarer Weise
zur Verfügung stellt. Sie können dieses Tool in kommerziellen Anwendungen einbin-
den, beachten Sie aber die Copyright-Bestimmungen. Hier noch der im September 2005
aktuelle Link:

http://sourceforge.net/project/showfiles.php?group_id=45222

API steht übrigens für Application programmer’s interface, und in aller Regel tun sie
gut daran, alles was mit API zu tun hat zu vermeiden. Windows stellt für Program-
me eine eigene API zur Verfügung, mit der alles gesteuert werden kann, und in der
Praxis würden Sie schnell feststellen, dass es äußerst mühsam ist, ein gewöhnliches
Windows-Programm zu Fuß“ mit der API zu schreiben. Der C++Builder nimmt Ihnen

diese Arbeit ab, indem er die VCL = Visual Conponent Library zur Verfügung stellt, was
praktisch darauf hinausläuft, dass Sie alle graphischen Windowselemente per Drag and
Drop einrichten können und im Hintergrund erledigen C++-Klassen in effizienter Weise
die Arbeit für Sie. Ein anderer Ansatz zur Schachtelung der Windows-API bietet Mi-
crosofts MFC = Microsoft Foundation Classes. Das ist ebenfalls ein Rahmenwerk zur
Schachtelung der Windows-API, die Sie aber direkt anwenden, nicht indirekt per Drag-
and-Drop. Borland bietet sozusagen ein Gegenstück mit der OWL = Object Windows
Library an. Es versteht sich von selbst, welche der Varianten VCL, MFC und OWL sich
am Markt durchgesetzt hat (ja, die MFC).

224
10.1. Erstellung von DLLs

2. Im folgenden wird davon ausgegangen, dass Ihre Funktionen fertig geschrieben in einem
Projekt auf dem PC liegen. Öffnen Sie nun das Beispiel zu XLW, das Sie im XLW-
Verzeichnis finden.

3. Fügen Sie dem Beispielprojekt Ihre Programmdateien ( = cpp-files) hinzu. Ergänzen Sie
unter Extras/Optionen/Verzeichnisse“ die Pfade zu Ihren header-files.

4. Unter Projekt/Einstellungen/Linker“ legen Sie den Namen Ihrer Ausgabedatei fest.

Die Datei muß dabei die spezielle Endung .xll haben, damit Excel diese Bibliothek
als Add-in erkennt. Später werden Sie noch zwischen den grundlegenden Konfigura-
tionen Win32 OnTheEdgeRelease“ und Win32 OnTheEdgeDebug“ wechseln wollen.
” ”
Für diese müssen all diese Einstellungen separat vorgenommen werden!

5. Wechseln Sie in die Datei xlwExample.cpp“. Fügen Sie oben Ihre benötigten header-

files ein. Benötigen Sie z.B. während der gesamten Ausführungszeit des Add-ins eine
Klasseninstanz im Hintergrund, so können Sie diese als ”globale Variable” vor den An-
fang des extern "C" - Blocks schreiben.

6. Wir stellen nun eine Funktion

int meineFunktion( double x, double y ){


return x*y;
}

für Excel zur Verfügung. In dieser Funktion könnten Sie dann auch Ihre bereits vorbe-
reiteten Funktionen aufrufen. Fügen Sie im extern C"-Block folgende Funktion ein:

LPXLOPER EXCEL_EXPORT xlmeineFunktion(XlfOper x, XlfOper y){

EXCEL_BEGIN

if(x.IsMissing() || y.IsMissing() )
return XlfOper("");
else{
double x_ = x.AsDouble();
double y_ = y.AsDouble();
double result = x_ * y_;
return XlfOper(result);
}

EXCEL_END
}

225
10. Ausblick: Nützliches für den Alltag

Als allgemeiner Variablencontainer wird die Klasse XlfOper verwendet, die Konstruktoren
für double, char* usw. zur Verfügung stellt. Auch alle Argumente sind zunächst vom Typ
XlfOper, und das können durchaus auch ganze Zellbereiche sein. Konvertiert werden die
Variablen wie ersichtlich etwa mit AsDouble, die Werterückgabe an Excel ist ebenfalls selbst-
erklärend. Zu Beginn der Funktion steht immer LPXLOPER EXCEL EXPORT,d.h. Excel erhält
einen long pointer auf XlfOper zurück, worüber wir uns aber keine weiteren Gedanken ma-
chen wollen.
EXCEL EXPORT wird in declspec(dllexport) umgewandelt wie wir es bereits für den all-
gemeinen Fall einer DLL gesehen haben. Sollte bei der Konvertierung der XlfOper-Container
etwas schief gehen, wird eine Exception ausgeworfen. Damit ausgeworfene Exceptions Excel
nicht erreichen und somit abstürzen würden, fängt man diese Exceptions im Programm ab:
EXCEL BEGIN und EXCEL END sind Makros, die in die übliche catch- Struktur umgewandelt
werden. Klicken Sie auf diese Befehle und lassen Sie sich über das Menü der rechten Mausta-
ste einmal den Inhalt der Makros anzeigen! Ein weiterer wichtiger Punkt ist, dass Excel Ihre
Funktion manchmal schon aufruft, obwohl der Benutzer gerade erst beginnt, alle Funktions-
parameter einzugeben. Damit dies nicht zum Programmabsturz führt, fragt man zu Beginn ab,
ob alle Parameter mit Werten belegt sind. Im Zweifel wird ein leerer String als Ergebnis gelie-
fert, so dass der Excel-User das Gefühl hat, dass sich nichts tut. Sie können mit der gleichen
Methode auch einfache Fehlermeldungen an den Benutzer liefern, etwa in folgendem Sinne:
if(Katastrophe)
return XlfOper("Sie haben eine Katastrophe ausgeloest!");
Bevor Sie Ihre Funktion in Microsoft Excel verwenden können, müssen Sie diese noch in der
weiter unten in ”xlwExample.cpp” vorhandenen Funktion
long EXCEL\_EXPORT xlAutoOpen()
registrieren. Das funktioniert so:
long EXCEL_EXPORT xlAutoOpen(){

oldStreamBuf = std::cerr.rdbuf(&debuggerStreamBuf);
std::cerr << __HERE__ <<
"std::cerr redirected to MSVC debugger" << std::endl;

XlfExcel::Instance().SendMessage("Registriere MathFinance-Projekt...");

// lege Klassen x,y zur Beschreibung der Argumente an


XlfArgDesc x("x","Beschreibung die in Excel erscheint");
XlfArgDesc y("y","Beschreibung der zweiten Variablen.");

// lege Klasse BlackScholesTV zur Beschreibung der Funktion an


XlfFuncDesc BlackScholesTV(
"xlBlackScholesTV",
"xlBlackScholesTV",

226
10.1. Erstellung von DLLs

"Computes the standard Black Scholes TV",


"MathFinance Analyzer");

// Mehrere Argumente in Funktionsbeschreibung aufnehmen


BlackScholesTV.SetArguments(x+y);

// und registrieren
BlackScholesTV.Register();

// weitere Funktionsregistrierungen...

XlfExcel::Instance().SendMessage();
return 1;
}
Um Verwirrung zu vermeiden, versieht man seine Funktionen in der Datei xlwExample.cpp
mit dem Präfix xl“. Die Argumente im Konstruktor der Klasse XlfFuncDesc sind:

1. Name der Funktion in xlwExample.cpp

2. Name der Funktion, wie er in Excel erscheinen soll

3. Beschreibung, die in Excel erscheinen soll

4. Name der Gruppe von Funktionen, in der die Funktion liegen soll
Die Argumente im Konstruktor von XlfArgDesc sind:
1. Name der Variable, wie er in Excel erscheinen soll

2. Beschreibung der Variablen, die in Excel erscheinen soll


In der Funktion SetArguments zählt man mehrere Argumente wie demonstriert mit einem +
auf.

Der letzte Schritt wäre noch, das fertige Produkt in Microsoft Excel unter Extras/Add-Ins-

Manager einzufügen“. Nun kann der Benutzer in eine Zelle klicken und über Einfügen/Funk-

tion“ auf die Funktion meineFunktion“ zugreifen, als wäre sie fest in Excel integriert.

Ausblick: Debuggen einer DLL aus Microsoft Excel heraus
Führen Sie folgende Schritte durch:
1. Stellen Sie die Konfiguration um auf Win32 OnTheEdgeDebug“ und passen Sie die

Konfiguration analog an wie oben beschrieben.

2. Im Abschnitt Projekt/Einstellungen/Debug“ geben Sie als auszuführendes Programm



den Pfad der Excel.exe an, als Argument des Programms schreiben Sie hintereinander
den Pfad der xll-Datei und den Pfad eines von Ihnen erstellten Excel-Beispiel-Sheets.

227
10. Ausblick: Nützliches für den Alltag

3. Fügen Sie in Ihrem Programm Haltepunkte ein.

4. Starten Sie das Programm mit Ausführen/Debug“ oder F5. Die xll wird erstellt und

Excel wird mit dem passenden Sheet gestartet. Wenn Sie nun Ihre Funktion in Excel
aufrufen, wird sie automatisch am Haltepunkt gestoppt und Sie können wie gewohnt in
Visual C++ debuggen und am Funktionsende nach Excel zurückspringen.

Bemerkung 10.1.1. i) Zum Debuggen ist es sehr wichtig, dass Sie die xll nicht bereits über
den Add-Ins-Manager in Excel eingebunden haben. Selbst wenn Sie diese neu compilie-
ren wird Excel dann an der alten Version festhalten und alle Änderungen, insbesondere
Ihre Debug-Wünsche, ignorieren. Es empfiehlt sich also, erst am Ende der Entwicklung
die xll über den Add-Ins-Manager in Excel aufzunehmen.

ii) Alternativ zu den Einstellungen im Abschnitt Debug können Sie auch Ihre xll erstellen,
Excel starten, und dann mit einem Doppelklick auf die xll Excel veranlassen, für die
aktuelle Sitzung Ihre xll aufzunehmen. Damit das so einfach funktioniert, ist natürlich
die Dateinamenerweiterung xll im Gegensatz zu dll sehr wichtig.

iii) Ganze Zellbereiche aus Excel als Funktionsargument


Verwenden Sie dazu das folgende Beispiel als Vorlage. Um die Standard-Template-
Library aus C++ im konkreten Fall nutzen zu können, müssen Sie zu Beginn der Datei
#include <vector> einfügen. Leere Zellen im Funktionsargument werden im folgen-
den Programm als 0 interpretiert. Um die Anzahl der Zeilen und Spalten des Zellbe-
reichs ausrechnen zu können, der im Container XlfOper enthalten ist, muß es als Re-
ferenz interpretiert werden, wie man im Code sehen kann. Auf die i-te Zeile und j-te
Spalte des übergebenen Zellbereichs würde man im gegebenen Beispiel etwa mit

XlfOper test = range(i,j);

zugreifen. Die genaue Verwendung der Standard-Template-Library kann hier nicht be-
handelt werden, das angegebene Beispiel sollte für den Anfang aber ausreichen. Genug
der Vorrede, hier das Beispiel:

LPXLOPER EXCEL_EXPORT xlBlackScholesTV(XlfOper args){

EXCEL_BEGIN

XlfRef range = args.AsRef();


short numberOfItems = range.GetNbRows();
if( numberOfItems < 8 )
return XlfOper("Not enough arguments.");

std::vector<double> temp = args.AsDoubleVector(


XlfOper::RowMajor);
std::vector<double>::iterator it = temp.begin();

228
10.2. Interview mit einem Quant

double spot_ = *it; it++;


double strike_ = *it; it++;
double rd_ = *it; it++;
double rf_ = *it; it++;
double vol_ = *it; it++;
double notional_ = *it; it++;
double tau_ = *it; it++;
double phi_ = *it;

// die Funktion computeTV muss irgendwo


// vorhanden sein, klar
double result = computeTV(spot_,strike_,rd_,rf_,vol_,
tau_, notional_, phi_);

return XlfOper( result);

EXCEL_END
}

10.2. Interview mit einem Quant


Das folgende Interview wurde so ähnlich im September 2005 geführt zwischen jemandem, der
an diesem Kurs teilgenommen hat ( Einsteiger“) und einem Quant einer deutschen Großbank,

der bereits mehrere Jahre Berufserfahrung hat. Viel Vergnügen!

Einsteiger: Ich habe jetzt an einem einführenden Kurs in Monte Carlo und C++ teilgenommen,
und würde Sie gerne zu ein paar praktischen Aspekten interviewen, hätten Sie vielleicht ein
paar Minuten Zeit?
Quant: (freut sich) Ja, gerne.
E: Dass man all die Dinge, die ich über objektorientiertes Programmieren gelernt habe, tat-
sächlich wieder braucht, habe ich nach unserem Entwurf für eine Bewertungsbibliothek wohl
verstanden. Bei der Durchführung einer Monte Carlo-Simulation und einigen praktischen
Aspekten hätte ich aber noch ein paar Fragen.
Q: (freut sich) Nur zu.
E: Fangen wir doch ganz vorne an: Wenn ich einen Zufallsgenerator nehmen soll für die
U [0, 1]-Verteilung, welchen verwenden Sie da in der Praxis? Ich habe nämlich mittlerweile
diverse Generatoren zur Auswahl...
Q: Also, zunächst einmal ist die Wahl des Zufallszahlengenerators in der Praxis nicht zentral,
auch wenn ein besonders schlechter Generator natürlich das Ergebnis wertlos machen kann.
Wissen Sie, die Unsicherheiten, die wir an anderen Stellen im Modell haben, sind sowieso viel

229
10. Ausblick: Nützliches für den Alltag

größer als das, was man mit einer genauen Analyse des Zufallsgenerators gut machen könn-
te. Oft kämpft man auch mit weniger verlässlichen Marktdaten, die durch nicht ausreichend
liquide Märkte entstehen können, weil diese die Kalibrierung des Modells sehr erschweren.
E: In der Praxis verwendet man also pauschal den Mersenne-Twister, richtig?
Q: Ganz genau, normalerweise hat man die Wahl zwischen Mersenne-Twister und Stratified
sampling.
E: Das kenne ich noch gar nicht. Tauscht man die Generatoren nie aus?
Q: Stratified sampling ist nicht sonderlich schwierig, schauen Sie sich das bei Gelegenheit mal
an. Generatoren tauscht man gelegentlich aus, wenn die speziellen Eigenschaften des aktuellen
Generators für den speziellen Payoff verfälschend wirken könnten. (winkt ab) Machen Sie sich
darüber aber am Anfang nicht zu viele Gedanken. Vielleicht experimentieren Sie auch mal ein
wenig herum.
E: In Ordnung. Der nächste Punkt wäre die Transformation der U [0, 1]-verteilten Variable in
eine normalverteilte Zufallsvariable. Dazu habe ich nur die Variante mit Φ−1 kennengelernt.
Nimmt man die wirklich?
Q: Ja, genau so macht man das auch! Es gibt noch andere Methoden, etwa die von Box-
Muller, aber damit generiert man dann immer gleich zwei Zufallszahlen, das will man oft
nicht. Außerdem muß man die dann wieder zwischenspeichern, das ist beim Programmieren
unpraktisch, und auch sonst spricht wenig dafür, von der Methode abzurücken, die Sie genannt
haben.
E: Gut, als nächstes komme ich zur Simulation des Underlying. Im Kurs behandelt wurde
nur die Simulaton einer geometrischen Brown’schen Bewegung. In den Übungen haben wir
dann noch das Euler-Diskretisierungsschema für alle anderen Fälle kennengelernt. Gleichzei-
tig wurden wir aber auch vor diesem Schema gewarnt, weil man die Schrittweite h oft sehr
klein wählen muß, um vernünftige Ergebnisse zu erhalten.
Q: (hebt besänftigend die Hände) Die geometrische Brown’sche Bewegung ist natürlich sehr
sehr wichtig, das geht in Ordnung. Was man ansonsten im wesentlichen braucht ist, wie Sie
also bereits sagen, das Euler-Schema. Die Warnung Ihres Dozenten ist in gewisser Weise
natürlich gerechtfertigt, in der Praxis sind die stochastischen Differentialgleichungen aber
wenn, dann nur leicht nicht-linear, und in diesen Fällen genügt das Euler-Schema vollkom-
men. Für den Anfang reichen Ihre Kenntnisse da sicher aus.
E: Die nächste Sache ist die Varianzreduktion, dort haben wir nur Control Variates“ kennen-

gelernt. Benutzt man das sehr oft?
Q: Bei der Varianzreduktion gibt es noch ein paar andere Dinge, die Sie sich irgendwann mal
anschauen sollten, aber es ist in der Tat so, dass man Control Variates“ am häufigsten benutzt.

E: (nachhakend) Wie oft verwendet man das? Die Frage ist sicher nicht richtig gestellt, aber
mein Bauch würde sich über eine gewisse Vorstellung von der Häufigkeit sehr freuen.
Q: Naja, so in 20% der Fälle vielleicht, obwohl, nein, es ist unsinnig, hier eine Zahl zu nennen,
da haben Sie schon recht. Wenn Sie mit der Geschwindigkeit und der Approximationsgenau-
igkeit zufrieden sind, dann lassen Sie es bleiben, wenn Sie schneller und genauer sein müssen

230
10.2. Interview mit einem Quant

oder das ganze auf günstigerer Hardware laufen lassen wollen, dann würden Sie sich wohl die
Mühe machen, Control Variates“ zu implementieren.

E: (jetzt forscher) Mmh, okay. Aber wie entscheide ich denn in der Praxis, ob ich genau genug
bin, den wahren Wert kenne ich ja logischerweise nicht . . .
Q: (lapidar) Die Konvergenzrate beträgt
 
1
O √
n
E: (unzufrieden) Ja richtig, aber das heißt doch lediglich, dass
1
|Approximation − wahrerWert| ≤ C × √
n
gilt, und diese Konstante C kenne ich nicht. Also kann ich auch keinen absoluten Fehler ange-
ben. Das ganze sind zwar nur Wahrscheinlichkeitsaussagen, aber sagen wir in der Simulation
hat das Konfidenzintervall eine Wahrscheinlichkeit von 99.99%, das C ergibt sich in mir un-
bekannter Weise, und fertig. Was mache ich jetzt, wenn mir einer sagt, er braucht 2 Stellen
mehr Genauigkeit? Wie viele Iterationen mache ich dann zusätzlich?
Q: Nun ja, normalerweise schaut man sich einen Plot an, auf der x-Achse tragen Sie die Zahl
der Iterationen ein, und auf der y-Achse tragen Sie Ihren Schätzer ein.
E: (freut sich) Ja, das kenne ich! Das kam mal in den Übungen.
Q: Sehr schön, diesen Plot schaut man sich an, und die Stelle, ab der der Schätzer nicht mehr
viel zappelt, die nehmen Sie dann in Ihrer praktischen Implementation. Vergessen Sie aber
nicht, ein paar Parameterszenarien durchzuspielen.
E: Das hört sich mehr nach Bauchgefühl an, geht es nicht präziser?
Q: Wenn Sie unbedingt wollen, dann nehmen Sie einen hoffentlich guten Schätzwert und
fassen ihn als exakte Lösung auf. Mit ein paar Schätzern für unterschiedliche Iterationszahlen
können Sie dann auf die Größe der Konstanten C Rückschlüsse ziehen.
E: Ach ja, das macht Sinn. Gut, mehr fällt mir dazu spontan auch nicht ein, nächstes Thema
wären die Greeks. Da habe ich einiges über die Likelihood-Ratio Method und die Pathwise
method gelernt, der Weg über die finiten Differenzen wurde nur kurz angeschnitten, weil er
intuitiv recht klar ist. Außerdem wäre bei finiten Differenzen die Varianz bei ungeschickter
Wahl der Schrittweite oft viel zu groß, der Rechenaufwand ist höher und man hat einen Bias.
Was nimmt man denn nun?
Q: (winkt ab) Die finiten Differenzen sind hier sicher problematisch, aber wir nutzen sie den-
noch um eine erste Orientierung zu bekommen. Als Schrittweite kann ich auch keine Pa-
tentlösung angeben, aber man wählt h in aller Regel nicht allzu klein, in der Zinswelt gibt es
ja im Prinzip mit dem Basispunkt bereits eine relativ große natürliche Grenze, die man dann
auch nicht unterschreitet. Das pfadweise Ableiten ist in der Praxis meist unrealistisch weil
man die Prozesse einfach nicht genau genug kennt, während LRM sehr gerne benutzt wird,
schließlich braucht man dort auch nur die Verteilung, und die hat man ja normalerweise.

231
10. Ausblick: Nützliches für den Alltag

E: Aha, okay. Das wäre auch schon das Wichtigste für mich zu diesem Thema (. . . überlegt. . . )
ach ja, eine Sache hat mich noch gewundert: In meinem Einführungskurs gab es einen recht
ausführlichen Abschnitt über Rundungsfehler und wie man sie z.B. beim numerischen Diffe-
renzieren vermeiden kann. Macht man sich in der Praxis darüber wirklich Gedanken?
Q: (lebhaft) Oh ja, allerdings! Man achtet auf gewisse typische Fälle, z.B. wenn man eine sehr
große und eine sehr kleine Zahl addiert oder subtrahiert. So etwas ist immer böse und dann
fragt man sich schon, ob man die Formel nicht lieber etwas passender umstellt um nicht zu
viele Stellen an Genauigkeit zu verlieren.
E: (doch etwas erstaunt) Ach so, in Ordnung. (. . . überlegt . . . ) Nein, sonst habe ich erstmal
keine Fragen. Vielen Dank nochmal!
Q: (freut sich jetzt sehr) Ja gern geschehen. Kommen Sie ruhig mal wieder rein wenn etwas
ist.

232
Teil V.

Anhang

233
Besprechung der Übungsaufgaben
10.3. Übung Nr.1
10.3.1. Teil a)
Das Programm besteht aus folgenden Dateien:

• exercise1.cpp enthält das Hauptprogramm

• helperFunctions.cpp enthält die Funktion Φ der Standardnormalverteilung

• VanillaPricingBlackScholes.cpp enthält die Black-Scholes - Formeln

• CodyAlg.cpp wird von helperFunctions.cpp benötigt

• constants.h enthält benutzerdefinierte Konstanten für TV und DELTA

• VanillaPricingBlackScholes.h enthält lediglich eine Funktionsdeklaration

• cdflib.h ist die Deklarationsdatei zu CodyAlg.cpp

• helperFunctions.h ist die Deklarationsdatei zu helperFunctions.cpp

Folgende Vorgehensweise sollte das Programm in Ihrer Entwicklungsumgebung zum Lau-


fen bringen:

1. Erstellen Sie ggf mit einem Assistenten unter Projekt/Neu eine Konsolenanwendung
ohne MFC oder ähnliches.

2. Sorgen Sie dafür, dass alle genannten cpp-files und header-files im Projektverzeichnis
liegen. Das ist zwar nicht zwingend, reicht aber für den Anfang aus.

3. Wurde automatisch eine Datei erzeugt, die ein leeres main-Programm enthält, fügen Sie
dort den Inhalt der main-Funktion aus exercise1.cpp ein. Ergänzen Sie die Deklarati-
onsanweisungen. Ansonsten fügen Sie exercise1.cpp dem leeren Projekt hinzu.

4. Fügen Sie nun alle übrigen cpp-Dateien zum Projekt hinzu, fügen Sie nicht die header-
files hinzu.

235
Besprechung der Übungsaufgaben

Wenn Sie das Projekt nicht ’exercise1’ genannt haben, sollte es zu keinen Namenskonflikten
beim Kopieren von exercise1.cpp ins Projektverzeichnis kommen und Sie können das Pro-
jekt nun compilieren und ausführen. Das Programm ist selbsterklärend, zu bemerken ist noch
die kleine Änderung der Black-Scholes-Formel, dass statt rd der Ausdruck log(1 + rd) be-
nutzt wird. Dies wird in der Praxis deswegen gerne gemacht, weil die Zinssätze annualisiert
angegeben werden, während die Black-Scholes-Welt von stetiger Verzinsung ausgeht. Also:

exp(rBS T ) = 1 + rannu ⇐⇒ rBS = log(1 + rannu ) , mit T = 1

10.3.2. Teil b)
Das Programm besteht aus den gleichen Teilen wie oben, wird nun aber um eine Schleife über
h erweitert. Die Zwischenergebnisse werden in einer Datei gespeichert. Als Grafik ergibt sich

Abbildung 10.1.: Fehler bei der finite Differenzen-Approximation

Genauere Informationen liefert das Kapitel 7.1, hier sei nur so viel gesagt: Offenbar ist der
Fehler sowohl für kleine Schrittweite h groß als auch für sehr kleine h. Dies liegt einmal am
Diskretisierungsfehler - bei großem h ist die Approximation natürlich nicht optimal - und am
Rundungsfehler, denn die Genauigkeit beträgt beim IBM-PC 15 Dezimalstellen. Damit wird
der Verlauf der beiden Kurven plausibel. Ferner erkennt man, dass es für jedes Schema eine
untere Genauigkeitsgrenze gibt, die nicht unterschritten werden kann. Möchte man die Ge-
nauigkeit weiter erhöhen, muß man also ein Verfahren höherer Ordnung verwenden, wie man
durch Vergleich beider Kurven auch
√ direkt erkennt. Bemerkenswert ist, dass beide Verfahren
optimale Werte im Bereich h ∼ 10−15 ≈ 10−7 liefern und die in (7.1) angegebenen theore-

236
10.4. Übung Nr.2

tischen Größenordnungen sowohl für das optimale h als auch die Qualität der Approximation
sehr gut wiedergeben.

10.4. Übung Nr.2


Das Programm besteht aus folgenden Teilen:

• CodyAlg.cpp, siehe oben

• exercise2.cpp, das Hauptprogramm

• helperFunctions.cpp enthält insbesondere Funktion mit Newtonverfahren

• VanillaPricingBlackScholes.cpp, siehe oben

sowie einigen header-files. Wir beschränken uns auf die Besprechung des Newtonverfahrens.
Als Startwert bietet sich die At-the-money-Volatilität an, da diese bereits einen vernünftigen
Marktpreis widerspiegelt. Nun wird man das Newtonverfahren wie angegeben laufen lassen,
wobei das Verfahren in wenigen Schritten konvergieren sollte. Liegt der gewünschte Preis
zu weit weg, kann es aber passieren, dass das Verfahren überhaupt nicht konvergiert (lokale
Konvergenz des Newtonverfahrens). Deshalb werden zu Beginn der Funktion die Marktpreise
von extremen Volatilitäten berechnet, σ = 0.001 - denn in diesem Bereich wird das Verfahren
langsam numerisch instabil - und σ = 2.0 - denn das ist die Grenze des praktisch Sinnvollen.
Liegt der gesuchte Preis nicht in diesen Schranken, bricht das Verfahren ab.
Ein weiteres Problem kann sein, dass der Startwert der Newtoniteration in dem Bereich liegt,
in dem die Abbildung
Volatilität → TV

sehr flach ist. Je nach Parameterwahl kann dieser Bereich sehr groß sein. In diesem Fall einer
sehr flachen Zielfunktion muß das Problem deshalb geeignet präkonditioniert werden, wobei
es hier ausreicht, die Funktion mit einem genügend großen Faktor zu multiplizieren, so dass
die Funktion im Bereich des Startpunktes genügend steil verläuft.

10.5. Übung Nr. 3


Dieses Programm ist im wesentlichen das Programm aus Übung Nr. 2 plus eine Schleife über
mehrere implied-vol - Berechnungen. Die Ergebnisse werden in einer Datei ausgegeben und
in Excel als Grafik dargestellt.

237
Besprechung der Übungsaufgaben

Spot Implied Vol


1.10 0.109480
1.15 0.099278
1.18 0.094865
1.20 0.093557
1.22 0.093478
1.25 0.095587
1.30 0.103207
1.35 0.109661

Abbildung 10.2.: Typischer Volatility-Smile beim Call

10.6. Übung Nr. 4


Dieses Programm besteht im wesentlichen aus zwei geschachtelten for-Schleifen: die äußere
Schleife durchläuft die gewünschten Iterationsgrößen, die innere Schleife durchläuft so oft wie
von der ersten Schleife angegeben den Algorithmus für einen naiven Monte Carlo Schätzer.
Die Ergebnisse werden in einer Textdatei gespeichert. Die empirische Varianz wird über eine
Schleife über alle N Realisierungen des Payoffs bestimmt, was zwar ganz schön illustriert,
wie man Felder benutzt, in der Praxis aber wegen Speicherverschwendung vermieden würde.
Stattdessen würde man die Beziehung

Var(X) = E(X 2 ) − (E(X))2

238
10.6. Übung Nr. 4

Abbildung 10.3.: Typische Simulationsergebnisse bei Monte Carlo-Methoden, hier für einen
Call

ausnutzen und eine zusätzliche Variable estimatorXhoch2 in der inneren Schleife einführen.
Es ergibt sich etwa
Diese Grafik ist auch praktisch recht bedeutsam, da man hier z.B. erste Hinweise für die
Größenordnung der benötigten Iterationen erhält. Im Abschnitt (10.2) wird ebenfalls auf diese
Grafik eingegangen. Um mit einer Wahrscheinlichkeit von 1 − α = 0.999 eine Genauigkeit
von mindestens ε := 10−5 zu erreichen, betrachtet man die Länge des Konfidenzintervalls
sn
2z α2 √ ≤ ε
n
Wegen sn → σX (n → ∞) folgt asymptotisch
1 2
n ≥ 4z2α σ
2 ε2 X
wobei z α2 das α2 -Fraktil der Standardnormalverteilung ist.
Der Flaschenhals der Simulation ist die Erzeugung standard-normalverteilter Zufallszahlen,
wie folgende Laufzeiten bestätigen:

erzeuge 10 Mio Pfade 37,1 sec


erzeuge 10 Mio N(0,1) - verteilte Zahlen 36,2 sec
In der Praxis wird das Laufzeitverhalten fast immer von der Erzeugung geeigneter Zufalls-
zahlen dominiert. In diesem konkreten Fall ist die Inversion der Verteilungsfunktion Φ der mit
Abstand rechenintensivste Teil. Bei der Auswahl eines Diskretisierungsverfahrens ist somit
neben der Genauigkeit, die es theoretisch liefert, die Zahl der benötigten Zufallszahlen pro
Pfad eine zentrale Größe.

239
Besprechung der Übungsaufgaben

10.7. Übung Nr. 5


10.7.1. Teil A
Da das betrachtete Integral recht harmlos ist, unterscheiden sich der naive Monte Carlo Schätzer
und hit-or-miss- Monte Carlo in der graphischen Analyse nicht sehr deutlich:

Abbildung 10.4.: Vergleich der Simulation des hit-or-miss-Schätzers mit dem naiven Monte
Carlo-Schätzer

Immerhin erkennt man beim Standardfehler einen klaren Unterschied. Für 50000 Simula-
tionen erhält man

naiver MC-Schätzer 0,001279


hit or miss 0,002206
Das Programm ist selbsterklärend und befindet sich auf der CD.

10.7.2. Teil B
Auch dieses Programm ist nach den vorangegangenen Übungen sehr leicht und befindet sich
auf der CD. Als Grafik erhält man etwa

240
10.8. Übung Nr. 7

Abbildung 10.5.: Illustration des Gesetzes vom iterierten Logarithmus

10.8. Übung Nr. 7


Folgende Daten wurden verwendet:
Spot = 1.20;
T = 92.0/365.0;
Volatilität = 0.0935;
rd 1year = 0.0215;
rf 1year = 0.02;
Iterationen = 10000;
Man erhält etwa folgende Ergebnisse (abhängig von den verwendeten Zufallszahlen):

Strike StdErr MC StdErr AV


1.00 0.000399 0.000019
1.05 0.000398 0.000022
1.10 0.000388 0.000051
1.15 0.000340 0.000121
1.20 0.000237 0.000179
1.25 0.000122 0.000119
1.30 0.000047 0.000050

241
Besprechung der Übungsaufgaben

bzw graphisch:

Abbildung 10.6.: Vergleich des Antithetic Variates-Schätzers mit dem naiven Monte Carlo-
Schätzer
Man erkennt: Je linearer der Payoff, desto effizienter sind die Antithetic Variates. Wenn das
Problem schlecht gestellt ist, kann man aber sogar einen etwas schlechteren Schätzer erhalten.

10.9. Übung zu LRM und PM


Das Programm ist leicht, deshalb wollen wir es nicht weiter kommentieren. Als Ergebnis erhält
man

Man sieht, dass die Pathwise method die besseren Ergebnisse liefert, was durch den Schätzer
für den Standardfehler zusätzlich bestätigt wird. Für 100000 Iterationen ergibt sich

Pathwise method 0.556036


LRM 1.315004

10.10. Aufgabe zu Diskretisierung


Als Ausgabe in Excel erhält man etwa
und
Beide PC-Experimente bestätigen die theoretischen Resultate. Das Problem in der Praxis ist
nun, dass Payoff-Funktionen gewöhnlich nicht differenzierbar (Call) oder sogar Unstetigkeits-
stellen (Barrier-Optionen) haben. Die theoretischen Resultate für die Approximationsqualität

242
10.10. Aufgabe zu Diskretisierung

Abbildung 10.7.: Vergleich der Pathwise-method und der Likelihood-ratio-method

Abbildung 10.8.: Graphische Analyse der schwachen Konvergenzordnung des Eulerschemas

243
Besprechung der Übungsaufgaben

Abbildung 10.9.: Graphische Analyse der starken Konvergenzordnung des Eulerschemas

der schwachen Konvergenz beziehen sich aber stets auf Testfunktionen, die sehr glatt sind.
Man könnte nun z.B. versuchen, den Call-Payoff durch glatte Funktionen zu approximieren
und so die Resultate hinüberzuretten. Normalerweise wird man sich an der mangelnden Theo-
rie nicht stören und die Verfahren mit gegebener Vorsicht trotzdem anwenden.

10.11. Übungen zu C++


10.11.1. Übung Nr. 7
Wenn Sie keine Idee haben sollten, wie diese Aufgabe zu lösen ist, hier noch ein Hinweis:
Man bedenke, dass es auch Variablen außerhalb von Funktionen, hier der main-Funktion,
gibt: nämlich globale Variablen. Ferner wird ohne dass dies explizit hingeschrieben werden
muß zu Beginn und am Ende der Lebensdauer der Konstruktor bzw Destruktor eines Objekts
aufgerufen.
Das Programm besteht nun aus folgenden drei Teilen:

// Hauptprogramm.cpp
#include "example.h"

Example example;
void main(){

244
10.11. Übungen zu C++

// Example.h
class Example{
public:
Example();
~Example();
};

// Example.cpp
#include "example.h"
#include "iostream.h"
#include "conio.h"

Example::Example(){
cout << "Program start\n";
}

Example::~Example(){
cout << "Program end";
getch();
}

10.11.2. Übung Nr. 8


Die Aufgabe lässt sich leicht mit einer statischen Variablen lösen:

// Counter.h
class Counter{
public:
void display() const;
Counter();
~Counter();

private:
static int number;
};

// Counter.cpp
#include "Counter.h"
#include "stdio.h"

void Counter::display() const{


printf("There are %d active instances.",number);
}

245
Besprechung der Übungsaufgaben

int Counter::number = 0;

Counter::Counter(){
number++;
}

Counter::~Counter(){
number--;
}

// Hauptprogramm.cpp

#include "counter.h"

int main()
{
Counter test1,test2,test3;
test1.display();
return 0;
}

10.11.3. Übung Nr. 9


Hier verwendet man erneut eine statische Objektvariable und erstellt zusätzlich mit dem Schlüssel-
wort enum statische Konstanten.

// Option.h
class Option{
public:
static int TV_Quotation;
enum{FOREIGN, DOMESTIC};
void print() const;
};

// Option.cpp
#include "Option.h"
#include "stdio.h"

int Option::TV_Quotation = Option::FOREIGN;

void Option::print() const{


if(TV_Quotation == FOREIGN)
printf("Quotation scheme is foreign.\n");
else

246
10.11. Übungen zu C++

printf("Quotation scheme is domestic.\n");


}

// Hauptprogramm.cpp
#include "Option.h"
#include "conio.h"

void main()
{
Option option;
option.print();
option.TV_Quotation = Option::DOMESTIC;
option.print();
getch();
}

10.11.4. Übung Nr. 10


Hier gibt es nicht ’die’ Lösung, daher folgt lediglich ein Vorschlag:

class Y{

public:
int value;
Y(){value = 1;}
};

Der Einfachheit halber haben wir die Variable ’value’ in den public-Teil gesetzt, was man
praktisch natürlich nie machen würde. Hier soll aber gerade damit experimentiert werden. Die
Klasse X ist sehr leicht:

#include "stdio.h"

class X: public Y {
public:
void print(){ printf("%d",value);}

};

Und schließlich noch das Hauptprogramm:

#include "Y.h"
#include "X.h"

247
Besprechung der Übungsaufgaben

int main()
{
X x;
Y* ptr = &x;
ptr->value = 3;
x.print();

return 0;
}

10.11.5. Übung Nr. 11


Die Klassen X und Y sind völlig gleich aufgebaut, etwa so:

#include "stdio.h"
class X{
public:
void print(){printf("X::value : %d\n",value);}
int value;
};

Diese werden nun in eine Klasse Z vererbt:

#include "X.h"
#include "Y.h"

class Z: public X, public Y{


};

Im Hauptprogramm sieht man nun, dass es keine unmittelbare Variable ’value’ in der Klasse
Z gibt, sondern dass beide geerbte Variablen ’value’ und die zugehörigen print-Funktionen
koexistieren:

#include "Z.h"

int main()
{
Z z;
z.value = 3; // Fehler!!
z.Y::value =34;
z.Y::print();
return 0;
}

Das Überladen von Funktionen funktioniert beim Vererben übrigens nicht: Überladen kann
man immer nur auf einer festen Ableitungsebene.

248
10.11. Übungen zu C++

10.11.6. Übung Nr. 12


Bei dieser Aufgabe muß man lediglich daran denken, geeignete Operationen in der Initialisie-
rungsliste des Konstruktors durchzuführen und nicht im Anweisungsteil. Zunächst die Datei
’X.h’:

#include "stdio.h"
class X{
public:
const int x;
void print(){printf("X : %d\n",x);}
X(int a):x(a) { }
};

Vererbt man diese Klasse in eine Klasse Y, so muß der Konstruktor Y dafür Sorge tragen,
dass der X-Konstruktor korrekt aufgerufen wird:

class Y: public X{
public:
int y;
Y(int a): X(a){}
};

Im Hauptprogramm könnte man sich den Sachverhalt nochmals veranschaulichen mit:

#include "X.h"
#include "Y.h"

int main(){
X x(5);
x.print();
Y y(4);
y.print();
return 0;
}

10.11.7. Übung Nr.13


Für die Klassendeklarationen erhält man:
class Class1{
public:
int x;
void print();
Class1(int a);
};

249
Besprechung der Übungsaufgaben

Variationsmöglichkeiten hinsichtlich virtueller bzw nicht-virtueller Vererbung hat man nun


hier:

#include "class1.h"
class Class2:virtual public Class1{
public:
Class2(int a): Class1(a) {}
};

class Class3:virtual public Class1{


public:
Class3(int a): Class1(a){}

};

Der Mechanismus der virtuellen Vererbung wird nur wirksam, wenn in beiden Fällen virtuell
vererbt wird. Die Klassen Class2 und Class3 werden nun vererbt, wobei die übergebenen
Werte der Einfachheit halber fest vorgeschrieben werden:

class Class4: virtual public Class2, virtual public Class3{


public:
Class4():Class2(0),Class3(2), Class1(1){}
};

Im Hauptprogramm

#include "class4.h"

int main()
{
Class4 class4; // x hat nun Wert 1
class4.Class2::x = 2; // nun x = 2
class4.Class3::x = 3; // nun x = 3

class4.Class2::print();
class4.Class3::print();

return 0;
}

erhält man als Ausgabe

class 1 x : 3
class 1 x : 3

250
10.11. Übungen zu C++

Streicht man bei der Vererbung von Class1 an einer Stelle das Schlüsselwort virtual, so
erhält man:
class 1 x : 2
class 1 x : 3
Verstanden?

10.11.8. Übung Nr.14


Zunächst die Klassendeklarationen:
#include "stdio.h"

class Class1{
public:
virtual void f(){printf("Class 1\n");}
};

class Class2: public Class1{

public:
void f(){printf("Class 2\n");}

};
Das Hauptprogramm

#include "Class1.h"
#include "Class2.h"

int main(){
Class1 k1;
Class2 k2;
k1.f();
k2.f();
Class1& ref = k2;
ref.f();
Class1* ptr = &k2;
ptr->f();
Class1 k3 = k2;
k3.f();
return 0;
}

liefert als Ausgabe

251
Besprechung der Übungsaufgaben

Class 1
Class 2
Class 2
Class 2
Class 1

Mit anderen Worten: Der Mechanismus der virtuellen Funktionen wird nur wirksam, wenn
man auf das abgeleitete Objekt mit einem Pointer oder einer Referenz zugreift, es genügt
nicht, das abgeleitete Objekt in ein Basisklassenobjekt zu kopieren.

10.11.9. Übung Nr.15


Das Programm liefert

A1

und zeigt, wie komplex die Aufrufe werden können, wenn man ohne Not multiple Vererbung
benutzt. Die Idee ist hier, dass man auf ein Objekt mit einem Basisklassenpointer zugreift, so
dass also die ’neueste’ Version der virtuellen Funktion aufgerufen wird. Dies ist aber die aus
A1.

252
Literaturhinweise
Der Leser, der sich in den in diesem Buch angesprochenen Themenfeldern weiter vertiefen
möchte, findet hier unsere (persönliche!) Meinung zu einigen Büchern, die uns besonders
relevant erscheinen.

Zur Programmiersprache C:
1. ”Programmieren in C”, Kernighan / Ritchie, 1990
Das ist das Standard - Lehrbuch zur Programmiersprache C, geschrieben von den Ent-
wicklern der Sprache. 180 anspruchsvolle, aber dennoch gut lesbare Seiten zuzüglich
Anhang.

2. ”Die Programmiersprache C. Ein Nachschlagewerk.” Regionales Rechenzentrum für


Niedersachen / Universität Hannover
Kurz und knapp stellt dieses Buch auf 140 Seiten alles vor, was man im Alltag in C
braucht. Dieses Buch möchten wir besonders empfehlen.

Zur Programmiersprache C++:

• ”Die C++ Programmiersprache”, Bjarne Stroustrup, Addison Wesley.


In diesem Buch stellt der Erfinder von C++ den Einsatz der Sprache in der Praxis vor,
geht auf Programmdesign und Entwicklung ein und liefert im Anhang eine Sprachre-
ferenz. Gesamtumfang gut 700 Seiten, die definitiv alles zum Thema C++ liefern, aber
für den Anfang vielleicht etwas schwer verdaulich sind. Zitat: ”[...] richtet sich das
Buch vorwiegend an erfahrene Programmierer, deren Intelligenz und Erfahrung nicht
beleidigt werden soll.” Für Fortgeschrittene sehr zu empfehlen.

• ”C++ für C - Programmierer”, Regionales Rechenzentrum für Niedersachen / Univer-


sität Hannover
Gewohnt prägnant erfährt man auf 130 Seiten alles, was man über objektorientierte
Programmierung mit C++ wissen sollte. Wieder gilt: Besonders empfehlenswert!

• N. Josuttis, ”The C++ Standard Library: A Tutorial and Reference”, Edison Wesley,
1999
Hier findet man eine ausführliche Darstellung der C++-Standardbibliothek und der Stan-
dard Template Library (STL).

Zu Monte Carlo:

253
Literaturhinweise

• Monte Carlo Methods in Financial Engineering, Paul Glasserman


Das Standardlehrbuch zu Monte Carlo, gut lesbar, aber oft mit leichter Hand geschrieben
was die mathematische Genauigkeit angeht. Wer Monte Carlo vor allem in der Praxis
anwenden will ist mit diesem Buch bestens beraten.

• Monte Carlo Methods, Hammersley and Handscomb


Dies ist ein noch heute sehr gut lesbares und dazu sehr kurzes Buch, das den Entwick-
lungsstand von Monte Carlo im Jahr 1964 beschreibt. Die Grundlagen haben sich bis
heute kaum verändert, nur die beschriebenen Anwendungen sind im Vergleich zu heute
weniger komplex. Leider wird das Buch nicht mehr gedruckt und es ist entsprechend
schwer zu finden, aber es lohnt sich.

• Numerical solution of Stochastic Differential Equations, Kloeden, Platen


Sehr umfassende Darstellung verschiedener Verfahren zur Lösung stochastischer Dif-
ferentialgleichungen und ihrer Approximationsgüte. Das Standardwerk in diesem Be-
reich. Für Fortgeschrittene.

Einbindung in Microsoft Excel

• Excel Add-in Development in C/C++ : Applications in Finance


(The Wiley Finance Series), Steve Dalton
Ein empfehlenswertes Buch, wenn Sie über die Hinweise des Kurses hinausgehende
Informationen benötigen sollten.

Quellen im Internet

• Komplette Bibliotheken und Sourcecode


Dies hört sich vielleicht verlockender an, als es dann nachher ist, weil diese Projekte
teilweise doch sehr ausgereift und entsprechend komplex zu verwenden sind. Dennoch
sollten Sie sich die folgenden Seiten einmal angeschaut haben:

– http://www.boost.org/
Sammlung portierbarer C++-Bibliotheken.
– http://quantlib.org/
Dies ist eine open-source - library speziell für quantitative finance.
– http://sourceforge.net/
Nach eigenem Bekunden die weltweit größte Fundgrube für open-source-Code.

• Weitere Links

– ”The Premier Online Journal for the C++ Community”.


http://www.artima.com/cppsource/index.html

254
– C++ Standards Committee
http://www.open-std.org/jtc1/sc22/wg21/
– Association of C / C++ Users
http://www.accu.org/

255
Literaturhinweise

256
Literaturverzeichnis
[1] Leif B. Andersen. Monte carlo simulation of barrier and lookback options with conti-
nuous or high-frequency monitoring of the underlying asset. 1996. Preliminary Version.

[2] William H. Press et al. Numerical Recipes in C++, The Art of Scientific Computing.
Cambridge University Press, 2 edition, feb 2002.

[3] Erich Gamma, Richard Helm, Ralph Johnson, and John Vlissides. Design Patterns - Ele-
ments of Reusable Object-Oriented Software. Addison Wesley Professional, july 1997.

[4] Paul Glasserman. Monte Carlo Methods in Financial Engineering. Springer Verlag,
Berlin, Heidelberg, New York, 1 edition, 2004.

[5] J.M. Hammersley and D.C. Handscomb. Monte Carlo Methods. London: Methuen &
Co LTD, London, 1964.

[6] Mark Joshi. C++ Design Patterns and Derivatives Pricing. Cambridge University Press,
Cambridge, 2 edition, 2005.

[7] Nicolai M. Josuttis. The C++ Standard Library: A Tutorial and Reference. Addison
Wesley Professional, aug 1999.

[8] Uwe Wystup Jürgen Hakala. Foreign Exchange Risk:Models,Instruments and Strategies.
Risk Books, oct 2001.

[9] Brian W. Kernighan and Dennis M. Ritchie. Programmieren in C. Carl Hanser Verlag,
München, Wien, 2 edition, 1990.

[10] Peter E. Kloeden and Eckhard Platen. Numerical Solution of Stochastic Differential
Equations. Springer Verlag, Berlin, Heidelberg, New York, 3 edition, 1999.

[11] Jan Vecer. A new pde approach for pricing arithmetic average asian options. Journal of
Computational Finance, 4, No.4:105 – 113, 2001.

257
Literaturverzeichnis

258
Abbildungsverzeichnis
1.1. Typischer Funktionsverlauf beim Call . . . . . . . . . . . . . . . . . . . . . 22

4.1. Korrelation und Effizienz der Kontrollvariable in Abhängigkeit von Kontrakt-


daten . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 91

5.1. Finite Differenzen-Approximation für das Delta beim Call und beim europäischen
Up and Out Call für verschiedene Schrittweiten h . . . . . . . . . . . . . . . 107

6.1. Ergebnis des naiven Schätzers für die Up and out Digital Option . . . . . . . 117

9.1. Schema zum Zusammenspiel von Pricingfunktion und (abgeleiteten) Optionen 177
9.2. Erweiterung der Klasse Payoff zu einer Klasse Option mit Hilfe einer Klasse
PathIndependent . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 181
9.3. Verbessertes Zusammenspiel mit virtual copy constructor . . . . . . . . . . . 186
9.4. Schema zum Entwurfsmuster der Brücke für den Payoff . . . . . . . . . . . . 187
9.5. Klassenentwurf für im Zeitablauf variable Parameter . . . . . . . . . . . . . 191
9.6. Schema zum StatisticsGatherer . . . . . . . . . . . . . . . . . . . . . . . . . 200
9.7. Übersicht zur PricingEngine . . . . . . . . . . . . . . . . . . . . . . . . . . 210
9.8. Übersicht zum Zusammenspiel der Klassen . . . . . . . . . . . . . . . . . . 211

10.1. Fehler bei der finite Differenzen-Approximation . . . . . . . . . . . . . . . . 236


10.2. Typischer Volatility-Smile beim Call . . . . . . . . . . . . . . . . . . . . . . 238
10.3. Typische Simulationsergebnisse bei Monte Carlo-Methoden, hier für einen Call 239
10.4. Vergleich der Simulation des hit-or-miss-Schätzers mit dem naiven Monte
Carlo-Schätzer . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 240
10.5. Illustration des Gesetzes vom iterierten Logarithmus . . . . . . . . . . . . . 241
10.6. Vergleich des Antithetic Variates-Schätzers mit dem naiven Monte Carlo-Schätzer242
10.7. Vergleich der Pathwise-method und der Likelihood-ratio-method . . . . . . . 243
10.8. Graphische Analyse der schwachen Konvergenzordnung des Eulerschemas . 243
10.9. Graphische Analyse der starken Konvergenzordnung des Eulerschemas . . . 244

259
Abbildungsverzeichnis

260
Index

Überladen von Operatoren, 178 multiple Vererbung, 25, 44

abgeleitete Klasse, 43 Option


Ableitung, 25 Asiatische Option, 91
abstrakte Klasse, 54 Lookback Put, 119
abstrakter Datentyp, 54 Up-and-in Cash-at-Hit, 118
API, 224 Up-and-out Digital, 117
owl, 224
Basisklasse, 25, 43
Punktschätzer, 76
C
Funktionsdefinition, 11 Rahmenwerk, 171
Funktionsdeklaration, 11 rein virtuelle Funktion, 54
Call by reference, 13, 15 roundoff error, 133
Call by value, 13
score function, 103
Design pattern, 166 Standard Template Library, 58

function-object, 178 templates, 57


functor, 178 truncation error, 133

Geometrische Brownsche Bewegung Varianzreduktion


running maximum, 115 Antithetic Variates, 93
Control Variates, 88
Haltepunkte, 141 Tractable Options, 91
bedingte, 141 Underlying Assets, 90
VCL, 224
information hiding, 46, 152 Vererbung, 25, 43
Klassenhierarchie, 25 virtual copy constructor, 182
Klassentemplate, 59 Zufallsgenerator
Likeliihood Ratio Method, 102 linear congruential generators, 78
Score function, 103 Mersenne Twister, 79

Maschinengenauigkeit, 134
Mean Square Error, 76
Methodenprotokoll, 24
MFC, 224

261

You might also like