작성자: admin 작성일시: 2016-07-21 17:06:24 조회수: 179 다운로드: 18
카테고리: 금융 공학 태그목록:

블랙-숄즈-머튼 모형과 이항 트리를 이용한 옵션 가치 계산

다음은 유러피안 콜 옵션에 대해 블랙-숄즈-머튼 모형에 따른 주가 모형을 기반으로 이항 트리 방식의 옵션 가치를 계산하는 함수이다.

블랙-숄즈-머튼 모형에 따른 주가 모형은 위험 중립 측도하에서 다음 방정식을 따른다.

$$ dS_t = S_t \left( r_t \,dt + \sigma \,dW_t \right) $$

이를 오일러 이산화를 하면 다음과 같아진다.

$$ S_{t + \Delta t} = S_t \exp \left( (r_t - 0.5\sigma^2) \,\Delta t + \sigma \sqrt{\Delta t} \,Z \right) $$

위 식을 참고로 하여 $t+\Delta t$시간에서의 주가 $S_{t + \Delta t}$ 가 다음과 같이 $S_u$와 $S_d$ 라는 두 가지 값만 가진다고 가정하자. (Cox-Ross-Rubinstein 이항 트리)

$$ S_u = S_t \exp \left(+\sigma \sqrt{\Delta t} \right) = uS_t$$$$ S_d = S_t \exp \left(-\sigma \sqrt{\Delta t} \right) = dS_t $$

이렇게 하면 $\dfrac{S_{t + \Delta t}}{S_{t}}$의 분산은 $\Delta t$가 작은 경우 대략 $\sigma \sqrt{\Delta t}$ 이 된다.

위험 중립 측도의 정의로부터 $S_u$와 $S_d$ 라는 두 가지 값에 대한 확률 $p(S_u)$, $p(S_d)$을 구할 수 있다.

$$ p(S_u) + p(S_d) = 1 $$$$ S_{t} = \text{E}^Q \left[ \exp(-r\Delta t) S_{t + \Delta t} \right] = \exp(-r\Delta t) \big( p(S_u)S_u + p(S_d)S_d \big) $$$$ p(S_u) = \dfrac{\exp(-r\Delta t) - d}{u - d} $$
In [1]:
# 모형 & 옵션 파라미터
S0 = 100.  # 초기 주가
T = 1.  # 콜 옵션 만기
r = 0.05  # 무위험 고정 단기 이자율
vola = 0.20  # 고정 변동성

# 시간 파리미터
M = 1000  # 시간 간격의 수
dt = T / M  # 시간 간격의 길이
df = np.exp(-r * dt)  # 단일 시간 간격당 할인율

# 이항 트리 파라미터
u = np.exp(vola * np.sqrt(dt))  # 주가 상승비
d = 1 / u  # 주가 하락비
q = (np.exp(r * dt) - d) / (u - d)  # 위험 중립 확률
In [2]:
u, d
Out[2]:
(1.0063445975507901, 0.99369540258254341)
In [3]:
q, 1-q
Out[3]:
(0.50237178598554477, 0.49762821401445523)

순수 파이썬과 반복문을 사용하는 경우

트리는 원래 그래프(graph)자료 구조를 사용하지만 이항 트리 옵션 가치 계산의 경우 속도 향상을 위해 2차원 배열을 사용한다.

In [4]:
def binomial_py(strike):
    ''' 반복문을 사용한 이항 트리 옵션 가격 계산
    
    인수
    ==========
    strike : float
        유러피안 콜 옵션의 행사가
    '''
    
    # 반복문 1 - 주가 계산
    S = np.zeros((M + 1, M + 1), dtype=np.float64)  # 주가 배열
    S[0, 0] = S0
    for j in range(1, M + 1):   # 열
        for i in range(j + 1):  # 행
            S[i, j] = S[0, 0] * (u ** (j-i)) * (d ** i)
            
    # 반복문 2 - 내재 가치
    iv = np.zeros((M + 1, M + 1), dtype=np.float64) # 내재 가치 배열
    for j in range(0, M + 1, 1):
        for i in range(j + 1):
            iv[i, j] = max(S[i, j] - strike, 0)
        
    # 반복문 3 - 현재 가치
    pv = np.zeros((M + 1, M + 1), dtype=np.float64) # 현재 가치 배열
    pv[:, M] = iv[:, M]          # 만기 Payoff
    for j in range(M - 1, -1, -1):
        for i in range(j + 1):
            pv[i, j] = (q * pv[i, j + 1] + (1 - q) * pv[i + 1, j + 1]) * df

    return pv[0, 0], S, iv, pv
In [5]:
v, S, iv, pv = binomial_py(100)

주가 배열

In [6]:
S[:4, :4]
Out[6]:
array([[ 100.        ,  100.63445976,  101.2729449 ,  101.91548098],
       [   0.        ,   99.36954026,  100.        ,  100.63445976],
       [   0.        ,    0.        ,   98.74305531,   99.36954026],
       [   0.        ,    0.        ,    0.        ,   98.1205201 ]])

