코스피200 변동성 조정 위클리 양매도 전략

25년도 봄학기 금융공학 특수논제 <사례로 보는 금융공학 실무> A조

김경재(20259048) / 강상묵(20259013) / 김형환(20249132) / 어환석(20259248) / 유수형(20259273)

1. 개요

본 리포트는 옵션과 변동성지수를 활용한 변동성 조정 위클리 양매도전략을 알아보고, 과거 데이터를 통해 구현 및 검증하기 위해 작성되었습니다. 변동성 조정 위클리 양매도1.변동성 조정, 2.주단위 매도 두가지 특징을 통해 기존 단점을 보완한 양매도 전략으로, “매주” V-Kospi200를 이용해 행사가격 범위를 결정하고 콜/풋옵션을 매도(양매도)하게 됩니다.

전략 소개에 앞서 필요한 배경지식을 다루고, 전략 소개 및 구현, 백테스팅 및 성과를 차례로 설명하겠습니다.

2. 배경지식

양매도 (Short strangle)

양매도란 옵션 거래전략으로, 일반적으로 만기일이 같은 외가격(OTM) 콜/풋옵션을 매도하는 것을 의미합니다.

OTM 옵션을 매도하므로, 만기일에 기초자산의 가격이 풋옵션의 행사가격과 콜옵션의 행사가격 사이라면 권리행사되지 않게 되고 옵션 프리미엄(매도수익)을 그대로 얻을 수 있게 됩니다.

반면, 양매도는 시장의 변동성이 예상과 달리 큰 경우, 큰 손실이 발생할 수 있다는 단점도 존재합니다. 수익은 프리미엄으로 한정되어있으나, 시황 급변에 따른 옵션 손실에는 제한이 없어 원금 이상의 손실이 발생할 수도 있기 때문입니다.

즉, 양매도는 높은 확률로 안정적인 중수익을 보장하나 매우 낮은 확률로 큰 손실이 발생할 수 있는 전략이며, 향후 시장의 변동성이 크지 않을 것이라 예측될 때 주로 사용됩니다.

양매도 전략 예시

국내의 경우 행사가격의 상하단을 등가격(ATM) \(\pm\) 5%로 설정한 양매도 ETN이 한 때 큰 인기를 끌었습니다. 대표적인 예시인 월별 코스피200 OTM 5% 양매도 전략의 거래방법을 살펴보면, 다음과 같습니다.

  1. 현재 코스피200지수를 기준으로 ATM+5%의 콜옵션과 ATM-5%의 풋옵션을 매도 (프리미엄 수익 발생)
  2. 다음달 만기일이 되면, 옵션은 청산(권리행사시 손실)되고 다시 ATM \(\pm\) 5%의 콜,풋옵션을 매도

즉, 월단위로 옵션을 교체하게 되며 행사가격의 상하단은 ATM \(\pm\) 5%로 고정한 양매도 전략입니다. 중위험-중수익으로 흥행에 성공한 상품이지만, 아래와 같은 한계점도 존재합니다.

  1. 행사가격 범위가 고정되어있어 변동성 확대시 손실 가능성이 커지고, 변동성 축소시 프리미엄 수익이 크게 감소
  2. 옵션 교체주기가 1개월로, 그간 지수의 하락이 누적된다면 손실이 과도하게 커질 수 있음 (유동성 리스크)

코스피200 변동성지수 (V-Kospi200 index)

코스피200 변동성 지수란 주식 시장의 변동성을 측정하는 지표입니다. 코스피200 옵션 가격 기반의 내재변동성을 이용하여 산출되며, 향후 30일간 시장 변동성에 대한 투자자들의 기대를 나타내는 지수입니다.

주요 특징

  • 시장 심리 반영: 투자자들의 불안 심리를 반영하며, ’공포 지수(Fear Index)’라고도 불림
  • 옵션 가격 기반: 코스피200 옵션 가격 기반의 내재변동성을 통해 산출되며, 여기에는 투자자들의 기대가 반영

그래프1 : 일별 코스피200지수 및 코스피200 변동성지수

코스피200 위클리옵션

코스피200 위클리옵션이란 코스피200 지수를 기초자산으로 하는 주간 단위의 옵션 거래 상품을 말합니다. ’19년 상장되었으며 기존의 월 단위 만기가 도래하는 옵션과는 달리, 매주 월/목요일에 만기가 도래하는 단기 옵션입니다.

주요 특징

  • 세밀한 거래 지원: 매주 월/목요일에 만기가 도래하여 단기적인 시장 변동에 대한 투자 및 위험 관리에 유리
  • 다양한 투자 전략 활용: 단기적인 시장 예측을 기반으로 다양한 투자 전략을 구사할 수 있습니다.

3. 전략 소개 및 구현

전략 개요

변동성 조정 위클리 양매도(Volatility Adjusted Weekly Short strangle, VWss) 전략이란, 기존에 널리 활용되던 월별 OTM 5% 양매도 전략에 아래 두가지 방법을 결합한 전략을 의미합니다.

  1. 옵션의 교체주기를 “매월” -> “매주” 단위로 세분화
  2. 행사가격의 범위를 고정하는 것이 아니라 매 옵션 매도시점마다 시장 변동성을 고려하여 조정

따라서, 기존과 달리 주단위로 옵션을 교체하며, 행사가격의 범위는 교체시점에 매번 달라지게 됩니다. 이를 위해 코스피200위클리옵션을 사용하고, 변동성 지표는 내재변동성 기반의 V-Kospi200지수를 사용**하여 구현할 계획입니다.

행사가격 범위 설정방법

행사가격의 상하단은 옵션 매도시점의 “전일 V-Kospi200 종가”를 참조하여 설정할 예정입니다. V-Kospi200는 향후 30일간 코스피200 지수의 변동성을 수치화한 지표로서, 현재시점에서 변동성을 잘 예측할 수 있는 수단이기 때문입니다.

다만, 본 전략에서 옵션 매도포지션의 유지기간은 약 7일이므로 지수 종가(연율)를 주단위 기간에 맞도록 환산하여 행사가격의 상하단을 산출하였습니다.

\[\sigma_{Target}\;=\;\frac{VKospi200_{(T-1)}\times\sqrt{Calendar\;days}}{\sqrt{365}}\]

\[Call\;strike\;=\;Kospi200_{(T)}\times (1+\sigma_{Target})\;rounded\;up\;to\;2.5pt\]

\[Put\;strike\;=\;Kospi200_{(T)}\times (1-\sigma_{Target})\;rounded\;down\;to\;2.5pt\]

이렇게 행사가격을 설정하면 V-Kospi200가 15pt일 때 월환산 변동성이 약 5%(주환산 2.5%)가 됩니다. 즉,

  • 시장의 예상 변동성(V-Kospi200)이 연 15% 이상 -> 행사가격 범위를 기존보다 넓게 설정하여 손실 가능성 축소
  • 시장의 예상 변동성이 연 15% 미만 -> 행사가격 범위를 기존보다 좁게 설정하여 프리미엄 수익 극대화

본 전략은 위클리옵션을 사용하므로 향후 1주일 변동성이 필요합니다. 따라서, 30일 변동성을 나타내는 V-Kospi200은 만기 mismatch가 있지만, 단기간의 예측치에는 큰 차이가 없고 이외의 대안(e.g. 7-day VIX)이 없어 V-Kospi200를 그대로 사용하였습니다.

포트폴리오 구성 방법

