-
파이썬을 활용한 ELS 가치 평가하기: ELS Valuation3금융퀀트/자산평가&프로그램매매 2024. 1. 1. 16:34반응형
ELS 평가하기
파이썬을 활용한 주가 경로 구하기: ELS Valuation2 를 통해서 주가 경로를 구할 수 있게 되었다면 몬테카를로 시뮬레이션을 통한 ELS 가치평가가 가능해진다. 각 주가 경로 시뮬레이션 별로 ELS 상품에 제시된 계약 조건에 따라서 내 투자금이 얼마로 늘어나는지 혹은 줄어드는지 파악하면 되는 것이다. 그리고 각 시뮬레이션별로 나오는 미래 가치를 현재 가치로 환산하고 그 금액들의 평균을 구하면 ELS 계약의 가치를 알 수 있다.
ELS 계약조건
다양한 ELS 상품이 있겠지만 이번에 평가해 볼 ELS는 낙인, 낙아웃 조건 없는 가장 기본적인 아래 그림 1의 상품이다.
아래 그림 2의 평가 일정에 따라서 평가일에 계약시점 대비 기초자산인 S&P500 지수가 "조기/만기 상환조건"보다 크면 조기에 상품이 종료된다. 조기 상환 전까지는 위 그림 1에서 보이는 연 5.01% 의 수익률(월 0.4175%)을 매월 지급받게 된다.(실제 수익은 상환지급일에 받게 된다.)
만기에 S&P500 지수가 계약시점 대비 65% 이상이면 상환조건에 충족되어서 원금에 계약기간인 3 년간 연 5.01%의 수익률을 얻을 수 있지만 65% 이하로 떨어지면 3 년간 연 5.01%의 수익률은 받을 수 있으나 계약시점 대비 지수가 하락한 비율만큼 원금 손실이 발생하게 된다. 이 원금 손실 조건이 아래 그림 3에 나타난다.
계약 조건별 함수 구성
조기상환 일자 구하기: Early_PaymentDate 함수 만들기
ELS의 수익률 및 상환 조건들을 부분으로 쪼개서 각각 함수로 표현할 수 있는데, 먼저 위 그림 2에 나온 상환스케줄 함수부터 구현해보자. 각 상환 스케줄의 사이의 구간을 step이라고 하고, 조기상환일자가 도래할 때마다 step 이 0 -> 1 -> 2 -> 3...으로 점점 늘어나는 구조로 생각한다면 아래 그림 4와 같은 구조를 생각해 볼 수 있다.
이 구조를 함수로 나타내면 아래 코드로 나타낼 수 있다.
def Early_PaymentDate(self, datelist: list, step: int, curdate: datetime): # step 이 0 일 때 현재일자가 평가일자 리스트의 첫번째 날짜보다 같거나 크면 step 1 반환 if step == 0 and curdate >= datetime.strptime(datelist[0], "%Y-%m-%d"): step = 1 return step # step 이 1 일 때 현재일자가 평가일자 리스트의 두번째 날짜보다 같거나 크면 step 2 반환 elif step == 1 and curdate >= datetime.strptime(datelist[1], "%Y-%m-%d"): step = 2 return step # step 이 2 일 때 현재일자가 평가일자 리스트의 세번째 날짜보다 같거나 크면 step 3 반환 elif step == 2 and curdate >= datetime.strptime(datelist[2], "%Y-%m-%d"): step = 3 return step # step 이 3 일 때 현재일자가 평가일자 리스트의 네번째 날짜보다 같거나 크면 step 4 반환 elif step == 3 and curdate >= datetime.strptime(datelist[3], "%Y-%m-%d"): step = 4 return step # step 이 4 일 때 현재일자가 평가일자 리스트의 다섯번째 날짜보다 같거나 크면 step 5 반환 elif step == 4 and curdate >= datetime.strptime(datelist[4], "%Y-%m-%d"): step = 5 return step # step 이 5 일 때 현재일자가 평가일자 리스트의 여섯번째 날짜보다 같거나 크면 step 6 반환 elif step == 5 and curdate >= datetime.strptime(datelist[5], "%Y-%m-%d"): step = 6 return step else: return step
조기상환 가격 조건 구하기: Early_PaymentPrice 함수 만들기
조기상환 기준 가격도 각 상환 스케줄의 사이의 구간을 step이라고 한다면 step 1로 변할 때 초기가격 * 0.95, step 2로 변할 때 초기가격 * 0.9... 와 같은 구조로 생각해 볼 수 있다. 이를 함수로 나타내면 아래 코드로 나타낼 수 있다.
def Early_PaymentPrice(self, basevalue: float, step): # step 1 일 때 입수된 basevalue 에 0.95 로 조기상환 기준가 생성 if step == 1: price = basevalue * 0.95 # step 2 일 때 입수된 basevalue 에 0.90 로 조기상환 기준가 생성 elif step == 2: price = basevalue * 0.90 # step 3 일 때 입수된 basevalue 에 0.85 로 조기상환 기준가 생성 elif step == 3: price = basevalue * 0.85 # step 4 일 때 입수된 basevalue 에 0.80 로 조기상환 기준가 생성 elif step == 4: price = basevalue * 0.80 # step 5 일 때 입수된 basevalue 에 0.75 로 조기상환 기준가 생성 elif step == 5: price = basevalue * 0.75 # step 6 일 때 입수된 basevalue 에 0.65 로 조기상환 기준가 생성 elif step == 6: price = basevalue * 0.65 return price
상환 시점의 가치 현재가치화 하기: Get_CurrentValue 함수 만들기
주가 시나리오별로 step 1에서 조기상환 조건을 만족해서 6개월의 수익을 얻은 시나리오도 있을 것이고, 만기까지 간 뒤에 기준가가 65% 밑으로 떨어져서 3년 동안 월별 이자는 받았지만 원금의 손실은 발생한 시나리오도 있을 것이다. 시나리오별 미래 가치를 현재 가치화해서 평균을 내면 상품의 현재가치가 된다. 현재 가치화하기 위한 금리, 투자기간(연 환산을 위해서 365로 나눔), 미래가치를 변수로 받는 함수를 아래와 같이 만들 수 있다.
def Get_CurrentValue(self, discountrate: int, duration: int, futurevalue: int): # 미래가치 * exp(-할인율 * 투자기간(일수)/365 ) currentvalue = futurevalue * np.exp(-discountrate*duration/365) # 현재가치 반환 return currentvalue
ELS 가치 평가 모형: Get_StochasticReturn 함수 만들기
Get_StochasticReturn으로 시나리오 구성
지금까지 만든 함수를 활용해서 주가 시나리오 별로 ELS 가입에 따른 최종 수익 및 자산의 현재가치를 만들어주는 함수를 만들 수 있다. 처음에(datelist[0]) 최초 포트폴리오 가치 100을 설정해 주고, 다음 주가 경로를 GeometricBrownianMotion 함수를 이용해서 만들어 준다. 그 과정에서 재평가일자에 해당되는 경우 조기상환 조건을 충족하는지 확인한다. 조기상환 조건을 충족하면 계약기간 동안 연 5.01%의 수익률로 월별 수익을 지급하고, 원금을 지급한다. 특히 만기인 마지막 재평가일자에는 기초자산가치가 초기 가격보다 65% 이하로 떨어졌을 경우 원금에 기초자산이 하락한 만큼의 비율을 감안해서 원금 손실을 부여해 주는 것을 감안한다. 이를 그림으로 나타내면 아래 그림 5와 같다.
그림 5에 나타낸 구조를 코드로 구성하면 아래와 같다.
# Get_StochasticReturn 함수는 maxno로 시뮬레이션 횟수, rfr로 현재가치화 하는 금리, assetvol 로 # 기초자산 변동성, timeunit으로 시간단위, elsreturn으로 els 수익률, datelist로 영업일 list # contractdates로 계약 시작일, 종료일 리스트, terminationdates로 중도청산일자 list 등을 변수로 받는다. def Get_StochasticReturn(self, maxno: int, rfr: int, assetvol: int, timeunit: int, elsreturn: float, \ datelist, contractdates: list, terminationdates: list): # 시나리오별 주가경로를 저장할 빈 dataframe을 만든다. totdf = pd.DataFrame() # 시나리오별 수익률, 종료일, 미래가치, 현재가치 등을 저장할 빈 dataframe를 만든다. asset_fin_result = [] # 계약시작일자를 날짜형식으로 변수화 contstartdate = datetime.strptime(contractdates[0], "%Y-%m-%d") for simulationno in range(1, maxno + 1): columns = ["DATE", f"SCENARIO{simulationno}"] columns_return = ["SCENARIO", "TERMINATIONDATE", "RETURN", "TOTVALUE", "CURRENTVALUE", "DURATION"] subdf = pd.DataFrame(columns=columns) # 하나의 시나리오 결과를 results list에 저장 results = [] # step 0부터 시작 step = 0 maxreturn = elsreturn for date in datelist: # 첫번째 평가일의 경우 자산가치 = 최초가치 if date == datelist[0]: curvalue = basevalue result = [date, curvalue] results.append(result) # 그 외의 경우 else: # GeometricBrownianMotion함수로 다음날의 기초자산 가치 계산 nextvalue = self.GeometricBrownianMotion(curvalue, rfr, assetvol, timeunit) curvalue = nextvalue # 투자기간 구하기 dur = (date - contstartdate).days # 재평가일자에 해당되고, 마지막 이전의 재평가일자일 경우 if step!=5 and step != self.Early_PaymentDate(terminationdates, step, date): step = self.Early_PaymentDate(terminationdates, step, date) # 조기상환 조건충종할 경우 if curvalue >= self.Early_PaymentPrice(basevalue, step): fv = maxreturn/12*step*6*basevalue+basevalue pv = self.Get_CurrentValue(rfr, dur, fv) # 원금 + 연 5.01% 수익 받고 해당 시뮬레이션 종료 asset_result = [f"SCENARIO{simulationno}", date, maxreturn, fv, pv, dur/365] break # 재평가일자에 해당되고 마지막 재평가일자일 경우 elif step==5 and step != self.Early_PaymentDate(terminationdates, step, date): # 기초자산가치가 "초기가치 * 65%"보다 큰 경우 if curvalue >= self.Early_PaymentPrice(basevalue, step): fv = maxreturn/12*step*6*basevalue+basevalue pv = self.Get_CurrentValue(rfr, dur, fv) # 원금 + 연 5.01% 수익 받고 해당 시뮬레이션 종료 asset_result = [f"SCENARIO{simulationno}", date, maxreturn, fv, pv, dur/365] break # 그 외의 경우(기초자산가치가 65% 보다 더 적은 경우) else: fv = maxreturn/12*step*6*basevalue + basevalue * (curvalue/basevalue) pv = self.Get_CurrentValue(rfr, dur, fv) # 원금손실 + 연 5.01% 수익 받고 해당 시뮬레이션 종료 asset_result = [f"SCENARIO{simulationno}", date, (fv/basevalue-1)*365/dur, fv, pv, dur/365] break # 재평가일자에 해당되지 않을 경우 시나리오list(results)에 주가경로 결과만 추가 else: result = [date, curvalue] results.append(result) # datelist를 거치면서 만들어진 results list를 dataframe 형식으로 변환 subdf = pd.DataFrame(data=results, columns=columns) # 시뮬레이션 결과를 "DATE"컬럼을 기준으로 오른쪽컬럼에 붙이기 if simulationno == 1: totdf = subdf else: totdf = pd.merge(totdf, subdf, on="DATE") # asset_fin_result에 시뮬레이션 결과 요약을 추가하기(row 추가) asset_fin_result.append(asset_result) return_df = pd.DataFrame(data=asset_fin_result, columns=columns_return) return totdf, return_df
모든 시나리오의 평균 수익률, 평균 가치, 평균 현재가치, 평균 투자기간 산출
계약기간은 2023-11-27 ~ 2026-12-04 이고, 파이썬을 활용한 주가 경로 구하기: ELS Valuation2 에서 작업한 내용과 동일하게 무위험 금리는 SOFR 금리인 0.0532%, 변동성 산출 기간은 2007-06-01 ~ 2009-06-30으로 설정한다. 그리고 중도청산일자인 2024-06-05, 2024-12-04, 2025-06-04, 2025-12-04, 2026-06-05, 2026-12-04를 리스트로 변수화하고, 시뮬레이션 횟수를 100번으로 해서 계약의 현재가치를 계산하는 코드는 아래와 같다.
import yfinance as yf import numpy as np import pandas_market_calendars as mcal from datetime import datetime import pandas as pd class SNP500ELS001(): def __init__(self, ticker: str, calendar: str): self.ticker = ticker self.cal = calendar def Get_Variance(self, startdate: str, enddate: str): asset_price = yf.download(self.ticker, start=startdate, end=enddate) asset_returns = asset_price["Adj Close"].pct_change().dropna() asset_vol = np.std(asset_returns) return asset_vol def Get_TimeUnit(self, startdate: str, enddate: str): calname = mcal.get_calendar(self.cal) stdate = datetime.strptime(startdate, "%Y-%m-%d") eddate = datetime.strptime(enddate, "%Y-%m-%d") schedule = calname.valid_days(start_date=stdate, end_date=eddate) datestrlist = [date.strftime('%Y-%m-%d') for date in schedule] datelist = [datetime.strptime(date, '%Y-%m-%d') for date in datestrlist] return datelist def GeometricBrownianMotion(self, curvalue: float, rfr: float, asset_vol: float, timeunit: float): z = np.random.randn() delta_t = 1/timeunit nextvalue = curvalue * np.exp((rfr - 0.5*(asset_vol**2))*delta_t \ + asset_vol*np.sqrt(delta_t)*z) return nextvalue def Early_PaymentDate(self, datelist: list, step: int, curdate: datetime): if step == 0 and curdate >= datetime.strptime(datelist[0], "%Y-%m-%d"): step = 1 return step elif step == 1 and curdate >= datetime.strptime(datelist[1], "%Y-%m-%d"): step = 2 return step elif step == 2 and curdate >= datetime.strptime(datelist[2], "%Y-%m-%d"): step = 3 return step elif step == 3 and curdate >= datetime.strptime(datelist[3], "%Y-%m-%d"): step = 4 return step elif step == 4 and curdate >= datetime.strptime(datelist[4], "%Y-%m-%d"): step = 5 return step elif step == 5 and curdate >= datetime.strptime(datelist[5], "%Y-%m-%d"): step = 6 return step else: return step def Early_PaymentPrice(self, basevalue: float, step): if step == 1: price = basevalue * 0.95 elif step == 2: price = basevalue * 0.90 elif step == 3: price = basevalue * 0.85 elif step == 4: price = basevalue * 0.80 elif step == 5: price = basevalue * 0.75 elif step == 6: price = basevalue * 0.65 return price def Get_CurrentValue(self, discountrate: int, duration: int, futurevalue: int): currentvalue = futurevalue * np.exp(-discountrate*duration/365) return currentvalue def Get_StochasticStockProcess(self, maxno: int, rfr: int, assetvol: int, timeunit: int, datelist): totdf = pd.DataFrame() for simulationno in range(1, maxno+1): columns = ["DATE", f"SCENARIO{simulationno}"] subdf = pd.DataFrame(columns=columns) results = [] for date in datelist: if date == datelist[0]: curvalue = basevalue result = [date, curvalue] results.append(result) else: nextvalue = self.GeometricBrownianMotion(curvalue, rfr, assetvol, timeunit) curvalue = nextvalue result = [date, curvalue] results.append(result) subdf = pd.DataFrame(data=results, columns=columns) if simulationno == 1: totdf = subdf else: totdf = pd.merge(totdf, subdf, on="DATE") return totdf def Get_StochasticReturn(self, maxno: int, rfr: int, assetvol: int, timeunit: int, elsreturn: float, \ datelist, contractdates: list, terminationdates: list): totdf = pd.DataFrame() asset_fin_result = [] contstartdate = datetime.strptime(contractdates[0], "%Y-%m-%d") for simulationno in range(1, maxno + 1): columns = ["DATE", f"SCENARIO{simulationno}"] columns_return = ["SCENARIO", "TERMINATIONDATE", "RETURN", "TOTVALUE", "CURRENTVALUE", "DURATION"] subdf = pd.DataFrame(columns=columns) results = [] step = 0 maxreturn = elsreturn for date in datelist: if date == datelist[0]: curvalue = basevalue result = [date, curvalue] results.append(result) else: nextvalue = self.GeometricBrownianMotion(curvalue, rfr, assetvol, timeunit) curvalue = nextvalue dur = (date - contstartdate).days if step!=5 and step != self.Early_PaymentDate(terminationdates, step, date): step = self.Early_PaymentDate(terminationdates, step, date) if curvalue >= self.Early_PaymentPrice(basevalue, step): fv = maxreturn/12*step*6*basevalue+basevalue pv = self.Get_CurrentValue(rfr, dur, fv) asset_result = [f"SCENARIO{simulationno}", date, maxreturn, fv, pv, dur/365] break elif step==5 and step != self.Early_PaymentDate(terminationdates, step, date): if curvalue >= self.Early_PaymentPrice(basevalue, step): fv = maxreturn/12*step*6*basevalue+basevalue pv = self.Get_CurrentValue(rfr, dur, fv) asset_result = [f"SCENARIO{simulationno}", date, maxreturn, fv, pv, dur/365] break else: fv = maxreturn/12*step*6*basevalue + basevalue * (curvalue/basevalue) pv = self.Get_CurrentValue(rfr, dur, fv) asset_result = [f"SCENARIO{simulationno}", date, (fv/basevalue-1)*365/dur, fv, pv, dur/365] break else: result = [date, curvalue] results.append(result) subdf = pd.DataFrame(data=results, columns=columns) if simulationno == 1: totdf = subdf else: totdf = pd.merge(totdf, subdf, on="DATE") asset_fin_result.append(asset_result) return_df = pd.DataFrame(data=asset_fin_result, columns=columns_return) return totdf, return_df if __name__ =="__main__": els = SNP500ELS001("^GSPC", "XNYS") contractdates = ["2023-11-27", "2026-12-04"] datadates = ["2007-06-01", "2009-06-30"] assetvol = els.Get_Variance(datadates[0], datadates[1]) datelist = els.Get_TimeUnit(contractdates[0], contractdates[1]) timeunit = len(datelist) basevalue = 100 rfr = 0.0532 elsreturn = 0.0501 terminationdates = ["2024-06-05", "2024-12-04", "2025-06-04", "2025-12-04"\ , "2026-06-05", "2026-12-04"] trialno = 100 totdf, return_df = els.Get_StochasticReturn(trialno, rfr, assetvol, timeunit, elsreturn, \ datelist, contractdates, terminationdates) meandf = pd.DataFrame(columns=["RETURN", "TOTVALUE", "CURRENTVALUE", "DURATION"]) for column in meandf.columns: meandf.loc[0, column] = return_df[column].mean() print(meandf)
StochasticProcess이기 때문에 실행때마다 결과가 다를 것이지만 해당 코드를 실행하면 아래 그림 6과 같이 계약의 현재가치는 99.690728 이 나오고, 평균 투자기간은 반년 정도 나온다.
그림 6에 따르면 해당 ELS는 100만큼 투자했을 때 투자금의 현재가치가 99.69728로 100보다 못한 가치를 지닌다. 투자의 무위험수익률인 SOFR가 5.32%인데, 위험성이 들어갔는데도 불구하고 ELS 수익으로 고작 5.01%를 지급하니 당연히 현재가치는 최초 투자액을 100으로 잡았을 때보다 더 낮은 것이다. 그리고 해당 ELS의 기초자산은 S&P500으로 변동성이 매우 낮다. 따라서 보통 6개월 뒤에도 큰 변화가 없을 확률이 매우 높다. 그 결과 최초 조기상환일인 2024-06-05 일에 대부분 조기상환 조건을 충족하여 투자기간도 6개월 정도로(1년 기준으로 0.5) 시뮬레이션 결과가 나오는 것이다. 시뮬레이션 결과 그렇게 좋은 상품 같지는 않다.
반응형'금융퀀트 > 자산평가&프로그램매매' 카테고리의 다른 글
미국 ETF 대표 종목 뽑아내기: python yahoo finance 활용 (0) 2024.07.14 수익률의 개념의 근본적인 이해: HPR, TWRR, CAGR, MWRR (0) 2024.02.18 NPV 계산기 (0) 2023.12.24 파이썬을 활용한 주가 경로 구하기: ELS Valuation2 (0) 2023.12.16 파이썬을 활용한 주가 변동성 구하기: ELS Valuation1 (0) 2023.12.14