WSAJ Premium - 독점 콘텐츠 | Valley AI
월간 퀀트 스터디 보고 오시면 좋습니다.
아래에 간략히 설명해뒀으니 그거만 보셔도 충분하긴해요
우선 LPPL로 crash도 분석해보면 좋을 것 같아서 업데이트했다.
업데이트한 코드는 문라이트에 남겨두겠다.
(이전 글로!)

오늘의 주제는 저번 달 퀀트 스터디인 S&P500 옵션 마켓메이커(MM)들의 포지션이 주식시장에 미치는 영향
옵션 시장에서 마켓 메이커(MM)가 어떻게 주식시장 변동성에 영향을 미치는지를 설명해주신 글이다
요약해보자면 아래와 같다
델타(Δ)
기초자산(예: S&P500 지수)이 1만큼 움직일 때 옵션 가격이 얼마나 변하는지를 나타낸다.
마켓 메이커(MM)는 고객과 거래할 때 생기는 옵션 포지션의 “델타 위험”을 없애려고, 기초자산을 사거나 팔면서 델타를 0으로 맞추려고 하고, 이를 ‘델타 헤징’이라고 부른다.
감마(Γ)
델타가 얼마나 빨리 변하는지를 보는 지표입니다.
감마가 크면 기초자산이 움직일 때마다 MM이 더 큰 규모로 헷지 매매를 해야 한다.
롱 감마 vs. 숏 감마
롱 감마(+) 상태: 주가가 오르면 MM은 기초자산을 팔고, 주가가 내리면 사들이게 된다. 결과적으로 시장 변동성을 완화한다
숏 감마(–) 상태: 주가가 오르면 더 사고, 내리면 더 파는 방향으로 매매가 이루어져 변동성이 커질 수 있다.
실제 데이터로 감마 노출도 계산
감마 × 미결제약정 × 지수 × 계약단위 등을 합산해, MM 전체가 얼만큼 기초자산을 사고팔지 예상해볼 수 있다.
만기별, 행사가격별로 감마가 얼마나 쌓여 있는지도 살피면, 시장이 특정 가격대에서 갑자기 출렁일지 예측하는 힌트를 얻을 수 있다.
감마 플립(Gamma Flip)
어떤 지수 수준을 기점으로 시장 전체가 숏 감마에서 롱 감마로 바뀌는 구간을 뜻한다.
예를 들어 지수가 5,900 근처일 때 숏 감마였다가, 5,985 이상으로 올라가면 롱 감마로 뒤집힌다면, 그 지점부터는 변동성이 줄어들 가능성이 커진다.
결국 이 글이 말해주는 건, “옵션 시장을 주시하면 주가의 단기 움직임(특히 변동성)에 대한 추가적인 단서를 얻을 수 있다”라는 점아다. 숏 감마면 시장이 더 크게 출렁이고, 롱 감마면 비교적 차분해진다는 것이 핵심 포인트
그래서 구현해봤다.
2025-03-02 기준 옵션 분포다.


(Visualizing gamma by strike)

(Visualizing gamma by call/put)
현재는 숏 감마 상태로 변동성이 커질 수 있다.

(Visualizing gamma by expiration)
가까운 만기일에 숏감마인 옵션이 많은 모습 (매 만기일 별로 표기했더니 난잡하다. 수정이 필요)
그런데 여기서부터 문제가 발생했다

?????? 내가 받은 파일에는 내재 변동성이 없던데?? 어떻게 구현하지?
답을 찾아 헤매던 중
How to Calculate Gamma Exposure (GEX) and Zero Gamma Level
(감사합니다)


아... IV 이걸 못봤네..... 미치겠다
그래도 찾았으니 다행 다시 시작
그래서 현재 감마플립 포인트는 어디냐?

