├── __init__.py ├── data ├── __init__.py └── cboe_model.py ├── requirements.txt ├── README.md ├── LICENSE └── cboe.py /__init__.py: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /data/__init__.py: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | datetime 2 | numpy 3 | pandas 4 | requests 5 | streamlit 6 | streamlit-aggrid 7 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # CBOE Options Dashboard 2 | A Streamlit app for delayed CBOE options data. 3 | 4 | Run in the cloud: https://deeleeramone-cboedashboard-cboe-a2lpit.streamlit.app/ 5 | 6 | Or to run locally in a Python virtual environment: 7 | 8 | streamlit run cboe.py 9 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Danglewood 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /cboe.py: -------------------------------------------------------------------------------- 1 | """CBOE Options Dashboard""" 2 | __docformat__ = "numpy" 3 | 4 | import pandas as pd 5 | import numpy as np 6 | import streamlit as st 7 | from pandas import DataFrame 8 | from st_aggrid import AgGrid,ColumnsAutoSizeMode 9 | from data import cboe_model as cboe 10 | 11 | 12 | pd.set_option('display.max_rows', None) 13 | pd.set_option('display.max_columns', None) 14 | pd.set_option('display.max_colwidth', 0) 15 | pd.set_option('display.colheader_justify', 'left') 16 | 17 | CBOE_DIRECTORY: DataFrame = cboe.get_cboe_directory() 18 | CBOE_INDEXES: DataFrame = cboe.get_cboe_index_directory() 19 | 20 | # Start of Dashboard Section 21 | st.set_page_config( 22 | page_title = 'Options Analysis Dashboard', 23 | layout = 'wide', 24 | menu_items={ 25 | "Get help":"https://discord.com/invite/Y4HDyB6Ypu", 26 | }, 27 | ) 28 | st.title('CBOE Options Dashboard') 29 | 30 | col_1,col_2,col_3,col_4,col_5,col_6,col_7,col_8,col_9,col_10 = st.columns([0.20,0.33,0.20,0.20,0.20,0.20,0.20,0.20,0.20,1]) 31 | with col_1: 32 | symbol = st.text_input(label = 'Ticker', value = '', key = 'symbol') 33 | if symbol == '': 34 | st.write('Please enter a symbol') 35 | 36 | else: 37 | def get_ticker(symbol) -> object: 38 | ticker: object = cboe.ticker.get_ticker(symbol) 39 | 40 | return ticker 41 | ticker = get_ticker(symbol) 42 | 43 | if ticker: 44 | try: 45 | ticker.by_expiration.index = ticker.by_expiration.index.astype(str) 46 | ticker.skew.index = ticker.skew.index.astype(str) 47 | ticker.chains = ticker.chains.reset_index() 48 | ticker.chains.Expiration = ticker.chains.Expiration.astype(str) 49 | ticker.chains = ticker.chains.set_index('Expiration') 50 | 51 | with col_2: 52 | st.write('\n') 53 | st.write('\n') 54 | st.write(ticker.name) 55 | 56 | with col_3: 57 | st.metric(label = 'Current Price', value = ticker.details['Current Price'], delta = ticker.details['Change %']) 58 | st.write('% Change') 59 | 60 | with col_4: 61 | st.metric(label = "IV 30", value = ticker.details['IV30'], delta = ticker.details['IV30 Change']) 62 | st.write('IV 30 Change') 63 | 64 | with col_5: 65 | iv_diff: float = round((ticker.details['IV30'] - ticker.iv['IV30 1Y High']), ndigits = 4) 66 | st.metric(label = 'IV 30 1 Year High', value = round(ticker.iv['IV30 1Y High'], ndigits = 4), delta = iv_diff) 67 | st.write('IV 30 - 1 Year High') 68 | 69 | with col_6: 70 | iv_low_diff: float = round((ticker.details['IV30'] - ticker.iv['IV30 1Y Low']), ndigits = 4) 71 | st.metric(label = 'IV 30 1 Year Low', value = round(ticker.iv['IV30 1Y Low'], ndigits = 4), delta = iv_low_diff) 72 | st.write('IV 30 - 1 Year Low') 73 | 74 | with col_7: 75 | net_pcr: int = ((int(ticker.by_expiration['Put OI'].sum())) - (int(ticker.by_expiration['Call OI'].sum()))) 76 | st.metric(label = 'Put/Call OI Ratio', value = round(ticker.details['Put-Call Ratio'], ndigits = 4), delta = net_pcr) 77 | st.write('Net Put - Call OI') 78 | 79 | with col_8: 80 | net_volume: int = ((int(ticker.by_expiration['Put Vol'].sum())) - (int(ticker.by_expiration['Call Vol'].sum()))) 81 | vol_ratio: float = ((ticker.by_expiration['Put Vol'].sum()) / (ticker.by_expiration['Call Vol'].sum())) 82 | st.metric(label = 'Put/Call Vol Ratio', value = round(vol_ratio, ndigits = 4), delta = net_volume) 83 | st.write('Net Put - Call Vol') 84 | 85 | with col_9: 86 | turnover_ratio: float = round( 87 | ((ticker.by_expiration['Put Vol'].sum()) + (ticker.by_expiration['Call Vol'].sum())) 88 | /((ticker.by_expiration['Put OI'].sum()) + (ticker.by_expiration['Call OI'].sum())) 89 | ,ndigits = 4) 90 | 91 | turnover = ( 92 | ((ticker.by_expiration['Put Vol'].sum()) + (ticker.by_expiration['Call Vol'].sum())) 93 | - ((ticker.by_expiration['Put OI'].sum()) + (ticker.by_expiration['Call OI'].sum())) 94 | ) 95 | 96 | st.metric(label = 'Turnover Ratio', value = turnover_ratio, delta = int(turnover)) 97 | st.write('Net Volume - OI') 98 | 99 | with col_10: 100 | put_gex: float = ((ticker.by_expiration['Put GEX'].sum()) * (-1)) 101 | call_gex: float = (ticker.by_expiration['Call GEX'].sum()) 102 | net_gex = put_gex + call_gex 103 | st.metric(label = 'Net Gamma Exposure', value = int(net_gex), delta = int(call_gex - put_gex)) 104 | st.write('Net Call - Put GEX') 105 | 106 | tab1,tab2,tab3 = st.tabs(["Summary", "Chains", "Charts"]) 107 | 108 | with tab1: 109 | tab5,tab6 = st.tabs(['By Expiration', 'By Strike']) 110 | with tab5: 111 | AgGrid( 112 | ticker.by_expiration.reset_index(), 113 | update_mode="value_changed", 114 | fit_columns_on_grid_load = True, 115 | ) 116 | with tab6: 117 | AgGrid( 118 | ticker.by_strike.reset_index(), 119 | update_mode="Value_changed", 120 | fit_columns_on_grid_load = True, 121 | ) 122 | 123 | with tab2: 124 | st.write('\n') 125 | AgGrid( 126 | ticker.chains.reset_index(), 127 | height = 600, 128 | update_mode="value_changed", 129 | columns_auto_size_mode = ColumnsAutoSizeMode.FIT_CONTENTS, 130 | ) 131 | 132 | with tab3: 133 | st.write('\n') 134 | tab4,tab5,tab6 = st.tabs(["Open Interest", "Gamma", "Volatility"]) 135 | with tab4: 136 | st.write('\n') 137 | tab7,tab8,tab11 = st.tabs(["By Strike", "By Expiration", "Ratios"]) 138 | with tab7: 139 | st.header(f"{ticker.symbol}"' Open Interest by Strike') 140 | chart1_data = pd.DataFrame(columns = ['Puts', 'Calls']) 141 | chart1_data.Puts = ticker.by_strike['Put OI']*(-1) 142 | chart1_data.Calls = ticker.by_strike['Call OI'] 143 | st.bar_chart( 144 | chart1_data, 145 | y=['Puts', 'Calls'], 146 | width=0, 147 | height=600, 148 | use_container_width = True, 149 | ) 150 | with tab8: 151 | st.write('\n') 152 | st.header(f"{ticker.symbol}"' Open Interest by Expiration') 153 | chart2_data = pd.DataFrame(columns = ['Puts', 'Calls']) 154 | chart2_data.Puts = ticker.by_expiration['Put OI']*(-1) 155 | chart2_data.Calls = ticker.by_expiration['Call OI'] 156 | chart2_data.fillna(axis = 1, value = 0, inplace = True) 157 | st.bar_chart( 158 | chart2_data, 159 | y=['Puts', 'Calls'], 160 | width=0, 161 | height=600, 162 | use_container_width=True, 163 | ) 164 | with tab11: 165 | st.write('\n') 166 | st.header('Open Interest and Volume Ratios by Expiration for 'f"{ticker.symbol}") 167 | 168 | chart7_data = pd.DataFrame(columns = ['OI Ratio', 'Vol Ratio', 'Vol-OI Ratio']) 169 | chart7_data['OI Ratio'] = ticker.by_expiration['OI Ratio'] 170 | chart7_data['Vol Ratio'] = ticker.by_expiration['Vol Ratio'] 171 | chart7_data['Vol-OI Ratio'] = ticker.by_expiration['Vol-OI Ratio'] 172 | chart7_data.replace([np.inf, -np.inf], np.nan, inplace=True) 173 | chart7_data.fillna(value = 0.0000, inplace = True) 174 | 175 | st.line_chart( 176 | data = chart7_data, 177 | use_container_width = True, 178 | height = 450, 179 | y = ['OI Ratio', 'Vol Ratio', 'Vol-OI Ratio'], 180 | ) 181 | 182 | with tab5: 183 | st.write('\n') 184 | tab7,tab8 = st.tabs(["By Strike", "By Expiration"]) 185 | with tab7: 186 | st.header('Nominal Gamma Exposure Per 1% Change in 'f"{ticker.symbol}") 187 | chart3_data = pd.DataFrame(columns = ['Puts', 'Calls']) 188 | chart3_data.Puts = ticker.by_strike['Put GEX'] 189 | chart3_data.Calls = ticker.by_strike['Call GEX'] 190 | chart3_data.fillna(axis = 1, value = 0, inplace = True) 191 | st.bar_chart( 192 | chart3_data, 193 | y= ['Puts','Calls'], 194 | use_container_width = True, 195 | width=0, 196 | height=600 197 | ) 198 | with tab8: 199 | st.header('Nominal Gamma Exposure per 1% Change in 'f"{ticker.symbol}") 200 | chart4_data = pd.DataFrame(columns = ['Calls', 'Puts']) 201 | chart4_data.Puts = ticker.by_expiration['Put GEX'] 202 | chart4_data.Calls = ticker.by_expiration['Call GEX'] 203 | chart4_data.fillna(axis = 1, value = 0, inplace = True) 204 | st.bar_chart( 205 | chart4_data, 206 | y=['Puts', 'Calls'], 207 | use_container_width = True, 208 | width=0, 209 | height=600 210 | ) 211 | with tab6: 212 | st.write('\n') 213 | tab9,tab10,tab11 = st.tabs(["Skew", "Smile", "Surface"]) 214 | with tab9: 215 | st.subheader('Implied Volatility Skew of 'f"{ticker.symbol}") 216 | chart5_data = ticker.skew 217 | chart5_data = ( 218 | chart5_data.rename(columns = { 219 | 'Call IV': 'ATM Call IV', 220 | 'Put IV': '5% OTM Put IV' 221 | }) 222 | ) 223 | chart5_data.index = chart5_data.index.astype(str) 224 | st._arrow_area_chart( 225 | chart5_data, 226 | y = ['IV Skew'], 227 | use_container_width = True, 228 | width = 0, 229 | height = 450, 230 | ) 231 | AgGrid( 232 | ticker.skew.reset_index(), 233 | update_mode="value_changed", 234 | columns_auto_size_mode = ColumnsAutoSizeMode.FIT_CONTENTS, 235 | ) 236 | 237 | with tab10: 238 | def get_chart6_data(choice) -> pd.DataFrame: 239 | chart6_data = pd.DataFrame(columns = ['Call IV', 'Put IV']) 240 | chart6_data['Call IV'] = ticker.calls.loc[choice]['IV'].reset_index(['Type'])['IV'] 241 | chart6_data['Put IV'] = ticker.puts.loc[choice]['IV'].reset_index(['Type'])['IV'] 242 | chart6_data = chart6_data.query("0 < `Call IV` and 0 < `Put IV`") 243 | return chart6_data 244 | 245 | choice = st.selectbox(label = "Expiration Date", options = ticker.expirations) 246 | 247 | chart6_data = get_chart6_data(choice) 248 | 249 | st.subheader("Volatility Smile of "f"{ticker.symbol}") 250 | st.line_chart( 251 | chart6_data, 252 | y = ['Call IV', 'Put IV'], 253 | height = 450, 254 | width = 0, 255 | use_container_width = True, 256 | ) 257 | with tab11: 258 | st.write("Coming soon!") 259 | 260 | except Exception: 261 | st.write('Sorry, no data found') 262 | else: 263 | pass 264 | -------------------------------------------------------------------------------- /data/cboe_model.py: -------------------------------------------------------------------------------- 1 | """CBOE Model""" 2 | 3 | import pandas as pd 4 | import numpy as np 5 | import requests 6 | from typing import Literal, Tuple 7 | from pandas import DataFrame 8 | from datetime import datetime 9 | from requests.exceptions import HTTPError 10 | 11 | __docformat__: Literal["numpy"] = "numpy" 12 | 13 | symbol: str = "" 14 | TICKER_EXCEPTIONS: list[str] = ["NDX", "RUT"] 15 | 16 | #%% 17 | def get_cboe_directory() -> DataFrame: 18 | """Gets the US Listings Directory for the CBOE 19 | 20 | Returns 21 | ------- 22 | pd.DataFrame: CBOE_DIRECTORY 23 | DataFrame of the CBOE listings directory 24 | 25 | Example 26 | ------- 27 | CBOE_DIRECTORY = get_cboe_directory() 28 | """ 29 | 30 | CBOE_DIRECTORY: DataFrame = pd.read_csv( 31 | "https://www.cboe.com/us/options/symboldir/equity_index_options/?download=csv" 32 | ) 33 | CBOE_DIRECTORY = CBOE_DIRECTORY.rename( 34 | columns = { 35 | ' Stock Symbol':'Symbol', 36 | ' DPM Name':'DPM Name', 37 | ' Post/Station':'Post/Station', 38 | } 39 | ).set_index('Symbol') 40 | 41 | return CBOE_DIRECTORY 42 | 43 | def get_cboe_index_directory() -> DataFrame: 44 | """Gets the US Listings Directory for the CBOE 45 | 46 | Returns 47 | ------- 48 | pd.DataFrame: CBOE_INDEXES 49 | 50 | Example 51 | ------- 52 | CBOE_INDEXES = get_cboe_index_directory( 53 | """ 54 | 55 | CBOE_INDEXES: DataFrame = pd.read_json( 56 | "https://cdn.cboe.com/api/global/us_indices/definitions/all_indices.json" 57 | ) 58 | 59 | CBOE_INDEXES = DataFrame(CBOE_INDEXES).rename( 60 | columns={ 61 | "calc_end_time": "Close Time", 62 | "calc_start_time": "Open Time", 63 | "currency": "Currency", 64 | "description": "Description", 65 | "display": "Display", 66 | "featured": "Featured", 67 | "featured_order": "Featured Order", 68 | "index_symbol": "Ticker", 69 | "mkt_data_delay": "Data Delay", 70 | "name": "Name", 71 | "tick_days": "Tick Days", 72 | "tick_frequency": "Frequency", 73 | "tick_period": "Period", 74 | "time_zone": "Time Zone", 75 | }, 76 | ) 77 | 78 | indices_order: list[str] = [ 79 | "Ticker", 80 | "Name", 81 | "Description", 82 | "Currency", 83 | "Tick Days", 84 | "Frequency", 85 | "Period", 86 | "Time Zone", 87 | ] 88 | 89 | CBOE_INDEXES = DataFrame(CBOE_INDEXES, columns=indices_order).set_index( 90 | "Ticker" 91 | ) 92 | 93 | return CBOE_INDEXES 94 | 95 | # %% 96 | 97 | indexes = get_cboe_index_directory().index.tolist() 98 | directory = get_cboe_directory() 99 | 100 | # %% 101 | # Get Ticker Info and Expirations 102 | 103 | def get_ticker_info(symbol: str) -> Tuple[pd.DataFrame, list[str]]: 104 | """Gets basic info for the symbol and expiration dates 105 | 106 | Parameters 107 | ---------- 108 | symbol: str 109 | The ticker to lookup 110 | 111 | Returns 112 | ------- 113 | Tuple[pd.DataFrame, pd.Series]: ticker_details,ticker_expirations 114 | 115 | Examples 116 | -------- 117 | ticker_details,ticker_expirations = get_ticker_info('AAPL') 118 | 119 | ticker_details,ticker_expirations = get_ticker_info('VIX') 120 | """ 121 | 122 | # Variables for exception handling 123 | 124 | stock = "stock" 125 | index = "index" 126 | ticker: str = symbol 127 | new_ticker: str = "" 128 | ticker_details: DataFrame = pd.DataFrame() 129 | ticker_expirations: list = [] 130 | 131 | try: 132 | # Checks ticker to determine if ticker is an index or an exception that requires modifying the request's URLs 133 | 134 | if ticker in TICKER_EXCEPTIONS: 135 | new_ticker = ("^" + ticker) 136 | else: 137 | if ticker not in indexes: 138 | new_ticker = ticker 139 | 140 | elif ticker in indexes: 141 | new_ticker = ("^" + ticker) 142 | 143 | # Gets the data to return, and if none returns empty Tuple # 144 | 145 | symbol_info_url = ( 146 | "https://www.cboe.com/education/tools/trade-optimizer/symbol-info/?symbol=" 147 | f"{new_ticker}" 148 | ) 149 | 150 | symbol_info = requests.get(symbol_info_url) 151 | symbol_info_json = pd.Series(symbol_info.json()) 152 | 153 | if symbol_info_json.success is False: 154 | ticker_details = pd.DataFrame() 155 | ticker_expirations = [] 156 | print("No data found for the symbol: " f"{ticker}" "") 157 | else: 158 | symbol_details = pd.Series(symbol_info_json["details"]) 159 | symbol_details = pd.DataFrame(symbol_details).transpose() 160 | symbol_details = symbol_details.reset_index() 161 | ticker_expirations = symbol_info_json["expirations"] 162 | 163 | # Cleans columns depending on if the security type is a stock or an index 164 | 165 | type = symbol_details.security_type 166 | 167 | if stock[0] in type[0]: 168 | stock_details = symbol_details 169 | ticker_details = pd.DataFrame(stock_details).rename( 170 | columns={ 171 | "symbol": "Symbol", 172 | "current_price": "Current Price", 173 | "bid": "Bid", 174 | "ask": "Ask", 175 | "bid_size": "Bid Size", 176 | "ask_size": "Ask Size", 177 | "open": "Open", 178 | "high": "High", 179 | "low": "Low", 180 | "close": "Close", 181 | "volume": "Volume", 182 | "iv30": "IV30", 183 | "prev_day_close": "Previous Close", 184 | "price_change": "Change", 185 | "price_change_percent": "Change %", 186 | "iv30_change": "IV30 Change", 187 | "iv30_percent_change": "IV30 Change %", 188 | "last_trade_time": "Last Trade Time", 189 | "exchange_id": "Exchange ID", 190 | "tick": "Tick", 191 | "security_type": "Type", 192 | } 193 | ) 194 | details_columns = [ 195 | "Symbol", 196 | "Type", 197 | "Tick", 198 | "Bid", 199 | "Bid Size", 200 | "Ask Size", 201 | "Ask", 202 | "Current Price", 203 | "Open", 204 | "High", 205 | "Low", 206 | "Close", 207 | "Volume", 208 | "Previous Close", 209 | "Change", 210 | "Change %", 211 | "IV30", 212 | "IV30 Change", 213 | "IV30 Change %", 214 | "Last Trade Time", 215 | ] 216 | ticker_details = ( 217 | pd.DataFrame(ticker_details, columns=details_columns) 218 | .set_index(keys="Symbol") 219 | .dropna(axis=1) 220 | .transpose() 221 | ) 222 | 223 | if index[0] in type[0]: 224 | index_details = symbol_details 225 | ticker_details = pd.DataFrame(index_details).rename( 226 | columns={ 227 | "symbol": "Symbol", 228 | "security_type": "Type", 229 | "current_price": "Current Price", 230 | "price_change": "Change", 231 | "price_change_percent": "Change %", 232 | "tick": "Tick", 233 | "open": "Open", 234 | "high": "High", 235 | "low": "Low", 236 | "close": "Close", 237 | "prev_day_close": "Previous Close", 238 | "iv30": "IV30", 239 | "iv30_change": "IV30 Change", 240 | "iv30_change_percent": "IV30 Change %", 241 | "last_trade_time": "Last Trade Time", 242 | } 243 | ) 244 | 245 | index_columns = [ 246 | "Symbol", 247 | "Type", 248 | "Tick", 249 | "Current Price", 250 | "Open", 251 | "High", 252 | "Low", 253 | "Close", 254 | "Previous Close", 255 | "Change", 256 | "Change %", 257 | "IV30", 258 | "IV30 Change", 259 | "IV30 Change %", 260 | "Last Trade Time", 261 | ] 262 | 263 | ticker_details = ( 264 | pd.DataFrame(ticker_details, columns=index_columns) 265 | .set_index(keys="Symbol") 266 | .dropna(axis=1) 267 | .transpose() 268 | ) 269 | 270 | except HTTPError: 271 | print("There was an error with the request'\n") 272 | ticker_details = pd.DataFrame() 273 | ticker_expirations = list() 274 | 275 | return ticker_details,ticker_expirations 276 | 277 | # %% 278 | # Gets annualized high/low historical and implied volatility over 30/60/90 day windows. 279 | 280 | def get_ticker_iv(symbol: str) -> pd.DataFrame: 281 | """Gets annualized high/low historical and implied volatility over 30/60/90 day windows. 282 | 283 | Parameters 284 | ---------- 285 | symbol: str 286 | The loaded ticker 287 | 288 | Returns 289 | ------- 290 | pd.DataFrame: ticker_iv 291 | 292 | Examples 293 | -------- 294 | ticker_iv = get_ticker_iv('AAPL') 295 | 296 | ticker_iv = get_ticker_iv('NDX') 297 | """ 298 | 299 | ticker = symbol 300 | 301 | # Checks ticker to determine if ticker is an index or an exception that requires modifying the request's URLs 302 | try: 303 | if ticker in TICKER_EXCEPTIONS: 304 | quotes_iv_url = ( 305 | "https://cdn.cboe.com/api/global/delayed_quotes/historical_data/_" 306 | f"{ticker}" 307 | ".json" 308 | ) 309 | else: 310 | if ticker not in indexes: 311 | quotes_iv_url = ( 312 | "https://cdn.cboe.com/api/global/delayed_quotes/historical_data/" 313 | f"{ticker}" 314 | ".json" 315 | ) 316 | 317 | elif ticker in indexes: 318 | quotes_iv_url = ( 319 | "https://cdn.cboe.com/api/global/delayed_quotes/historical_data/_" 320 | f"{ticker}" 321 | ".json" 322 | ) 323 | 324 | # Gets annualized high/low historical and implied volatility over 30/60/90 day windows. 325 | 326 | h_iv = requests.get(quotes_iv_url) 327 | 328 | if h_iv.status_code != 200: 329 | print("No data found for the symbol: " f"{ticker}" "") 330 | return pd.DataFrame() 331 | 332 | else: 333 | h_iv_json = pd.DataFrame(h_iv.json()) 334 | h_columns = [ 335 | "annual_high", 336 | "annual_low", 337 | "hv30_annual_high", 338 | "hv30_annual_low", 339 | "hv60_annual_high", 340 | "hv60_annual_low", 341 | "hv90_annual_high", 342 | "hv90_annual_low", 343 | "iv30_annual_high", 344 | "iv30_annual_low", 345 | "iv60_annual_high", 346 | "iv60_annual_low", 347 | "iv90_annual_high", 348 | "iv90_annual_low", 349 | "symbol", 350 | ] 351 | h_data = h_iv_json[1:] 352 | h_data = pd.DataFrame(h_iv_json).transpose() 353 | h_data = h_data[1:2] 354 | quotes_iv_df = pd.DataFrame(data=h_data, columns=h_columns).reset_index() 355 | 356 | quotes_iv_df = pd.DataFrame(quotes_iv_df).rename( 357 | columns={ 358 | "annual_high": "1Y High", 359 | "annual_low": "1Y Low", 360 | "hv30_annual_high": "HV30 1Y High", 361 | "hv30_annual_low": "HV30 1Y Low", 362 | "hv60_annual_high": "HV60 1Y High", 363 | "hv60_annual_low": "HV60 1Y Low", 364 | "hv90_annual_high": "HV90 1Y High", 365 | "hv90_annual_low": "HV90 1Y Low", 366 | "iv30_annual_high": "IV30 1Y High", 367 | "iv30_annual_low": "IV30 1Y Low", 368 | "iv60_annual_high": "IV60 1Y High", 369 | "iv60_annual_low": "IV60 1Y Low", 370 | "iv90_annual_high": "IV90 1Y High", 371 | "iv90_annual_low": "IV90 1Y Low", 372 | "symbol": "Symbol", 373 | }, 374 | ) 375 | 376 | quotes_iv_df = quotes_iv_df.set_index(keys="Symbol") 377 | 378 | iv_order = [ 379 | "IV30 1Y High", 380 | "HV30 1Y High", 381 | "IV30 1Y Low", 382 | "HV30 1Y Low", 383 | "IV60 1Y High", 384 | "HV60 1Y High", 385 | "IV60 1Y Low", 386 | "HV60 1Y low", 387 | "IV90 1Y High", 388 | "HV90 1Y High", 389 | "IV90 1Y Low", 390 | "HV 90 1Y Low", 391 | ] 392 | 393 | ticker_iv = ( 394 | pd.DataFrame(quotes_iv_df, columns=iv_order) 395 | .fillna(value="N/A") 396 | .transpose() 397 | ) 398 | except HTTPError: 399 | print("There was an error with the request'\n") 400 | 401 | return ticker_iv 402 | 403 | # Gets quotes and greeks data and returns a dataframe: options_quotes 404 | 405 | 406 | # %% 407 | def get_ticker_chains(symbol: str) -> pd.DataFrame: 408 | """Gets the complete options chains for a ticker 409 | 410 | Parameters 411 | ---------- 412 | symbol: str 413 | The ticker get options data for 414 | 415 | Returns 416 | ------- 417 | ticker_options: pd.DataFrame 418 | DataFrame of all options chains for the ticker 419 | 420 | Examples 421 | -------- 422 | ticker_options = get_ticker_chains('SPX') 423 | 424 | ticker_options = get_ticker_chains('SPX').filter(like = '2027-12-17', axis = 0) 425 | 426 | ticker_calls = get_ticker_chains('AAPL').filter(like = 'Call', axis = 0) 427 | 428 | vix_20C = ( 429 | get_ticker_chains('VIX') 430 | .filter(like = 'Call', axis = 0) 431 | .reset_index(['Expiration', 'Type']) 432 | .query('20.0') 433 | ) 434 | """ 435 | 436 | ticker: str = symbol 437 | 438 | # Checks ticker to determine if ticker is an index or an exception that requires modifying the request's URLs 439 | 440 | try: 441 | 442 | ticker_info, _ = get_ticker_info(ticker) 443 | if not ticker_info.empty: 444 | last_price = float(ticker_info.loc["Current Price"]) 445 | else: 446 | return pd.DataFrame() 447 | 448 | if ticker in TICKER_EXCEPTIONS: 449 | quotes_url = ( 450 | "https://cdn.cboe.com/api/global/delayed_quotes/options/_" 451 | f"{ticker}" 452 | ".json" 453 | ) 454 | else: 455 | if ticker not in indexes: 456 | quotes_url = ( 457 | "https://cdn.cboe.com/api/global/delayed_quotes/options/" 458 | f"{ticker}" 459 | ".json" 460 | ) 461 | if ticker in indexes: 462 | quotes_url = ( 463 | "https://cdn.cboe.com/api/global/delayed_quotes/options/_" 464 | f"{ticker}" 465 | ".json" 466 | ) 467 | 468 | r = requests.get(quotes_url) 469 | if r.status_code != 200: 470 | print("No data found for the symbol: " f"{ticker}" "") 471 | return pd.DataFrame() 472 | else: 473 | r_json = r.json() 474 | data = pd.DataFrame(r_json["data"]) 475 | options = pd.Series(data.options, index=data.index) 476 | options_columns = list(options[0]) 477 | options_data = list(options[:]) 478 | options_df = pd.DataFrame(options_data, columns=options_columns) 479 | options_df = pd.DataFrame(options_df).rename( 480 | columns={ 481 | "option": "Option Symbol", 482 | "bid": "Bid", 483 | "bid_size": "Bid Size", 484 | "ask": "Ask", 485 | "ask_size": "Ask Size", 486 | "iv": "IV", 487 | "open_interest": "OI", 488 | "volume": "Vol", 489 | "delta": "Delta", 490 | "gamma": "Gamma", 491 | "theta": "Theta", 492 | "rho": "Rho", 493 | "vega": "Vega", 494 | "theo": "Theoretical", 495 | "change": "Change", 496 | "open": "Open", 497 | "high": "High", 498 | "low": "Low", 499 | "tick": "Tick", 500 | "last_trade_price": "Last Price", 501 | "last_trade_time": "Timestamp", 502 | "percent_change": "% Change", 503 | "prev_day_close": "Prev Close", 504 | } 505 | ) 506 | 507 | options_df_order: list[str] = [ 508 | "Option Symbol", 509 | "Tick", 510 | "Theoretical", 511 | "Last Price", 512 | "Prev Close", 513 | "% Change", 514 | "Open", 515 | "High", 516 | "Low", 517 | "Bid Size", 518 | "Bid", 519 | "Ask", 520 | "Ask Size", 521 | "Vol", 522 | "OI", 523 | "IV", 524 | "Theta", 525 | "Delta", 526 | "Gamma", 527 | "Vega", 528 | "Rho", 529 | "Timestamp", 530 | ] 531 | 532 | options_df: DataFrame = DataFrame( 533 | options_df, columns=options_df_order 534 | ).set_index(keys=["Option Symbol"]) 535 | 536 | option_df_index = pd.Series(options_df.index).str.extractall( 537 | r"^(?P\D*)(?P\d*)(?P\D*)(?P\d*)" 538 | ) 539 | 540 | option_df_index: DataFrame = option_df_index.reset_index().drop( 541 | columns=["match", "level_0"] 542 | ) 543 | 544 | option_df_index.Expiration = pd.DatetimeIndex( 545 | option_df_index.Expiration, yearfirst=True 546 | ) 547 | 548 | option_df_index.Type = option_df_index.Type.str.replace( 549 | "C", "Call" 550 | ).str.replace("P", "Put") 551 | 552 | option_df_index.Strike = [ele.lstrip("0") for ele in option_df_index.Strike] 553 | option_df_index.Strike = option_df_index.Strike.astype(float) 554 | option_df_index.Strike = option_df_index.Strike * (1 / 1000) 555 | option_df_index = option_df_index.drop(columns=["Ticker"]) 556 | ticker_chains = option_df_index.join(options_df.reset_index()) 557 | 558 | ticker_chains = ticker_chains.drop(columns=["Option Symbol"]).set_index( 559 | keys=["Expiration", "Strike", "Type"] 560 | ) 561 | 562 | ticker_chains["Theoretical"] = round( 563 | ticker_chains["Theoretical"], ndigits=2 564 | ) 565 | ticker_chains["Prev Close"] = round(ticker_chains["Prev Close"], ndigits=2) 566 | ticker_chains["% Change"] = round(ticker_chains["% Change"], ndigits=4) 567 | 568 | ticker_chains.Tick = ( 569 | ticker_chains["Tick"] 570 | .str.capitalize() 571 | .str.replace(pat="No_change", repl="No Change") 572 | ) 573 | 574 | ticker_chains.OI = ticker_chains["OI"].astype(int) 575 | ticker_chains.Vol = ticker_chains["Vol"].astype(int) 576 | ticker_chains["Bid Size"] = ticker_chains["Bid Size"].astype(int) 577 | ticker_chains["Ask Size"] = ticker_chains["Ask Size"].astype(int) 578 | ticker_chains: DataFrame = ticker_chains.sort_index() 579 | ticker_calls: DataFrame = ticker_chains.filter(like="Call", axis=0).copy() 580 | ticker_puts: DataFrame = ticker_chains.filter(like="Put", axis=0).copy() 581 | ticker_calls = ticker_calls.reset_index() 582 | 583 | ticker_calls.loc[:, ("$ to Spot")] = round( 584 | (ticker_calls.loc[:, ("Strike")]) 585 | + (ticker_calls.loc[:, ("Ask")]) 586 | - (last_price), 587 | ndigits=2, 588 | ) 589 | 590 | ticker_calls.loc[:, ("% to Spot")] = round( 591 | (ticker_calls.loc[:, ("$ to Spot")] / last_price) * 100, ndigits=4 592 | ) 593 | 594 | ticker_calls.loc[:, ("Breakeven")] = ( 595 | ticker_calls.loc[:, ("Strike")] + ticker_calls.loc[:, ("Ask")] 596 | ) 597 | 598 | ticker_calls.loc[:, ("Delta $")] = ( 599 | (ticker_calls.loc[:, ("Delta")] * 100) 600 | * (ticker_calls.loc[:, ("OI")]) 601 | * last_price 602 | ) 603 | 604 | ticker_calls.loc[:, ("GEX")] = ( 605 | ticker_calls.loc[:, ("Gamma")] 606 | * 100 607 | * ticker_calls.loc[:, ("OI")] 608 | * (last_price * last_price) 609 | * 0.01 610 | ) 611 | 612 | ticker_calls.GEX = ticker_calls.GEX.astype(int) 613 | ticker_calls["Delta $"] = ticker_calls["Delta $"].astype(int) 614 | ticker_calls = ticker_calls.set_index(keys=["Expiration", "Strike", "Type"]) 615 | 616 | ticker_puts = ticker_puts.reset_index() 617 | 618 | ticker_puts.loc[:, ("$ to Spot")] = round( 619 | (ticker_puts.loc[:, ("Strike")]) 620 | - (ticker_puts.loc[:, ("Ask")]) 621 | - (last_price), 622 | ndigits=2, 623 | ) 624 | 625 | ticker_puts.loc[:, ("% to Spot")] = round( 626 | (ticker_puts.loc[:, ("$ to Spot")] / last_price) * 100, ndigits=4 627 | ) 628 | 629 | ticker_puts.loc[:, ("Breakeven")] = ( 630 | ticker_puts.loc[:, ("Strike")] - ticker_puts.loc[:, ("Ask")] 631 | ) 632 | 633 | ticker_puts.loc[:, ("Delta $")] = ( 634 | (ticker_puts.loc[:, ("Delta")] * 100) 635 | * (ticker_puts.loc[:, ("OI")]) 636 | * last_price 637 | * (-1) 638 | ) 639 | 640 | ticker_puts.loc[:, ("GEX")] = ( 641 | ticker_puts.loc[:, ("Gamma")] 642 | * 100 643 | * ticker_puts.loc[:, ("OI")] 644 | * (last_price * last_price) 645 | * 0.01 646 | ) 647 | 648 | ticker_puts.GEX = ticker_puts.GEX.astype(int) 649 | ticker_puts["Delta $"] = ticker_puts["Delta $"].astype(int) 650 | ticker_puts.set_index(keys=["Expiration", "Strike", "Type"], inplace=True) 651 | 652 | ticker_chains = pd.concat([ticker_puts, ticker_calls]).sort_index() 653 | 654 | temp = ticker_chains.reset_index().get(["Expiration"]) 655 | temp.Expiration = pd.DatetimeIndex(data=temp.Expiration) 656 | temp_ = temp.Expiration - datetime.now() 657 | temp_ = temp_.astype(str) 658 | temp_ = temp_.str.extractall(r"^(?P\d*)") 659 | temp_ = temp_.droplevel("match") 660 | temp_.DTE = temp_.DTE.fillna("-1") 661 | temp_.DTE = temp_.DTE.astype(int) 662 | temp_.DTE = temp_.DTE + 1 663 | ticker_chains = temp_.join(ticker_chains.reset_index()).set_index( 664 | ["Expiration", "Strike", "Type"] 665 | ) 666 | 667 | ticker_chains["Expected Move"] = round( 668 | (ticker_chains["Last Price"] * ticker_chains["IV"]) 669 | * (np.sqrt(ticker_chains["DTE"] / 252)), 670 | ndigits=2, 671 | ) 672 | 673 | ticker_chains_cols: list[str] = [ 674 | "DTE", 675 | "Tick", 676 | "Last Price", 677 | "Expected Move", 678 | "% Change", 679 | "Theoretical", 680 | "$ to Spot", 681 | "% to Spot", 682 | "Breakeven", 683 | "Vol", 684 | "OI", 685 | "Delta $", 686 | "GEX", 687 | "IV", 688 | "Theta", 689 | "Delta", 690 | "Gamma", 691 | "Vega", 692 | "Rho", 693 | "Open", 694 | "High", 695 | "Low", 696 | "Prev Close", 697 | "Bid Size", 698 | "Bid", 699 | "Ask", 700 | "Ask Size", 701 | "Timestamp", 702 | ] 703 | 704 | ticker_chains = DataFrame(data=ticker_chains, columns=ticker_chains_cols) 705 | 706 | except HTTPError: 707 | print("There was an error with the request'\n") 708 | 709 | return ticker_chains 710 | 711 | 712 | # %% 713 | def separate_chains(chains_df: pd.DataFrame) -> Tuple[pd.DataFrame, pd.DataFrame]: 714 | """Helper function to separate Options Chains into Call and Put Chains. 715 | Parameters 716 | ---------- 717 | chains_df : pd.DataFrame 718 | DataFrame of options chains data. 719 | 720 | Returns 721 | ------- 722 | Tuple: [pd.DataFrame, pd.DataFrame] 723 | Tuple of options DataFrames separated by calls and puts. 724 | 725 | Example 726 | ------- 727 | calls,puts = separate_chains(chains_df) 728 | """ 729 | 730 | if not chains_df.empty and chains_df is not None: 731 | calls: pd.DataFrame = chains_df.filter(like="Call", axis=0).copy() 732 | puts: pd.DataFrame = chains_df.filter(like="Put", axis=0).copy() 733 | 734 | return calls, puts 735 | 736 | else: 737 | print( 738 | "There was an error with the input data, or the DataFrame passed was empty." 739 | "\n" 740 | ) 741 | calls = pd.DataFrame() 742 | puts = pd.DataFrame() 743 | return calls, puts 744 | 745 | 746 | # %% 747 | def calc_chains_by_expiration(chains_df: pd.DataFrame) -> pd.DataFrame: 748 | """Calculates stats for options chains by expiration. 749 | Parameters 750 | ---------- 751 | chains_df: pd.DataFrame 752 | DataFrame of options chains to use. 753 | 754 | Returns 755 | ------- 756 | pd.DataFrame 757 | DataFrame with stats by expiration date. 758 | 759 | Example 760 | ------- 761 | chains_by_expiration = calc_chains_by_expiration(chains_df) 762 | """ 763 | 764 | if not chains_df.empty and chains_df is not None: 765 | 766 | calls, puts = separate_chains(chains_df) 767 | 768 | calls_by_expiration = ( 769 | calls.reset_index() 770 | .groupby("Expiration") 771 | .sum(numeric_only=True)[["OI", "Vol", "Delta $", "GEX"]] 772 | ) 773 | 774 | calls_by_expiration = calls_by_expiration.rename( 775 | columns={ 776 | "OI": "Call OI", 777 | "Vol": "Call Vol", 778 | "Delta $": "Call Delta $", 779 | "GEX": "Call GEX", 780 | } 781 | ) 782 | 783 | puts_by_expiration = ( 784 | puts.reset_index() 785 | .groupby("Expiration") 786 | .sum(numeric_only=True)[["OI", "Vol", "Delta $", "GEX"]] 787 | ) 788 | 789 | puts_by_expiration["Delta $"] = puts_by_expiration["Delta $"] * (-1) 790 | puts_by_expiration["GEX"] = puts_by_expiration["GEX"] * (-1) 791 | 792 | puts_by_expiration = puts_by_expiration.rename( 793 | columns={ 794 | "OI": "Put OI", 795 | "Vol": "Put Vol", 796 | "Delta $": "Put Delta $", 797 | "GEX": "Put GEX", 798 | } 799 | ) 800 | 801 | chains_by_expiration = calls_by_expiration.join(puts_by_expiration) 802 | 803 | chains_by_expiration["OI Ratio"] = round( 804 | chains_by_expiration["Put OI"] / chains_by_expiration["Call OI"], ndigits=4 805 | ) 806 | 807 | chains_by_expiration["Net OI"] = ( 808 | chains_by_expiration["Call OI"] + chains_by_expiration["Put OI"] 809 | ) 810 | 811 | chains_by_expiration["Vol Ratio"] = round( 812 | chains_by_expiration["Put Vol"] / chains_by_expiration["Call Vol"], 813 | ndigits=4, 814 | ) 815 | 816 | chains_by_expiration["Net Vol"] = ( 817 | chains_by_expiration["Call Vol"] + chains_by_expiration["Put Vol"] 818 | ) 819 | 820 | chains_by_expiration["Vol-OI Ratio"] = round( 821 | chains_by_expiration["Net Vol"] / chains_by_expiration["Net OI"], ndigits=4 822 | ) 823 | 824 | chains_by_expiration["Net Delta $"] = ( 825 | chains_by_expiration["Call Delta $"] + chains_by_expiration["Put Delta $"] 826 | ) 827 | 828 | chains_by_expiration["Net GEX"] = ( 829 | chains_by_expiration["Call GEX"] + chains_by_expiration["Put GEX"] 830 | ) 831 | 832 | cols_order = [ 833 | "Call OI", 834 | "Put OI", 835 | "Net OI", 836 | "OI Ratio", 837 | "Call Vol", 838 | "Put Vol", 839 | "Net Vol", 840 | "Vol Ratio", 841 | "Vol-OI Ratio", 842 | "Call Delta $", 843 | "Put Delta $", 844 | "Net Delta $", 845 | "Call GEX", 846 | "Put GEX", 847 | "Net GEX", 848 | ] 849 | 850 | chains_by_expiration = pd.DataFrame(chains_by_expiration, columns=cols_order) 851 | 852 | return chains_by_expiration 853 | 854 | else: 855 | print( 856 | "There was an error with the input data, or the DataFrame passed was empty." 857 | "\n" 858 | ) 859 | chains_by_expiration = pd.DataFrame() 860 | return chains_by_expiration 861 | 862 | 863 | # %% 864 | def calc_chains_by_strike(chains_df: pd.DataFrame) -> pd.DataFrame: 865 | """ 866 | Parameters 867 | ---------- 868 | chains_df: pd.DataFrame 869 | Dataframe of the chains by expiration 870 | 871 | Returns 872 | ------- 873 | pd.DataFrame: 874 | Dataframe of the chains by strike 875 | 876 | Example 877 | ------- 878 | chains_by_strike = calc_chains_by_strike(chains_df) 879 | """ 880 | 881 | if not chains_df.empty and chains_df is not None: 882 | 883 | calls, puts = separate_chains(chains_df) 884 | 885 | calls_by_strike = ( 886 | calls.reset_index() 887 | .groupby("Strike") 888 | .sum(numeric_only=True)[["OI", "Vol", "Delta $", "GEX"]] 889 | ) 890 | 891 | calls_by_strike = calls_by_strike.rename( 892 | columns={ 893 | "OI": "Call OI", 894 | "Vol": "Call Vol", 895 | "Delta $": "Call Delta $", 896 | "GEX": "Call GEX", 897 | } 898 | ) 899 | 900 | puts_by_strike = ( 901 | puts.reset_index() 902 | .groupby("Strike") 903 | .sum(numeric_only=True)[["OI", "Vol", "Delta $", "GEX"]] 904 | ) 905 | 906 | puts_by_strike["Delta $"] = puts_by_strike["Delta $"] * (-1) 907 | puts_by_strike["GEX"] = puts_by_strike["GEX"] * (-1) 908 | 909 | puts_by_strike = puts_by_strike.rename( 910 | columns={ 911 | "OI": "Put OI", 912 | "Vol": "Put Vol", 913 | "Delta $": "Put Delta $", 914 | "GEX": "Put GEX", 915 | } 916 | ) 917 | chains_by_strike = calls_by_strike.join(puts_by_strike) 918 | 919 | chains_by_strike["Net OI"] = ( 920 | chains_by_strike["Call OI"] + chains_by_strike["Put OI"] 921 | ) 922 | chains_by_strike["Net Vol"] = ( 923 | chains_by_strike["Call Vol"] + chains_by_strike["Put Vol"] 924 | ) 925 | chains_by_strike["Net Delta $"] = ( 926 | chains_by_strike["Call Delta $"] + chains_by_strike["Put Delta $"] 927 | ) 928 | chains_by_strike["Net GEX"] = ( 929 | chains_by_strike["Call GEX"] + chains_by_strike["Put GEX"] 930 | ) 931 | 932 | cols_order = [ 933 | "Call OI", 934 | "Put OI", 935 | "Net OI", 936 | "Call Vol", 937 | "Put Vol", 938 | "Net Vol", 939 | "Call Delta $", 940 | "Put Delta $", 941 | "Net Delta $", 942 | "Call GEX", 943 | "Put GEX", 944 | "Net GEX", 945 | ] 946 | 947 | chains_by_strike = pd.DataFrame(data=chains_by_strike, columns=cols_order) 948 | 949 | return chains_by_strike 950 | 951 | else: 952 | print( 953 | "There was an error with the input data, or the DataFrame passed was empty." 954 | "\n" 955 | ) 956 | chains_by_strike = pd.DataFrame() 957 | return chains_by_strike 958 | 959 | # %% 960 | 961 | class Ticker(object): 962 | """Class object for a single ticker""" 963 | 964 | def __init__(self) -> None: 965 | return None 966 | self = __init__ 967 | 968 | def get_ticker(self, symbol:str) -> object: 969 | """Gets all data from the CBOE for a given ticker and returns an object 970 | 971 | Parameters 972 | ---------- 973 | symbol: str 974 | The ticker symbol to get data for. 975 | 976 | Returns 977 | ------- 978 | object: An object containing all the options data for the ticker. 979 | ticker.symbol 980 | ticker.details 981 | ticker.expirations 982 | ticker.iv 983 | ticker.chains 984 | ticker.calls 985 | ticker.puts 986 | ticker.by_expiration 987 | ticker.by_strike 988 | ticker.skew 989 | 990 | Examples 991 | -------- 992 | spx = get_ticker('SPX') 993 | 994 | chains = get_ticker('spx').chains 995 | 996 | """ 997 | try: 998 | self.symbol = symbol.upper() 999 | self.details, self.expirations = get_ticker_info(self.symbol) 1000 | symbol_ = self.details.columns[0] 1001 | self.details = self.details[symbol_] 1002 | stock_price = self.details["Current Price"] 1003 | self.stock_price = stock_price 1004 | self.iv = get_ticker_iv(self.symbol) 1005 | self.iv = self.iv[symbol_] 1006 | self.chains = get_ticker_chains(self.symbol) 1007 | self.calls, self.puts = separate_chains(self.chains) 1008 | self.by_expiration = calc_chains_by_expiration(self.chains) 1009 | self.by_strike = calc_chains_by_strike(self.chains) 1010 | self.details["Put-Call Ratio"] = ( 1011 | self.by_expiration.sum()["Put OI"] / self.by_expiration.sum()["Call OI"] 1012 | ) 1013 | self.name = str(directory.query("`Symbol` == @ticker.symbol")['Company Name'][0]) 1014 | 1015 | # Calculate IV Skew by Expiration 1016 | 1017 | atm_calls: DataFrame = self.calls.reset_index()[ 1018 | ["Expiration", "Strike", "IV"] 1019 | ] 1020 | atm_calls = atm_calls.query( 1021 | "@self.stock_price*0.995 <= Strike <= @self.stock_price*1.05" 1022 | ) 1023 | atm_calls = atm_calls.groupby("Expiration")[["Strike", "IV"]] 1024 | atm_calls = atm_calls.apply(lambda x: x.loc[x["Strike"].idxmin()]) 1025 | atm_calls = atm_calls.rename(columns={"Strike": "Call Strike", "IV": "Call IV"}) 1026 | otm_puts: DataFrame = self.puts.reset_index()[["Expiration", "Strike", "IV"]] 1027 | otm_puts = otm_puts.query( 1028 | "@self.stock_price*0.94 <= Strike <= @self.stock_price" 1029 | ) 1030 | otm_puts = otm_puts.groupby("Expiration")[["Strike", "IV"]] 1031 | otm_puts = otm_puts.apply(lambda x: x.loc[x["Strike"].idxmin()]) 1032 | otm_puts = otm_puts.rename(columns={"Strike": "Put Strike", "IV": "Put IV"}) 1033 | iv_skew: DataFrame = atm_calls.join(otm_puts) 1034 | iv_skew["IV Skew"] = iv_skew["Put IV"] - iv_skew["Call IV"] 1035 | self.skew = iv_skew 1036 | self.by_expiration["IV Skew"] = iv_skew["IV Skew"] 1037 | 1038 | except Exception: 1039 | print("\n") 1040 | 1041 | return ticker 1042 | 1043 | ticker: Ticker = Ticker() --------------------------------------------------------------------------------