내재 가치 배열

In [7]:
iv[:4, :4]
Out[7]:
array([[ 0.        ,  0.63445976,  1.2729449 ,  1.91548098],
       [ 0.        ,  0.        ,  0.        ,  0.63445976],
       [ 0.        ,  0.        ,  0.        ,  0.        ],
       [ 0.        ,  0.        ,  0.        ,  0.        ]])

현재 가치 배열

In [8]:
pv[:4, :4]
Out[8]:
array([[ 10.4485841 ,  10.84994565,  11.26131037,  11.68270734],
       [  0.        ,  10.0444465 ,  10.43574985,  10.83702801],
       [  0.        ,   0.        ,   9.65042236,  10.03169513],
       [  0.        ,   0.        ,   0.        ,   9.26648483]])

계산 속도

In [9]:
%time round(binomial_py(100)[0], 3)
CPU times: user 1.03 s, sys: 0 ns, total: 1.03 s
Wall time: 1.03 s
Out[9]:
10.449

NumPy 벡터화 연산을 사용하는 경우

In [10]:
def binomial_np(strike):
    ''' NumPy를 사용한 이항 트리 옵션 가격 계산
    
    인수
    ==========
    strike : float
        유러피안 콜 옵션의 행사가
    '''
    # 주가 배열
    mu = np.arange(M + 1)
    mu = np.resize(mu, (M + 1, M + 1))
    md = np.transpose(mu)
    mu = u ** (mu - md)
    md = d ** md
    S = S0 * mu * md
    
    # 가치 계산 반복문
    pv = np.maximum(S - strike, 0)

    z = 0
    for t in range(M - 1, -1, -1):  # 역방항 반복
        pv[0:M - z, t] = (q * pv[0:M - z, t + 1] + (1 - q) * pv[1:M - z + 1, t + 1]) * df
        z += 1
    return pv[0, 0]

내부 구조

In [11]:
M = 4  # four time steps only
mu = np.arange(M + 1)
mu
Out[11]:
array([0, 1, 2, 3, 4])
In [12]:
mu = np.resize(mu, (M + 1, M + 1))
mu
Out[12]:
array([[0, 1, 2, 3, 4],
       [0, 1, 2, 3, 4],
       [0, 1, 2, 3, 4],
       [0, 1, 2, 3, 4],
       [0, 1, 2, 3, 4]])
In [13]:
md = np.transpose(mu)
md
Out[13]:
array([[0, 0, 0, 0, 0],
       [1, 1, 1, 1, 1],
       [2, 2, 2, 2, 2],
       [3, 3, 3, 3, 3],
       [4, 4, 4, 4, 4]])
In [14]:
mu - md
Out[14]:
array([[ 0,  1,  2,  3,  4],
       [-1,  0,  1,  2,  3],
       [-2, -1,  0,  1,  2],
       [-3, -2, -1,  0,  1],
       [-4, -3, -2, -1,  0]])
In [15]:
mu = u ** (mu - md)
mu.round(3)
Out[15]:
array([[ 1.   ,  1.006,  1.013,  1.019,  1.026],
       [ 0.994,  1.   ,  1.006,  1.013,  1.019],
       [ 0.987,  0.994,  1.   ,  1.006,  1.013],
       [ 0.981,  0.987,  0.994,  1.   ,  1.006],
       [ 0.975,  0.981,  0.987,  0.994,  1.   ]])
In [16]:
md = d ** md
md.round(3)
Out[16]:
array([[ 1.   ,  1.   ,  1.   ,  1.   ,  1.   ],
       [ 0.994,  0.994,  0.994,  0.994,  0.994],
       [ 0.987,  0.987,  0.987,  0.987,  0.987],
       [ 0.981,  0.981,  0.981,  0.981,  0.981],
       [ 0.975,  0.975,  0.975,  0.975,  0.975]])
In [17]:
S = S0 * mu * md
S.round(3)
Out[17]:
array([[ 100.   ,  100.634,  101.273,  101.915,  102.562],
       [  98.743,   99.37 ,  100.   ,  100.634,  101.273],
       [  97.502,   98.121,   98.743,   99.37 ,  100.   ],
       [  96.276,   96.887,   97.502,   98.121,   98.743],
       [  95.066,   95.669,   96.276,   96.887,   97.502]])
In [18]:
M = 1000  # 시간 구간의 갯수를 원래대로
%time round(binomial_np(100), 3)
CPU times: user 150 ms, sys: 0 ns, total: 150 ms
Wall time: 149 ms
Out[18]:
10.449

Numba를 사용한 JIT 최적화 적용

In [19]:
import numba as nb
In [20]:
binomial_nb = nb.jit(binomial_py)
In [22]:
%timeit round(binomial_nb(100)[0], 3)
10 loops, best of 3: 35.5 ms per loop

질문/덧글

아직 질문이나 덧글이 없습니다. 첫번째 글을 남겨주세요!