天天看點

量化分析師的Python日記【Q Quant兵器譜之二叉樹】

通過之前幾天的學習,Q Quant們應該已經熟悉了Python的基本文法,也了解了Python中常用數值庫的算法。到這裡為止,小Q們也許早就對之前簡單的例子不滿意,希望能在Python裡面玩票大的!Ok,我們這裡引入一個不怎麼像玩具的模型——二叉樹算法。我們仍然以期權為例子,教會大家:

  1. 如何利用Python的控制語句與基本内置計算方法,構造一個二叉樹模型;
  2. 如何使用類封裝的方式,抽象二叉樹算法,并進行擴充;
  3. 利用繼承的方法為已有二叉樹算法增加美式期權算法。
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 代表股息率。

之是以該算法被稱為 二叉樹,因為這個算法的基礎結構是一個逐層遞增的樹杈式結構:

一個基本的二叉樹機構由以下三個參數決定:

  1. up

     标的資産價格向上跳升的比例, 

    up

    必然大于1 (對應上圖中的  u )
  2. down

     标的資産價格向下跳升的比例, 

    down

    必然小于1 (對應上圖中的  d )
  3. 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]
           
量化分析師的Python日記【Q Quant兵器譜之二叉樹】
# 在節點上計算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)
           
量化分析師的Python日記【Q Quant兵器譜之二叉樹】

在我們從樹最茂盛的枝葉向根部回溯的時候,第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

)

這個類負責二叉樹架構的構造,也是基本的二叉樹算法的調用入口。它有三個成員:

  • 構造函數(

    __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

類隻要通過它的靜态成員傳回這些特征就可以:

  1. up

     傳回向上跳升的比例;
  2. down

     傳回向下調降的比例;
  3. 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=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)
           
量化分析師的Python日記【Q Quant兵器譜之二叉樹】

我們也可以繪制兩種算法的誤差随着步長下降的過程。

量化分析師的Python日記【Q Quant兵器譜之二叉樹】
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)
           
量化分析師的Python日記【Q Quant兵器譜之二叉樹】

更多開源源碼請戳連結:https://uqer.io/community/search/%E9%87%8F%E5%8C%96%E5%88%86%E6%9E%90%E5%B8%88