먼저, 편의를 위해 세금/수수료/호가스프레드 등의 거래비용은 없으며 종가에 원하는 수량만큼 거래할 수 있는 완전자본시장을 가정하도록 하겠습니다. 전략 구현을 위한 포트폴리오(투자원금 100억원) 구성 과정은 아래와 같습니다.

  1. 전일 V-Kospi200 지수를 주단위로 환산하여 예상변동성(\(\hat\sigma=(VKospi200\times \sqrt{day})/\sqrt{365}\)) 산출
  2. 예상변동성을 당일 Kospi200지수에 적용하여 행사가격 상하단(ATM \(\pm\hat\sigma\)을 2.5pt 단위로 올림/내림) 산출
  3. 당일 Kospi200지수 및 승수(25만)를 적용, 옵션 매도수량(\(Q_{sell}=nominal/(kospi200\times multiplier)\)) 산출
  4. 행사가격 상단 콜옵션, 하단 풋옵션 매도 (\(Premium=(call price+put price)\times Q_{sell}\times multiplier\))
  5. 원금과 매도수익을 다음주 만기일까지 MMF에 투자 (\(Interest=(nominal+Premium)\times MMF \times \frac{day}{365}\))
  6. 다음주 만기일이 되면, 권리행사 손실을 포함하여 최종손익 산출(\(Revenue=Premium+Interest-Exercise\))
    • Kospi200 > 행사가격 상단, 콜옵션에서 손실 발생(\(Exercise=(Kospi200-Strike_{call})\times multiplier\))
    • Kospi200 < 행사가격 하단, 풋옵션에서 손실 발생(\(Exercise=(Strike_{put}-Kospi200)\times multiplier\))
    • 이외의 경우, 권리행사되지 않아 옵션 포지션 청산(\(Exercise=0\))
  7. 주간 최종손익을 정산(\(nominal_{new}=nominal_{old}+Revenue\))하고, 투자종료시점까지 1. ~ 6. 과정을 반복

3. Backtesting

데이터 수집 및 전처리, 포트폴리오 구현

과거 5개년(2020~2024) 코스피200 등의 지수/옵션 가격, 금리를 수집하였으며, 출처는 아래와 같습니다.

한국거래소 정보데이터시스템(data.krx.co.kr) : 코스피200 및 V-Kospi200지수
한국거래소 OpenAPI(openapi.krx.co.kr) : 코스피200옵션 및 위클리옵션 종목별 가격
한국은행 경제통계시스템(ecos.bok.or.kr) : 일별 MMF(7일) 금리
한국거래소 OpenAPI를 활용한 데이터 수집 예시

API 호출시 json 데이터를 수집할 수 있으며, 이를 Dataframe(python) 및 tibble(r)로 변환하여 활용하였습니다.

import requests; import json
url = 'http://data-dbg.krx.co.kr/svc/sample/apis/drv/opt_bydd_trd?basDd=20250312'
headers = {'AUTH_KEY': '74D1B99DFBF345BBA3FB4476510A4BED4C78D13A'}
res = requests.get(url=url, headers=headers); res.text
'{"OutBlock_1":[{"BAS_DD":"20250312","PROD_NM":"코스피200 옵션","RGHT_TP_NM":"CALL","ISU_CD":"201W3195","ISU_NM":"코스피200 C 202503 195.0","TDD_CLSPRC":"145.90","CMPPREVDD_PRC":"6.85","TDD_OPNPRC":"144.95","TDD_HGPRC":"146.10","TDD_LWPRC":"144.95","IMP_VOLT":"64.00","NXTDD_BAS_PRC":"145.90","ACC_TRDVOL":"31","ACC_TRDVAL":"1129625000","ACC_OPNINT_QTY":"154"},{"BAS_DD":"20250312","PROD_NM":"코스피200 옵션","RGHT_TP_NM":"CALL","ISU_CD":"201W3197","ISU_NM":"코스피200 C 202503 197.5","TDD_CLSPRC":"-","CMPPREVDD_PRC":"-","TDD_OPNPRC":"-","TDD_HGPRC":"-","TDD_LWPRC":"-","IMP_VOLT":"64.00","NXTDD_BAS_PRC":"143.40","ACC_TRDVOL":"0","ACC_TRDVAL":"0","ACC_OPNINT_QTY":"0"},{"BAS_DD":"20250312","PROD_NM":"코스피200 옵션","RGHT_TP_NM":"CALL","ISU_CD":"201W3200","ISU_NM":"코스피200 C 202503 200.0","TDD_CLSPRC":"140.60","CMPPREVDD_PRC":"5.00","TDD_OPNPRC":"140.00","TDD_HGPRC":"141.15","TDD_LWPRC":"139.95","IMP_VOLT":"64.00","NXTDD_BAS_PRC":"140.60","ACC_TRDVOL":"31","ACC_TRDVAL":"1088750000","ACC_OPNINT_QTY":"290"},{"BAS_DD":"20250312","PROD_NM":"코스피200 옵션","RGHT_TP_NM":"CALL","ISU_CD":"201W3202","ISU_NM":"코스피200 C 202503 202.5","TDD_CLSPRC":"-","CMPPREVDD_PRC":"-","TDD_OPNPRC":"-","TDD_HGPRC":"-","TDD_LWPRC":"-","IMP_VOLT":"64.00","NXTDD_BAS_PRC":"138.40","ACC_TRDVOL":"0","ACC_TRDVAL":"0","ACC_OPNINT_QTY":"0"},{"BAS_DD":"20250312","PROD_NM":"코스피200 옵션","RGHT_TP_NM":"CALL","ISU_CD":"201W3205","ISU_NM":"코스피200 C 202503 205.0","TDD_CLSPRC":"-","CMPPREVDD_PRC":"-","TDD_OPNPRC":"-","TDD_HGPRC":"-","TDD_LWPRC":"-","IMP_VOLT":"64.00","NXTDD_BAS_PRC":"135.90","ACC_TRDVOL":"0","ACC_TRDVAL":"0","ACC_OPNINT_QTY":"0"},{"BAS_DD":"20250312","PROD_NM":"코스피200 옵션","RGHT_TP_NM":"CALL","ISU_CD":"201W3207","ISU_NM":"코스피200 C 202503 207.5","TDD_CLSPRC":"-","CMPPREVDD_PRC":"-","TDD_OPNPRC":"-","TDD_HGPRC":"-","TDD_LWPRC":"-","IMP_VOLT":"64.00","NXTDD_BAS_PRC":"133.40","ACC_TRDVOL":"0","ACC_TRDVAL":"0","ACC_OPNINT_QTY":"0"},{"BAS_DD":"20250312","PROD_NM":"코스피200 옵션","RGHT_TP_NM":"CALL","ISU_CD":"201W3210","ISU_NM":"코스피200 C 202503 210.0","TDD_CLSPRC":"130.60","CMPPREVDD_PRC":"5.60","TDD_OPNPRC":"130.60","TDD_HGPRC":"130.60","TDD_LWPRC":"130.60","IMP_VOLT":"64.00","NXTDD_BAS_PRC":"130.60","ACC_TRDVOL":"1","ACC_TRDVAL":"32650000","ACC_OPNINT_QTY":"20"},{"BAS_DD":"20250312","PROD_NM":"코스피200 옵션","RGHT_TP_NM":"CALL","ISU_CD":"201W3212","ISU_NM":"코스피200 C 202503 212.5","TDD_CLSPRC":"-","CMPPREVDD_PRC":"-","TDD_OPNPRC":"-","TDD_HGPRC":"-","TDD_LWPRC":"-","IMP_VOLT":"62.47","NXTDD_BAS_PRC":"128.40","ACC_TRDVOL":"0","ACC_TRDVAL":"0","ACC_OPNINT_QTY":"0"},{"BAS_DD":"20250312","PROD_NM":"코스피200 옵션","RGHT_TP_NM":"CALL","ISU_CD":"201W3215","ISU_NM":"코스피200 C 202503 215.0","TDD_CLSPRC":"-","CMPPREVDD_PRC":"-","TDD_OPNPRC":"-","TDD_HGPRC":"-","TDD_LWPRC":"-","IMP_VOLT":"60.94","NXTDD_BAS_PRC":"125.90","ACC_TRDVOL":"0","ACC_TRDVAL":"0","ACC_OPNINT_QTY":"59"},{"BAS_DD":"20250312","PROD_NM":"코스피200 옵션","RGHT_TP_NM":"CALL","ISU_CD":"201W3217","ISU_NM":"코스피200 C 202503 217.5","TDD_CLSPRC":"-","CMPPREVDD_PRC":"-","TDD_OPNPRC":"-","TDD_HGPRC":"-","TDD_LWPRC":"-","IMP_VOLT":"59.42","NXTDD_BAS_PRC":"123.40","ACC_TRDVOL":"0","ACC_TRDVAL":"0","ACC_OPNINT_QTY":"0"}]}'

수집한 데이터는 R을 이용하여 전처리하였으며, 두개의 데이터셋으로 요약하였습니다.

  1. 포트폴리오 기본 정보 : 옵션 만기일(매주), 옵션 보유일, MMF금리, Kospi200, 전일 V-K200, 거래대상옵션 정보
