본문 바로가기
금융공학

Brownian Bridge를 이용한 금융상품의 계산

by hustler78 2022. 11. 28.
728x90
반응형

이번 글에서는 Brownian Bridge를 사용하여 파생상품의 평가를 어떻게 할 수 있는지 한번 알아보겠습니다.

 

다음과 같은 가상의 옵션상품을 설정합니다.

 

 

우선 기초자산은 어떤 지수나 주식의 퍼포먼스(기준가 대비 움직임)로 정의합니다.

 

이 기초자산이 만기 때 어디에 위치하느냐, 낙인은 친 상황이냐에 따라 수익구조가 나뉩니다.

조건 조건 상세 수익/손실
만기시 기초자산의 80% 이상으로 끝날 때 10%
만기까지 한번도 70% 미만을 하회하지 않고 80% 미만으로 끝났을 때 10%
만기까지 한번이라도 70% 하회한 적이 있고 80% 미만으로 끝났을 때 -10%

 

이 옵션을 어떻게 계산해야 할까요? 낙인을 쳤는지 여부 등 복잡한 조건이 많으므로 시뮬레이션 방법을 쓰는 것이 좋을 것 같습니다. 시뮬레이션 방법은

2022.08.08 - [금융공학] - Black Scholes Equation의 풀이: 시뮬레이션

 

Black Scholes Equation의 풀이: 시뮬레이션

이번 글은 2022.08.05 - [금융공학] - Black Scholes Equation의 풀이: 델타원 상품 Black Scholes Equation의 풀이: 델타원 상품 이번 글은 2022.08.05 - [금융공학] - Black Scholes Equation의 풀이: 확률프로세스를 이용하

sine-qua-none.tistory.com

을 참고하시기 바랍니다.

 

 

옵션의 계산(시뮬레이션)

 

정석적인 방법

낙인을 관찰하기 위해서는 이론적으로 주가 패스를 데일리(daily)로 만들어야 합니다. 언제 낙인 배리어를 칠지 모르기 때문이죠. 따라서

○ 주가패스를 만기까지 일일단위로 만든다.
○ 만기 주가가 배리어(80%)를 상회하면 수익 지급한다.
○ 만기 주가가 배리어(80%)보다 밑이면,
   ▶ 낙인을 쳤으면 손실 쿠폰(-10%) 지급
   ▶ 낙인을 안쳤으면 full dummy 쿠폰(10%) 지급

이 과정을 시뮬레이션 횟수만큼(ex, 10,000번) 반복한다.

 

발생시켜야 할 난수는 총 몇 개일까요?

 

상품의 만기를 $T$(year)라 하고, 1년을 250日이라 가정하면, 일일 패스를 만들기 위해 총 $250T$ 만큼의 랜덤 변수가 필요합니다.

또 이러한 패스를 시뮬레이션 횟수만큼 발생하여야 하죠. 횟수를 $N$이라 하면 총

$$ 250T\cdot N$$

의 난수를 발생해야 합니다. $T=1~,~N=10,000$이라 잡아도, 250만 개의 난수발생이 필요하죠. 고작 1만 번의 시뮬레이션을 위해 난수가 너무 많이 필요한 느낌입니다. 

 

이를 좀 더 줄일 방법이 있을까요?

 

 

Brown Bridge 방법

 

위의 옵션의 상품구조를 보니, 

 

낙인을 치던 안치던, 만기 주가가 배리어 위에 끝나면 수익 지급

 

입니다. 따라서, 만기 주가가 배리어 이상이면 낙인 여부를 관찰할 필요가 없다는 뜻이지요. 따라서 다음과 같이 해봅니다.

○ 하나의 랜덤 변수로 만기 주가를 모델링한다.
○ 이 만기 주가가 배리어(80%) 이상이면 수익(+10%) 지급한다.
○ 만기주가가 배리어 밑이면, 만기 주가와 현재가를 잇는 데일리 Brownian Bridge를 설계한다.
   ▶ 이 Brownian Bridge가 한 번도 낙인 배리어를 하회한 적이 없고 70~80% 사이에서 끝나면 ,
        full dummy 쿠폰(+10%) 지급
   ▶ 낙인 배리어를 하회한 적이 한 번이라도 있고 배리어(80%) 미만으로 끝나면 손실(-10%) 

