通過之前幾天的學習,Q Quant們應該已經熟悉了Python的基本文法,也了解了Python中常用數值庫的算法。到這裡為止,小Q們也許早就對之前簡單的例子不滿意,希望能在Python裡面玩票大的!Ok,我們這裡引入一個不怎麼像玩具的模型——二叉樹算法。我們仍然以期權為例子,教會大家:
- 如何利用Python的控制語句與基本内置計算方法,構造一個二叉樹模型;
- 如何使用類封裝的方式,抽象二叉樹算法,并進行擴充;
- 利用繼承的方法為已有二叉樹算法增加美式期權算法。
import numpy as np
import math
import seaborn as sns
from matplotlib import pylab
from CAL.PyCAL import *
font.set_size(15)
1. 小Q的第一棵“樹”——二叉樹算法Python描述
我們這邊隻會簡單的描述二叉樹的算法,不會深究其原理,感興趣的讀者可以很友善的從公開的文獻中擷取細節。
我們這裡仍然考慮基礎的 Black - Scholes 模型:
dS=(r−d)Sdt+σSdWt
這裡各個字母的含義如之前介紹,多出來的 r 代表股息率。
之是以該算法被稱為 二叉樹,因為這個算法的基礎結構是一個逐層遞增的樹杈式結構:
一個基本的二叉樹機構由以下三個參數決定:
-
标的資産價格向上跳升的比例,up
必然大于1 (對應上圖中的 u )up
-
标的資産價格向下跳升的比例,down
必然小于1 (對應上圖中的 d )down
-
标的資産價格向上跳升的機率upProbability
這裡我們用一個具體的例子,使用Python實作二叉樹算法。以下為具體參數:
-
到期時間,機關年ttm
-
時間方向步數tSteps
-
無風險利率r
-
标的股息率d
-
波動率sigma
-
期權行權價strike
-
标的現價spot
這裡我們隻考慮看漲期權。
# 設定基本參數
ttm = 3.0
tSteps = 25
r = 0.03
d = 0.02
sigma = 0.2
strike = 100.0
spot = 100.0
我們這裡用作例子的樹結構被稱為 Jarrow - Rudd 樹,其中:
\begin{align}up &= \mathrm{exp}((r - d - \frac{1}{2}\sigma^2) \Delta t + \sigma\sqrt{\Delta t}) \\\[5pt]down &= \mathrm{exp}((r - d - \frac{1}{2}\sigma^2) \Delta t + \sigma\sqrt{\Delta t}) \\\[5pt]upProb &= 0.5\end{align}
dt = ttm / tSteps
up = math.exp((r - d - 0.5*sigma*sigma)*dt + sigma*math.sqrt(dt))
down = math.exp((r - d - 0.5*sigma*sigma)*dt - sigma*math.sqrt(dt))
discount = math.exp(-r*dt)
在我們的例子中,這個樹的深度為16層(時間節點數+1),第i層節點與第i+1層節點有以下的關系式:
\begin{align*}
\mathrm{Lattice}[i+1][j] &= \mathrm{Lattice}[i][j] \times down \\\[5pt]\mathrm{Lattice}[i+1][j+1] &= \mathrm{Lattice}[i][j] \times up
\end{align*}
# 構造二叉樹
lattice = np.zeros((tSteps+1, tSteps+1))
lattice[0][0] = spot
for i in range(tSteps):
for j in range(i+1):
lattice[i+1][j+1] = up * lattice[i][j]
lattice[i+1][0] = down * lattice[i][0]
# 在節點上計算payoff
def call_payoff(spot):
global strike
return max(spot - strike, 0.0)
pylab.figure(figsize = (12,8))
pylab.plot(map(call_payoff, lattice[tSteps]))
pylab.title(u'二叉樹到期時刻标的Pay off分布', fontproperties = font, fontsize = 18)
在我們從樹最茂盛的枝葉向根部回溯的時候,第i層節點與第i+1層節點的關系滿足:
Lattice[i][j]=discount×(upProb×Lattice[i+1][j+1]+(1−upProb)×Lattice[i+1][j])
# 反方向回溯整棵樹
for i in range(tSteps,0,-1):
for j in range(i,0,-1):
if i == tSteps:
lattice[i-1][j-1] = 0.5 * discount * (call_payoff(lattice[i][j]) + call_payoff(lattice[i][j-1]))
else:
lattice[i-1][j-1] = 0.5 * discount * (lattice[i][j] + lattice[i][j-1])
print u'二叉樹價格: %.4f' % lattice[0][0]
print u'解析法價格: %.4f' % BSMPrice(1, strike, spot, r, d, sigma, ttm, rawOutput= T
二叉樹價格: 14.2663 解析法價格: 14.1978
2. 從“樹”到“森林”—— 面向對象方式實作二叉樹算法
之前的部分展示了一個樹算法的基本結構。但是現在的實作由很多缺點:
- 沒有明确接口,作為使用者優雅簡潔的使用既有算法;
- 沒有完整封裝,十分不利于算法的擴充;
下面我們将給出一個基于Python類的二叉樹算法實作,實際上我們通過上面的實驗性探索,發現整個程式可以拆成三個互相獨立的功能子產品:
- 二叉樹架構
樹的架構結構,包括節點數以及基本參數的儲存;
- 二叉樹類型描述
具體數算法的參數,例如上例中的 Jarrow Rudd樹;
- 償付函數
到期的償付形式,即為Payoff Function。
2.1 二叉樹架構( BinomialTree
)
BinomialTree
這個類負責二叉樹架構的構造,也是基本的二叉樹算法的調用入口。它有三個成員:
- 構造函數(
)__init__
負責接受使用者定義的具體參數,例如:
spot
等;真正二叉樹的構造方法,由私有方法
_build_lattice
以及傳入參數
treeTraits
共同完成;
- 樹構造細節(
)_build_lattice
接手具體的樹構造過程,這裡需要依賴根據
treeTraits
擷取的參數例如:
up
,
down
。
- 樹回溯(
)roll_back
從樹的最茂盛枝葉節點向根節點回溯的過程。最終根節點的值即為期權的價值。這裡它要求的參數是一個
pay_off
函數。
# 二叉樹架構(可以通過傳入不同的treeTraits類型,設計不同的二叉樹結構)
class BinomialTree:
def __init__(self, spot, riskFree, dividend, tSteps, maturity, sigma, treeTraits):
self.dt = maturity / tSteps
self.spot = spot
self.r = riskFree
self.d = dividend
self.tSteps = tSteps
self.discount = math.exp(-self.r*self.dt)
self.v = sigma
self.up = treeTraits.up(self)
self.down = treeTraits.down(self)
self.upProbability = treeTraits.upProbability(self)
self.downProbability = 1.0 - self.upProbability
self._build_lattice()
def _build_lattice(self):
'''
完成構造二叉樹的工作
'''
self.lattice = np.zeros((self.tSteps+1, self.tSteps+1))
self.lattice[0][0] = self.spot
for i in range(self.tSteps):
for j in range(i+1):
self.lattice[i+1][j+1] = self.up * self.lattice[i][j]
self.lattice[i+1][0] = self.down * self.lattice[i][0]
def roll_back(self, payO
2.2 二叉樹類型描述( Tree Traits
)
Tree Traits
正像我們之前描述的那樣,任意的樹隻要描述三個方面的特征就可以。是以我們設計的
Tree Traits
類隻要通過它的靜态成員傳回這些特征就可以:
-
傳回向上跳升的比例;up
-
傳回向下調降的比例;down
-
傳回向上跳升的機率upProbability
下面的類定義了 Jarrow - Rudd 樹的描述:
class JarrowRuddTraits:
@staticmethod
def up(tree):
return math.exp((tree.r - tree.d - 0.5*tree.v*tree.v)*tree.dt + tree.v*math.sqrt(tree.dt))
@staticmethod
def down(tree):
return math.exp((tree.r - tree.d - 0.5*tree.v*tree.v)*tree.dt - tree.v*math.sqrt(tree.dt))
@staticmethod
def upProbability(tree):
return 0.5
我們這裡再給出另一個 Cox - Ross - Rubinstein 樹的描述:
class CRRTraits:
@staticmethod
def up(tree):
return math.exp(tree.v * math.sqrt(tree.dt))
@staticmethod
def down(tree):
return math.exp(-tree.v * math.sqrt(tree.dt))
@staticmethod
def upProbability(tree):
return 0.5 + 0.5 * (tree.r - tree.d - 0.5 * tree.v*tree.v) * tree.dt / tree.v / math.sqrt(tree.dt)
2.3 償付函數( pay_off
)
pay_off
這部分很簡單,就是一進制函數,輸入為标的價格,輸出的償付收益,對于看漲期權來說就是:
pay=max(S−K,0)
def pay_off(spot):
global strike
return max(spot - strike, 0.0)
2.4 組裝
讓我們三部分組裝起來,現在整個調用過程變得什麼清晰明了,同時最後的結果和第一部分是完全一緻的。
testTree = BinomialTree(spot, r, d, tSteps, ttm, sigma, JarrowRuddTraits)
testTree.roll_back(pay_off)
print u'二叉樹價格: %.4f' % testTree.lattice[0][0]
二叉樹價格: 14.2663
這裡我們想更進一步,用我們現在的算法架構來測試二叉樹的收斂性。這裡我們用來作比較的算法即為之前描述的 Jarrow - Rudd 以及 Cox - Ross - Rubinstein 樹:
stepSizes = range(25, 500,25)
jrRes = []
crrRes = []
for tSteps in stepSizes:
# Jarrow - Rudd 結果
testTree = BinomialTree(spot, r, d, tSteps, ttm, sigma, JarrowRuddTraits)
testTree.roll_back(pay_off)
jrRes.append(testTree.lattice[0][0])
# Cox - Ross - Rubinstein 結果
testTree = BinomialTree(spot, r, d, tSteps, ttm, sigma, CRRTraits)
testTree.roll_back(pay_off)
crrRes.append(testTree.lattice[0][0])
我們可以繪制随着步數的增加,兩種二叉樹算法逐漸向真實值收斂的過程。
anyRes = [BSMPrice(1, strike, spot, r, d, sigma, ttm, rawOutput= True)[0]] * len(stepSizes)
pylab.figure(figsize = (16,8))
pylab.plot(stepSizes, jrRes, '-.', marker = 'o', markersize = 10)
pylab.plot(stepSizes, crrRes, '-.', marker = 'd', markersize = 10)
pylab.plot(stepSizes, anyRes, '--')
pylab.legend(['Jarrow - Rudd', 'Cox - Ross - Rubinstein', u'解析解'], prop = font)
pylab.xlabel(u'二叉樹步數', fontproperties = font)
pylab.title(u'二叉樹算法收斂性測試', fontproperties = font, fontsize = 20)
我們也可以繪制兩種算法的誤差随着步長下降的過程。
class ExtendBinomialTree(BinomialTree):
def roll_back_american(self, payOff):
'''
節點計算,并反向倒推
'''
for i in range(self.tSteps,0,-1):
for j in range(i,0,-1):
if i == self.tSteps:
europeanValue = self.discount * (self.upProbability * payOff(self.lattice[i][j]) + self.downProbability * payOff(self.lattice[i][j-1]))
else:
europeanValue = self.discount * (self.upProbability * self.lattice[i][j] + self.downProbability * self.lattice[i][j-1])
# 處理美式行權
exerciseValue = payOff(self.lattice[i-1][j-1])
self.lattice[i-1][j-1] = max(europeanValue, exerciseValue)
我們将使用同樣的參數測試美式期權算法的實作:
stepSizes = range(25, 500,25)
jrRes = []
crrRes = []
for tSteps in stepSizes:
# Jarrow - Rudd 結果
testTree = ExtendBinomialTree(spot, r, d, tSteps, ttm, sigma, JarrowRuddTraits)
testTree.roll_back_american(pay_off)
jrRes.append(testTree.lattice[0][0])
# Cox - Ross - Rubinstein 結果
testTree = ExtendBinomialTree(spot, r, d, tSteps, ttm, sigma, CRRTraits)
testTree.roll_back_american(pay_off)
crrRes.append(testTree.lattice[0][0])
我們畫出美式期權價格的收斂圖,價格始終高于歐式期權的價格,符合預期。
anyRes = [BSMPrice(1, strike, spot, r, d, sigma, ttm, rawOutput= True)[0]] * len(stepSizes)
pylab.figure(figsize = (16,8))
pylab.plot(stepSizes, jrRes, '-.', marker = 'o', markersize = 10)
pylab.plot(stepSizes, crrRes, '-.', marker = 'd', markersize = 10)
pylab.plot(stepSizes, anyRes, '--')
pylab.legend([u'Jarrow - Rudd(美式)', u'Cox - Ross - Rubinstein(美式)', u'解析解(歐式)'], prop = font)
pylab.xlabel(u'二叉樹步數', fontproperties = font)
pylab.title(u'二叉樹算法美式期權', fontproperties = font, fontsize = 20)
更多開源源碼請戳連結:https://uqer.io/community/search/%E9%87%8F%E5%8C%96%E5%88%86%E6%9E%90%E5%B8%88