BAS_DD VOL DIFF_DAY MMF KOSPI200 LAG_VK200 PRIOR TARGET_C_K TARGET_P_K
20241219 0.0248719 7 0.0334 322.38 17.96 24124 332.5 312.5
20241212 0.0285279 7 0.0335 329.04 20.60 24123 340.0 317.5
20241205 0.0295527 7 0.0341 323.87 21.34 24122 335.0 312.5
20241128 0.0252043 7 0.0340 331.45 18.20 24121 340.0 322.5
20241121 0.0276001 7 0.0344 329.49 19.93 24114 340.0 320.0
20241114 0.0344274 7 0.0340 317.70 24.86 24113 330.0 305.0
  1. 거래대상옵션 정보 : 포트폴리오 기본 정보에 대응되는 거래대상옵션의 종가, 거래량(0인 경우 제외)
BAS_DD PRICE_TARGET_C_K PRICE_TARGET_P_K ACC_TRDVOL_TARGET_C_K ACC_TRDVOL_TARGET_P_K
20241219 0.29 0.74 343 137
20241212 0.42 0.78 190 196
20241205 0.44 0.91 65452 51417
20241128 0.34 0.43 288 201
20241121 0.37 0.53 115 201
20241114 0.43 0.74 329 165

이를 통해 매도수량, 프리미엄, 이자수익, 권리행사손실 등을 산출하여 포트폴리오를 구현하였습니다.

BAS_DD VOL SELL_AMT PREMIUM INTEREST EXERCISE REVENUE RATE
20200102 0.0203434 137.7648 29963837 2789154 0 32752991 0.0032753
20200109 0.0221160 135.8650 20040080 2844044 9510547 13373578 0.0013374
20200116 0.0185154 132.1091 18825550 2824485 0 21650035 0.0021650
20200123 0.0197895 132.3058 23815037 2787444 219296795 -192694314 -0.0192694
20200130 0.0235286 138.7107 45080972 2793358 109234664 -61360333 -0.0061360
20200206 0.0252458 133.0451 46565774 2774504 0 49340278 0.0049340
변동성 조정 방법에 따른 행사가격 범위 추이(과거 5년)

V-Kospi200과 연동한 행사가격의 범위는 지난 5년간 1.6% ~ 8.7%까지 광범위하게 형성되었습니다.

코스피200이 급등락하는 경우 범위가 넓게 형성되고 보합장에서는 좁게 형성되는 추이를 확인할 수 있으며, 이를 월환산(\(\times\sqrt{4}\))하여 기존 5%와 비교하면 지수의 상황에 따라 유동적으로 조절되고 있음을 의미합니다.


그래프2 : 월평균 코스피200지수(적색) 및 행사가격 범위(청색)

Backtesting 성과

지난 5년간(2020~2024) VW양매도와 코스피200지수 / OTM 5% 양매도를 비교해보았습니다.

표1 : 연도별 변동성 조정 위클리 양매도 포트폴리오 및 코스피200지수 성과

YEAR Expiry NoExercise Premium Interest Loss Revenue Return Return_K200
2020 50 0.74 4385 184 5338 -769 -0.04 0.33
2021 47 0.91 2974 142 728 2387 0.12 0.01
2022 52 0.79 3304 421 4152 -427 -0.03 -0.26
2023 52 0.81 2456 732 1758 1430 0.08 0.23
2024 49 0.80 3697 719 3454 962 0.05 -0.11

표2 : 연도별 일반 양매도 포트폴리오 및 코스피200지수 성과

YEAR Expiry NoExercise Premium Interest Loss Revenue Return Return_K200
2020 12 0.58 16507 847 37851 -20497 -0.23 0.33
2021 12 1.00 9887 605 0 10492 0.13 0.01
2022 12 0.50 10207 1819 15098 -3071 -0.04 -0.26
2023 12 0.83 4591 3176 884 6883 0.09 0.23
2024 12 0.75 9850 3072 9589 3333 0.04 -0.11

YEAR : 산출대상 연도 / Expiry : 옵션만기 횟수 (프리미엄 수익 발생 횟수)

NoExercise : 옵션만기일에 행사되지 않은 비율 (손실이 발생하지 않은 거래일 비율)

Premium : 평균 옵션 프리미엄 수익 (만원) / Interest : 원금 및 프리미엄에서 발생한 평균 이자수익 (만원)

Loss : 옵션권리행사로 인한 평균 손실 (미행사 포함) / Revenue : 평균 이익 (Premium + Interest - Loss)

Return : 포트폴리오의 연환산 수익률 / Return_K200 : 코스피200지수의 연환산 수익률

변동성 조정 위클리 양매도 포트폴리오의 주요 성과는 다음과 같습니다.

  1. 옵션 매도주기 축소(월간 \(\rightarrow\) 주간) : 현금흐름 개선 및 프리미엄 수익 극대화
  • VW양매도 포트폴리오는 연평균 50회의 수익이 발생하는 반면, 일반 양매도 포트폴리오는 12회(월1회) 발생.
  • 1회 발생 수익은 4배 미만으로 감소하여 평균 수익이 증가하였고, 수익주기가 짧아지면서 현금유동성 개선
  1. 행사가격 범위 조정(5% \(\rightarrow\) \(\sigma\)연동) : 포트폴리오 안정성 증가 및 리스크 축소
  • 일반 양매도 전략의 손실발생비율(권리행사비율)은 시장 상황에 따라 0~50%까지 큰 폭으로 변동
  • 반면, 본 포트폴리오는 행사가격 범위를 변동성 수준에 조정하므로 비율이 10~30%수준으로 안정화
  • 손실에 대한 예측가능성 및 포트폴리오 변동성이 개선되었으며 1회 손실도 감내 가능한 수준으로 축소
  1. 소결 : VW양매도 전략은 수익률, 리스크 측면에서 코스피200지수 및 일반 양매도 전략을 Outperform

그래프3, 4 : 지난 5년 및 3년간 코스피200지수, VW양매도, 5%양매도 누적수익률 및 초과수익

표3 : 연도별 VW양매도, 일반 양매도, 코스피200지수의 수익률 및 변동성(월간수익률의 연환산)

YEAR Return_main Return_sub Return_K200 Vol_main Vol_sub Vol_K200
2020 -0.04 -0.23 0.33 0.08 0.19 0.27
2021 0.12 0.13 0.01 0.03 0.02 0.11
2022 -0.03 -0.04 -0.26 0.08 0.08 0.25
2023 0.08 0.09 0.23 0.04 0.01 0.17
2024 0.05 0.04 -0.11 0.07 0.08 0.16

그래프와 표를 통해 VW양매도 전략이 타 전략 대비 안정적이고 높은 수익을 실현하였음을 확인할 수 있었습니다.

한계점

그러나, 행사가격 범위가 전일 V-Kospi200 지수에 따라 결정되므로 옵션 매도일 및 보유기간 중 V-Kospi200 지수가 급등락하는 경우 시장 변동성을 적절히 반영되지 않을 가능성이 있습니다.

  • 당일 장중 지수를 사용할 수 있겠으나 종가보다 신뢰성이 높다고 보기 어렵고, 당일 종가는 매도시점에서 확인 불가
  • 보유기간 중 V-Kospi200의 변동에 따라 옵션 행사가격을 리밸런싱하면 보다 정확히 변동성을 반영할 수 있으나, 잦은 거래로 인해 수반되는 비용이 급증
  • 시장 변동성이 적절히 반영되지 않는 경우, 프리미엄 수익성이 낮아지고 옵션 권리행사 위험이 증가할 수 있음

또한, 실제로 VW양매도 포트폴리오를 운영한다면 거래비용 및 유동성 등의 한계점으로 인해 보완해야할 점이 있으며 이 과정에서 초과수익률 등의 성과가 다소 희석될 것으로 예상됩니다.

  • 수수료/유동성 등으로 인해 자주 옵션을 매도하는 전략 특성상 거래비용 상승이 수반됨
  • 월물 옵션 대비 위클리옵션의 유동성이 낮아, 변동성 확대 국면에서 원하는 위클리옵션의 거래가 없을 수 있음

