본문 바로가기
금융공학

1star 스텝다운 ELS의 계산(시뮬레이션)

by hustler78 2022. 12. 14.
728x90
반응형

이번 글부터 몇 회에 걸쳐 스텝다운 ELS의 가격을 계산하는 방법을 알아보겠습니다.

 

 

스텝다운 ELS란?

스텝다운 ELS은 몇 번의 조기상환 기회를 제공하는 주가연계 상품으로써, 주가가 많이 떨어지게 되어 낙인(knock in) 상황이 발생하면 만기 때 손실이 발생할 수 있는 상품입니다. 자세한 내용은 2022.11.16 - [금융공학] - 스텝다운 ELS

 

스텝다운 ELS

요즘 홍콩 H 증시가 폭락을 했죠. 2022년 7월부터 시작된 하락세는 10월 말 정점을 보여줘 급기야 HSCEI(항셍 차이나기업 지수)가 5천 포인트 밑을 하회하는 지경까지 이르렀습니다. HSCEI 지수의 폭락

sine-qua-none.tistory.com

을 살펴보면 됩니다.

 

 

스텝다운ELS를 가장 잘 이해(?)할 수 있는 자료는 아무래도 페이오프 구조 그림입니다. 큰 특징들을 정리해 보면

 

○ 조기상환 기회는 일정한 주기로 한 번씩 찾아온다(보통 6개월, 만기는 3년)

○ 조기상환 성공을 결정하는 배리어는 조기상환 단계별로 낮아진다. (step down)

○ 기초자산의 퍼포먼스에 의해 수익/손실이 결정되고, 기초자산의 퍼포먼스가 안 좋아 한번이라도 낙인 배리어(knock in barrier)를 하회하는 날에는 손실 날 가능성이 생기는 구조로 바뀐다.

○ 상품 기간 동안 낙인이 발생하지 않으면, 그 공로를 인정하여 만기 때 상환 배리어를 넘지 못하더라도 수익을 챙겨준다.(full dummy)

○ 낙인이 발생한 상황에서 마지막 배리어 찬스까지 놓치면 아쉽지만 손실 상환으로 끝난다.

 

[그림1]

 

 

 

스텝다운 ELS의 계산

스텝다운 ELS는 그 구조가 복잡하므로 보통 시뮬레이션 방법으로 계산할 수 있습니다. 

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

에서 시뮬레이션 방법론을 볼 수 있습니다. 요약해보면,

 

①   주가 패스를 생성한다.

②   생성된 주가 패스에 대해서 상품의 페이오프를 계산한다. 이 페이오프를 현재 계산시점으로 할인한다.

③   ①~②번을 여러번 반복하여 여러 개의 할인 페이오프 샘플을 얻는다.

④   ③에서 얻은 샘플들의 기댓값을 구한다.

 

③ 의 '여러번' 이라는 단어가 가격의 품질을 좌우합니다. 샘플의 개수가 많을수록 정확한 값을 얻어낼 수 있지만 계산 속도가 느려지겠죠. 반면에 샘플의 개수가 빈약하면 계산 결과는 빨리 얻을 수 있으나 가격이 엉성할 수 있습니다.

 

 

 

스텝다운 ELS을 평가하기 위해 주가패스를 어떻게 설정해야 할까요?

콜옵션/풋옵션처럼 만기 주가 하나만 생성해도 될까요? 직관적으로 드는 생각은

 

스텝다운 ELS은 낙인 여부를 관찰해야 하니 데일리로 주가 패스를 만들어야겠다!

 

라는 생각이 듭니다. 낙인을 치고 만기까지 질질 끌다가 마지막 배리어를 넘지 못하여 손실 상환이 되는 경우가 있고, 이 부분을 처리하기 위해 낙인 여부를 판단해야 하므로 데일리 주가를 생성해내는 게 맞아 보입니다.

 

그러면, 데일리 주가를 생성하는 방법으로 스텝다운 ELS의 가격을 계산해 보도록 하겠습니다.  구하고자 하는 상품은 위에 그래프 [그림 1]의 구조인

 