이 과정을 시뮬레이션 횟수(ex. 10,000번) 만큼 반복한다.

 

이러면 어떤 효과가 있나요?

만기 주가가 80% 이상이면 굳이 일일 주가 패스를 만들지 않아도 되니, 산출할 난수가 대폭 줄어듭니다.

이론적으로 산출을 해 보겠습니다.

 

만기 주가 $S_T$는 기준가 $S_0$ 대비

$$ \exp\left( (r-q-\frac12\sigma^2)T +\sigma \sqrt{T}z \right)~~,~~ z\sim \mathcal{N}(0,1)$$

입니다. GBM 의 결과이지요. 따라서 이 값이 배리어 $B$를 상회활 확률은

$$ \mathbb{P} \left( z \geq \frac{\ln B -(r-q-\frac12\sigma^2)T}{\sigma\sqrt{T}} \right)$$

입니다. 표준정규분포의 cdf를 $\Phi(\cdot)$라 하고, $1-\Phi(a) = \Phi(-a)$라는 관계식을 이용하면,

확률의 이론값은

$$ \Phi \left( \frac{-\ln B +(r-q-\frac12\sigma^2)T}{\sigma\sqrt{T}} \right)$$

 

입니다. 예컨대, $r=2\%, q=0, \sigma =30\%, T=1$이고 배리어가 $80\%$이면

 

이런 식으로 Excel에서 확률이 0.746 정도임을 확인할 수가 있습니다.

따라서 1만 번의 시뮬레이션 횟수 중, 7,460번은 만기 주가만 뽑으면 되니, 총 7,460개의 난수 여기에 나머지 2,540번의 시뮬레이션에서는 데일리 Brownian bridge를 산출하므로 얼추 2,540 × 250 = 63.5만 번, 총 64.4만 개의 난수가 필요하게 되죠.

 

위의 이론적인 방법에서 살펴본 250만 개 대비는 1/4 수준이라는 것을 알 수 있습니다. 금융 상품 가격 계산에서 가장 많은 계산 시간을 차지하는 난수 발생이 드라마틱하게 줄어들 수 있다는 얘기죠.

 

그럼 과연 위의 정석적인 방법과 Brownian bridge를 사용한 방법의 상품 가격이 똑같을지 직접 코딩을 해보겠습니다.

 

 

Python Code

import numpy as np
import matplotlib.pyplot as plt
from scipy.stats import norm
import time

def ki_digital(s0, maturity, ki_barrier, barrier, gain_coupon, loss_coupon, fulldummy, rfr, div, vol, nSim):
    start_time = time.time()
    days = 250
    ntime = days * maturity
    time_grid = np.linspace(0, maturity, ntime + 1)
    drift = rfr - div - 0.5 * vol ** 2
    dt = time_grid[1] - time_grid[0]
    sum_payoff = 0
    for _ in range(nSim):
        sPath = np.zeros(ntime + 1)
        sPath[0] = s0
        for i in range(ntime):
            sPath[i + 1] = sPath[i] * np.exp(drift * dt + vol * np.sqrt(dt) * np.random.normal())

        if sPath[-1] / s0 >= barrier:
            payoff = gain_coupon
        elif ((min(sPath) / s0 >= ki_barrier) and (sPath[-1] / s0 < barrier)):
            payoff = fulldummy
        else:
            payoff = loss_coupon
        sum_payoff += payoff

    val = round(sum_payoff / nSim * np.exp(-rfr * maturity), 3)
    cal_time = round(time.time() - start_time, 3)
    return val, cal_time