4. 포트폴리오 검증 (’25.3.13 ~ 4.10)

포트폴리오의 성과 검증을 위해 ’25년 3월 옵션만기일부터 1개월간의 성과를 측정해보겠습니다.

Backtesting과 동일한 방식으로 진행하였으며, 날짜 등 일부를 제외하고 동일 코드를 재사용하였습니다.

표4 : 검증기간(’25.3.13 ~ 4.10) VW양매도 및 일반 양매도 전략 성과

BAS_DD Group Premium Interest Loss Revenue Return
20250313 Monthly 12653 2400 0 15053 0.02
20250313 Vol-adj. Weekly 5624 596 2929 3291 0.00
20250320 Vol-adj. Weekly 2687 587 0 3274 0.00
20250327 Vol-adj. Weekly 4283 578 20214 -15353 -0.02
20250403 Vol-adj. Weekly 11594 578 0 12173 0.01
81001363 Vol-adj. Weekly Sum 24188 2338 23142 3384 0.00

검증기간 중 VW양매도 전략은 옵션을 4회 매도하였고, 일반 양매도는 1회 매도하였습니다.

VW양매도 전략의 프리미엄이 4회 발생하면서 수익성은 개선되었지만, 세번째 매도시기에 큰 손실이 발생하면서 순이익이 악화되었습니다. 벡테스팅 결과와 비교할 때 상반된 결과입니다.

원인은 최근 트럼프 행정부의 관세 부과 정책의 영향으로, 증시가 이례적으로 급등락한 데에서 찾을 수 있었습니다.

그래프5 : 검증기간(’25.3.13 ~ 4.10) Kospi200 및 V-Kospi200 지수 추이

먼저, 3.27일 예상변동성(V-K200)이 낮아 행사가격 범위가 좁게 형성되어 VW양매도 전략이 실행되었고, 이후 관세 정책 영향으로 지수가 급락하면서 큰 손실이 발생하게 되었습니다.

반면 월별 전략은 만기 직전에 관세가 연기되면서 증시가 회복하였고(304 > 325), 손실을 피하게 되었습니다.

다소 아쉬운 결과이나, VW양매도 전략의 한계점과 특수한 상황에서는 오히려 불리하다는 점을 알 수 있었습니다.

  1. VW양매도 전략은 시장 변동성이 적절히 반영되지 않을 가능성이 있고, 이 경우 예기치 못한 손실이 발생할 수 있음
  2. 옵션 만기가 짧은 것은 일반적으로 수익성, 유동성 등에서 장점이 있으나, 증시가 급락 후 회복하는 상황에서는 만기가 긴 옵션이 유리할 수 있음

5. Appendix : Python, R code

Pyhon code : 데이터 수집 및 전처리

import requests
import json
import pandas as pd
import aiohttp
import asyncio

# KRX OpenAPI 예시
url = 'http://data-dbg.krx.co.kr/svc/sample/apis/drv/opt_bydd_trd?basDd=20250312'
headers = {'AUTH_KEY': '74D1B99DFBF345BBA3FB4476510A4BED4C78D13A'}
res = requests.get(url=url, headers=headers)
res.text

# KRX OpenAPI를 이용한 옵션 데이터 수집 (네트워크 병렬 처리)
idx_data = pd.read_csv('data/idx_data.csv')
url = 'http://data-dbg.krx.co.kr/svc/apis/drv/opt_bydd_trd?basDd='
key = 'BDFA640BBCE84C4B8A465EA024D50D6F3FD909FF'

async def fetch(session, url, bas_dd):
    async with session.get(url + bas_dd, headers={'AUTH_KEY': key}) as response:
        return await response.json()

async def fetch_all(bas_dd_list, url):
    async with aiohttp.ClientSession() as session:
        tasks = [fetch(session, url, str(bas_dd)) for bas_dd in bas_dd_list]
        return await asyncio.gather(*tasks)

async def main():
    bas_dd_list = idx_data['BAS_DD'].astype(str).tolist()
    responses = await fetch_all(bas_dd_list, url)

    data_list = [pd.json_normalize(res['OutBlock_1']) for res in responses if 'OutBlock_1' in res and res['OutBlock_1']]
    options = pd.concat(data_list, axis=0, ignore_index=True)

    # CSV 저장
    options.to_csv("options_data.csv", encoding="utf-8-sig", index=False)
    print("complete")

R code 1 : 데이터 가공, Backtesting

rm(list=ls())
library(tidyverse)
library(knitr)
library(patchwork)

# 1. 원본데이터 준비
options_raw <- read_csv("data/options_data.csv") %>% tibble()
idx_raw <- read_csv("data/idx_data.csv") %>% tibble()
mmf_raw <- read_csv("data/mmf.csv") %>% tibble()

# 2-1. 데이터 전처리 : KRX openAPI 옵션데이터 전처리
options <- options_raw %>% 
  # K200, WK200 이외의 옵션 제외
  filter(substr(PROD_NM,1,3)=="코스피") %>%
  # 종가가 없는 경우 익일 기준가격(이론가격) 사용
  mutate(PRICE = as.double(if_else(TDD_CLSPRC=="-",NXTDD_BAS_PRC,TDD_CLSPRC))) %>% 
  drop_na(PRICE) %>% 
  select(BAS_DD,ISU_NM,PRICE,ACC_TRDVOL) %>% 
  separate(ISU_NM, into=c("PROD","RGHT","EXP","EXER_PRC"), sep=' ') %>%
  mutate(EXER_PRC=as.double(EXER_PRC),
         EXPMM=if_else(PROD=="코스피200",substr(EXP,3,6),substr(EXP,1,4)),
         EXPWW=if_else(PROD=="코스피200",2,as.integer(substr(EXP,6,6)))) %>% 
  filter(EXER_PRC>min(idx_raw$KOSPI200)*0.85,
         EXER_PRC<max(idx_raw$KOSPI200)*1.15,
         PROD!="코스피위클리M") %>% # 월요일 만기 위클리옵션 제외
  mutate(PRIOR=as.integer(EXPMM)*10+EXPWW) # 만기가 짧은 순으로 우선순위 부여

# 2-2. 데이터 전처리 : 옵션 만기일 데이터 생성
target_date <- options %>% 
  distinct(.,BAS_DD, PRIOR) %>% 
  arrange(PRIOR, desc(BAS_DD)) %>% 
  group_by(PRIOR) %>% 
  slice(1) %>% 
  ungroup() %>% 
  distinct(BAS_DD) %>% 
  arrange(desc(BAS_DD)) %>% 
  filter(BAS_DD<20241231, BAS_DD>20200101)

# 2-3. 데이터 전처리 : KRX 정보데이터시스템 K200 및 V-K200지수 전처리
idx_data <- idx_raw %>%
  arrange(BAS_DD) %>% 
  mutate(LAG_K200=lag(KOSPI200),
         LAG_VK200=lag(VKOSPI200))

K200_portfolio=idx_data %>% 
  left_join(mmf_raw, by="BAS_DD") %>% 
  mutate(RATE=(KOSPI200-LAG_K200)/LAG_K200,
         YEAR = substr(BAS_DD, 1, 4),
         YM = ym(substr(BAS_DD, 1, 6))) %>%
  group_by(YEAR, YM) %>%
  summarise(RATE_K200 = if_else(is.na(prod(1 + RATE)),0,prod(1 + RATE)),
            MMF = mean(MMF)/100,
            .groups = "drop") %>%
  ungroup()

# 3-1. 포트폴리오 구성 : VW양매도 포트폴리오 기초정보 생성
target_info <- target_date %>% 
  left_join(options, by="BAS_DD") %>% 
  distinct(BAS_DD,PRIOR) %>% 
  arrange(desc(BAS_DD),PRIOR) %>% 
  group_by(BAS_DD) %>% 
  # 만기가 도래하는 옵션을 제외하고 우선순위가 높은 옵션 선택
  slice(2) %>% 
  ungroup() %>% 
  arrange(desc(BAS_DD)) %>% 
  left_join(idx_data, by="BAS_DD") %>% 
  left_join(mmf_raw, by="BAS_DD") %>% 
  # 옵션 보유기간 및 단기금리(MMF) 계산
  mutate(DIFF_DAY=as.integer(ymd(lag(BAS_DD))-ymd(BAS_DD)),
         MMF=MMF*0.01) %>% 
  # 옵션 보유기간에 해당하는 시장기대변동성 계산(V-K200활용)
  mutate(VOL=LAG_VK200*sqrt(DIFF_DAY)/sqrt(365)/100) %>%
  # 변동성에 따른 옵션 행사가격 상단(2.5단위 올림) 및 하단(2.5단위 내림) 계산
  mutate(TARGET_C_K=ceiling(KOSPI200*(1+VOL)*0.4)/0.4,
         TARGET_P_K=floor(KOSPI200*(1-VOL)*0.4)/0.4) %>% 
  drop_na(DIFF_DAY) %>% 
  select(BAS_DD,VOL,DIFF_DAY,MMF,KOSPI200,LAG_VK200,PRIOR,TARGET_C_K,TARGET_P_K)