3y 6m, 90/90/85/85/80/80, KI 60, coupon 10% p.a.

 

입니다.(p.a. 는 per annum의 약어로서, 연 쿠폰 10%라는 얘기입니다(따라서 6개월 뒤에 조기 상환되면 10%의 절반인 5% 지급 상품입니다.)

 

 

 

Python으로 코딩한 1 star 스텝다운 ELS 가격

 

일일 주가를 생성하기 위하여 1년을 $\frac1{250}$이라고 가정합니다. 

또한, 기초자산과 시장 파라미터를 효율적으로 관리하기 위해 class 문법을 도입했습니다.

 

금융시장의 주류를 이루고 있는 ELS 상품은 기본적으로 기초자산이 2개 이상입니다. 요즘은 3개짜리들이 더 많습니다. 하지만 이 예제에서는 이해를 돕기 위해 기초자산이 하나일 때, 즉 1 star인 경우를 계산해 보기로 하죠.

 

1 Star : 기초자산 1개라는 뜻~

 

import numpy as np
import time

class Market:
    def __init__(self, rfr):
        self._rfr = rfr

class Underlying:
    def __init__(self, refprice, spotvalue, volatility, dividend):
        self._volatility = volatility
        self._dividend = dividend
        self._spot = spotvalue
        self._refprice = refprice

def OneDimELS_MC(objUnderlying: Underlying, objMarket: Market, redemption_schedule, coupon,
                 full_dummy, barrier, ki_barrier, n_iteration):
    vol = objUnderlying._volatility
    div = objUnderlying._dividend
    rfr = objMarket._rfr
    current_spot = objUnderlying._spot
    refprice = objUnderlying._refprice

    dt = 1 / 250
    drift = (rfr - div - 0.5 * vol ** 2) * dt
    diffusion = vol * np.sqrt(dt)
    maturity = redemption_schedule[-1]

    sum_price = 0

    redemp_prob = np.zeros(len(redemption_schedule) + 2)
    np.random.seed(0)
    start_time = time.time()
    for i in range(n_iteration):
        temp_price = 0
        sSeries = [current_spot]
        payoff = 0
        discount_factor = 0
        old_spot = current_spot
        for j in range(maturity):
            new_spot = old_spot * np.exp(drift + diffusion * np.random.normal())
            sSeries.append(new_spot)
            old_spot = new_spot
        sSeries = np.array(sSeries) / refprice

        for k in range(len(redemption_schedule)):
            ind = redemption_schedule[k]
            redemption_mat = ind
            if sSeries[ind] >= barrier[k]:
                payoff = 1 + coupon[k]
                discount_factor = np.exp(-rfr * redemption_mat / 250)
                redemp_prob[k] += 1
                break
            else:
                discount_factor = np.exp(-rfr * redemption_mat / 250)
                if (k == len(redemption_schedule) - 1) & (min(sSeries) < ki_barrier):
                    payoff = sSeries[redemption_mat]
                    redemp_prob[-1] += 1
                elif (k == len(redemption_schedule) - 1) & (min(sSeries) >= ki_barrier):
                    payoff = 1 + full_dummy
                    redemp_prob[-2] += 1

        temp_price = payoff * discount_factor
        sum_price += temp_price

    els_value = sum_price / n_iteration
    cal_time = round(time.time() - start_time, 3)
    redemp_prob /= n_iteration
    return els_value, cal_time, redemp_prob

 

간략히 살펴보도록 하겠습니다.

 

class Market:	#market parameter를 관리하기 위한 class 도입
    def __init__(self, rfr):
        self._rfr = rfr	#class 멤버 변수 rfr : 무위험 이자율을 뜻함

 


class Underlying:	#Underlying class 설정
    def __init__(self, refprice, spotvalue, volatility, dividend):
        self._volatility = volatility	#멤버변수 _volatility : 변동성
        self._dividend = dividend		#멤버변수 _dividend : 연속 배당률
        self._spot = spotvalue			#멤버변수 _spot : 기초자산 현재가
        self._refprice = refprice		#멤버변수 _refprice : 기초자산 기준가

 


def OneDimELS_MC(objUnderlying: Underlying, objMarket: Market, redemption_schedule, coupon,
                 full_dummy, barrier, ki_barrier, n_iteration):

# 입력파라미터
# objUnderlying : 기초자산 class
# objMarket : 시장 파라미터 class
# redemption_schedule : 상환스케줄 array (발행시점부터 경과된 일자, 예를들어 
# 6m = 125, 1y=250,..3y=750  -> (125, 250, 375, 500, 625, 750))
# coupon : 상환시점에 수익상환시 지급되는 쿠폰 array (예컨대 {5%, 10%, 15%, 20%, 25%, 30%)
# full_dummy : 만기시 full dummy 쿠폰 값
# barrier : 상환시점의 배리어 array ( 예컨대, (0.9, 0.9, 0.85, 0.85, 0.8, 0.8))
# ki_barrier : 낙인 배리어
# n_iteration : MonteCarlo Simulation 횟수

 

 


    vol = objUnderlying._volatility		#class 멤버변수에서 얻어온 변동성
    div = objUnderlying._dividend
    rfr = objMarket._rfr
    current_spot = objUnderlying._spot
    refprice = objUnderlying._refprice

    dt = 1 / 250	#하루 단위를 연단위로 표시
    drift = (rfr - div - 0.5 * vol ** 2) * dt	#일간 drift term
    diffusion = vol * np.sqrt(dt)				#일간 diffusion term
    maturity = redemption_schedule[-1]			#상환스케쥴의 마지막 원소는 만기일자

    sum_price = 0	# MC시뮬레이션 하나당 발생하는 결과를 누적할 변수

    redemp_prob = np.zeros(len(redemption_schedule) + 2)	
    # 각 상환시점 쿠폰 상환 확률, full_dummy 지급확률, 손실상환 확률을 구할 변수
    np.random.seed(0)	# random number 시드고정
    start_time = time.time()	#계산소요시간을 산출하기 위해 start time 기록

 

 


    for i in range(n_iteration):    # simulation을 돌리기 시작
        temp_price = 0              # 각 simulation 당 할인된 페이오프 저장할 변수
        sSeries = [current_spot]    # sSeries라는 list에 우선 현재가 집어넣음
        payoff = 0                  # payoff 변수
        discount_factor = 0         # 할인팩터 변수
        old_spot = current_spot     # 재귀적으로 일일주가패스 생성을 위한 변수 old_spot도입
        for j in range(maturity):
            new_spot = old_spot * np.exp(drift + diffusion * np.random.normal())
            # GBM model로 내일의 주가 생성
            sSeries.append(new_spot)    # 새로이 생성된 new_spot 을 sSeries list에 삽입
            old_spot = new_spot         # 재귀적 생성을 위해 다시 old_spot에 대입
        sSeries = np.array(sSeries) / refprice	# 퍼포먼스 계산을 위해 기준가 refprice로 나눔

        for k in range(len(redemption_schedule)):  # 상환스케쥴 배열 for문을 돌리며
            ind = redemption_schedule[k]           # k번째 상환스케쥴 일자를 ind에 저장
            redemption_mat = ind                   # 저장된 값이 k번째 상환만기일자가 됨
            if sSeries[ind] >= barrier[k]:         # 딱 그날 퍼포먼스가 배리어 상회하면
                payoff = 1 + coupon[k]             # 수익쿠폰 및 원금 상환
                discount_factor = np.exp(-rfr * redemption_mat / 250)	# 할인펙터 계산
                redemp_prob[k] += 1                # k번째 상환시점에 상환됐음을 count함
                break                              # 상품이 상환되어 끝났으므로 for문을 끝냄
            else:	# 쿠폰 수익 상환이 안되었다면,
                discount_factor = np.exp(-rfr * redemption_mat / 250)	#만기까지 간 상황, 할인팩터 구함
                if (k == len(redemption_schedule) - 1) & (min(sSeries) < ki_barrier):
                # 만기상환시점이고, 주가패스 중 최저점이 낙인배리어보다 아래면(즉, 낙인 발생)
                    payoff = sSeries[redemption_mat]	# 만기퍼포먼스가 payoff가 됨
                    redemp_prob[-1] += 1				# redemp_prob의 마지막 원소에 counting함
                elif (k == len(redemption_schedule) - 1) & (min(sSeries) >= ki_barrier):
                #만기상한시점이고 낙인을 안쳤으면
                    payoff = 1 + full_dummy	#full dummy지급구간임
                    redemp_prob[-2] += 1        #마지막에서 두번째 원소가 full_dummy 지급 counting

        temp_price = payoff * discount_factor   #할인된 페이오프
        sum_price += temp_price                 #할인된 페이오프를 누적함

    els_value = sum_price / n_iteration         #누적한 결과를 시뮬레이션 횟수로 나눔, 즉 기댓값

 

 


    cal_time = round(time.time() - start_time, 3)	#계산 종료, 계산경과시간 계산
    redemp_prob /= n_iteration	#redemp_prob 는counting만 한 것이므로 simul횟수로 나눠 확률을 구해줌
    return els_value, cal_time, redemp_prob	# elsvalue와 계산시간, 상환확률 return

 

이제 결과를 보겠습니다.

 

 

 

python coding 결과

underlying_spot = Underlying(refprice=100, spotvalue=100, volatility=0.3, dividend=0)
# 기준가 100, 현재가격 100, 변동성 30%, 배당 0인 기초자산 객체 설계
interest_rate = Market(0.03)
# 이자율이 3%인 시장 파라미터 클래스 설계
redemption_schedule = np.array([1, 2, 3, 4, 5, 6]) * 125    #3y 6m, 6 Chance Stepdown ELS
coupon = np.array([1, 2, 3, 4, 5, 6]) * 0.05                # coupon : 10% p.a.
full_dummy = coupon[-1]	                                    # full dummy cpn = 30%
barrier = np.array([0.9, 0.9, 0.85, 0.85, 0.8, 0.8])        # 조기/만기상환 배리어
ki_barrier = 0.6                                            # knock in barrier
n_iteration = 10000                                         # simulation 횟수

els_price, cal_time, prob = OneDimELS_MC(underlying_spot, interest_rate, redemption_schedule, coupon,
                                         full_dummy, barrier, ki_barrier, n_iteration)
np.set_printoptions(suppress=True)   #scientific notation 을 피하기 위한 print 옵션
print(els_price)
print(cal_time)
print(prob)

 

실행시켜 보면,

 

0.9833693017422173
44.893
[0.6798 0.0886 0.0557 0.0237 0.0231 0.0131 0.0016 0.1144]

ELS의 가격은 현재가치로 0.983 정도 나옵니다. 만일 내가 위 ELS에 1억을 투자한다면, 사실은 

9,830만 원짜리 상품들 1억 주고 산 셈

이죠. 

 

10,000번 시뮬레이션에 45초 정도 걸렸군요.  역시 첫 번째 조기상환 확률이 68% 정도로 유효 만기가 6개월이 될 확률이 높네요.

 

다만, 이런 복잡한 구조의 상품을 10,000번 밖에 시뮬레이션하는 것은 결과가 엉성할 수 있습니다. 시뮬레이션 횟수를 더 늘리고 싶어도 계산시간의 제약이 따를 것 같습니다. 예컨대 10만 번을 돌리면, 450초 정도가 소요되어 7~8분 뒤에 결과를 만나볼 수 있죠. 계산이 더 빠른 알고리즘을 찾는다면, 빨라진 계산만큼 시뮬레이션 횟수를 늘릴 수 있으니 더욱 정교한 값을 찾을 수 있습니다.

 

계산 시간을 단축할 만한 좋은 아이디어가 없을까요? 다음 글에서 고민해 보도록 하겠습니다.

728x90
반응형

댓글