[Quant] Option Implied Probability
in Quant on Quant
개요
옵션에 내재된 기초 자산 확률 분포를 봅니다.
옵션에서 내재되었다는 표현이 나오면 내재 변동성이 가장 많이 언급됩니다. 옵션에서 내재 변동성은 가격을 표현한다고 할 수 있습니다. 그런데 변동성만 가지고는 기초 자산에 대한 정보를 직관적으로 알기가 어렵기 때문에, 옵션에 녹아 있는 기초 자산의 가격 확률 분포를 대신 유도해 보는 것도 좋습니다.
옵션은 미래에 특정 가격으로 자산을 매입하거나 매도할 권리입니다. 만기에서의 콜옵션은 행사가보다 기초 자산 가격이 높아야 의미가 있으므로, 옵션 가격은 만기에서 기초 자산의 가격이 행사가 대비 어떻게 될지에 대한 정보를 가지고 있습니다. 위험 중립을 가정하기 때문에, 위험 회피를 좋아하는 현실 세계의 분포와는 같지 않을 수 있습니다. 그럼에도 불구하고, 시장에 녹아 있는 예상치를 체크할 수 있다면 각자의 관점과 결합해 더 싸게 살 수 있는 조합을 찾는데는 도움이 될 수 있습니다.
방법은 두 가지가 있습니다. Long Butterfly로 찾는 방법이 있고, 콜옵션 가격 공식에서 직접 유도하는 방법이 있습니다. Long Butterfly 먼저 소개해 보겠습니다. Long Butterfly는 낮은 행사가 콜 1계약 매수, 중간 행사가 콜 2계약 매도, 높은 행사가 콜 1계약 매수로 만듭니다. Short Straddle처럼 가격 변동이 적어야 수익이 나는 포지션이지만, 가격 변동이 심해도 양 날개가 있어 일정 수준 이상 터져 나가지 않습니다. 예시로는 행사가 3800 콜이 20달러, 3900 콜이 17달러, 4000 콜이 15달러라고 가정해 보겠습니다. (값이 조금 현실성이 없어도 넘어갑시다) Long Butterfly를 잡으려면 20달러 매수, 17달러*2 매도, 15달러 매수니 1달러를 내야 됩니다. 최대로 먹을 수 있는 payoff는 기초 자산 가격이 3900일 때 100입니다. 확률에 최대 payoff 곱한, 일종의 기대값이 Long Butterfly의 구축 비용이라고 가정한다면 구축 비용 1달러이니 3900으로 마감할 확률이 1%로 보는 것입니다. 이것을 모든 행사가에 적용하면 각 행사가 별 확률을 얻을 수 있습니다. 간단한 것 같은데, 뭔가 정교하다는 생각은 들지 않습니다. 또한, 거래가 아주 활발하다고 해도 모든 행사가에서 일정한 간격으로 있으리라는 보장도 없습니다. 예를 들어, 행사가 3500 - 4100까지는 100 단위로 잘 나오다가 갑자기 4500으로 뛸 수도 있습니다. 이럴 경우 100 간격으로 계산된 확률과 더 넓은 단위로 계산된 확률에는 차이가 날 수밖에 없습니다.
Long Butterfly 방식이 이런 문제점을 가지고 있어서 얼마 전부터 올리고 있는 Option Analytics는 Breeden-Litzenberger라는 다른 방식을 참고해서 만들었습니다. 결국은 Long Butterfly 방식에서 행사가 간격을 무한히 좁힌 것으로 생각할 수도 있습니다.
그러면 식을 유도해 보겠습니다. 논문은 꽤 기니 딱 필요한 것만 챙겨 옵니다.
우선 콜옵션 기준입니다. Payoff 기대값을 저렇게 표현할 수 있는데, 뒤집어서 pdf를 콜옵션 가격으로 표현하면 됩니다.
한 번 미분하면 cdf 형태가 됩니다.
한 번 더 미분하니 pdf가 나왔습니다. 콜옵션 가격을 행사가로 두 번 미분한 것과 연관이 있습니다. 필요한 것만 가져오니 매우 짧아집니다. 어쨌든, 콜옵션 가격을 사용해서 내재된 pdf를 얻었습니다.
그런데, 문제는 옵션 행사 가격이 연속분포가 아닙니다. 그래서 불연속한 점 사이는 적당히 interpolation을 해야 됩니다. 작업을 시작하기에 앞서, 예시로 5월 13일자 기준으로 5월 20일 만기 콜옵션을 가져와 보겠습니다. 콜옵션 가격과 행사가를 점 찍은 그래프입니다. 이것으로 직접 매매를 할 것은 아니라서 데이터는 공짜인 yahoo를 활용했고, 데이터의 품질은 장담할 수 없습니다. 그래프가 꽤 험악합니다만, 콜옵션 같이 생기긴 했습니다.
여기에 위에서 구한 식을 적용하면, 아래와 같이 얻어집니다. 내재 변동성을 계산해서 블랙 숄즈로 갔다가 pdf로 다시 나오면 곡선이 조금 더 깔끔하고 매끄러울 수 있겠는데, 굳이 거기까지 가지 않아도 용도는 충족합니다.
핵심적으로 필요한 코드입니다. 행사가별로 떨어져 있는 부분을 3차로 이어놓고 가우시안 스무딩을 적용합니다.
def calc_pdf_price(strikes, o_price, tau, r):
# ac/ak
o_price_prime = np.gradient(o_price, strikes)
# a2c/ak2
o_price_pprime = np.gradient(o_price_prime, strikes)
# f(K) = e^rt * a2c/ak2
return np.exp(tau*r)*o_price_pprime
from scipy.ndimage import gaussian_filter1d
pdf_raw_call = calc_pdf_price(fstrikes, cubic_spx_call(fstrikes), tau, r)
pdf_raw_call = pd.Series(pdf_raw_call)
pdf_gaussian_call = pdf_raw_call
pdf_gaussian_call.loc[pdf_gaussian_call < 0] = 0
pdf_gaussian_call = gaussian_filter1d(pdf_raw_call, 5)
아래는 안 쓰고 있는데, 내재 변동성과 Black Scholes 통해서 더 매끄럽게 뽑으려면 (또는 내재 변동성을 같이 보려면) 사용하면 될 것입니다.
from scipy.stats import norm
# C = SN(d1) - N(d2)Ke^-rt
def call_val(S, K, sigma, tau=0, r=0):
# avoid divide-by-zero
with np.errstate(divide='ignore'):
d1 = np.divide(1, sigma * np.sqrt(tau)) * (np.log(S/K) + (r+sigma**2 / 2) * tau)
d2 = d1 - sigma * np.sqrt(tau)
return np.multiply(norm.cdf(d1),S) - np.multiply(norm.cdf(d2), K * np.exp(-r * tau))
# v = ac/asigma = SN'(d1) * sqrt(t) = ap/asigma
def vega(S, K, sigma, tau=0, r=0):
with np.errstate(divide='ignore'):
d1 = np.divide(1, sigma * np.sqrt(tau)) * (np.log(S/K) + (r+sigma**2 / 2) * tau)
return np.multiply(S, norm.pdf(d1)) * np.sqrt(tau)
# Implied Volatility
def bs_iv_call(price, S, K, tau=0, r=0, precision=1e-4, initial_guess=0.2, max_iter=1000, verbose=False):
iv = initial_guess
for _ in range(max_iter):
P = call_val(S, K, iv, tau, r)
diff = price - P
if abs(diff) < precision:
return iv
grad = vega(S, K, iv, tau, r)
iv += diff/grad
if verbose:
print(f"Did not converge after {max_iter} iterations")
return iv