# 3-2. 포트폴리오 구성 : VW양매도 포트폴리오 거래대상 옵션 정보 생성
target_options <- target_info %>% 
  select(BAS_DD, PRIOR, TARGET_C_K, TARGET_P_K) %>% 
  pivot_longer(cols=c("TARGET_C_K","TARGET_P_K"),names_to = "GRP", values_to = "EXER_PRC") %>% 
  mutate(RGHT=substr(GRP,8,8)) %>% 
  left_join(options,by=c("BAS_DD","PRIOR","RGHT","EXER_PRC")) %>%
  select(BAS_DD,GRP,PRICE,ACC_TRDVOL) %>% 
  pivot_wider(names_from = "GRP", values_from = c("PRICE","ACC_TRDVOL"))

# 원금 100억원 가정
cash = 10000*10000*100

# 4. 포트폴리오 구현 : VW양매도 전략을 구현하여 일자별 손익 계산
portfolio <- target_info %>% 
  left_join(target_options,by="BAS_DD") %>% 
  arrange(BAS_DD) %>% 
  # 원금에 따른 옵션 매도수량 계산
  mutate(SELL_AMT=cash/250000/KOSPI200) %>% 
  # 옵션 프리미엄(매도수익)) 계산
  mutate(PREMIUM=SELL_AMT*(PRICE_TARGET_C_K+PRICE_TARGET_P_K)*250000) %>% 
  # 원금 및 프리미엄의 MMF 이자수익 및 권리행사손실 계산
  mutate(INTEREST=(cash+replace_na(PREMIUM,0))*MMF*DIFF_DAY/365,
         EXERCISE=(if_else(lead(KOSPI200)-TARGET_C_K>0,lead(KOSPI200)-TARGET_C_K,0)+
                     if_else(TARGET_P_K-lead(KOSPI200)>0,TARGET_P_K-lead(KOSPI200),0))*250000*SELL_AMT) %>% 
  # 주단위 포트폴리오 손익 계산(원금 100억 가정, 당일 투자시 다음주 실현손익을 의미)
  mutate(REVENUE=PREMIUM+INTEREST-EXERCISE) %>% 
  # 거래대상 옵션이 상장되어있지 않은 경우, MMF수익만 고려
  mutate(REVENUE=if_else(is.na(REVENUE),INTEREST,REVENUE)) %>% 
  mutate(RATE=REVENUE/cash)  # 주단위 포트폴리오 수익률

# 5. 비교군 포트폴리오 생성 : 월별 5% OTM 양매도 전략
options2 <- options %>% 
  filter(PROD=="코스피200")

# 옵션 만기일(매달) 생성
target_date2 <- options2 %>% 
  distinct(.,BAS_DD, PRIOR) %>% 
  arrange(PRIOR, desc(BAS_DD)) %>% 
  group_by(PRIOR) %>% 
  slice(1) %>% 
  ungroup() %>% 
  distinct(BAS_DD) %>% 
  arrange(desc(BAS_DD)) %>% 
  filter(BAS_DD<20241231, BAS_DD>20200101)

# 일반 양매도 포트폴리오 기본 정보 생성
target_info2 <- target_date2 %>% 
  left_join(options2, by="BAS_DD") %>% 
  distinct(BAS_DD,PRIOR) %>% 
  arrange(desc(BAS_DD),PRIOR) %>% 
  group_by(BAS_DD) %>% 
  # 만기가 도래하는 옵션을 제외하고 우선순위가 높은 옵션 선택
  slice(2) %>% 
  ungroup() %>% 
  arrange(desc(BAS_DD)) %>% 
  left_join(idx_data, by="BAS_DD") %>% 
  left_join(mmf_raw, by="BAS_DD") %>% 
  # 옵션 보유기간 및 단기금리(MMF) 계산
  mutate(DIFF_DAY=as.integer(ymd(lag(BAS_DD))-ymd(BAS_DD)),
         MMF=MMF*0.01,
         VOL=0.05) %>%
  # 변동성에 따른 옵션 행사가격 상단(2.5단위 올림) 및 하단(2.5단위 내림) 계산
  mutate(TARGET_C_K=ceiling(KOSPI200*(1+VOL)*0.4)/0.4,
         TARGET_P_K=floor(KOSPI200*(1-VOL)*0.4)/0.4) %>% 
  mutate(DIFF_DAY=if_else(is.na(DIFF_DAY),28,DIFF_DAY)) %>% 
  select(BAS_DD,VOL,DIFF_DAY,MMF,KOSPI200,LAG_VK200,PRIOR,TARGET_C_K,TARGET_P_K)

# 일반 양매도 포트폴리오 거래대상 옵션
target_options2 <- target_info2 %>% 
  select(BAS_DD, PRIOR, TARGET_C_K, TARGET_P_K) %>% 
  pivot_longer(cols=c("TARGET_C_K","TARGET_P_K"),names_to = "GRP", values_to = "EXER_PRC") %>% 
  mutate(RGHT=substr(GRP,8,8)) %>% 
  left_join(options2,by=c("BAS_DD","PRIOR","RGHT","EXER_PRC")) %>%
  select(BAS_DD,GRP,PRICE,ACC_TRDVOL) %>% 
  pivot_wider(names_from = "GRP", values_from = c("PRICE","ACC_TRDVOL"))

# 일반 양매도 포트폴리오 구현
portfolio2 <- target_info2 %>% 
  left_join(target_options2,by="BAS_DD") %>% 
  arrange(BAS_DD) %>% 
  # 원금에 따른 옵션 매도수량 계산
  mutate(SELL_AMT=cash/250000/KOSPI200) %>% 
  # 옵션 프리미엄(매도수익)) 계산
  mutate(PREMIUM=SELL_AMT*(PRICE_TARGET_C_K+PRICE_TARGET_P_K)*250000) %>% 
  # 원금 및 프리미엄의 MMF 이자수익 및 권리행사손실 계산
  mutate(INTEREST=(cash+replace_na(PREMIUM,0))*MMF*DIFF_DAY/365,
         EXERCISE=(if_else(lead(KOSPI200)-TARGET_C_K>0,lead(KOSPI200)-TARGET_C_K,0)+
                     if_else(TARGET_P_K-lead(KOSPI200)>0,TARGET_P_K-lead(KOSPI200),0))*250000*SELL_AMT) %>% 
  mutate(EXERCISE=if_else(is.na(EXERCISE),0,EXERCISE)) %>% 
  # 주단위 포트폴리오 손익 계산(원금 100억 가정, 당일 투자시 다음주 실현손익을 의미)
  mutate(REVENUE=PREMIUM+INTEREST-EXERCISE) %>% 
  # 거래대상 옵션이 상장되어있지 않은 경우, MMF수익만 고려
  mutate(REVENUE=if_else(is.na(REVENUE),INTEREST,REVENUE)) %>% 
  mutate(RATE=REVENUE/cash) # 주단위 포트폴리오 수익률

# 6. 성과분석 : VW양매도 포트폴리오
performance <- portfolio %>%
  drop_na(PREMIUM, EXERCISE) %>%
  mutate(YEAR = substr(BAS_DD, 1, 4),
         YM = ym(substr(BAS_DD, 1, 6))) %>%
  group_by(YEAR, YM) %>%
  summarise(PREMIUM = sum(PREMIUM),
            INTEREST = sum(INTEREST),
            EXERCISE = sum(EXERCISE),
            REVENUE = sum(REVENUE),
            RATE = prod(1 + RATE),
            .groups = "drop") %>%
  ungroup()

