import streamlit as st import requests import pandas as pd import plotly.express as px import plotly.io as pio import os # Update the default color sequence for the plotly_dark template to green. pio.templates["plotly_dark"].layout.colorway = ["#00cc44"] # Global API key and constant limit (backend only) API_KEY = os.getenv("FMP_API_KEY") BASE_URL = "https://financialmodelingprep.com/api/v3/stock-screener" LIMIT = 1000 # Set wide layout and page title st.set_page_config(layout="wide", page_title="Stock Screener") # App explanation in main area st.title("Stock Screener") st.write( "A research tool for filtering stocks by price, volume, market cap, sector, and other fundamentals. " "Useful for identifying sets of securities based on specific criteria." ) st.info( "Use the sidebar to set filters. Click **Run Analysis** to retrieve results and display charts. " "All filters are optional. If no results appear, try loosening the constraints." ) # Sidebar inputs st.sidebar.header("Filters") # Numeric Filters with st.sidebar.expander("Numeric Filters", expanded=True): market_cap_min = st.number_input( "Market Cap More Than", value=1000000000, help="Minimum market capitalization." ) market_cap_max = st.number_input( "Market Cap Lower Than", value=50000000000, help="Maximum market capitalization." ) price_min = st.number_input( "Price More Than", value=10.0, help="Minimum stock price." ) price_max = st.number_input( "Price Lower Than", value=500.0, help="Maximum stock price." ) beta_min = st.number_input( "Beta More Than", value=0.5, help="Minimum beta value." ) beta_max = st.number_input( "Beta Lower Than", value=2.0, help="Maximum beta value." ) volume_min = st.number_input( "Volume More Than", value=10000, help="Minimum trading volume." ) volume_max = st.number_input( "Volume Lower Than", value=1000000, help="Maximum trading volume." ) dividend_min = st.number_input( "Dividend More Than", value=0.0, help="Minimum dividend value." ) dividend_max = st.number_input( "Dividend Lower Than", value=5.0, help="Maximum dividend value." ) # Categorical Filters with st.sidebar.expander("Categorical Filters", expanded=True): sector_options = [ "Technology", "Financial Services", "Consumer Cyclical", "Energy", "Industrials", "Basic Materials", "Communication Services", "Consumer Defensive", "Healthcare", "Real Estate", "Utilities", "Industrial Goods", "Financial", "Services", "Conglomerates" ] selected_sectors = st.multiselect( "Sector", options=sector_options, help="Select one or more sectors." ) industry_options = [ "Autos", "Banks", "Banks Diversified", "Software", "Banks Regional", "Beverages Alcoholic", "Beverages Brewers", "Beverages Non-Alcoholic" ] selected_industries = st.multiselect( "Industry", options=industry_options, help="Select one or more industries." ) country_options = ["US", "UK", "MX", "BR", "RU", "HK", "CA"] selected_countries = st.multiselect( "Country", options=country_options, help="Select one or more countries." ) exchange_options = ["nyse", "nasdaq", "amex", "euronext", "tsx", "etf", "mutual_fund"] selected_exchanges = st.multiselect( "Exchange", options=exchange_options, help="Select one or more exchanges." ) # Boolean Filters with st.sidebar.expander("Boolean Filters", expanded=True): is_etf = st.checkbox( "Is ETF", value=False, help="Check to return only ETFs." ) is_fund = st.checkbox( "Is Fund", value=False, help="Check to return only funds." ) is_actively_trading = st.checkbox( "Is Actively Trading", value=True, help="Check to return only actively traded stocks." ) # Run Analysis button (placed outside expanders) run_analysis = st.sidebar.button("Run Analysis") def get_stock_data(params): # Copy parameters and add API key. filters = params.copy() filters["apikey"] = API_KEY # Convert list values to comma-separated strings. for key, value in filters.items(): if isinstance(value, list) and value: filters[key] = ",".join(map(str, value)) try: response = requests.get(BASE_URL, params=filters, timeout=10) response.raise_for_status() data = response.json() if not data: st.error("No results found for the provided filters.") return pd.DataFrame() return pd.DataFrame(data) except Exception: st.error("An error occurred while fetching data.") return pd.DataFrame() if run_analysis: # Build parameter dictionary. params = {} params["marketCapMoreThan"] = market_cap_min params["marketCapLowerThan"] = market_cap_max params["priceMoreThan"] = price_min params["priceLowerThan"] = price_max params["betaMoreThan"] = beta_min params["betaLowerThan"] = beta_max params["volumeMoreThan"] = volume_min params["volumeLowerThan"] = volume_max params["dividendMoreThan"] = dividend_min params["dividendLowerThan"] = dividend_max if selected_sectors: params["sector"] = selected_sectors if selected_industries: params["industry"] = selected_industries if selected_countries: params["country"] = selected_countries if selected_exchanges: params["exchange"] = selected_exchanges params["isEtf"] = is_etf params["isFund"] = is_fund params["isActivelyTrading"] = is_actively_trading # Set limit in the backend. params["limit"] = LIMIT with st.spinner("Fetching stock data..."): df = get_stock_data(params) if not df.empty: st.success("Data fetched successfully!") #with st.expander("Results", expanded=True): with st.container(border=True): # Display the data table. with st.container(border=True): st.dataframe(df) # Common hover data for scatter plots. hover_fields = ["symbol", "companyName", "sector", "industry", "exchangeShortName", "country"] # Chart 1: Price vs Market Cap (Scatter) with st.container(border=True): try: fig1 = px.scatter( df, x="price", y="marketCap", size="volume", title="Price vs Market Cap", hover_data=hover_fields, template="plotly_dark" ) st.plotly_chart(fig1, use_container_width=True) except Exception: st.warning("Scatter plot could not be generated.") # Chart 2: Sector Distribution (Bar Chart) with st.container(border=True): try: sector_counts = df["sector"].value_counts().reset_index() sector_counts.columns = ["Sector", "Count"] fig2 = px.bar( sector_counts, x="Sector", y="Count", title="Sector Distribution", template="plotly_dark" ) st.plotly_chart(fig2, use_container_width=True) except Exception: st.warning("Sector distribution chart could not be generated.") # Chart 3: Price Distribution (Histogram) with st.container(border=True): try: fig3 = px.histogram( df, x="price", nbins=30, title="Price Distribution", template="plotly_dark" ) st.plotly_chart(fig3, use_container_width=True) except Exception: st.warning("Price distribution chart could not be generated.") # Chart 4: Market Cap vs Volume (Scatter) with st.container(border=True): try: fig4 = px.scatter( df, x="volume", y="marketCap", size="price", title="Market Cap vs Volume", hover_data=hover_fields, template="plotly_dark" ) st.plotly_chart(fig4, use_container_width=True) except Exception: st.warning("Market Cap vs Volume chart could not be generated.") # Chart 5: Country Breakdown (Bar Chart) with st.container(border=True): try: country_counts = df["country"].value_counts().reset_index() country_counts.columns = ["Country", "Count"] fig5 = px.bar( country_counts, x="Country", y="Count", title="Country Breakdown", template="plotly_dark", #color_discrete_sequence=px.colors.qualitative.Plotly # override global green ) st.plotly_chart(fig5, use_container_width=True) except Exception: st.warning("Country breakdown chart could not be generated.") # Chart 6: Dividend vs Price (Scatter) with st.container(border=True): try: if "lastAnnualDividend" in df.columns: fig6 = px.scatter( df, x="price", y="lastAnnualDividend", title="Dividend vs Price", hover_data=hover_fields, template="plotly_dark" ) st.plotly_chart(fig6, use_container_width=True) else: st.info("Dividend data is not available.") except Exception: st.warning("Dividend vs Price chart could not be generated.") # Chart 7: Beta Distribution (Histogram) with st.container(border=True): try: fig7 = px.histogram( df, x="beta", nbins=30, title="Beta Distribution", template="plotly_dark" ) st.plotly_chart(fig7, use_container_width=True) except Exception: st.warning("Beta distribution chart could not be generated.") # Chart 8: Exchange Breakdown (Bar Chart) with st.container(border=True): try: if "exchangeShortName" in df.columns: exchange_counts = df["exchangeShortName"].value_counts().reset_index() exchange_counts.columns = ["Exchange", "Count"] fig8 = px.bar( exchange_counts, x="Exchange", y="Count", title="Exchange Breakdown", template="plotly_dark" ) st.plotly_chart(fig8, use_container_width=True) else: st.info("Exchange data is not available.") except Exception: st.warning("Exchange breakdown chart could not be generated.") # Hide default Streamlit style st.markdown( """ """, unsafe_allow_html=True )