(Visualizing gamma profile)
현물: 5954.5
감마플립 포인트: 6049
저 위로 올라가야 시장 변동성이 좀 가라 앉겠구나 정도로 받아들이면 된다.
오늘의 코드 역시 문라이트에 공유드립니다. (스스로 검증해보고 쓰기)
그림 나오는게 마음에 안 들어서 수정할 예정
읽어주셔서 감사합니다.
(2025-03-04 업데이트 옵션 코드)
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
import matplotlib.dates as mdates
import os, re
from scipy.stats import norm
from datetime import datetime, timedelta, date
import requests
pd.options.display.float_format = '{:,.4f}'.format
# -----------------------------------#
# Black-Scholes 기반 감마 계산 함수 #
# -----------------------------------#
def black_scholes_delta_gamma(S, K, IV, T, r, sigma, option_type, open_interest):
if T == 0 or IV == 0:
return 0
dp = (np.log(S/K) + (r - sigma + 0.5*IV**2)*T) / (IV*np.sqrt(T))
dm = dp - IV*np.sqrt(T)
if option_type == 'call':
gamma = np.exp(-sigma*T) * norm.pdf(dp) / (S * IV * np.sqrt(T))
return open_interest * 100 * S * S * 0.01 * gamma
else:
gamma = K * np.exp(-r*T) * norm.pdf(dm) / (S * S * IV * np.sqrt(T))
return open_interest * 100 * S * S * 0.01 * gamma
# -----------------------------------#
# 안전한 데이터 변환 함수들 #
# -----------------------------------#
def safe_int_convert(s):
try:
if pd.isna(s):
return 0
return int(s)
except (ValueError, TypeError):
digits = ''.join(filter(str.isdigit, str(s)))
return int(digits) if digits else 0
def safe_float_convert(s):
try:
if pd.isna(s):
return 0.0
return float(s)
except (ValueError, TypeError):
numeric_chars = re.sub(r'[^\d.]', '', str(s).replace(',', '.'))
parts = numeric_chars.split('.')
if len(parts) > 2:
numeric_chars = parts[0] + '.' + ''.join(parts[1:])
return float(numeric_chars) if numeric_chars else 0.0
# --------------------------------------------------#
# API에서 SPX 옵션 데이터 받아오기 및 전처리 함수 #
# --------------------------------------------------#
def fetch_spx_options_data(index):
url = "https://cdn.cboe.com/api/global/delayed_quotes/options/_" + index + ".json"
print(f"Fetching data from API: {url}")
response = requests.get(url)
options = response.json()
# 스팟 가격 및 오늘 날짜 추출
spot_price = options["data"]["close"]
today_date = date.today()
# 옵션 데이터를 DataFrame으로 변환
data_df = pd.DataFrame(options["data"]["options"])
# 옵션 문자열에서 콜/풋, 만기, 행사가격 추출
data_df['CallPut'] = data_df['option'].str.slice(start=-9, stop=-8)
data_df['ExpirationDate'] = data_df['option'].str.slice(start=-15, stop=-9)
data_df['ExpirationDate'] = pd.to_datetime(data_df['ExpirationDate'], format='%y%m%d', errors='coerce')
data_df['Strike'] = data_df['option'].str.slice(start=-8, stop=-3)
data_df['Strike'] = data_df['Strike'].str.lstrip('0')
# 콜과 풋 데이터 분리 후 재정렬
data_df_calls = data_df.loc[data_df['CallPut'] == "C"].reset_index(drop=True)
data_df_puts = data_df.loc[data_df['CallPut'] == "P"].reset_index(drop=True)
df = data_df_calls[['ExpirationDate','option','last_trade_price','change','bid','ask','volume','iv','delta','gamma','open_interest','Strike']]
df_puts = data_df_puts[['ExpirationDate','option','last_trade_price','change','bid','ask','volume','iv','delta','gamma','open_interest','Strike']]
df_puts.columns = ['put_exp','put_option','put_last_trade_price','put_change','put_bid','put_ask','put_volume','put_iv','put_delta','put_gamma','put_open_interest','put_strike']
df = pd.concat([df, df_puts], axis=1)
df['check'] = np.where((df['ExpirationDate'] == df['put_exp']) & (df['Strike'] == df['put_strike']), 0, 1)
if df['check'].sum() != 0:
print("PUT CALL MERGE FAILED - OPTIONS ARE MISMATCHED.")
exit()
df.drop(['put_exp', 'put_strike', 'check'], axis=1, inplace=True)
df.columns = ['ExpirationDate','Calls','CallLastSale','CallNet','CallBid','CallAsk','CallVol',
'CallIV','CallDelta','CallGamma','CallOpenInt','StrikePrice','Puts','PutLastSale',
'PutNet','PutBid','PutAsk','PutVol','PutIV','PutDelta','PutGamma','PutOpenInt']
# 데이터 타입 변환 및 시간 보정 (만기일 오후 4시로 고정)
df['StrikePrice'] = df['StrikePrice'].astype(float)
df['ExpirationDate'] = df['ExpirationDate'] + timedelta(hours=16)
df['CallIV'] = pd.to_numeric(df['CallIV'], errors='coerce').fillna(0.001).clip(lower=0.001)
df['PutIV'] = pd.to_numeric(df['PutIV'], errors='coerce').fillna(0.001).clip(lower=0.001)
df['CallGamma'] = pd.to_numeric(df['CallGamma'], errors='coerce').fillna(0)
df['PutGamma'] = pd.to_numeric(df['PutGamma'], errors='coerce').fillna(0)
...