# 6. 성과분석 : 일반 양매도 포트폴리오
performance2 <- portfolio2 %>%
  drop_na(PREMIUM, EXERCISE) %>%
  mutate(YEAR = substr(BAS_DD, 1, 4),
         YM = ym(substr(BAS_DD, 1, 6))) %>%
  group_by(YEAR, YM) %>%
  summarise(PREMIUM = sum(PREMIUM),
            INTEREST = sum(INTEREST),
            EXERCISE = sum(EXERCISE),
            REVENUE = sum(REVENUE),
            RATE = prod(1 + RATE),
            .groups = "drop") %>%
  ungroup()

# 시각화 등
graph1_1 <- performance %>%
  arrange(YM) %>%
  left_join(K200_portfolio,by=c("YEAR","YM")) %>% 
  mutate(CUM_RATE = cumprod(RATE) * 100 - 100,
         CUM_RATE_K200 = cumprod(RATE_K200) * 100 - 100) %>% 
  bind_rows(tibble(YM=ym(201912),CUM_RATE=0,CUM_RATE_K200=0))
  

graph1_2 <- performance2 %>%
  arrange(YM) %>%
  left_join(K200_portfolio,by=c("YEAR","YM")) %>% 
  mutate(CUM_RATE = cumprod(RATE) * 100 - 100,
         CUM_RATE_K200 = cumprod(RATE_K200) * 100 - 100) %>% 
  bind_rows(tibble(YM=ym(201912),CUM_RATE=0,CUM_RATE_K200=0))

graph1_3 <- graph1_1 %>%
  inner_join(graph1_2, by = "YM", suffix = c("_1", "_2")) %>%
  mutate(REVENUE_DIFF = (REVENUE_1 - REVENUE_2) / 1e8)

graph2_1 <- performance %>%
  arrange(YM) %>%
  left_join(K200_portfolio,by=c("YEAR","YM")) %>% 
  filter(as.integer(YEAR) > 2021) %>%
  mutate(CUM_RATE = cumprod(RATE) * 100 - 100,
         CUM_RATE_K200 = cumprod(RATE_K200) * 100 - 100) %>% 
  bind_rows(tibble(YM=ym(202112),CUM_RATE=0,CUM_RATE_K200=0))

graph2_2 <- performance2 %>%
  arrange(YM) %>%
  left_join(K200_portfolio,by=c("YEAR","YM")) %>% 
  filter(as.integer(YEAR) > 2021) %>%
  mutate(CUM_RATE = cumprod(RATE) * 100 - 100,
         CUM_RATE_K200 = cumprod(RATE_K200) * 100 - 100) %>% 
  bind_rows(tibble(YM=ym(202112),CUM_RATE=0,CUM_RATE_K200=0))

graph2_3 <- graph2_1 %>%
  inner_join(graph2_2, by = "YM", suffix = c("_1", "_2")) %>%
  mutate(REVENUE_DIFF = (REVENUE_1 - REVENUE_2) / 1e8)

graph3_1 <- portfolio %>%
  mutate(YM = ym(substr(BAS_DD, 1, 6))) %>%
  group_by(YM) %>%
  summarise(TargetVol=mean(VOL),
            Kospi200=mean(KOSPI200)) %>% ungroup()

graph1=ggplot() +
  geom_line(data = graph1_1, aes(x = YM, y = CUM_RATE, color = "Vol-adj. Weekly"), size = 1) +
  geom_point(data = graph1_1, aes(x = YM, y = CUM_RATE, color = "Vol-adj. Weekly"), shape = 16, size = 2) +
  geom_line(data = graph1_2, aes(x = YM, y = CUM_RATE, color = "5% OTM Monthly"), size = 1) +
  geom_point(data = graph1_2, aes(x = YM, y = CUM_RATE, color = "5% OTM Monthly"), shape = 16, size = 2) +
  geom_line(data = graph1_1, aes(x = YM, y = CUM_RATE_K200, color = "Kospi 200"), size = 1, alpha=0.7) +
  geom_point(data = graph1_1, aes(x = YM, y = CUM_RATE_K200, color = "Kospi 200"), shape = 16, size = 2,alpha=0.7) +
  geom_bar(data = graph1_3, aes(x = YM, y = REVENUE_DIFF * 3),
           stat = "identity", alpha = 0.5, fill = "gray") +
  scale_x_date(date_breaks = "1 year", date_labels = "%Y") +
  scale_y_continuous(limits = c(-30, 50), breaks = seq(-30, 50, 10), labels = scales::number_format(accuracy = 0.1),
                     sec.axis = sec_axis(~ . / 3, name = "Overperform (100M)", breaks = c(-10, -5, 0, 5, 10), labels = c("-10", "-5", "0", "5", "10"))) +
  scale_color_manual(values = c("Vol-adj. Weekly" = "red", "5% OTM Monthly" = "blue", "Kospi 200" = "Green")) +
  labs(x = NULL, y = "Cumulative Return (%)", color = "") +
  theme_minimal() +
  theme(legend.position = c(0.4, 0.93), legend.direction = "horizontal")

graph2=ggplot() +
  geom_line(data = graph2_1, aes(x = YM, y = CUM_RATE, color = "Vol-adj. Weekly"), size = 1) +
  geom_point(data = graph2_1, aes(x = YM, y = CUM_RATE, color = "Vol-adj. Weekly"), shape = 16, size = 2) +
  geom_line(data = graph2_2, aes(x = YM, y = CUM_RATE, color = "5% OTM Monthly"), size = 1) +
  geom_point(data = graph2_2, aes(x = YM, y = CUM_RATE, color = "5% OTM Monthly"), shape = 16, size = 2) +
  geom_line(data = graph2_1, aes(x = YM, y = CUM_RATE_K200, color = "Kospi 200"), size = 1, alpha=0.7) +
  geom_point(data = graph2_1, aes(x = YM, y = CUM_RATE_K200, color = "Kospi 200"), shape = 16, size = 2,alpha=0.7) +
  geom_bar(data = graph2_3, aes(x = YM, y = REVENUE_DIFF * 3),
           stat = "identity", alpha = 0.5, fill = "gray") +
  scale_x_date(date_breaks = "1 year", date_labels = "%Y") +
  scale_y_continuous(limits = c(-30, 20), breaks = seq(-30, 30, 10), labels = scales::number_format(accuracy = 0.1),
                     sec.axis = sec_axis(~ . / 3, name = "Overperform (100M)", breaks = c(-10, -5, 0, 5, 10), labels = c("-10", "-5", "0", "5", "10"))) +
  scale_color_manual(values = c("Vol-adj. Weekly" = "red", "5% OTM Monthly" = "blue", "Kospi 200" = "Green")) +
  labs(x = NULL, y = "Cumulative Return (%)", color = "") +
  theme_minimal() +
  theme(legend.position = c(0.4, 0.93), legend.direction = "horizontal")

graph3=ggplot() +
  geom_line(data = graph3_1, aes(x = YM, y = Kospi200, color = "Kospi 200"), size = 1) +
  geom_point(data = graph3_1, aes(x = YM, y = Kospi200, color = "Kospi 200"), shape = 16, size = 2) +
  geom_bar(data = graph3_1, aes(x = YM, y = TargetVol * 7000),
           stat = "identity", alpha = 0.7, fill = "blue") +
  geom_hline(yintercept = 200, size=0.5, color = "black", alpha=0.5)+
  scale_x_date(date_breaks = "1 year", date_labels = "%Y") +
  scale_y_continuous(limits = c(0, 500), breaks = seq(100, 500, 100),
                     sec.axis = sec_axis(~ . / 7000, name = "Strike Range(Monthly)", breaks = c(0, 0.025,0.05,0.075, 0.1), labels = c("0","5%", "10%","15%", "20%"))) +
  scale_color_manual(values = c("Kospi 200" = "red")) +
  labs(x = NULL, y = "Kospi 200", color = "") +
  theme_minimal() +
  theme(legend.position = c(0.1, 0.93), legend.direction = "horizontal")

