# streamlit import streamlit as st import pandas as pd import numpy as np import plotly.express as px import os import time from dotenv import load_dotenv from datetime import datetime from utils import upload_to_hf_dataset, download_from_hf_dataset, load_hf_dataset # Get current date and time # current_datetime = datetime.now().strftime("%Y-%m-%d_%H-%M-%S") current_datetime = datetime.now().strftime("%Y-%m-%d") # Load environment variables from .env file load_dotenv() # Get the name of the HuggingFace dataset for TradingView to read from dataset_name_TradingView_input = os.getenv("dataset_name_TradingView_input") # Get the name of the HuggingFace dataset for YfOptions to export dataset_name_YfOptions_output = os.getenv("dataset_name_YfOptions_output") # Get the Hugging Face API token from the environment; either set in .env file or in the environment directly in GitHub HF_TOKEN_YfOptions = os.getenv("HF_TOKEN_YfOptions") # Set page configuration st.set_page_config(page_title="Option Data Screener App", page_icon="📊", layout="wide") ######################################################################################################## # Functions @st.cache_data def get_TD_DF(current_datetime): # Load lastest TradingView DataSet from HuggingFace Dataset which is always america.csv # download_from_hf_dataset("america.csv", "AmirTrader/TradingViewData", HF_TOKEN_YfOptions) DF = load_hf_dataset( "america.csv", HF_TOKEN_YfOptions, dataset_name_TradingView_input ) # get ticker list by filtering only above 1 billion dollar company # DF = pd.read_csv(f'america_2024-03-01.csv') tickerlst = list(DF.query("`Market Capitalization`>10e9").Ticker) return DF, tickerlst @st.cache_data def get_options_DF(current_datetime): DF = load_hf_dataset( "optionchain.csv", HF_TOKEN_YfOptions, dataset_name_YfOptions_output ) return DF def convert_df(df): return df.to_csv().encode("utf-8") def convert_df_watchlist(df): return ",".join(map(str, df["Ticker"].unique())) @st.cache_data def get_options_merge(current_datetime): DF, tickerlst = get_TD_DF(current_datetime) DF_options_origin = get_options_DF(current_datetime) # To safely compute the Volume_OpenInterest_Ratio in your DataFrame without encountering division by zero or null value errors, you can utilize pandas' div() method combined with appropriate handling for infinite and missing values. # DF_options_origin["Volume_OpenInterest_Ratio"] = ( # DF_options_origin["volume"] / DF_options_origin["openInterest"] # ) DF_options_origin["Volume_OpenInterest_Ratio"] = ( DF_options_origin["volume"] .div(DF_options_origin["openInterest"]) .replace([np.inf, -np.inf], 0) # Replace infinite values resulting from division by zero .fillna(0) # Replace NaN values resulting from division by nulls ) # Extract ticker from contractSymbol and merge dataframes DF_options_origin["Ticker"] = DF_options_origin["contractSymbol"].str.extract( r"([A-Z]+)" ) TD_interestedColumns = ["Ticker", "Market Capitalization", "Relative Volume"] DF_options_merged = pd.merge( DF_options_origin, DF[TD_interestedColumns], on="Ticker", how="left" ) # Pivot the DataFrame to separate 'Call' and 'Put' for volume volume_pivot = ( DF_options_merged.groupby(["Ticker", "Type"])["volume"].sum().unstack() ) volume_pivot.columns = ["Call_Volume", "Put_Volume"] # Pivot the DataFrame to separate 'Call' and 'Put' for openInterest openInterest_pivot = ( DF_options_merged.groupby(["Ticker", "Type"])["openInterest"].sum().unstack() ) openInterest_pivot.columns = ["Call_openInterest", "Put_openInterest"] # Merge the volume and open interest DataFrames merged_df = volume_pivot.merge( openInterest_pivot, left_index=True, right_index=True ) # Calculate Put/Call Volume Ratio merged_df["Put_Call_Volume_Ratio"] = ( merged_df["Put_Volume"] / merged_df["Call_Volume"] ) # .replace(0, pd.NA) # Calculate Put/Call Open Interest Ratio merged_df["Put_Call_OI_Ratio"] = ( merged_df["Put_openInterest"] / merged_df["Call_openInterest"] ) # .replace(0, pd.NA) DFtotal = pd.merge( DF_options_merged, merged_df, left_on="Ticker", right_index=True, how="left" ) DFtotal["Moneyvolume"] = DFtotal["lastPrice"] * DFtotal["volume"] *100 DFtotal["MoneyopenInterest"] = DFtotal["lastPrice"] * DFtotal["openInterest"] * 100 return DFtotal, tickerlst ######################################################################################################## # Main DF_options, tickerlst = get_options_merge(current_datetime) # Title st.title("📊 Unusual Options Activity Dashboard") st.write(f"Number of avialable tickers: **{len(tickerlst)}**") st.write(f"Number of options contract records: **{len(DF_options)}** with volume of **{round(DF_options['volume'].sum()/1e+6,2)}M** contracts and **{round(DF_options['openInterest'].sum()/1e+6,2)}M** open interest") if st.sidebar.button("Options Statistics", use_container_width=True): st.header("Statistics of Options Data") st.write(f'Value of Today volume options contracts: **{round(DF_options["Moneyvolume"].sum()/1e+9,2)}$B**') st.write(f'Value of open interest options contracts: **{round(DF_options["MoneyopenInterest"].sum()/1e+9,2)}$B**') st.write(f'Maximum Volume {int( DF_options["volume"].max())} beloing to Ticker **{DF_options.loc[DF_options["volume"].idxmax(), "Ticker"]}** and contractSymbol {DF_options.loc[DF_options["volume"].idxmax(), "contractSymbol"]}') st.write(f'Maximum Open Interest { int(DF_options["openInterest"].max())} beloing to Ticker **{DF_options.loc[DF_options["openInterest"].idxmax(), "Ticker"]}** and contractSymbol {DF_options.loc[DF_options["openInterest"].idxmax(), "contractSymbol"]}') st.write(f'Maximum Implied Volatility { round(DF_options["impliedVolatility"].max(),2)} beloing to Ticker **{DF_options.loc[DF_options["impliedVolatility"].idxmax(), "Ticker"]}**') st.write(f'Maximum Volume/Open Interest Ratio { DF_options["Volume_OpenInterest_Ratio"].max()} beloing to Ticker {DF_options.loc[DF_options["Volume_OpenInterest_Ratio"].idxmax(), "Ticker"]}') st.write(f'Maximum Relative Volume { round(DF_options["Relative Volume"].max(),2)} beloing to Ticker {DF_options.loc[DF_options["Relative Volume"].idxmax(), "Ticker"]}') # plot top 10 volume and open interest; x is Ticker, y is volume and open interest fig = px.bar( DF_options.nlargest(100, "volume"), x="Ticker", y="volume", color="Type", title="Top Options by Volume", color_discrete_map={"CALL": "green", "PUT": "red"}, ) fig.update_layout( xaxis_title="Ticker", yaxis_title="Volume", autosize=True, height=600, ) st.plotly_chart(fig, use_container_width=True) fig = px.bar( DF_options.nlargest(100, "openInterest"), x="Ticker", y="openInterest", color="Type", title="Top Options by Open Interest", color_discrete_map={"CALL": "green", "PUT": "red"}, ) fig.update_layout( xaxis_title="Ticker", yaxis_title="Open Interest", autosize=True, height=600, ) st.plotly_chart(fig, use_container_width=True) # Sidebar st.sidebar.header("Controls") st.sidebar.markdown("### Filter Options Data") # Change number inputs to range sliders for volume and open interest volume_range = st.sidebar.slider( "Volume Range", min_value=0, max_value=DF_options["volume"].max().astype(int), value=(0, DF_options["volume"].max().astype(int)), step=100, ) open_interest_range = st.sidebar.slider( "Open Interest Range", min_value=0, max_value=DF_options["openInterest"].max().astype(int), value=(0, DF_options["openInterest"].max().astype(int)), step=100, ) # Add range selector for Volume/Open Interest Ratio vol_oi_ratio_range = st.sidebar.slider( "Volume/Open Interest Ratio Range", min_value=0.0, max_value= np.nanmax( DF_options["Volume_OpenInterest_Ratio"][ ~np.isinf(DF_options["Volume_OpenInterest_Ratio"]) ] ), value=(0.0, 10.0), # ( # 0.5, # np.nanmax( # DF_options["Volume_OpenInterest_Ratio"][ # ~np.isinf(DF_options["Volume_OpenInterest_Ratio"]) # ] # ) # / 2, # ), step=0.1, ) # Add expiration date filter expiration_dates = sorted(DF_options["expirationDate"].unique()) selected_expiry = st.sidebar.multiselect( "Select Expiration Dates", options=expiration_dates, default=expiration_dates[:4], # Default to first 3 dates ) st.sidebar.markdown("---") # Add a horizontal line as a visual separator st.sidebar.markdown("### Filter Stock Data") min_relative_volume = st.sidebar.number_input( "Minimum Relative Volume", min_value=0.0, value=1.5, step=0.1 ) # Changed to range sliders put_call_volume_range = st.sidebar.slider( "Put/Call Volume Ratio Range", min_value=0.0, max_value=np.nanmax( DF_options["Put_Call_Volume_Ratio"][ ~np.isinf(DF_options["Put_Call_Volume_Ratio"]) ] ), value=(0.0, 0.6), step=0.1, ) put_call_oi_range = st.sidebar.slider( "Put/Call OI Ratio Range", min_value=0.0, max_value=np.nanmax( DF_options["Put_Call_OI_Ratio"][~np.isinf(DF_options["Put_Call_OI_Ratio"])] ), value=(0.0, 3.0), step=0.1, ) if st.sidebar.button("Filter", use_container_width=True): # Display options data st.header("Filtering Options Data") # Filter the dataframe with the range and expiry dates filtered_df = DF_options[ (DF_options["volume"] >= volume_range[0]) & (DF_options["volume"] <= volume_range[1]) & (DF_options["openInterest"] >= open_interest_range[0]) & (DF_options["openInterest"] <= open_interest_range[1]) & (DF_options["Relative Volume"] >= min_relative_volume) & (DF_options["Put_Call_Volume_Ratio"] >= put_call_volume_range[0]) & (DF_options["Put_Call_Volume_Ratio"] <= put_call_volume_range[1]) & (DF_options["Put_Call_OI_Ratio"] >= put_call_oi_range[0]) & (DF_options["Put_Call_OI_Ratio"] <= put_call_oi_range[1]) & (DF_options["Volume_OpenInterest_Ratio"] >= vol_oi_ratio_range[0]) & (DF_options["Volume_OpenInterest_Ratio"] <= vol_oi_ratio_range[1]) & (DF_options["expirationDate"].isin(selected_expiry)) ] st.write(f"Filtered records: {len(filtered_df)} rows") interestedColumns = [ "contractSymbol", "expirationDate", "volume", "openInterest", "impliedVolatility", "Volume_OpenInterest_Ratio", "Relative Volume", "Put_Call_Volume_Ratio", "Put_Call_OI_Ratio", ] # selected_columns = st.sidebar.multiselect( # "Select columns to display", # options=DF_options.columns.tolist(), # default=interestedColumns, # ) if interestedColumns: st.dataframe(filtered_df[interestedColumns].reset_index(drop=True)) # Download button for the DataFrame csv = convert_df(filtered_df) st.download_button( label="Download Options Data as CSV", data=csv, file_name=f"options_data_{current_datetime}.csv", mime="text/csv", ) csv = convert_df_watchlist(filtered_df) st.download_button( label="Download Stock WatchList for TradingView", data=csv, file_name=f"filtered_options_data_{current_datetime}.txt", mime="text/csv", ) st.write(f"Unique Filtered Tickers: **{csv}** ") st.sidebar.markdown("---") # Add a horizontal line as a visual separator st.sidebar.header("Ticker") selectedTicker = st.sidebar.selectbox( "Select Ticker", options=tickerlst, index=tickerlst.index("NVDA") if "NVDA" in tickerlst else 0, ) if st.sidebar.button("Option Chain Visualization", use_container_width=True): st.header(f"Options Chain Visualization for Ticker {selectedTicker}") # filter DF_options for selectedTicker filtered_ticker_df = DF_options[DF_options["Ticker"] == selectedTicker] # Create two columns for the charts col1, col2, col3 = st.columns(3) with col1: fig_3d = px.scatter_3d( filtered_ticker_df, x="volume", y="openInterest", z="impliedVolatility", color="Type", title=f"3D Scatter Plot for {selectedTicker}", hover_data=[ "strike", "lastPrice", "mark", "daysleft", "contractSymbol", "expirationDate", ], color_discrete_map={"CALL": "green", "PUT": "red"}, ) fig_3d.update_layout( autosize=True, height=600, margin=dict(l=50, r=50, b=50, t=50) ) st.plotly_chart(fig_3d, use_container_width=True) with col2: # 3D Volatility Surface: Implied Volatility by Strike and Days to Expiration fig_surface = px.scatter_3d( filtered_ticker_df, x="strike", y="daysleft", z="impliedVolatility", color="Type", title=f"Implied Volatility Surface for {selectedTicker}", hover_data=[ "volume", "openInterest", "lastPrice", "mark", "contractSymbol", ], color_discrete_map={"CALL": "green", "PUT": "red"}, ) fig_surface.update_traces(marker=dict(size=5)) fig_surface.update_layout( scene=dict( xaxis_title="Strike Price", yaxis_title="Days to Expiration", zaxis_title="Implied Volatility", ), autosize=True, height=600, margin=dict(l=50, r=50, b=50, t=50), ) st.plotly_chart(fig_surface, use_container_width=True) with col3: # 3D Surface Plot: Option Price vs. Strike Price and Days to Expiration fig_surface2 = px.scatter_3d( filtered_ticker_df, x="strike", y="daysleft", z="lastPrice", color="Type", title=f"Option Price Surface for {selectedTicker}", hover_data=[ "volume", "openInterest", "impliedVolatility", "mark", "contractSymbol", ], color_discrete_map={"CALL": "green", "PUT": "red"}, ) fig_surface2.update_traces(marker=dict(size=5)) fig_surface2.update_layout( scene=dict( xaxis_title="Strike Price", yaxis_title="Days to Expiration", zaxis_title="Option Price", ), autosize=True, height=600, margin=dict(l=50, r=50, b=50, t=50), ) st.plotly_chart(fig_surface2, use_container_width=True) # Display the filtered DataFrame st.dataframe( filtered_ticker_df.query("Ticker ==@selectedTicker")[ [ "contractSymbol", "daysleft", "Type", "strike", "lastPrice", "volume", "openInterest", "impliedVolatility", "inTheMoney", ] ].reset_index(drop=True), use_container_width=True, hide_index=True, height=600, ) # Create two columns for the charts col1, col2, col3 = st.columns(3) with col1: # Bar chart for Volume by Strike Price fig_bar = px.bar( filtered_ticker_df, x="strike", y="volume", color="Type", title=f"Volume by Strike Price for {selectedTicker}", barmode="group", color_discrete_map={"CALL": "green", "PUT": "red"}, ) fig_bar.update_layout( xaxis_title="Strike Price", yaxis_title="Volume", autosize=True, height=600 ) st.plotly_chart(fig_bar, use_container_width=True) with col2: # Bar chart for Open Interest by Expiration Date fig_bar2 = px.bar( filtered_ticker_df, x="expirationDate", y="openInterest", color="Type", title=f"Open Interest by Expiration Date for {selectedTicker}", barmode="group", color_discrete_map={"CALL": "green", "PUT": "red"}, ) fig_bar.update_layout( xaxis_title="Expiration Date", yaxis_title="Open Interest", autosize=True, height=600, ) st.plotly_chart(fig_bar2, use_container_width=True) with col3: # mplied Volatility by Strike Price fig_bar3 = px.bar( filtered_ticker_df, x="strike", y="impliedVolatility", color="Type", title=f"Implied Volatility by Strike Price for {selectedTicker}", barmode="group", color_discrete_map={"CALL": "green", "PUT": "red"}, ) st.plotly_chart(fig_bar3, use_container_width=True) st.write( "*Tips: in Open Interest by Strike Bar Chart, High call OI at a particular strike may indicate a resistance level, while high put OI may suggest support!*" ) # plot Open Interest by Strike Bar ChartFilter DF_options for soon-to-expire options less than 14 days filtered_ticker_df_soon = filtered_ticker_df[filtered_ticker_df["daysleft"] <= 14] fig_bar4 = px.bar( filtered_ticker_df_soon, x="strike", y="openInterest", color="Type", title=f"Open Interest by Strike Price for {selectedTicker}", barmode="group", color_discrete_map={"CALL": "green", "PUT": "red"}, ) fig_bar4.update_layout( xaxis_title="Strike Price", yaxis_title="Open Interest", autosize=True, height=600, ) st.plotly_chart(fig_bar4, use_container_width=True) # Display the filtered DataFrame st.markdown("---") # Add a horizontal line as a visual separator st.write( "*Tips: in Open Interest Heatmap visualizes where significant open interest concentrations exist across different strikes and expirations." ) fig_bar5 = px.density_heatmap( filtered_ticker_df_soon, x="strike", y="expirationDate", z="openInterest", color_continuous_scale="Viridis", title=f"Open Interest Heatmap for {selectedTicker}", labels={"x": "Strike Price", "y": "Expiration Date", "z": "Open Interest"}, ) fig_bar5.update_layout( xaxis_title="Strike Price", yaxis_title="Expiration Date", autosize=True, height=600, ) st.plotly_chart(fig_bar5, use_container_width=True) # Combined OI and Volume Chart # Description: Overlay line plots of open interest and volume for each strike price. # Purpose: Identifies strikes with both high open interest and volume, indicating active trading zones. # Implementation: Aggregate openInterest and volume by strike. # Plot both metrics on the same chart using different y-axes or colors for clarity. st.markdown("---") # Add a horizontal line as a visual separator st.write( "*Tips: in Combined Open Interest and Volume Chart, High open interest with high volume may indicate strong interest in that strike price.*" ) fig_bar6 = px.line( filtered_ticker_df_soon, x="strike", y=["openInterest", "volume"], title=f"Combined Open Interest and Volume for {selectedTicker}", labels={"value": "Value", "variable": "Metric"}, ) fig_bar6.update_layout( xaxis_title="Strike Price", yaxis_title="Value", autosize=True, height=600, ) st.plotly_chart(fig_bar6, use_container_width=True) st.markdown("---") # Add a horizontal line as a visual separator # Price vs. Strike Scatter Plot with OI Sizing # Description: Scatter plot where each point represents an option contract, with the x-axis as strike price, y-axis as option price, and point size proportional to open interest. # Purpose: Visualizes the relationship between option pricing and open interest across strikes. # Implementation: Use strike for the x-axis and lastPrice for the y-axis. Set the size of each point based on openInterest. Differentiate calls and puts using color or markers st.write( "*Tips: in Price vs. Strike Scatter Plot with OI Sizing, Larger points indicate higher open interest, helping to identify popular strike prices.*" ) fig_bar7 = px.scatter( filtered_ticker_df_soon, x="strike", y="lastPrice", size="openInterest", color="Type", title=f"Price vs. Strike Scatter Plot for {selectedTicker}", labels={"x": "Strike Price", "y": "Option Price"}, color_discrete_map={"CALL": "green", "PUT": "red"}, ) fig_bar7.update_layout( xaxis_title="Strike Price", yaxis_title="Option Price", autosize=True, height=600, ) st.plotly_chart(fig_bar7, use_container_width=True) st.markdown("---") # Add a horizontal line as a visual separator # Price vs. Strike Scatter Plot with OI Sizing # Description: Scatter plot where each point represents an option contract, with the x-axis as strike price, y-axis as option price, and point size proportional to open interest. # Purpose: Visualizes the relationship between option pricing and open interest across strikes. # Implementation: Use strike for the x-axis and lastPrice for the y-axis. Set the size of each point based on openInterest. Differentiate calls and puts using color or markers st.write( "*Tips: in Price vs. Strike Scatter Plot with Days Left Sizing, Larger points indicate higher open interest, helping to identify popular strike prices.*" ) fig_bar7_2 = px.scatter( filtered_ticker_df, x="strike", y="lastPrice", size="daysleft", color="Type", title=f"Price vs. Strike Scatter Plot for {selectedTicker}", labels={"x": "Strike Price", "y": "Option Price"}, color_discrete_map={"CALL": "green", "PUT": "red"}, ) fig_bar7_2.update_layout( xaxis_title="Strike Price", yaxis_title="Option Price", autosize=True, height=600, ) st.plotly_chart(fig_bar7_2, use_container_width=True) st.markdown("---") # Add a horizontal line as a visual separator # Support and Resistance Levels on Price Chart # Description: Overlay horizontal lines on the underlying asset's price chart at strike prices with significant open interest. # Purpose: Directly correlates high OI strikes with potential support and resistance levels on the price chart. # Implementation: Identify top N strikes with highest call and put open interest. Plot the underlying asset's price chart. # Add horizontal lines at the identified strike prices, labeling them as support or resistance. st.write( "*Tips: in Support and Resistance Levels on Price Chart, High call OI at a particular strike may indicate a resistance level, while high put OI may suggest support!*" ) fig_bar8 = px.line( filtered_ticker_df_soon, x="strike", y="lastPrice", title=f"Support and Resistance Levels for {selectedTicker}", labels={"x": "Strike Price", "y": "Option Price"}, ) fig_bar8.update_layout( xaxis_title="Strike Price", yaxis_title="Option Price", autosize=True, height=600, ) st.plotly_chart(fig_bar8, use_container_width=True) st.sidebar.markdown("---") # Add a horizontal line as a visual separator st.sidebar.header("Advanced (Placeholder)") # **Daily Change in Open Interest** # Monitoring the increase or decrease in Open Interest compared to the previous day can indicate the inflow (rising OI) or outflow (declining OI) of capital. This factor helps assess the strength of the current trend. if st.sidebar.button("Daily Change in Open Interest ", use_container_width=True): st.write("Daily Change in Open Interest - Shows the change in open interest") st.info("Daily change in open interest analysis coming soon...") if st.sidebar.button("Gamma Squeeze", use_container_width=True): st.write( "Gamma Squeeze - Shows the potential for a rapid price movement in the underlying asset" ) st.info("Gamma squeeze analysis coming soon...") # Add buttons for each analysis type if st.sidebar.button("Vanna Exposure Analysis", use_container_width=True): st.write( "Vanna Exposure Analysis - Shows sensitivity of delta to volatility changes" ) # TODO: Implement Vanna exposure calculation and visualization # Would require calculating vanna values from the options data st.info("Vanna exposure analysis coming soon...") if st.sidebar.button("Charm Analysis", use_container_width=True): st.write("Charm Analysis - Shows rate of change of delta with respect to time") # TODO: Implement Charm calculation and visualization # Would require calculating charm values from the options data st.info("Charm analysis coming soon...") if st.sidebar.button("Gamma Exposure Analysis", use_container_width=True): st.write( "Gamma Exposure Analysis - Shows how delta changes with underlying price movement" ) # TODO: Implement gamma exposure calculation and visualization filtered_gamma = DF_options[DF_options["Ticker"] == selectedTicker] st.info("Gamma exposure analysis coming soon...") # contractSymbol # lastTradeDate # strike # lastPrice # bid # ask # change # percentChange # volume # openInterest # impliedVolatility # inTheMoney # contractSize # currency # Type # expirationDate # daysleft # mark # pricepercent # pricepercentstrike # interinsicvalue # interinsicvalue% # timevalue # timevalue% # breakevenprice # # Create sample data # np.random.seed(42) # data = pd.DataFrame({ # 'x': np.random.randn(100), # 'y': np.random.randn(100), # 'category': np.random.choice(['A', 'B', 'C'], 100) # }) # # Create two columns # col1, col2 = st.columns(2) # # First column - Scatter plot # with col1: # st.subheader("Scatter Plot") # fig = px.scatter(data, x='x', y='y', color='category') # st.plotly_chart(fig, use_container_width=True) # # Second column - Bar chart # with col2: # st.subheader("Bar Chart") # category_counts = data['category'].value_counts() # fig = px.bar(x=category_counts.index, y=category_counts.values) # st.plotly_chart(fig, use_container_width=True) # # Add a checkbox # if st.checkbox("Show raw data"): # st.dataframe(data) # # Add a download button # @st.cache_data # def convert_df(df): # return df.to_csv().encode('utf-8') # csv = convert_df(data) # st.download_button( # label="Download data as CSV", # data=csv, # file_name='sample_data.csv', # mime='text/csv', # )