def ki_digital_bb(s0, maturity, ki_barrier, barrier, gain_coupon, loss_coupon, fulldummy, rfr, div, vol, nSim):
    start_time = time.time()
    days = 250
    ntime = days * maturity
    time_grid = np.linspace(0, maturity, ntime + 1)
    drift = rfr - div - 0.5 * vol ** 2
    dt = time_grid[1] - time_grid[0]

    sum_payoff = 0

    s_mat = s0 * np.exp(drift * maturity + vol * np.sqrt(maturity) * np.random.normal(size=nSim))
    prob_cnt = 0
    for i in range(nSim):

        if s_mat[i] / s0 >= barrier:
            payoff = gain_coupon
            prob_cnt += 1
        else:
            Z_T = (np.log(s_mat[i] / s0) - drift * maturity) / (vol)
            rn = np.zeros(ntime + 1)
            for i in range(ntime):
                rn[i + 1] = rn[i] + np.random.normal() * np.sqrt(dt)
            brownian_bridge = Z_T * time_grid / maturity + rn - time_grid / maturity * rn[-1]
            sPath = s0 * np.exp(drift * time_grid + vol * brownian_bridge)
            if ((min(sPath) / s0 >= ki_barrier) and (sPath[-1] / s0 < barrier)):
                payoff = fulldummy
            else:
                payoff = loss_coupon

        sum_payoff += payoff
    val = round(sum_payoff / nSim * np.exp(-rfr * maturity), 3)
    cal_time = round(time.time() - start_time, 3)
    barrier_prob = round(prob_cnt / nSim, 3)
    return val, cal_time, barrier_prob


if __name__ == '__main__':
    s0 = 1
    maturity = 1
    ki_barrier = 0.7
    barrier = 0.8
    gain_coupon = 0.10
    loss_coupon = -0.10
    fulldummy = 0.10
    rfr = 0.02
    div = 0
    vol = 0.3
    nSim = 10000

    result = ki_digital(s0, maturity, ki_barrier, barrier, gain_coupon, loss_coupon, fulldummy, rfr, div, vol, nSim)
    result_bb = ki_digital_bb(s0, maturity, ki_barrier, barrier, gain_coupon, loss_coupon, fulldummy, rfr, div, vol,
                              nSim)
    print('Value : {}, Elapsed time : {} sec'.format(result[0], result[1]))
    print('Value(BB) : {}, Elapsed time : {} sec, UpBarrier_prob: {}'.format(result_bb[0], result_bb[1], result_bb[2]))

 

간략한 코드 설명은 아래 주석을 참고하면 됩니다.

 

import time	# 계산 시간을 측정하기 위해 필요한 python module

 


def ki_digital(s0, maturity, ki_barrier, barrier, gain_coupon, loss_coupon, fulldummy, rfr, div, vol, nSim):
    start_time = time.time()	# 계산 시작 시점의 time 측정
    days = 250					# 1년 = 250일 가정
    ntime = days * maturity		# 총 만기*250의 time node가 있음
    time_grid = np.linspace(0, maturity, ntime + 1)	# time을 총 node갯수로 균등분
    drift = rfr - div - 0.5 * vol ** 2		#GBM 주가모델의 drift 항
    dt = time_grid[1] - time_grid[0]	#time node 의 간격
    sum_payoff = 0	#시뮬레이션 결과를 평균하기 위해 결과를 합하는 변수
    
    for _ in range(nSim):		#시뮬레이션 횟수만큼 반복하며
        sPath = np.zeros(ntime + 1)	#일일주가를 담을 array 설정
        sPath[0] = s0				# array의 첫항은 현재가 s0
        for i in range(ntime):		# time node만큼 for문을 돌리며
            sPath[i + 1] = sPath[i] * np.exp(drift * dt + vol * np.sqrt(dt) * np.random.normal())
			#주가 패스 생성
            
        if sPath[-1] / s0 >= barrier:	#배리어보다 크다면
            payoff = gain_coupon		#수익쿠폰 지급
        elif ((min(sPath) / s0 >= ki_barrier) and (sPath[-1] / s0 < barrier)):
        	#낙인 친 적 없고, 종가가 배리어보다 작으면
            payoff = fulldummy	#풀더미 쿠폰 지급
        else:	#낙인 치고, 종가가 배리어보다 작으면,
            payoff = loss_coupon	#손실 쿠폰
        sum_payoff += payoff	#페이오프의 합

    val = round(sum_payoff / nSim * np.exp(-rfr * maturity), 3)	# 해당 옵션의 현재가격 계산
    cal_time = round(time.time() - start_time, 3)	# 계산 소요시간
    return val, cal_time	#옵션 현재가와 계산소요시간 리턴

 

 