# 요약표 1 : VW포트폴리오
table1 <- portfolio %>%
  drop_na(PREMIUM, EXERCISE) %>%
  mutate(YEAR = substr(BAS_DD, 1, 4),
         CNT=1,
         NO=if_else(EXERCISE==0,1,0)) %>%
  group_by(YEAR) %>%
  summarise(Expiry = sum(CNT),
            NoExercise = round(sum(NO)/sum(CNT),2),
            Premium = round(mean(PREMIUM)/10000,0),
            Interest = round(mean(INTEREST)/10000,0),
            Loss = round(mean(EXERCISE)/10000,0),
            Revenue = round(mean(REVENUE)/10000,0),
            Return = round(prod(1 + RATE)-1,2),
            .groups = "drop") %>% 
  left_join(K200_portfolio %>% group_by(YEAR) %>% summarise(Return_K200=round(prod(RATE_K200)-1,2)),
            by="YEAR")

# 요약표 2 : 일반 포트폴리오
table2 <- portfolio2 %>%
  drop_na(PREMIUM, EXERCISE) %>%
  mutate(YEAR = substr(BAS_DD, 1, 4),
         CNT=1,
         NO=if_else(EXERCISE==0,1,0)) %>%
  group_by(YEAR) %>%
  summarise(Expiry = sum(CNT),
            NoExercise = round(sum(NO)/sum(CNT),2),
            Premium = round(mean(PREMIUM)/10000,0),
            Interest = round(mean(INTEREST)/10000,0),
            Loss = round(mean(EXERCISE)/10000,0),
            Revenue = round(mean(REVENUE)/10000,0),
            Return = round(prod(1 + RATE)-1,2),
            .groups = "drop") %>% 
  left_join(K200_portfolio %>% group_by(YEAR) %>% summarise(Return_K200=round(prod(RATE_K200)-1,2)),
            by="YEAR")

# 요약표 3 : 포트폴리오 변동성 비교
Vol1 <- graph1_1 %>% group_by(YEAR) %>% summarise(Vol=round(sd(RATE)*sqrt(12),2),
                                                  Vol_K200=round(sd(RATE_K200)*sqrt(12),2))
Vol2 <- graph1_2 %>% group_by(YEAR) %>% summarise(Vol=round(sd(RATE)*sqrt(12),2))

table3 <- table1 %>% 
  select(YEAR,Return,Return_K200) %>% 
  left_join(Vol1, by="YEAR") %>% 
  mutate(Return_main=Return,
         Vol_main=Vol) %>% 
  select(YEAR, Return_main,Vol_main,Return_K200,Vol_K200) %>% 
  left_join(table2 %>% 
              select(YEAR,Return) %>% 
              left_join(Vol2, by="YEAR") %>% 
              mutate(Return_sub=Return,
                     Vol_sub=Vol) %>% 
              select(YEAR,Return_sub,Vol_sub), by="YEAR")

R code 2 : 전략 검증

rm(list=ls())
library(tidyverse)
library(knitr)
library(patchwork)
setwd("homepage/study_25spring/data/")

# 1. 원본데이터 준비
options_raw <- read_csv("options_data_test.csv") %>% tibble()
idx_raw <- read_csv("idx_data_test.csv") %>% tibble()
mmf_raw <- read_csv("mmf_test.csv") %>% tibble()

# 2-1. 데이터 전처리 : KRX openAPI 옵션데이터 전처리
options <- options_raw %>% 
  # K200, WK200 이외의 옵션 제외
  filter(substr(PROD_NM,1,3)=="코스피") %>%
  # 종가가 없는 경우 익일 기준가격(이론가격) 사용
  mutate(PRICE = as.double(if_else(TDD_CLSPRC=="-",NXTDD_BAS_PRC,TDD_CLSPRC))) %>% 
  drop_na(PRICE) %>% 
  select(BAS_DD,ISU_NM,PRICE,ACC_TRDVOL) %>% 
  separate(ISU_NM, into=c("PROD","RGHT","EXP","EXER_PRC"), sep=' ') %>%
  mutate(EXER_PRC=as.double(EXER_PRC),
         EXPMM=if_else(PROD=="코스피200",substr(EXP,3,6),substr(EXP,1,4)),
         EXPWW=if_else(PROD=="코스피200",2,as.integer(substr(EXP,6,6)))) %>% 
  filter(EXER_PRC>min(idx_raw$KOSPI200)*0.85,
         EXER_PRC<max(idx_raw$KOSPI200)*1.15,
         PROD!="코스피위클리M") %>% # 월요일 만기 위클리옵션 제외
  mutate(PRIOR=as.integer(EXPMM)*10+EXPWW) # 만기가 짧은 순으로 우선순위 부여

# 2-2. 데이터 전처리 : 옵션 만기일 데이터 생성
target_date <- options %>% 
  distinct(.,BAS_DD, PRIOR) %>% 
  arrange(PRIOR, desc(BAS_DD)) %>% 
  group_by(PRIOR) %>% 
  slice(1) %>% 
  ungroup() %>% 
  distinct(BAS_DD) %>% 
  arrange(desc(BAS_DD)) %>% 
  filter(BAS_DD<20250411)

# 2-3. 데이터 전처리 : KRX 정보데이터시스템 K200 및 V-K200지수 전처리
idx_data <- idx_raw %>%
  arrange(BAS_DD) %>% 
  mutate(LAG_K200=lag(KOSPI200),
         LAG_VK200=lag(VKOSPI200))

K200_portfolio=idx_data %>% 
  left_join(mmf_raw, by="BAS_DD") %>% 
  mutate(RATE=(KOSPI200-LAG_K200)/LAG_K200,
         YEAR = substr(BAS_DD, 1, 4),
         YM = ym(substr(BAS_DD, 1, 6))) %>%
  group_by(YEAR) %>%
  filter(is.na(RATE)==FALSE) %>% 
  summarise(RATE_K200 = prod(1 + RATE),
            MMF = mean(MMF)/100,
            .groups = "drop") %>%
  ungroup()

# 3-1. 포트폴리오 구성 : VW양매도 포트폴리오 기초정보 생성
target_info <- target_date %>% 
  left_join(options, by="BAS_DD") %>% 
  distinct(BAS_DD,PRIOR) %>% 
  arrange(desc(BAS_DD),PRIOR) %>% 
  group_by(BAS_DD) %>% 
  # 만기가 도래하는 옵션을 제외하고 우선순위가 높은 옵션 선택
  slice(2) %>% 
  ungroup() %>% 
  arrange(desc(BAS_DD)) %>% 
  left_join(idx_data, by="BAS_DD") %>% 
  left_join(mmf_raw, by="BAS_DD") %>% 
  # 옵션 보유기간 및 단기금리(MMF) 계산
  mutate(DIFF_DAY=as.integer(ymd(lag(BAS_DD))-ymd(BAS_DD)),
         MMF=MMF*0.01) %>% 
  # 옵션 보유기간에 해당하는 시장기대변동성 계산(V-K200활용)
  mutate(VOL=LAG_VK200*sqrt(DIFF_DAY)/sqrt(365)/100) %>%
  # 변동성에 따른 옵션 행사가격 상단(2.5단위 올림) 및 하단(2.5단위 내림) 계산
  mutate(TARGET_C_K=ceiling(KOSPI200*(1+VOL)*0.4)/0.4,
         TARGET_P_K=floor(KOSPI200*(1-VOL)*0.4)/0.4) %>% 
  # drop_na(DIFF_DAY) %>% 
  select(BAS_DD,VOL,DIFF_DAY,MMF,KOSPI200,LAG_VK200,PRIOR,TARGET_C_K,TARGET_P_K)

# 3-2. 포트폴리오 구성 : VW양매도 포트폴리오 거래대상 옵션 정보 생성
target_options <- target_info %>% 
  select(BAS_DD, PRIOR, TARGET_C_K, TARGET_P_K) %>% 
  pivot_longer(cols=c("TARGET_C_K","TARGET_P_K"),names_to = "GRP", values_to = "EXER_PRC") %>% 
  mutate(RGHT=substr(GRP,8,8)) %>% 
  left_join(options,by=c("BAS_DD","PRIOR","RGHT","EXER_PRC")) %>%
  select(BAS_DD,GRP,PRICE,ACC_TRDVOL) %>% 
  pivot_wider(names_from = "GRP", values_from = c("PRICE","ACC_TRDVOL"))

