Professional Documents
Culture Documents
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
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
v
Inhaltsverzeichnis
V. Anhang 233
vi
Inhaltsverzeichnis
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.
II Funktion1 {
I lokale Variablen
III Anweisungen
}
Funktion2 {
s.o.
}
3
1. Programmieren in C
Bemerkung
In C sind Strings kein direkter Bestandteil der Sprache, sondern sind als Felder von char zu
bilden.
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.
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
Beachte:
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:
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.
Bsp.:
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
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;
// ...
}
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:
Bem.:
Nach } kommt kein Strichpunkt! Strichpunkte kommen normalerweise am Ende eines Be-
fehls.
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
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
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
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!
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:
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.
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;
// ...
}
10
1.6. Include - Anweisungen für Bibliotheken
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.
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 Hauptprog.cpp-------
#include Arbeitstier.h
void main(){
int ergebnis = quadriere( 5 );
}
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)
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.
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.
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.
14
1.7. Felder und Zeiger
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
Bemerkungen
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.
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.
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.
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
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.
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(){
if(var1 == var3)
printf("Vergleich richtig!\n");
else
printf("Vergleich falsch!\n");
}
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(){
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
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:
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
19
1. Programmieren in C
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
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
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
Aufgabe 2. Schreiben Sie eine Funktion zur Berechnung der implied vol und verwenden Sie
dazu das Newtonverfahren. Beachten Sie dabei folgende Punkte:
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
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%.
21
1. Programmieren in C
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:
• welche Funktionen greifen warum und wie auf bestimmte Variablen zu?
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... |
}; _|
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:
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...
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)
+ 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
Also kurz: In C beginnt ein beliebig langer Kommentar mit /* und endet mit */. In C++
wurde eine weitere Möglichkeit eingeführt, nämlich
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.
// später...
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
29
2. Objektorientierte Programmierung
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
class CallOption{
private: // Internes
double strike_, vol_;
};
30
2.3. Klassen
// Datei CallOption.cpp
// = Implementation der Schnittstelle
#include "CallOption.h"
double CallOption::getStrike(){
return strike_;
}
// Datei Hauptprogramm.cpp
#include "CallOption.h"
int main(){
printf(sstrike : %lf",callPtr->getStrike() );
// Aufruf einer Memberfunktion von call über Pointer
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.
32
2.3. Klassen
}
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.
ä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:
#ifndef FINANCE_MARKET_H
#define FINANCE_MARKET_H
class Market{
public:
inline double getSpot() const;
// inline und const sind hier typisch
33
2. Objektorientierte Programmierung
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
#include "Market.h"
#include sstdio.h"
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();
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
class CallOption{
public:
bool setStrike(double strike);
inline double getStrike() const;
double tv() const;
private:
double strike_, vol_;
};
// Hauptprogramm
#include "CallOption.h"
void main()
36
2.3. Klassen
{
CallOption call(30); // Aufruf des 2.Konstruktors
// CallOption.cpp
#include "CallOption.h"
#include sstdio.h"
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.
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){
// ...
}
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{
//...
}
// im Hauptprogramm:
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
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 ”=”:
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:
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:
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
// Defaults.cpp
#include "Defaults.h"
// CallOption.h
#include "Defaults.h"
class CallOption{
// ...
private:
Defaults::Style style_; // in Ordnung, obwohl es keine
// Instanz der Klasse Defaults gibt!!
}
// CallOption.cpp
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
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"
// usw
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.
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.
// Vanilla.h
class Vanilla{
public:
inline double getStrike() const;
double tv() const; // (*)
private:
double strike_, vol_;
};
// SingleBarrier.h
44
2.3. Klassen
// SingleBarrier.cpp
// 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
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.
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.
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.
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.
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.
Objektname.mizeKlassenname::Komponente
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.
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
// SingleBarrier.h
// SingleBarrier.cpp
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
↑ ↑ ↑
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(){}
};
Klasse2 K2;
Klasse1 *PtrK1 = &K2; // schaue K2 durch die Brille der
// der Basisklasse an
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
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
class Klasse1{
public:
virtual void f()=0;
};
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.
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{
};
class Impl1{
public:
void f_impl1();
};
class Impl2{
54
2.4. Übungen
public:
void f_impl2();
};
public:
void f();
};
EndProdukt::f(){
2.4. Übungen
Aufgabe 4. Modifizieren Sie das Programm
void main() {
derart, dass es folgenden Text ausgibt und auf eine Benutzereingabe wartet:
Programmstart.
Programmende.
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.
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:
// 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
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
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;
};
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:
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.
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){
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){
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
• 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.
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
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.
pair<double, double> p;
// Datenausgabe
cout << "First: " << p.first
<< ssecond: " << p.second << endl;
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
vec.begin()
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()
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;
vec.clear();
Das folgende Beispiel zeigt, wie man die Algorithmen der STL verwenden kann, um einen
Vektor nach benutzerdefinierten Kriterien zu sortieren:
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.
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:
vec.clear();
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.
*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.
71
3. Monte Carlo-Methoden
3.0. Prolog
Berechne den theoretical value (im folgenden kurz TV) einer diskreten Barrieroption mit
Payoff :
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
Beweis. Klar.
Diese Aussage liefert bereits das übliche Kochrezept für einen naiven Monte-Carlo-Schätzer:
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
75
3. Monte Carlo-Methoden
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
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.
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]].
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:
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:
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
i) Die Periode des Generators beträgt die praktisch derzeit unerreichbare Zahl von
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 .
iv) Die Implementation des Algorithmus kommt ohne zeitaufwendige Multiplikationen und
Divisionen aus. Der fertige Generator ist also insbesondere schneller als die meisten
anderen Generatoren.
Beweis.
Bemerkung 3.1.1.
79
3. Monte Carlo-Methoden
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.
(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 )
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
σ2
St = S0 exp (µ − )t + σWt
2
σ2 √
Ŝti+1 = Ŝti exp µ− (ti+1 − ti ) + σ ti+1 − ti Zi+1 , i = 0...n−1 (3.5)
2
σ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
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 )
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
σ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
82
3.2. Übungen
3. erzeuge Pfade
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
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.
1 n
X̄n = ∑ f (Ui)
n i=1
85
4. Techniken zur Varianzreduktion
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, Θ)
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
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
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
gilt:
i) Der Schätzer Ȳ (b) ist erwartungstreu (engl. unbiased) und stark konsistent
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
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.
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)
Bemerkung 4.2.1.
• 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
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 ] ω
n −→ N (0, 1)
sn (b̂n )
92
4.3. Antithetic Variates
2. Bestimme X̄, Ȳ
3. Bestimme b̂∗
4. Bestimme sn (b̂∗ )
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
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
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
F −1 (1 −U) = −F −1 (U)
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
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
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:
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.
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.
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
101
5. Griechen in Monte-Carlo
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
(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
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
104
5.2. Likelihood Ratio Method (LRM)
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.
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
f (θ ) = E(X(θ ))
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
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:
5.4.2. Fazit
Die Pathwise method liefert die besten Ergebnisse, sofern sie anwendbar ist:
Die Likelihood-ratio-method ist wichtig, wenn die Pathwise method nicht anwendbar ist, aber:
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
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
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.
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
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
X̂0 = X0
√
X̂ti+1 = X̂ti + a(X̂ti ) (ti+1 − ti ) + b(X̂ti ) ti+1 − ti Zi+1
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
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
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.
113
6. Diskretisierung von Stochastischen Differentialgleichungen
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)
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
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).
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)
116
6.4. Pfadabhängige Payoffs und Diskretisierung
Beispiel 6.4.1. Wir betrachten eine Up-and-out Digital Option mit Payoff
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
Ui ∼ U [0, 1], i = 0, . . . , N − 1
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)
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:
118
6.4. Pfadabhängige Payoffs und Diskretisierung
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
mit
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
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.
Mt := max Su
0≤u≤T
121
6. Diskretisierung von Stochastischen Differentialgleichungen
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
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
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
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”)
σ12 τ1
(6.41)
σ22 τ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 τ)
τ
1 n
Ẑn := ∑ Zi
n i=1
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:
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 )
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.
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)
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.
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
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
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
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
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.
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.
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 .
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
( 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
136
7.2. Effiziente Implementation von Algorithmen
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:
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]].
#include <iostream>
#include <math.h>
#include <time.h>
#include <stdio.h>
137
7. Numerische Stabilität und Effizienz, Fehlerbehandlung, Bezeichnungen
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
- 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!
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.
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
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).
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), 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
class Exception {
public:
Exception(int errnum, const char *msg);
142
7.4. Fehlerbehandlung
private:
// Speicher fuer die Fehlernachricht
char msg_[128];
// Speicher fuer den Fehlercode
int errno_;
};
#endif
// SingleBarrier.cpp
#include "Exception.h"
#include "Defaults.h"
// Hauptprogramm.cpp
void main(){
143
7. Numerische Stabilität und Effizienz, Fehlerbehandlung, Bezeichnungen
Bemerkung 7.4.1.
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:
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
class 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.
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
}
namespace MathFinance{
void f();
}
// später...
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
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:
7.6. Übungen
Aufgabe 13. Modifizieren Sie das Programm
void main() {
derart, dass es folgenden Text ausgibt und auf eine Benutzereingabe wartet:
Programmstart.
Programmende.
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 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.
// 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.-1.2. Allgemein
i) Typnamen beginnen mit einem Großbuchstaben, dann gemischte Schreibweise:
class Exception;
class SavingsAccount;
Exception exception;
SavingsAccount savingsAccount;
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();
149
7. Numerische Stabilität und Effizienz, Fehlerbehandlung, Bezeichnungen
vi) Variablen haben den gleichen Namen wir ihr Typ, wenn sie keine tiefere Aufgabe haben
( generic variable“), etwa
”
void setTopic( Topic* topic);
Nicht aber:
Dadurch wird die Vielfalt der Ausdrücke und somit die Komplexität des Programms
etwas reduziert.
vii) Alle Bezeichner in englischer Sprache.
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();
150
7.7. Bezeichnungskonventionen und Layout
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.
#ifndef BEZEICHNER_H
#define BEZEICHNER_H
#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.
ii) Nicht-triviale Typumwandlungen stets explizit durchführen, damit der Compiler weder
einen Fehler noch eine Warnung ausgibt, d.h.
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.
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.
while( !done ){
doSomething();
}
void someMethod(){
int x;
}
if( Bedingung ){
//...
} else if( Bedingung ) {
//...
} else {
//..
}
iii) Leerzeichen sollten nach Bezeichnern, Operatoren wie +, -,*,/ eingesetzt werden, um
die Lesbarkeit zu erhöhen.
7.-1.4.6. Kommentare
i) Klarer Code sollte Kommentare weitgehend überflüssig machen.
tricky code should not be commented but rewritten!
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:
154
Teil IV.
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
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:
};
} // 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 {
158
8.1. Programm
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{
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.
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
};
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{
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;
};
161
8. Ein einfaches Klassenrahmenwerk aus der Praxis
namespace MathFinance_Project{
class FinancialProduct{
};
ii) Dann leiten wir eine Klasse Option ab, die z.B. die Laufzeit enthält.
namespace MathFinance_Project {
public:
Option(double maturity,int underlying,
int fd ) throw(Exception);
inline double getMaturity() const;
inline int getUnderlying() const;
inline virtual int get_fd() const;
protected:
private:
double maturity_; // time in years
Defaults::Underlying underlying_;
int fd_; // notional in foreign or domestic currency
};
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 {
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{
public:
163
8. Ein einfaches Klassenrahmenwerk aus der Praxis
private:
double strike_;
int phi_;
double notional_;
};
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.
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:
protected:
164
8.1. Programm
};
Fast alle Funktionen sind rein virtuelle Funktionen, entsprechend ist das cpp-file sehr
kurz:
}
}
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{
public:
VanillaPricingEngineAnalytic(
const Vanilla* const financialProduct,
const FX_Market* const fxmarket);
private:
const Vanilla* const finProd_; // financial product
165
8. Ein einfaches Klassenrahmenwerk aus der Praxis
};
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.
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;
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){
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{
public:
VanillaPricer(double spot,
double atmVol,
double rd,
double rf,
double strike,
int phi,
double maturity,
double notional,
int underlying,
int fd);
168
8.1. Programm
private:
Vanilla option_;
VanillaPricingEngineAnalytic pricingEngine_;
};
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.
#include "helperFunctions.h"
#include "stdio.h"
#include "math.h"
171
9. Design Patterns und ein verbessertes Rahmenwerk
double expiry;
double strike;
double spot;
double vol;
double r;
unsigned long numberOfPaths;
172
9.1. Flexibilität als zentrales Problem eines Rahmenwerks
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
• Das Programm ist nicht schnell genug, daher Anwendung von antithetic sampling
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.
174
9.2. Design Patterns - ein kurzer Überblick
Bevor wir den Programmcode mit diesem Rüstzeug ausgestattet optimieren, formulieren wir
zwei zentrale Voraussetzungen wiederverwendbaren Codes:
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.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:
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);
// ...
// ...
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:
};
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"
//...
}
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;
// usw
PayOffCall callPayOff(Strike);
double resultCall =
SimpleMonteCarlo2(callPayOff, expiry, spot, vol,
r, numberOfPaths);
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
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.
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
class PayOff
{
public:
PayOff(){};
private:
};
PayOffCall(double strike);
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:
183
9. Design Patterns und ein verbessertes Rahmenwerk
private:
double expiry_;
PayOff* thePayOffPtr_;
};
#endif
Die Implementation ist bis auf den operator= sehr naheliegend:
#include <PathIndependent.h>
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.
PayOffCall thePayoff(strike);
PathIndependent option(thePayoff, expiry);
185
9. Design Patterns und ein verbessertes Rahmenwerk
// Ausgabe
}
Im Ergebnis haben wir nun folgende Struktur realisiert: Die Klasse PathIndependent kann
nun also mit beliebigen pfadunabhängigen Payoffs arbeiten, die wir vorher erstellt haben.
Vorhandener Code kann somit verwendet werden.
186
9.3. Ein verbessertes Rahmenwerk
Abbildung 9.4.: Schema zum Entwurfsmuster der Brücke für den Payoff
187
9. Design Patterns und ein verbessertes Rahmenwerk
private:
PayOff* thePayOffPtr_;
};
#include<PayOffBridge.h>
PayOffBridge::~PayOffBridge()
{
delete thePayOffPtr_;
}
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:
private:
double expiry_;
PayOffBridge thePayOff_;
};
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
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(){}
190
9.3. Ein verbessertes Rahmenwerk
191
9. Design Patterns und ein verbessertes Rahmenwerk
private:
};
class Parameters{
public:
private:
ParametersInner* innerObjectPtr_;
};
private:
double constant_;
double constantSquare_;
192
9.3. Ein verbessertes Rahmenwerk
};
Und hier die Datei Parameters.cpp:
#include <Parameters.h>
Parameters::~Parameters(){
delete innerObjectPtr_;
}
ParametersConstant::ParametersConstant(double constant)
{
constant_ = constant;
constantSquare_ = constant*constant;
193
9. Design Patterns und ein verbessertes Rahmenwerk
Die Änderungen in der Implementation sind offensichtlich, wobei in dieser Funktion ein Auf-
ruf wie
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(){
ParametersConstant volParam(vol);
ParametersConstant rParam(r);
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.
Wrapper()
{ dataPtr_ =0;}
195
9. Design Patterns und ein verbessertes Rahmenwerk
dataPtr_ = inner.clone();
}
~Wrapper()
{
if (dataPtr_ !=0)
delete dataPtr_;
}
return *this;
}
T& operator*()
{
return *dataPtr_;
}
196
9.3. Ein verbessertes Rahmenwerk
T* operator->()
{
return dataPtr_;
}
private:
T* dataPtr_;
};
#include <vector>
class Statistics
{
public:
Statistics(){}
};
197
9. Design Patterns und ein verbessertes Rahmenwerk
StatisticsMean();
virtual void dumpOneResult(double result);
virtual std::vector<std::vector<double> > getResultsSoFar() const;
virtual Statistics* clone() const;
private:
double runningSum_;
unsigned long pathsDone_;
};
StatisticsMean::StatisticsMean()
:
runningSum_(0.0), pathsDone_(0)
{
}
results[0].resize(1);
198
9.3. Ein verbessertes Rahmenwerk
return results;
}
Die Anwendung in SimpleMonteCarlo ist trivial, wir geben daher nur die neue Deklaration
an:
int main(){
// Abfrage der Daten
// ...
StatisticsMean statistics;
SimpleMonteCarlo5( TheOption,
spot,
vol,
r,
numberOfPaths,
statistics );
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.
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:
200
9.3. Ein verbessertes Rahmenwerk
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:
if (pathsDone_ == stoppingPoint_){
stoppingPoint_ *= 2;
std::vector<std::vector<double> >
thisResult(inner_->getResultsSoFar());
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());
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.
#include <Arrays.h>
class RandomBase
{
public:
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.
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
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:
205
9. Design Patterns und ein verbessertes Rahmenwerk
{
return new AntiThetic(*this);
}
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::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.
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);
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;
PayOffCall thePayOff(Strike);
ParametersConstant VolParam(vol);
ParametersConstant rParam(r);
StatisticsMean gatherer;
ConvergenceTable gathererTwo(gatherer);
RandomMT generator(1);
AntiThetic GenTwo(generator);
208
9.4. Ein flexibler Monte-Carlo Option Pricer
GenTwo);
return 0;
}
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:
210
9.4. Ein flexibler Monte-Carlo Option Pricer
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-
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
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;
class PathDependent
{
public:
virtual ~PathDependent(){}
212
9.4. Ein flexibler Monte-Carlo Option Pricer
private:
MJArray lookAtTimes_;
};
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:
generatedFlows[0].timeIndex = 0UL;
generatedFlows[0].amount = thePayOff_(mean);
return 1UL;
}
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:
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:
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());
}
thesecashFlows.resize(theProduct_->maxNumberOfcashFlows());
double thisValue;
return;
}
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
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:
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.
217
9. Design Patterns und ein verbessertes Rahmenwerk
theGenerator_->getGaussians(variates_);
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_);
drifts_[0] = r.integral(0.0,times[0]) -
d.integral(0.0,times[0]) - 0.5 * variance;
standardDeviations[0] = sqrt(variance);
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()
{
PayOffCall thePayOff(strike);
MJArray times(numberOfDates);
ParametersConstant volParam(vol);
ParametersConstant rParam(r);
ParametersConstant dParam(d);
StatisticsMean gatherer;
ConvergenceTable gathererTwo(gatherer);
RandomMT generator(numberOfDates);
AntiThetic GenTwo(generator);
219
9. Design Patterns und ein verbessertes Rahmenwerk
theEngine.doSimulation(gathererTwo, numberOfPaths);
{
for (unsigned long i=0; i < results.size(); i++)
{
for (unsigned long j=0; j < results[i].size(); j++)
cout << results[i][j] << " ";
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.
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.
222
10.1. Erstellung von DLLs
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"
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.
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
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.
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:
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...");
226
10.1. Erstellung von DLLs
// 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
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
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.
227
10. Ausblick: Nützliches für den Alltag
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.
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:
EXCEL_BEGIN
228
10.2. Interview mit einem Quant
EXCEL_END
}
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:
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:
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
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.
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.
237
Besprechung der Übungsaufgaben
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:
239
Besprechung der Übungsaufgaben
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
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
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.
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
242
10.10. Aufgabe zu Diskretisierung
243
Besprechung der Übungsaufgaben
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.
// 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();
}
// Counter.h
class Counter{
public:
void display() const;
Counter();
~Counter();
private:
static int number;
};
// Counter.cpp
#include "Counter.h"
#include "stdio.h"
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;
}
// Option.h
class Option{
public:
static int TV_Quotation;
enum{FOREIGN, DOMESTIC};
void print() const;
};
// Option.cpp
#include "Option.h"
#include "stdio.h"
246
10.11. Übungen zu C++
// Hauptprogramm.cpp
#include "Option.h"
#include "conio.h"
void main()
{
Option option;
option.print();
option.TV_Quotation = Option::DOMESTIC;
option.print();
getch();
}
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);}
};
#include "Y.h"
#include "X.h"
247
Besprechung der Übungsaufgaben
int main()
{
X x;
Y* ptr = &x;
ptr->value = 3;
x.print();
return 0;
}
#include "stdio.h"
class X{
public:
void print(){printf("X::value : %d\n",value);}
int value;
};
#include "X.h"
#include "Y.h"
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++
#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){}
};
#include "X.h"
#include "Y.h"
int main(){
X x(5);
x.print();
Y y(4);
y.print();
return 0;
}
249
Besprechung der Übungsaufgaben
#include "class1.h"
class Class2:virtual public Class1{
public:
Class2(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:
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;
}
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?
class Class1{
public:
virtual void f(){printf("Class 1\n");}
};
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;
}
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.
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.
• 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
Quellen im Internet
– 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
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
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
259
Abbildungsverzeichnis
260
Index
Maschinengenauigkeit, 134
Mean Square Error, 76
Methodenprotokoll, 24
MFC, 224
261