Rozdział 2 Funkcje, pętle i tablice 2.1 Funkcje - na przykładzie wyceny europejskiej opcji kupna Aby zastosować nasz wzór do wyceny opcji (1.2) z Sekcji 1.2 musimy w szczególności umieć obliczyć w VBA dwumian Newtona dla 0 k N ( N k ) = N! k! (N k)!. (2.1) Wygodnie nam będzie również ustawić się tak żeby z łatwością się odwoływać do wzoru, bez potrzeby jego przepisywania. Do tego posłuży nam napisana za chwilę przez nas funkcja którą nazwiemy DwumianN. 2.1.1 Pierwsza funkcja Nasze obliczenia dwumianu Newtona będą bazować na tym że dla 1 k N wzór (2.1) można przepisać w postaci ( N k ) (N k + 1)(N k + 2)... N = 1 2 3... (k 2)(k 1)k k N k + j =, j j=1 17
18 ROZDZIAŁ 2. FUNKCJE, PĘTLE I TABLICE dla k = 0 zawsze natomiast mamy ( N k ) = 1. Wypiszmy najpierw fragment kodu dla naszej funkcji a później krok po kroku omówimy jego poszczególne części. Poniższy fragment kodu umieścimy zaraz za procedurą PobierzDane w nowym pliku 03 Funkcje.xls. Poniżej wypiszemy interesujący nas w tej chwili fragment pliku zawierający funkcję. Function DwumianN(N As Integer, k As Integer) As Integer Dim i As Integer Dim prod As Integer prod = 1 For i = 1 To k DwumianN = prod Przyjrzyjmy się teraz naszej funkcji: W pierwszej linijce DwumianN jest nazwą funkcji. Funkcja ta ma dwa argumenty N oraz k. Obie te wartości są liczbami naturalnymi wobec czego dodana jest deklaracja N As Integer, k As Integer. Funkcja zwraca wartości naturalne. Jest to sprecyzowane przez dodanie As Integer pod koniec linijki. W linijkach Dim i As Integer Dim prod As Integer Deklarujemy robocze (lokalne) zmienne które będą potrzebne w obliczeniach. Zmienna i do indeksowania, oraz prod do obliczania produktu. Następnie mamy pętlę For i = 1 To k... Polecenie wewnątrz pętli będzie wykonane k razy.
2.1. FUNKCJE - NA PRZYKŁADZIE WYCENY EUROPEJSKIEJ OPCJI KUPNA19 Końcowa linijka DwumianN = prod zwraca wartość funkcji. Kod zawarty wewnątrz funkcji zawsze jest wykonywana do konca, o ile jej po drodze nie wstrzymamy. Oznacza to że jeśli na końcu funkcji wpisalibyśmy DwumianN = 100 to funkcja zwróci nam 100 a nie prod. Czasem fakt że funkcja doczytuje do końca nie jest pożądany. Czasami chcielibyśmy żeby funkcja w danym momencie zakończyła działanie. Można to łatwo osiągnąć za pomocą polecenia Exit Function. Przykładowo, wstawienie linijek If k > N Or k < 0 Then DwumianN = 0 Exit Function End If na samym początku funkcji pozwoli nam wyłapać sytuację w której k > N lub k < 0 i w takim przypadku zwrócić wartość 0 zamiast doliczać do końca. Zauważmy na końcu że jedna ze zmiennych w funkcji, mianowicie N, została poprzednio przez nas zarezerwowana jako zmienna która przechowuje czas końcowy w naszym modelu. Naturalnym pytaniem jest to czy nie spowoduje to dla nas pewnych komplikacji. Powiedzmy że przed wypisaniem funkcji mamy deklarację zmiennej N Dim N as Integer N = 10 A później wywołamy funkcję DwumianN(5,2). Czy oznacza to że VBA wykonując kod z funkcji DwumianN w miejsce występującej w nim zmiennej N będzie wstawiał 10? Okazuje się że tak nie będzie. Ponieważ N jest jednym z argumentów funkcji, na potrzeby kodu wykonywanego wewnątrz funkcji będzie ona przyjmowała wartość 5. Wewnątrz funkcji N jest zmienną lokalną. Gdyby natomiast nasza funkcja została zapisana jako Function DwumianN(Dim k as integer)
20 ROZDZIAŁ 2. FUNKCJE, PĘTLE I TABLICE identyczny kod jak poprzednio ( ) 10 to po wywołaniu DwumianN(2) zostało by obliczone Ponieważ za N 2 wstawiona by została wcześniej wczytana zmienna globalna. Zmienne lokalne można również definiować wewnątrz funkcji i będą one traktowane rozdzielnie ze zmiennymi globalnymi. Przykładowo dla poniższego fragmentu kodu So = 100 Function jedynka() Dim So As Integer So = 1 jedynka = So Range("D1").Value = jedynka Range("D2").Value = So po jego wykonaniu w komórce D1 pojawi się 1 a w komórce D2 pojawi się 100. Wartość So nie zostanie zmieniona ponieważ So została zadeklarowana jako zmienna lokalna przez wpisanie linijki Dim So As Integer wewnątrz funkcji. Uwaga! gdyby tej linijki nie było to w obu komórkach pojawiło by się 1, gdyż funkcja potraktowała by zmienną So jako zmienną globalną i zmieniła jej wartość (zachęcamy do testów). Uwaga 3 Dla mniej wprawionych programistów proponujemy trzymać się prostej zasady żeby starać się stosować różne oznaczenia dla różnych zmiennych. Oszczędzi to sporo kłopotów związanych z przypadkowymi i niechcianymi zmianami zmiennych globalnych. Uwaga 4 Najlepszą zasadą jest świadome deklarowanie zmiennych. Jako regułę przyjmujemy że zawsze deklarujemy zmienne. Deklarujemy je również wewnątrz funkcji i procedur, z wyjątkiem przypadków w których świadomie chcemy żeby funkcja lub procedura zmieniała nam wcześniej zadeklarowane zmienne globalne (wtedy świadomie pomijamy deklarację zmiennej wewnątrz funkcji lub procedury).
2.1. FUNKCJE - NA PRZYKŁADZIE WYCENY EUROPEJSKIEJ OPCJI KUPNA21 2.1.2 Poste podejście do funkcji - dla tych którzy nie lubią zbędnych komplikacji. Tak jak wspomnieliśmy Visual Basic jest językiem przyjaznym. W prostym wydaniu nasz kod mógłby wyglądać następująco. Function DwumianN(N, k) prod = 1 For i = 1 To k DwumianN = prod Jednak tak jak poprzednio wspominaliśmy, brak deklaracji zmiennych i typów zawsze znacznie spowalnia obliczenia. Warto jest być jednak świadomym że można pisać szybko i zwięźle jeśli nie zależy nam na szybkości obliczeniowej. Pisząc kod, tak jak powyżej, bez żadnego deklarowania zmiennych powinniśmy się stosować do zaleceń z Uwagi 3. 2.1.3 Wycena europejskiej opcji kupna w modelu dwumianowym W tej sekcji wprowadzimy funkcję która pozwoli nam wyceniać opcje w modelu dwumianowym. Zauważmy że z punktu widzenia wyceny opcji prawdopodobieństwo rzeczywiste p nie odgrywa żadnej roli. Wykorzystamy więc zmienną p do do obliczenia prawdopodobieństwa martyngałowego p. Będziemy się trzymać tego oznaczenia do końca naszych wykładów. Jest istotne żebyśmy byli tego świadomi. Przy wycenie instrumentów pochodnych kluczowa jest świadomość względem której miary dokonywane są obliczenia. Powtarzamy więc, że p będzie dla nas od teraz miarą martyngałową. Wypiszmy najpierw kod który posłuży nam do wyceny a później dokonamy kilku uwag. W celu zachowania zwięzłości, fragmenty kodu omówione już wcześniej zostaną jedynie zasygnalizowane. Nie będziemy przepisywać zbędnych fragmentów i skoncentrujemy się nad tym co jest w danej chwili istotne. Plik 03 Funkcje.xls.
22 ROZDZIAŁ 2. FUNKCJE, PĘTLE I TABLICE Deklaracja zmiennych: So, N, R, p, U, D, X Sub PobierzDane() wczytywanie zmiennych So, N, R, U, D, X p = (R - D) / (U - D) Range("A4").Value = "p*" Range("B4").Value = p End Sub Funkcja: DwumianN(N, k) Function max(a, b) If a > b Then max = a Else max = b Function Wyplata(S As Double) As Double Wyplata = max(s - X, 0) Function Cena() As Double Dim i As Integer Dim suma As Double suma = 0 For i = 0 To N suma = suma + DwumianN(N, i) * p ^i * (1 - p) ^(N - i) * Wyplata(So * (1 + U) ^i * (1 + D) ^(N - i)) Cena = suma / (1 + R) ^N Sub Wycen() PobierzDane Range("A10").Value = "Cena Call" Range("B10").Value = Cena() End Sub Teraz omówmy krok po kroku interesujące nas fragmenty kodu:
2.1. FUNKCJE - NA PRZYKŁADZIE WYCENY EUROPEJSKIEJ OPCJI KUPNA23 W procedurze PobierzDane, tak jak w pliku 02 KomunikacjaZmienne.xls, wczytujemy zmienne So, N, R, U, D, X. Różnicą jest to że wewnątrz procedury liczymy też prawdopodobieństwo martyngałowe p i przypisujemy jego wartość zmiennej p poprzez p = (R - D) / (U - D) Plik 03 Funkcje.xls powstał poprzez zmodyfikowanie pliku 02 KomunikacjaZmienne.xls. Linijka Range("A4").Value = "p*" zapewni że w komórce A4 w Excelu zobaczymy p* zamiast p. powyższa linijka przypisuje komórce A4 tekst (zmienną typu string) zawierający p*. Następnie w komórce B4 wpisujemy wartość zmiennej p Range("B4").Value = p VBA nie posiada wbudowanej funkcji maksimum. Napisaliśmy sobie więc fragment kodu (funkcję max) który pozwoli nam ją implementować. Jedną rzeczą która jest godna uwagi jest to że świadomie nie zadeklarowaliśmy typu zmiennych na których funkcja pracuje oraz typu wartości które przyjmuje. Jest to podyktowane tym że świadomie chcemy żeby nasza funkcja działała na różnych typach zmiennych (np. zarówno dla double, integer jak i long) i odpowiednio przyjmowała różne typy wartości. Kolejną funkcją jest funkcja wypłaty. Nie jest ona absolutnie konieczna. Nasz program działałby równie dobrze bez niej gdyby wewnątrz funkcji Wycen wpisać kod liczący funkcję wypłaty. Niemniej jednak dobrą praktyką jest rozdzielanie kodu na części jeśli poprawi to klarowność programu. Jeśli napiszemy, tak jak wyżej, osobną funkcję która będzie nam liczyć wypłatę z opcji, to łatwo nam będzie modyfikować program. N przykład jeśli zamiast opcji kupna będziemy chcieli wycenić opcję sprzedaży to wystarczy powyższą funkcję Wyplata zmodyfikować jako Function Wyplata(S As Double) As Double Wyplata = max(x - S, 0)
24 ROZDZIAŁ 2. FUNKCJE, PĘTLE I TABLICE Funkcja Cena liczy wartość opcji za pomocą wzoru (1.2). Zauważmy że jedna z linijek w funkcji kończy się symbolem. Jest to symbol którego musimy użyć, jeśli chcemy żeby linijka wyświetlała się w dwóch wierszach. Bes symbolu cała linijka musiała by być zamknięta w jednym wierszu i wychodziłaby za ekran. Zawsze chcemy być w sytuacji w której możemy wywoływać funkcje z pozycji Excela, bez potrzeby wchodzenia w edytor VBA. Posłuży nam do tego procedura Wycen. Wczytuje ona dane, w komórce A10 wpisuje tekst Cena Call, a w komórce B10 liczy i wypisuje wartość opcji. Teraz żeby wyceniać opcje wystarczy na przykład stworzyć nowy przycisk z formularzy i przypisać mu makro Wycen. 2.2 Pętle W funkcji DwumianN w celu obliczenia dwumianu Newtona zastosowaliśmy pętle For. Nie jest to jedyna możliwość. W tej sekcji omówimy inne typowe pętle które mogły być w tym celu wykorzystane. ( ) Wszystkie nasze pętle będą N robić to samo, liczyć dwumian Newtona. Zanim pętla wystartuje w k każdym przypadku musimy zacząć od linijki prod = 1 Odrębne funkcje wykorzystujące odmienne pętle znajdują się w pliku 03 Petle.xls. Poniżej wypiszemy jedynie fragmenty kodu zawierające interesujące nas pętle. Jak zwykle, zaczniemy od wypisania kodu, po czym zrobimy kilka komentarzy. Następujące pętle są równoważne: Pętla For For i = 1 To k Pętla For z ustalonym skokiem For i = k To 1 Step -1
2.2. PĘTLE 25 Pętla Do While i = 1 Do While i <= k i = i + 1 Loop Pętla Do Until i = 1 Do i = i + 1 Loop Until i > k Pętla Do Loop i = 0 Do i = i + 1 If i > k Then Exit Do Loop Teraz kilka uwag. Pętla For jest najprostszą pętlą. Zadajemy wartość początkową i oraz krotność k ile razy chcemy żeby pętla została wywołana. Linijka Next i powiększa zmienną i o 1. Czasem przydatne jest żeby iteracja pętli przebiegała w tył zamiast do przodu. Dodanie komendy Step -1 spowoduje że linijka pomniejszy zmienną i o 1. Wielkość kroku może być dowolna. Na przykład jeśli zamiast Step -1 napiszemy Step -2 to komenda pomniejszy zmienną i o 2. Można oczywiście zażądać żeby krok był dodatni i większy od jeden; na przykład Step 5. Pętla Do While jest wykonywana dopóki warunek zdefiniowany po prawej od słowa While jest spełniony. Istotne jest to że warunek jest sprawdzony na początku pętli. Jeśli nie jest spełniony polecenie wewnątrz pętli nie jest wykonywane.
26 ROZDZIAŁ 2. FUNKCJE, PĘTLE I TABLICE Pętla Do Until jest wykonywana dopóki warunek zdefiniowany po prawej od słowa Until jest spełniony. Różnicą jest to że warunek jest sprawdzany pod koniec polecenia wewnątrz pętli. Oznacza to że polecenie będzie przynajmniej jeden raz wykonane (za pierwszym przebiegiem pętli). Pętla Do Loop pozwala na zapisanie warunku wychodzącego z pętli (Exit Do) w dowolnym miejscu. Brak warunku Exit Do oznacza że pętla będzie wykonywana bez przerwy (co przeważnie oznacza zawieszenie się programu a często również zawieszenie komputera). W pętlach typu For, jeśli istnieje taka potrzeba, można dodać polecenie Exit For które spowoduje wyjście z pętli. Uwaga 5 Pętle typu For są przeważnie bezpieczniejsze. Gwarantują że program się nie zawiesi w przypadku gdy błędnie zdefiniujemy warunek wyjścia. 2.3 Tablice - na przykładzie wyceny europejskiej opcji kupna Wracamy teraz do wyceny opcji. Najprawdopodobniej sporym zaskoczeniem dla większości będzie to że program opisany w Sekcji... z pliku 03 Funkcje.xls działa bardzo dobrze dla małych terminów realizacji N (poniżej dwudziestu), ale dla większych VBA zwraca raport błedu Overflow (Rysunek 2.1). Jest Rysunek 2.1: Raport błędu przekroczenia zakresu zmiennej.
2.3. TABLICE - NA PRZYKŁADZIE WYCENY EUROPEJSKIEJ OPCJI KUPNA27 ( ) N to związane z faktem iż wartość dwumianu Newtona rośnie w astronomicznym tempie i już przy N = 20 przekracza zakres zmiennej Integer k która jest mu przypisana. Drobną poprawą jest zmiana zmiennych użytych do definicji funkcji DwumianN z typu Integer na Long, ale jest to poprawka niewystarczająca. Już przy N równym czterdzieści pojawia się ten sam błąd. Jest to dla nas sytuacja niedopuszczalna. W przyszłości, kiedy będziemy mówić o wycenie opcji amerykańskich, zakres N który w praktyce będzie nas interesował będzie wynosić kilkaset lub powyżej tysiąca. Przy takich parametrach dwumian Newtona jest liczbą astronomiczną i z pewnością wyskoczy z zakresu. Są dwa rozwiązania tego problemu. Pierwszym jest zmiana deklaracji zmiennych użytych przy obliczeniu dwumianu oraz typu funkcji DwumianN z Integer na Double: Poprawka funkcji DwumianN Function DwumianN(N As Integer, k As Integer) As Double If k > N Or k < 0 Then DwumianN = 0 Exit Function End If Dim i As Integer Dim prod As Double prod = 1 For i = 1 To k DwumianN = prod Uwaga 6 Zauważmy że część powyższego kodu jest w innym kolorze. Od tej pory większość programów będzie powstawać na podstawie modyfikacji starego kodu. Zmienione części będziemy zaznaczać odmiennym kolorem żeby zwrócić na nie uwagę czytelnika. Wystarczyło zmienić typ funkcji DwumianN oraz typ zmiennej prod na Double. Przy takiej zmianie nasz program będzie działał przy dowolnym N. Jest tak
28 ROZDZIAŁ 2. FUNKCJE, PĘTLE I TABLICE dzięki temu że w momencie gdy dwumian staje się liczbą bardzo wielką zostaje on zaokrąglony do najniższej mu reprezentacji za pomocą liczby typu Double. W tym momencie, dla dużych N, funkcja DwumianN liczy po prostu przybliżenie dwumianu Newtona a nie jego dokładną wartość. Wiele osób może uznać to rozwiązanie za niezbyt eleganckie. Po pierwsze, mimo tego że wiemy że dwumian jest liczbą naturalną traktujemy go jako liczbę rzeczywistą. W wielu będzie to budzić sprzeciw wewnętrzny. Po drugie widać że nasze problemy są spowodowane sposobem w jaki cena opcji jest obliczana ( ) we wzorze (1.2). Żeby zastosować wzór najpierw liczymy dwumian N który jest liczbą astronomiczną, tylko po to żeby go później przemnożyć przez (p ) k (1 p ) N k która jest liczbą mikroskopijnie małą. Wygląda na k to że pomimo iż jesteśmy w stanie (sztucznie traktując dwumian jako liczbę rzeczywistą) zastosować wzór (1.2) do wyceny opcji, nie jest on optymalny z punktu widzenia numeryki. To spostrzeżenie prowadzi nas to do drugiego rozwiązania problemu: zmiany sposobu w jaki liczymy wartość opcji. W tej sekcji zastosujemy alternatywne podejście do wyceny opcji. Zamiast bezpośrednio stosować wzór, będziemy wyceniać opcje na drzewie dwumianowym za pomocą indukcji, cofając się w czasie krok po kroku od terminu realizacji N do chwili zero. Ta metoda jest dla nas ważna nie tylko ze względu na opcje europejskie, okaże się że to podejście będzie bardzo przydatne w przypadku opcji amerykańskich. Żeby zastosować wycenę na drzewie dwumianowym będą nam potrzebne tablice. Poniżej zamieszczamy kod wyceny opcji europejskiej call za pomocą tablic, a zaraz poniżej omówimy jego istotne fragmenty. Poniższy kod jest modyfikacją pliku 03 Funkcje.xls.) Plik 05 Tablice.xls. Deklaracja zmiennych: So, N, R, p, U, D, X Dim H as Variant Sub PobierzDane() wczytywanie zmiennych So, N, R, U, D, X obliczanie p ReDim H(N) as Double End Sub
2.3. TABLICE - NA PRZYKŁADZIE WYCENY EUROPEJSKIEJ OPCJI KUPNA29 Funkcja: max(a, b) Function Wyplata(S As Double) As Double Wyplata = max(s - X, 0) Function Cena() As Double Dim i As Integer Dim m As Integer For i = 0 To N H(i) = Wyplata(So * (1 + U) ^i * (1 + D) ^(N - i)) For m = N - 1 To 0 Step -1 For i = 0 To m H(i) = (p * H(i + 1) + (1 - p) * H(i)) / (1 + R) Next m Cena = H(0) Sub Wycen() PobierzDane Range("A10").Value = "Cena Call" Range("B10").Value = Cena() End Sub Linijka Dim H as Variant deklaruje nam zmienną typu Variant. Oznacza to że może ona przyjmować rozmaite typy. Może być ona użyta na przykład jako Double, Integer, Boolean. Dla nas istotniejsze jest jednak to że zmienne typu Variant mogą być użyte do przechowywania tablic.
30 ROZDZIAŁ 2. FUNKCJE, PĘTLE I TABLICE Żeby użyć zmiennej typu Variant jako tablicy trzeba zadeklarować jej rozmiar. Jest to zrobione w linijce ReDim H(N) as Double Od tej chwili zmienna H przechowuje tablicę rozmiaru N+1 zmiennych typu Double. W praktyce oznacza to że H jest ciągiem N+1 liczb H = (H(0), H(1), H(2),..., H(N)). Można teraz na przykład przypisać H(0)=0.1 oraz H(1)=0.2. Po takim przypisaniu zmienna H przechowuje 0.1 oraz 0.2 na pierwszej i drugiej współrzędnej. Ważnym w tym podejściu jest fakt że nie musimy sztywno ustalać rozmiaru tablicy. Wielkość tablicy jest zmienną N. W zależności od przebiegu programu może ona przyjmować różne wartości. Oznacza to że w momencie deklaracji ReDim H(N) as Double zmiennej H jest przypisana taka wielkość N (a co za tym idzie taka ilość pamięci komputera) jaka jest w danym momencie potrzebna. Warto w tym miejscu wspomnieć że można deklarować wielowymiarowe tablice: ReDim H(N,M) as Double da nam macierz o wymiarze (N+1) (M+1), a ReDim H(N,M,K) as Double tablicę o (N+1) (M+1) (K+1) współczynnikach. Omawiając kolejny fragment kodu naszkicujmy jak wygląda wycena w modelu dwumianowym (dla trzech kroków) H(3) H (2) H(2)... H (1) H(1)...... H (0) H(0)
2.3. TABLICE - NA PRZYKŁADZIE WYCENY EUROPEJSKIEJ OPCJI KUPNA31 Zaczynamy od chwili końcowej N. W chwili końcowej zmienna H = (H(0), H(1),..., H(N)) przechowuje wypłatę z opcji. Jest to zaimplementowane przez For i = 0 To N H(i) = Wyplata(So * (1 + U) ^i * (1 + D) ^(N - i)) Teraz wyceniamy opcję cofając się w czasie. Z rysunku widać że powinniśmy liczyć H (i) = p H(i + 1) + (1 p )H(i). 1 + R Nie ma jednak potrzeby deklarowania kolejnej zmiennej H. Możemy przechowywać wyniki cały czas w tej samej zmiennej H. Po zakończeniu działania pętli zostaniemy z H = (H(0), H(1), H(2),..., H(N)), gdzie H(0) będzie przechowywać dzisiejszą cenę opcji. Zmienne H(1), H(2),..., H(N) z tabeli są dla nas zupełnie nieistotne i je ignorujemy (przechowują one śmieci z obliczeń wykonanych w poprzednich krokach). Zauważmy na końcu że obliczyliśmy jedynie dzisiejszą cenę, a patrząc na powyższy kod widać że rozpatrując wyżej wymiarowe tablice moglibyśmy równie dobrze odtworzyć całe drzewko cen. To będzie kolejny krok, którym zajmiemy się w następnym rozdziale. Na razie istotniejsza jest dla nas nauka programowania niż sama wycena.