# 원금 100억원 가정
cash = 10000*10000*100

# 4. 포트폴리오 구현 : VW양매도 전략을 구현하여 일자별 손익 계산
portfolio <- target_info %>% 
  left_join(target_options,by="BAS_DD") %>% 
  arrange(BAS_DD) %>% 
  # 원금에 따른 옵션 매도수량 계산
  mutate(SELL_AMT=cash/250000/KOSPI200) %>% 
  # 옵션 프리미엄(매도수익)) 계산
  mutate(PREMIUM=SELL_AMT*(PRICE_TARGET_C_K+PRICE_TARGET_P_K)*250000) %>% 
  # 원금 및 프리미엄의 MMF 이자수익 및 권리행사손실 계산
  mutate(INTEREST=(cash+replace_na(PREMIUM,0))*MMF*DIFF_DAY/365,
         EXERCISE=(if_else(lead(KOSPI200)-TARGET_C_K>0,lead(KOSPI200)-TARGET_C_K,0)+
                     if_else(TARGET_P_K-lead(KOSPI200)>0,TARGET_P_K-lead(KOSPI200),0))*250000*SELL_AMT) %>% 
  # 주단위 포트폴리오 손익 계산(원금 100억 가정, 당일 투자시 다음주 실현손익을 의미)
  mutate(REVENUE=PREMIUM+INTEREST-EXERCISE) %>% 
  # 거래대상 옵션이 상장되어있지 않은 경우, MMF수익만 고려
  mutate(REVENUE=if_else(is.na(REVENUE),INTEREST,REVENUE)) %>% 
  mutate(RATE=REVENUE/cash,
         Group="Vol-adj. Weekly")  # 주단위 포트폴리오 수익률

# 5. 비교군 포트폴리오 생성 : 월별 5% OTM 양매도 전략
options2 <- options %>% 
  filter(PROD=="코스피200")

# 옵션 만기일(매달) 생성
target_date2 <- options2 %>% 
  distinct(.,BAS_DD, PRIOR) %>% 
  arrange(PRIOR, desc(BAS_DD)) %>% 
  group_by(PRIOR) %>% 
  slice(1) %>% 
  ungroup() %>% 
  distinct(BAS_DD) %>% 
  arrange(desc(BAS_DD)) %>% 
  filter(BAS_DD<20250411)

# 일반 양매도 포트폴리오 기본 정보 생성
target_info2 <- target_date2 %>% 
  left_join(options2, by="BAS_DD") %>% 
  distinct(BAS_DD,PRIOR) %>% 
  arrange(desc(BAS_DD),PRIOR) %>% 
  group_by(BAS_DD) %>% 
  # 만기가 도래하는 옵션을 제외하고 우선순위가 높은 옵션 선택
  slice(2) %>% 
  ungroup() %>% 
  arrange(desc(BAS_DD)) %>% 
  left_join(idx_data, by="BAS_DD") %>% 
  left_join(mmf_raw, by="BAS_DD") %>% 
  # 옵션 보유기간 및 단기금리(MMF) 계산
  mutate(DIFF_DAY=as.integer(ymd(lag(BAS_DD))-ymd(BAS_DD)),
         MMF=MMF*0.01,
         VOL=0.05) %>%
  # 변동성에 따른 옵션 행사가격 상단(2.5단위 올림) 및 하단(2.5단위 내림) 계산
  mutate(TARGET_C_K=ceiling(KOSPI200*(1+VOL)*0.4)/0.4,
         TARGET_P_K=floor(KOSPI200*(1-VOL)*0.4)/0.4) %>% 
  mutate(DIFF_DAY=if_else(is.na(DIFF_DAY),28,DIFF_DAY)) %>% 
  select(BAS_DD,VOL,DIFF_DAY,MMF,KOSPI200,LAG_VK200,PRIOR,TARGET_C_K,TARGET_P_K)

# 일반 양매도 포트폴리오 거래대상 옵션
target_options2 <- target_info2 %>% 
  select(BAS_DD, PRIOR, TARGET_C_K, TARGET_P_K) %>% 
  pivot_longer(cols=c("TARGET_C_K","TARGET_P_K"),names_to = "GRP", values_to = "EXER_PRC") %>% 
  mutate(RGHT=substr(GRP,8,8)) %>% 
  left_join(options2,by=c("BAS_DD","PRIOR","RGHT","EXER_PRC")) %>%
  select(BAS_DD,GRP,PRICE,ACC_TRDVOL) %>% 
  pivot_wider(names_from = "GRP", values_from = c("PRICE","ACC_TRDVOL"))

# 일반 양매도 포트폴리오 구현
portfolio2 <- target_info2 %>% 
  left_join(target_options2,by="BAS_DD") %>% 
  arrange(BAS_DD) %>% 
  # 원금에 따른 옵션 매도수량 계산
  mutate(SELL_AMT=cash/250000/KOSPI200) %>% 
  # 옵션 프리미엄(매도수익)) 계산
  mutate(PREMIUM=SELL_AMT*(PRICE_TARGET_C_K+PRICE_TARGET_P_K)*250000) %>% 
  # 원금 및 프리미엄의 MMF 이자수익 및 권리행사손실 계산
  mutate(INTEREST=(cash+replace_na(PREMIUM,0))*MMF*DIFF_DAY/365,
         EXERCISE=(if_else(lead(KOSPI200)-TARGET_C_K>0,lead(KOSPI200)-TARGET_C_K,0)+
                     if_else(TARGET_P_K-lead(KOSPI200)>0,TARGET_P_K-lead(KOSPI200),0))*250000*SELL_AMT) %>% 
  mutate(EXERCISE=if_else(is.na(EXERCISE),0,EXERCISE)) %>% 
  # 주단위 포트폴리오 손익 계산(원금 100억 가정, 당일 투자시 다음주 실현손익을 의미)
  mutate(REVENUE=PREMIUM+INTEREST-EXERCISE) %>% 
  # 거래대상 옵션이 상장되어있지 않은 경우, MMF수익만 고려
  mutate(REVENUE=if_else(is.na(REVENUE),INTEREST,REVENUE)) %>% 
  mutate(RATE=REVENUE/cash,
         Group="Monthly") # 주단위 포트폴리오 수익률

portfolio <- portfolio %>% filter(BAS_DD!=20250410)
portfolio2 <- portfolio2 %>% filter(BAS_DD!=20250410)
portfolio3 <- portfolio %>% 
  summarise(across(where(is.numeric), sum)) %>% 
  mutate(Group="Vol-adj. Weekly Sum")
  

# 요약표 4 : 검증 자료
table4 <- portfolio %>%
  union_all(portfolio2) %>%
  union_all(portfolio3) %>% 
  group_by(BAS_DD, Group) %>%
  summarise(Premium = round(mean(PREMIUM)/10000,0),
            Interest = round(mean(INTEREST)/10000,0),
            Loss = round(mean(EXERCISE)/10000,0),
            Revenue = round(mean(REVENUE)/10000,0),
            Return = round(prod(1 + RATE)-1,2),
            .groups = "drop")

# 그래프 4 : 검증기간중 코스피200지수 추이
graph4 <- ggplot(idx_raw, aes(x = ymd(BAS_DD))) +
  geom_line(aes(y = KOSPI200, color = "KOSPI200"), size = 1.2) +
  geom_line(aes(y = (VKOSPI200 - 19) / 26 * 60 + 300, color = "VKOSPI200"), size = 1.2) +
  geom_vline(xintercept = ymd(c("20250313", "20250320", "20250327", "20250403","20250410")),
             linetype = "dotted", color = "grey40") +
  scale_y_continuous(name = "KOSPI200",limits = c(300, 360),
                     sec.axis = sec_axis(
                       ~ (.-300) / 60 * 26 + 19, name = "VKOSPI200")) +
  scale_color_manual(values = c("KOSPI200" = "blue", "VKOSPI200" = "red"),
                     guide = guide_legend(direction = "horizontal")) +
  labs(title = "Kospi200 & V-Kospi200 indices",x = NULL,color = NULL) +
  theme_minimal() +
  scale_x_date(date_breaks = "3 days", date_labels = "%m-%d") +
  theme(axis.text.x = element_text(angle = 45, hjust = 1),
        legend.position = c(0.5, 0.95))