def ki_digital_bb(s0, maturity, ki_barrier, barrier, gain_coupon, loss_coupon, fulldummy, rfr, div, vol, nSim):
    start_time = time.time()
    days = 250
    ntime = days * maturity
    time_grid = np.linspace(0, maturity, ntime + 1)
    drift = rfr - div - 0.5 * vol ** 2
    dt = time_grid[1] - time_grid[0]

    sum_payoff = 0
	
    # 총 시뮬레이션 횟수개만큼의 만기 주가 생성
    s_mat = s0 * np.exp(drift * maturity + vol * np.sqrt(maturity) * np.random.normal(size=nSim))
    
    prob_cnt = 0
    for i in range(nSim):	#시뮬레이션 횟수만큼 반복하며

        if s_mat[i] / s0 >= barrier:	#미리 구한 종가의 퍼포먼스를 배리어와 비교하여 크면,
            payoff = gain_coupon		#수익쿠폰 지급
            prob_cnt += 1				#종가>배리어 인 확률을 구하기 위한 변수 prob_cnt
        else:	
        	# 종가가 배리어보다 작아 낙인 터치여부를 살펴야 한다면,
            # 브라운 브리지를 만들어야 함
            ######## Brownian Bridge ############################
            Z_T = (np.log(s_mat[i] / s0) - drift * maturity) / (vol)
            rn = np.zeros(ntime + 1)
            for i in range(ntime):
                rn[i + 1] = rn[i] + np.random.normal() * np.sqrt(dt)
            brownian_bridge = Z_T * time_grid / maturity + rn - time_grid / maturity * rn[-1]
            ######################################################
            
            #위에서 만든 브라운 브리지로 일일주가패스 생성
            sPath = s0 * np.exp(drift * time_grid + vol * brownian_bridge)
            
            #낙인 안치고, 만기종가가 배리어보다 작으면 풀더미 지급
            if ((min(sPath) / s0 >= ki_barrier) and (sPath[-1] / s0 < barrier)):
                payoff = fulldummy
            else:	#낙인 치고 배리어보다 작으면
                payoff = loss_coupon	#손실쿠폰지급

        sum_payoff += payoff
    val = round(sum_payoff / nSim * np.exp(-rfr * maturity), 3)
    cal_time = round(time.time() - start_time, 3)
    barrier_prob = round(prob_cnt / nSim, 3)
    return val, cal_time, barrier_prob	# 만기종가가 배리어 이상인 확률도 계산하여 리턴

 

 

계산 결과를 보겠습니다.

 

Value : 0.06, Elapsed time : 25.327 sec
Value(BB) : 0.059, Elapsed time : 3.324 sec, UpBarrier_prob: 0.749

Process finished with exit code 0

시뮬레이션 10,000번의 계산 소요시간을 보면, 브라운 브리지를 쓴 함수의 계산 시간이 훨씬 효율적임을 알 수 있습니다.

 

또한, 만기 주가가 배리어 이상인 확률도 이론값(위의 엑셀 그림에서 보면 0.746)과 거의 비슷하지요.

 

제일 중요한 비교인, 브라운 브리지를 사용한 값과 사용하지 않은 옵션 가격 결과가 거의 차이가 나지 않습니다. 이는 옵션의 가격 값의 정당성을 보장해주는 이른바 크로스체크이죠.

 

 

 

왜 이 옵션을 알아봤나?

제가 알기로는 이 글에선 다룬 위의 옵션 구조가 판매되거나, 투자된 적은 없습니다. 물론 사모 형식으로 OTC에서 거래되었을 수는 있습니다. 그럼에도 불구하고, 위 옵션을 가상으로 만들어 가격을 계산해 본 이유는

 

스텝다운 ELS의 구조와 흡사하기 때문

 

입니다. 스텝다운 ELS도 낙인을 치던, 말던 조기상환 배리어와 그 시점의 주가를 관찰하여 배리어보다 더 나은 포퍼먼스를 보인 경우에는 상환되어 끝나는 상품이기 때문입니다. 당연히 조기상환이 되지 않으면, 만기 상환까지 가서 낙인을 쳤는지, 낙인을 치지 않았으며 full dummy 지급 구간에 있는지를  따져야겠죠. 

이 과정을 간단히 함축해 놓은 구조가 바로 위의 옵션 구조인 것입니다.

 

언젠가는 스텝다운 ELS를 계산하는 글을 쓸 텐데요. 그때를 대비하여 미리 브라운 브리지를 소개하고, 이를 실제 상품 계산에 언제, 어떻게 이용하는지를 소개하기 위함이었다고 생각하시면 될 것 같습니다.

 

 

728x90
반응형

댓글