# Copyright (2025) # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # http://www.apache.org/licenses/LICENSE-2.0 # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. import spaces import streamlit as st import requests import pandas as pd import plotly.express as px import plotly.graph_objects as go from datetime import datetime import os # Set page configuration st.set_page_config(page_title="Institutional Investor Portfolios", layout="wide") # Global API key API_KEY = os.getenv("FMP_API_KEY") # Initialize Session State for Portfolio Allocation if 'portfolio_allocation_data' not in st.session_state: st.session_state.portfolio_allocation_data = None if 'portfolio_allocation_params' not in st.session_state: st.session_state.portfolio_allocation_params = {} # Initialize Session State for Investor Performance if 'investor_performance_data' not in st.session_state: st.session_state.investor_performance_data = None if 'investor_performance_params' not in st.session_state: st.session_state.investor_performance_params = {} # Initialize Session State for Symbol Ownership if 'symbol_ownership_data' not in st.session_state: st.session_state.symbol_ownership_data = None if 'symbol_ownership_params' not in st.session_state: st.session_state.symbol_ownership_params = {} # Function for Page 1: CIK List def page1(): st.title("CIK and Name Combinations") st.markdown(""" Fetch and display the list of Central Index Key (CIK). CIK is a unique identifier assigned by the SEC to entities that file disclosures. """) with st.spinner("Fetching CIK and Name combinations..."): cik_name_df = fetch_cik_name_combinations() if not cik_name_df.empty: st.success("Data retrieved successfully!") st.dataframe(cik_name_df, use_container_width=True, height=800) # Provide download option csv = cik_name_df.to_csv(index=False).encode('utf-8') st.download_button( label="Download Data as CSV", data=csv, file_name='cik_name_combinations.csv', mime='text/csv', ) else: st.error("No data retrieved.") # Function to fetch CIK and name combinations @st.cache_data def fetch_cik_name_combinations(): """ Fetches a list of CIK and name combinations from the API. Returns: pd.DataFrame: A DataFrame containing CIK and name pairs. """ url = f"https://financialmodelingprep.com/api/v4/institutional-ownership/list?apikey={API_KEY}" try: response = requests.get(url) if response.status_code == 200: data = response.json() df = pd.DataFrame(data) if not df.empty and 'cik' in df.columns and 'name' in df.columns: df = df[['cik', 'name']] return df else: return pd.DataFrame() else: st.error(f"Error fetching data: {response.status_code}, {response.text}") return pd.DataFrame() except Exception as e: st.error(f"An exception occurred: {e}") return pd.DataFrame() # Function for Page 2: Portfolio Allocation def page2(): st.title("Institutional Portfolio Allocation") st.markdown(""" Fetch and visualize portfolio allocation data over time for a specific CIK. Enter the CIK and specify a start date to analyze the portfolio's composition. """) # Sidebar Inputs within an Expander with st.sidebar.expander("Inputs", expanded=True): cik_input = st.text_input("Enter CIK", value="0001067983", help="Enter the investor's CIK, which is a unique SEC identifier (e.g., 0001067983).") start_date = st.date_input("Select Start Date", value=datetime(2021, 9, 30), help="Choose the earliest quarter-end date for which to retrieve portfolio allocation data. Data will be fetched from this date onward.") top_n = st.number_input("Top N Groups", min_value=1, max_value=100, value=10, step=1, help="Set the number of top groups (by cumulative weight) to display in the trend charts.") top_n_changes = st.number_input("Top N Changes", min_value=1, max_value=100, value=10, step=1, help="Set the number of top positive and negative changes to display in the changes chart (i.e. to determine the biggest allocation shifts).") run_button = st.sidebar.button("Run") if run_button or (st.session_state.portfolio_allocation_params.get('cik') == cik_input and st.session_state.portfolio_allocation_params.get('start_date') == start_date.strftime("%Y-%m-%d") and st.session_state.portfolio_allocation_params.get('top_n') == top_n): # Update Session State with current parameters st.session_state.portfolio_allocation_params = { 'cik': cik_input, 'start_date': start_date.strftime("%Y-%m-%d"), 'top_n': top_n } with st.spinner("Fetching and processing portfolio allocation data..."): allocation_data = fetch_data_over_time(cik_input, start_date.strftime("%Y-%m-%d")) if not allocation_data.empty: st.session_state.portfolio_allocation_data = allocation_data st.success("Data retrieved successfully!") # Latest date data for bar charts latest_date = allocation_data["date"].max() latest_data = allocation_data[allocation_data["date"] == latest_date] # Plot bar charts st.subheader(f"Portfolio Allocation by Ticker on {latest_date}") fig1 = plot_bar_chart(latest_data, "symbol", "Weight (%)", "Market Value", height=500) st.plotly_chart(fig1, use_container_width=True) st.subheader(f"Portfolio Allocation by Industry on {latest_date}") fig2 = plot_bar_chart(latest_data, "industryTitle", "Weight (%)", "Market Value", height=700) st.plotly_chart(fig2, use_container_width=True) # Time-series plots for trends st.subheader("Portfolio Allocation Trends by Symbol") st.markdown(""" **Explanation:** This chart shows the trends in portfolio allocation by individual symbols over time. The top N symbols by weight are displayed to highlight the most significant contributors to the portfolio. """) fig3 = plot_allocation_trends(allocation_data, "symbol", st.session_state.portfolio_allocation_params.get('top_n')) st.plotly_chart(fig3, use_container_width=True, height=600) st.subheader("Portfolio Allocation Trends by Industry") st.markdown(""" **Explanation:** This chart illustrates the trends in portfolio allocation across different industries over time. The top N industries by weight are displayed to emphasize the major sectors in the portfolio. """) fig4 = plot_allocation_trends(allocation_data, "industryTitle", st.session_state.portfolio_allocation_params.get('top_n')) st.plotly_chart(fig4, use_container_width=True, height=600) st.subheader("Top Changes in Portfolio Allocation") fig_top_changes = plot_top_changes_plotly(allocation_data, n=top_n_changes) st.plotly_chart(fig_top_changes, use_container_width=True) st.subheader("Total Portfolio Value by Date") grouped_df = allocation_data.groupby("date").agg( total_market_value=("marketValue", "sum"), total_weight=("weight", "sum") ).reset_index() # Round values to the nearest whole number grouped_df["total_market_value"] = grouped_df["total_market_value"].round(0).astype(int) grouped_df["total_weight"] = grouped_df["total_weight"].round(0).astype(int) # Create a formatted market value column using your formatting function grouped_df["market_value"] = grouped_df["total_market_value"].apply(format_market_value) # Select and rename columns for clarity grouped_df = grouped_df[["date", "market_value", "total_weight"]] grouped_df.columns = ["Date", "Total Market Value", "Total Weight"] st.dataframe(grouped_df, use_container_width=True) # Transpose data at the bottom st.markdown("---") st.subheader("Industry Allocation Overview by Date") st.markdown(""" **Explanation:** This table presents the portfolio allocation weights and market values for each ticker across different dates. It provides a detailed view of how each stock's weight and market value have evolved over time. """) transposed_ticker_data = transpose_data(allocation_data, "symbol") st.dataframe(transposed_ticker_data, use_container_width=True) st.subheader("Transposed Industry Data") st.markdown(""" **Explanation:** This table displays the portfolio allocation weights and market values for each industry across different dates. It offers insights into the sector-wise distribution and changes in the portfolio. """) transposed_industry_data = transpose_data(allocation_data, "industryTitle") st.dataframe(transposed_industry_data, use_container_width=True) else: st.error("No data found for the specified CIK and date range.") # If data exists in session state and parameters match, display it without rerunning elif st.session_state.portfolio_allocation_data and st.session_state.portfolio_allocation_params.get('cik') == cik_input and \ st.session_state.portfolio_allocation_params.get('start_date') == start_date.strftime("%Y-%m-%d") and \ st.session_state.portfolio_allocation_params.get('top_n') == top_n: allocation_data = st.session_state.portfolio_allocation_data st.success("Displaying previously retrieved data.") # Latest date data for bar charts latest_date = allocation_data["date"].max() latest_data = allocation_data[allocation_data["date"] == latest_date] # Plot bar charts st.subheader(f"Portfolio Allocation by Ticker on {latest_date}") fig1 = plot_bar_chart(latest_data, "symbol", "Weight (%)", "Market Value", height=500) st.plotly_chart(fig1, use_container_width=True) st.subheader(f"Portfolio Allocation by Industry on {latest_date}") fig2 = plot_bar_chart(latest_data, "industryTitle", "Weight (%)", "Market Value", height=700) st.plotly_chart(fig2, use_container_width=True) # Time-series plots for trends st.subheader("Portfolio Allocation Trends by Symbol") st.markdown(""" This chart shows the trends in portfolio allocation by individual symbols over time. The top N symbols by weight are displayed to highlight the most significant contributors to the portfolio. """) fig3 = plot_allocation_trends(allocation_data, "symbol", top_n) st.plotly_chart(fig3, use_container_width=True, height=600) st.subheader("Portfolio Allocation Trends by Industry") st.markdown(""" This chart illustrates the trends in portfolio allocation across different industries over time. The top N industries by weight are displayed to emphasize the major sectors in the portfolio. """) fig4 = plot_allocation_trends(allocation_data, "industryTitle", top_n) st.plotly_chart(fig4, use_container_width=True, height=600) # Transpose data at the bottom st.markdown("---") st.subheader("Transposed Ticker Data") st.markdown(""" This table presents the portfolio allocation weights and market values for each ticker across different dates. It provides a detailed view of how each stock's weight and market value have evolved over time. """) transposed_ticker_data = transpose_data(allocation_data, "symbol") st.dataframe(transposed_ticker_data, use_container_width=True) st.subheader("Transposed Industry Data") st.markdown(""" This table displays the portfolio allocation weights and market values for each industry across different dates. It offers insights into the sector-wise distribution and changes in the portfolio. """) transposed_industry_data = transpose_data(allocation_data, "industryTitle") st.dataframe(transposed_industry_data, use_container_width=True) def plot_top_changes_plotly(df, n=10): # Get unique dates in descending order unique_dates = sorted(df["date"].unique(), reverse=True) if len(unique_dates) < 2: return go.Figure() # Not enough data latest_date = unique_dates[0] prev_date = unique_dates[1] # Filter data for the two most recent dates df_latest = df[df["date"] == latest_date] df_prev = df[df["date"] == prev_date] # Aggregate data by symbol for each date df_latest_agg = df_latest.groupby("symbol", as_index=False).agg({ "weight": "sum", "marketValue": "sum" }) df_prev_agg = df_prev.groupby("symbol", as_index=False).agg({ "weight": "sum", "marketValue": "sum" }) # Merge data on symbol to include new additions or removals merged = pd.merge(df_prev_agg, df_latest_agg, on="symbol", suffixes=("_prev", "_latest"), how="outer") merged.fillna(0, inplace=True) # Compute changes in weight and market value merged['delta_weight'] = merged['weight_latest'] - merged['weight_prev'] merged['delta_marketValue'] = merged['marketValue_latest'] - merged['marketValue_prev'] # Select top n positive changes and top n negative changes top_up = merged[merged['delta_weight'] > 0].sort_values( by='delta_weight', ascending=False).head(n) top_down = merged[merged['delta_weight'] < 0].sort_values( by='delta_weight', ascending=True).head(n) combined = pd.concat([top_up, top_down]) combined['color'] = combined['delta_weight'].apply(lambda x: 'green' if x >= 0 else 'red') combined['text'] = combined.apply( lambda row: f"{row['delta_weight']:.1f}% ({format_market_value(row['delta_marketValue'])})", axis=1) fig = go.Figure(data=[go.Bar( x=combined['symbol'], y=combined['delta_weight'], text=combined['text'], textposition='auto', marker_color=combined['color'] )]) fig.update_layout( title=f"Top Up and Down Changes: {prev_date} to {latest_date}", xaxis_title="Symbol", yaxis_title="Change in Weight (%)", template="plotly_white", height=500 ) return fig # Functions used in Page 2 @st.cache_data def fetch_dates(cik): """ Fetches all available quarter-end dates for a specific CIK. Args: cik (str): Central Index Key of the institutional investor. Returns: list: A list of available quarter-end dates in descending order. """ endpoint = f"https://financialmodelingprep.com/api/v4/institutional-ownership/portfolio-date" params = {"cik": cik, "apikey": API_KEY} try: response = requests.get(endpoint, params=params) if response.status_code == 200: dates = sorted([item["date"] for item in response.json()], reverse=True) return dates else: st.error(f"Error fetching dates: {response.status_code}, {response.text}") return [] except Exception as e: st.error(f"An exception occurred: {e}") return [] @st.cache_data def fetch_portfolio_allocation(cik, date, page=0): """ Fetches portfolio allocation for a specific CIK and date from the API. Args: cik (str): Central Index Key of the institutional investor. date (str): Quarter-end date in YYYY-MM-DD format. page (int): Page number for large datasets (default is 0). Returns: pd.DataFrame: Processed DataFrame containing portfolio allocation. """ endpoint = f"https://financialmodelingprep.com/api/v4/institutional-ownership/portfolio-holdings" params = { "cik": cik, "date": date, "page": page, "apikey": API_KEY } try: response = requests.get(endpoint, params=params) if response.status_code == 200: data = response.json() df = pd.DataFrame(data) if not df.empty and all(col in df.columns for col in ["symbol", "industryTitle", "weight", "marketValue"]): df = df[["symbol", "industryTitle", "weight", "marketValue"]] df["weight"] = df["weight"].astype(float) df["marketValue"] = df["marketValue"].astype(float) df["date"] = date return df else: return pd.DataFrame() else: st.error(f"Error fetching allocation: {response.status_code}, {response.text}") return pd.DataFrame() except Exception as e: st.error(f"An exception occurred: {e}") return pd.DataFrame() def fetch_data_over_time(cik, start_date): available_dates = fetch_dates(cik) selected_dates = [date for date in available_dates if date >= start_date] all_data = [] for date in selected_dates: page = 0 while True: df = fetch_portfolio_allocation(cik, date, page=page) if df.empty: break all_data.append(df) page += 1 if all_data: return pd.concat(all_data, ignore_index=True) else: st.error("No data found for the specified time range.") return pd.DataFrame() def transpose_data(df, group_by_column): """ Transposes the DataFrame so that dates become columns for weight and market value. Args: df (pd.DataFrame): Original DataFrame containing portfolio allocation over time. group_by_column (str): Column to group data by (e.g., "symbol" or "industryTitle"). Returns: pd.DataFrame: Transposed DataFrame. """ pivoted_weight = df.pivot_table(values="weight", index=group_by_column, columns="date", aggfunc="sum", fill_value=0) pivoted_market_value = df.pivot_table(values="marketValue", index=group_by_column, columns="date", aggfunc="sum", fill_value=0) # Reorder columns: weight -> market value -> next weight -> next market value combined = pd.concat([pivoted_weight, pivoted_market_value], axis=1, keys=["Weight", "Market Value"]) combined = combined.swaplevel(axis=1).sort_index(axis=1) return combined def plot_bar_chart(df, group_by_column, weight_title, market_value_title, height=500): """ Plots a bar chart for portfolio allocation using Plotly. Args: df (pd.DataFrame): DataFrame containing portfolio allocation data. group_by_column (str): Column to group data by (e.g., "symbol" or "industryTitle"). weight_title (str): Title for the weight axis. market_value_title (str): Title for the market value. height (int): Height of the plot in pixels. Returns: plotly.graph_objects.Figure: Plotly figure object. """ # Ensure data only corresponds to the latest date and aggregate by the group column latest_date = df["date"].max() filtered_df = df[df["date"] == latest_date].groupby(group_by_column, as_index=False).sum() # Sort the data by weight in descending order filtered_df = filtered_df.sort_values(by="weight", ascending=False) # Format market values for display filtered_df["marketValueFormatted"] = filtered_df["marketValue"].apply(format_market_value) # Create bar chart fig = go.Figure() fig.add_trace(go.Bar( x=filtered_df[group_by_column], y=filtered_df["weight"], text=[f"{w:.1f}%
{mv}" for w, mv in zip(filtered_df["weight"], filtered_df["marketValueFormatted"])], textposition="outside", texttemplate="%{text}", marker=dict(color="teal"), textfont=dict(size=14), # Makes the labels larger cliponaxis=False # Ensures labels aren't clipped )) fig.update_layout( title=f"Portfolio Allocation by {group_by_column.capitalize()} on {latest_date}", xaxis_title=group_by_column.capitalize(), yaxis_title=weight_title, xaxis_tickangle=45, # Make labels vertical showlegend=False, height=height ) fig.update_traces( textfont_size=12, # Increase font size cliponaxis=False # Ensure text doesn't get clipped ) return fig def plot_allocation_trends(df, group_by_column, top_n=10): """ Plots trends for portfolio allocation over time using Plotly. Args: df (pd.DataFrame): DataFrame containing portfolio allocation data over time. group_by_column (str): Column to group data by (e.g., "symbol" or "industryTitle"). top_n (int): Number of top contributors to show in the chart. Returns: plotly.express.Figure: Plotly figure object. """ # Group and aggregate data by group_by_column and date aggregated_df = df.groupby([group_by_column, "date"], as_index=False).sum() # Summarize total weight for sorting total_weight = aggregated_df.groupby(group_by_column)["weight"].sum().sort_values(ascending=False) top_groups = total_weight.head(top_n).index # Filter data to include only the top_n groups filtered_df = aggregated_df[aggregated_df[group_by_column].isin(top_groups)] filtered_df["marketValueFormatted"] = filtered_df["marketValue"].apply(format_market_value) # Create the Plotly line chart fig = px.line( filtered_df, x="date", y="weight", color=group_by_column, title=f"Portfolio Allocation Trends by {group_by_column.capitalize()}", labels={"weight": "Weight (%)", "date": "Date"}, markers=True, hover_data={ group_by_column: True, "weight": ":.2f", # Weight with 2 decimals "marketValueFormatted": True # Show market value in hover } ) fig.update_layout( legend_title=group_by_column.capitalize(), xaxis_title="Date", yaxis_title="Weight (%)", hovermode="closest", height=600 ) return fig def format_market_value(value): sign = "-" if value < 0 else "" abs_value = abs(value) if abs_value >= 1e9: return f"{sign}{abs_value/1e9:.1f}B" elif abs_value >= 1e6: return f"{sign}{abs_value/1e6:.1f}M" elif abs_value >= 1e3: return f"{sign}{abs_value/1e3:.1f}K" else: return f"{sign}{abs_value:.1f}" # Function for Page 3: Investor Performance def page3(): st.title("Investor Performance") st.markdown(""" Fetch and visualize various performance metrics for a specific institutional investor identified by their CIK. Analyze portfolio value, market value changes, performance relative to benchmarks, turnover metrics, and more. """) # Sidebar Inputs within an Expander with st.sidebar.expander("Investor Performance Inputs", expanded=True): cik_input = st.text_input("Enter CIK", value="0001067983", help="Enter the investor's CIK (e.g., 0001067983) to retrieve performance metrics.") run_button = st.sidebar.button("Run") if run_button or (st.session_state.investor_performance_params.get('cik') == cik_input): # Update Session State with current parameters st.session_state.investor_performance_params = { 'cik': cik_input } with st.spinner("Fetching and processing investor performance data..."): data = fetch_investor_performance(cik_input) if not data.empty: st.session_state.investor_performance_data = data st.success("Data retrieved successfully!") st.dataframe(data, use_container_width=True) # Ensure the data is sorted by date data["date"] = pd.to_datetime(data["date"]) data = data.sort_values(by="date") # Plotting st.subheader("Portfolio Value and Change in Market Value (%)") st.markdown(""" **Explanation:** This chart displays the portfolio's total market value over time alongside the percentage change in market value. It helps in understanding the growth or decline of the portfolio's value. """) fig1 = plot_portfolio_value_and_change(data) st.plotly_chart(fig1, use_container_width=True) st.subheader("Performance and Relative Performance to S&P 500") st.markdown(""" **Explanation:** This chart compares the portfolio's performance against the S&P 500 benchmark. It shows how well the portfolio is performing relative to the broader market. """) fig2 = plot_performance_and_relative(data) st.plotly_chart(fig2, use_container_width=True) st.subheader("Portfolio Turnover Metrics") st.markdown(""" **Explanation:** This chart illustrates the portfolio's turnover rate, including buy and sell activities. Turnover metrics provide insight into the trading frequency and strategy. """) fig3 = plot_turnover_metrics(data) st.plotly_chart(fig3, use_container_width=True) st.subheader("Cumulative Performance Over Time") st.markdown(""" **Explanation:** This chart shows the cumulative performance of the portfolio over different time horizons (1-year, 3-year, 5-year, and since inception). It highlights long-term growth trends. """) fig4 = plot_cumulative_performance(data) st.plotly_chart(fig4, use_container_width=True) st.subheader("Holding Periods") st.markdown(""" **Explanation:** This chart depicts the average holding periods for the portfolio and its top holdings. Holding periods indicate the investment duration and strategy stability. """) fig5 = plot_holding_periods(data) st.plotly_chart(fig5, use_container_width=True) st.subheader("Portfolio Activity") st.markdown(""" **Explanation:** This chart displays portfolio size changes and the number of securities added or removed. It provides an overview of portfolio expansion or contraction. """) fig6 = plot_portfolio_activity(data) st.plotly_chart(fig6, use_container_width=True) st.subheader("Market Value and Securities Count") st.markdown(""" **Explanation:** This chart compares the portfolio's total market value against the number of securities held. It helps in understanding the diversification and valuation aspects of the portfolio. """) fig7 = plot_market_value_and_securities(data) st.plotly_chart(fig7, use_container_width=True) else: st.error("No data found for the specified CIK.") # If data exists in session state and parameters match, display it without rerunning elif st.session_state.investor_performance_data and st.session_state.investor_performance_params.get('cik') == cik_input: data = st.session_state.investor_performance_data st.success("Displaying previously retrieved data.") st.dataframe(data, use_container_width=True) # Ensure the data is sorted by date data["date"] = pd.to_datetime(data["date"]) data = data.sort_values(by="date") # Plotting st.subheader("Portfolio Value and Change in Market Value (%)") st.markdown(""" This chart displays the portfolio's total market value over time alongside the percentage change in market value. It helps in understanding the growth or decline of the portfolio's value. """) fig1 = plot_portfolio_value_and_change(data) st.plotly_chart(fig1, use_container_width=True) st.subheader("Performance and Relative Performance to S&P 500") st.markdown(""" This chart compares the portfolio's performance against the S&P 500 benchmark. It shows how well the portfolio is performing relative to the broader market. """) fig2 = plot_performance_and_relative(data) st.plotly_chart(fig2, use_container_width=True) st.subheader("Portfolio Turnover Metrics") st.markdown(""" This chart illustrates the portfolio's turnover rate, including buy and sell activities. Turnover metrics provide insight into the trading frequency and strategy. """) fig3 = plot_turnover_metrics(data) st.plotly_chart(fig3, use_container_width=True) st.subheader("Cumulative Performance Over Time") st.markdown(""" This chart shows the cumulative performance of the portfolio over different time horizons (1-year, 3-year, 5-year, and since inception). It highlights long-term growth trends. """) fig4 = plot_cumulative_performance(data) st.plotly_chart(fig4, use_container_width=True) st.subheader("Holding Periods") st.markdown(""" This chart depicts the average holding periods for the portfolio and its top holdings. Holding periods indicate the investment duration and strategy stability. """) fig5 = plot_holding_periods(data) st.plotly_chart(fig5, use_container_width=True) st.subheader("Portfolio Activity") st.markdown(""" This chart displays portfolio size changes and the number of securities added or removed. It provides an overview of portfolio expansion or contraction. """) fig6 = plot_portfolio_activity(data) st.plotly_chart(fig6, use_container_width=True) st.subheader("Market Value and Securities Count") st.markdown(""" This chart compares the portfolio's total market value against the number of securities held. It helps in understanding the diversification and valuation aspects of the portfolio. """) fig7 = plot_market_value_and_securities(data) st.plotly_chart(fig7, use_container_width=True) # Functions used in Page 3 @st.cache_data def fetch_investor_performance(cik): """ Fetches investor performance data from FMP API. Args: cik (str): Central Index Key of the institutional investor. Returns: pd.DataFrame: DataFrame containing investor performance data. """ url = f"https://financialmodelingprep.com/api/v4/institutional-ownership/portfolio-holdings-summary" params = { "cik": cik, "page": 0, "apikey": API_KEY } try: response = requests.get(url, params=params) if response.status_code == 200: data = response.json() df = pd.DataFrame(data) return df else: st.error(f"Failed to fetch data: {response.status_code}") return pd.DataFrame() except Exception as e: st.error(f"An exception occurred: {e}") return pd.DataFrame() def plot_portfolio_value_and_change(df): """ Plots portfolio value and change in market value percentage. Args: df (pd.DataFrame): DataFrame containing investor performance data. Returns: plotly.graph_objects.Figure: Plotly figure object. """ fig = go.Figure() # Portfolio Value Line fig.add_trace(go.Scatter( x=df["date"], y=df["marketValue"], mode='lines+markers', name="Portfolio Value", yaxis="y1" )) # Change in Market Value Percentage Line fig.add_trace(go.Scatter( x=df["date"], y=df["changeInMarketValuePercentage"], mode='lines+markers', name="Change in Market Value (%)", yaxis="y2" )) fig.update_layout( title="Portfolio Value and Change in Market Value (%)", xaxis_title="Date", yaxis=dict(title="Market Value ($)", tickformat="$,.0f"), yaxis2=dict(title="Change in Market Value (%)", overlaying="y", side="right", tickformat=".2f"), legend=dict(title="Metrics"), height=500 ) return fig def plot_performance_and_relative(df): """ Plots performance and relative performance to S&P 500. Args: df (pd.DataFrame): DataFrame containing investor performance data. Returns: plotly.graph_objects.Figure: Plotly figure object. """ fig = go.Figure() # Performance Percentage Line fig.add_trace(go.Scatter( x=df["date"], y=df["performancePercentage"], mode='lines+markers', name="Performance (%)", yaxis="y1" )) # Relative Performance Percentage Line fig.add_trace(go.Scatter( x=df["date"], y=df["performanceRelativeToSP500Percentage"], mode='lines+markers', name="Relative Performance to S&P 500 (%)", yaxis="y2" )) fig.update_layout( title="Performance and Relative Performance to S&P 500", xaxis_title="Date", yaxis=dict(title="Performance (%)", tickformat=".2f"), yaxis2=dict(title="Relative Performance (%)", overlaying="y", side="right", tickformat=".2f"), legend=dict(title="Metrics"), height=500 ) return fig def plot_turnover_metrics(df): """ Plots portfolio turnover metrics. Args: df (pd.DataFrame): DataFrame containing investor performance data. Returns: plotly.graph_objects.Figure: Plotly figure object. """ fig = go.Figure() # Turnover Line fig.add_trace(go.Scatter( x=df["date"], y=df["turnover"], mode='lines+markers', name="Turnover (%)", yaxis="y1" )) # Alternate Turnover Metrics fig.add_trace(go.Scatter( x=df["date"], y=df["turnoverAlternateSell"], mode='lines+markers', name="Turnover (Sell)", yaxis="y2" )) fig.add_trace(go.Scatter( x=df["date"], y=df["turnoverAlternateBuy"], mode='lines+markers', name="Turnover (Buy)", yaxis="y2" )) fig.update_layout( title="Portfolio Turnover Metrics", xaxis_title="Date", yaxis=dict(title="Turnover (%)", tickformat=".2f"), yaxis2=dict(title="Turnover (Buy/Sell)", overlaying="y", side="right", tickformat=".2f"), legend=dict(title="Metrics"), height=500 ) return fig def plot_cumulative_performance(df): """ Plots cumulative performance over time. Args: df (pd.DataFrame): DataFrame containing investor performance data. Returns: plotly.graph_objects.Figure: Plotly figure object. """ fig = go.Figure() # Cumulative Performance Percentages fig.add_trace(go.Scatter( x=df["date"], y=df["performancePercentage1year"], mode='lines+markers', name="1-Year Performance (%)", yaxis="y1" )) fig.add_trace(go.Scatter( x=df["date"], y=df["performancePercentage3year"], mode='lines+markers', name="3-Year Performance (%)", yaxis="y1" )) fig.add_trace(go.Scatter( x=df["date"], y=df["performancePercentage5year"], mode='lines+markers', name="5-Year Performance (%)", yaxis="y1" )) fig.add_trace(go.Scatter( x=df["date"], y=df["performanceSinceInceptionPercentage"], mode='lines+markers', name="Since Inception (%)", yaxis="y1" )) fig.update_layout( title="Cumulative Performance Over Time", xaxis_title="Date", yaxis=dict(title="Performance (%)", tickformat=".2f"), legend=dict(title="Metrics"), height=500 ) return fig def plot_holding_periods(df): """ Plots holding periods. Args: df (pd.DataFrame): DataFrame containing investor performance data. Returns: plotly.graph_objects.Figure: Plotly figure object. """ fig = go.Figure() # Overall Holding Period fig.add_trace(go.Scatter( x=df["date"], y=df["averageHoldingPeriod"], mode='lines+markers', name="Average Holding Period", yaxis="y1" )) # Top Holdings Periods fig.add_trace(go.Scatter( x=df["date"], y=df["averageHoldingPeriodTop10"], mode='lines+markers', name="Top 10 Holding Period", yaxis="y1" )) fig.add_trace(go.Scatter( x=df["date"], y=df["averageHoldingPeriodTop20"], mode='lines+markers', name="Top 20 Holding Period", yaxis="y1" )) fig.update_layout( title="Holding Periods", xaxis_title="Date", yaxis=dict(title="Holding Period (Quarters)"), legend=dict(title="Metrics"), height=500 ) return fig def plot_portfolio_activity(df): """ Plots portfolio activity. Args: df (pd.DataFrame): DataFrame containing investor performance data. Returns: plotly.graph_objects.Figure: Plotly figure object. """ fig = go.Figure() # Portfolio Size fig.add_trace(go.Scatter( x=df["date"], y=df["portfolioSize"], mode='lines+markers', name="Portfolio Size", yaxis="y1" )) # Securities Added and Removed fig.add_trace(go.Scatter( x=df["date"], y=df["securitiesAdded"], mode='lines+markers', name="Securities Added", yaxis="y2" )) fig.add_trace(go.Scatter( x=df["date"], y=df["securitiesRemoved"], mode='lines+markers', name="Securities Removed", yaxis="y2" )) fig.update_layout( title="Portfolio Activity", xaxis_title="Date", yaxis=dict(title="Portfolio Size"), yaxis2=dict(title="Activity (Count)", overlaying="y", side="right"), legend=dict(title="Metrics"), height=500 ) return fig def plot_market_value_and_securities(df): """ Plots market value and securities count. Args: df (pd.DataFrame): DataFrame containing investor performance data. Returns: plotly.graph_objects.Figure: Plotly figure object. """ fig = go.Figure() # Portfolio Value Line fig.add_trace(go.Scatter( x=df["date"], y=df["marketValue"], mode='lines+markers', name="Portfolio Value", yaxis="y1" )) # Portfolio Size Line fig.add_trace(go.Scatter( x=df["date"], y=df["portfolioSize"], mode='lines+markers', name="Portfolio Size", yaxis="y2" )) fig.update_layout( title="Market Value and Securities Count", xaxis_title="Date", yaxis=dict(title="Market Value ($)", tickformat="$,.0f"), yaxis2=dict(title="Portfolio Size", overlaying="y", side="right"), legend=dict(title="Metrics"), height=500 ) return fig # Function to fetch symbol ownership (Page 4) @st.cache_data def fetch_symbol_ownership(symbol): """ Fetches symbol ownership data from FMP API. Args: symbol (str): Stock symbol. Returns: pd.DataFrame: DataFrame containing symbol ownership data. """ url = f"https://financialmodelingprep.com/api/v4/institutional-ownership/symbol-ownership" params = { "symbol": symbol, "includeCurrentQuarter": "true", "apikey": API_KEY } try: response = requests.get(url, params=params) if response.status_code == 200: data = response.json() df = pd.DataFrame(data) return df else: st.error(f"Failed to fetch data: {response.status_code}") return pd.DataFrame() except Exception as e: st.error(f"An exception occurred: {e}") return pd.DataFrame() # Function for Page 4: Symbol Ownership def page4(): st.title("Symbol Ownership") st.markdown(""" Fetch and visualizes ownership data for a specific stock symbol. Analyze investor counts, ownership percentages, portfolio value changes, positions activity, derivative activity. """) # Sidebar Inputs within an Expander with st.sidebar.expander("Symbol Ownership Inputs", expanded=True): symbol = st.text_input("Enter Stock Symbol", value="AAPL", help="Enter a valid stock symbol (e.g., AAPL for Apple Inc.) to analyze its ownership data.") run_button = st.sidebar.button("Run") if run_button or (st.session_state.symbol_ownership_params.get('symbol') == symbol): # Update Session State with current parameters st.session_state.symbol_ownership_params = { 'symbol': symbol } with st.spinner("Fetching and processing symbol ownership data..."): data = fetch_symbol_ownership(symbol) if not data.empty: st.session_state.symbol_ownership_data = data st.success("Data retrieved successfully!") st.dataframe(data, use_container_width=True) # Ensure the data is sorted by date data["date"] = pd.to_datetime(data["date"]) data = data.sort_values(by="date") # Plotting st.subheader("Investor Count and Ownership Percentage") st.markdown(""" **Explanation:** This chart displays the number of investors holding the stock and the percentage of ownership over time. It provides insights into investor interest and ownership trends. """) fig1 = plot_investor_count_and_ownership(data) st.plotly_chart(fig1, use_container_width=True) st.subheader("Portfolio Value and Ownership Percentage Change") st.markdown(""" **Explanation:** This chart shows the total invested amount and how the ownership percentage has changed over time. It helps in understanding investment growth and shifts in ownership stakes. """) fig2 = plot_portfolio_value_and_change_symbol(data) st.plotly_chart(fig2, use_container_width=True) st.subheader("Positions Activity") st.markdown(""" **Explanation:** This chart illustrates the activity related to positions, including new and closed positions as well as increases and reductions. It reflects the trading dynamics of the stock. """) fig3 = plot_positions_activity_symbol(data) st.plotly_chart(fig3, use_container_width=True) st.subheader("Derivative Activity") st.markdown(""" **Explanation:** This chart displays derivative activities such as total calls and puts, along with the put/call ratio. It provides insights into options trading related to the stock. """) fig4 = plot_derivative_activity_symbol(data) st.plotly_chart(fig4, use_container_width=True) st.subheader("Changes in Metrics") st.markdown(""" **Explanation:** This chart shows the changes in key metrics like the number of 13F shares and total investment. It highlights significant shifts in investment positions. """) fig5 = plot_changes_in_metrics_symbol(data) st.plotly_chart(fig5, use_container_width=True) st.subheader("Ownership Percent and Total Invested") st.markdown(""" **Explanation:** This chart compares the ownership percentage with the total amount invested in the stock. It helps in assessing the investment intensity relative to ownership stake. """) fig6 = plot_ownership_and_investment_symbol(data) st.plotly_chart(fig6, use_container_width=True) else: st.error("No data found for the specified symbol.") # If data exists in session state and parameters match, display it without rerunning elif st.session_state.symbol_ownership_data and st.session_state.symbol_ownership_params.get('symbol') == symbol: data = st.session_state.symbol_ownership_data st.success("Displaying previously retrieved data.") st.dataframe(data, use_container_width=True) # Ensure the data is sorted by date data["date"] = pd.to_datetime(data["date"]) data = data.sort_values(by="date") # Plotting st.subheader("Investor Count and Ownership Percentage") st.markdown(""" **Explanation:** This chart displays the number of investors holding the stock and the percentage of ownership over time. It provides insights into investor interest and ownership trends. """) fig1 = plot_investor_count_and_ownership(data) st.plotly_chart(fig1, use_container_width=True) st.subheader("Portfolio Value and Ownership Percentage Change") st.markdown(""" **Explanation:** This chart shows the total invested amount and how the ownership percentage has changed over time. It helps in understanding investment growth and shifts in ownership stakes. """) fig2 = plot_portfolio_value_and_change_symbol(data) st.plotly_chart(fig2, use_container_width=True) st.subheader("Positions Activity") st.markdown(""" **Explanation:** This chart illustrates the activity related to positions, including new and closed positions as well as increases and reductions. It reflects the trading dynamics of the stock. """) fig3 = plot_positions_activity_symbol(data) st.plotly_chart(fig3, use_container_width=True) st.subheader("Derivative Activity") st.markdown(""" **Explanation:** This chart displays derivative activities such as total calls and puts, along with the put/call ratio. It provides insights into options trading related to the stock. """) fig4 = plot_derivative_activity_symbol(data) st.plotly_chart(fig4, use_container_width=True) st.subheader("Changes in Metrics") st.markdown(""" **Explanation:** This chart shows the changes in key metrics like the number of 13F shares and total investment. It highlights significant shifts in investment positions. """) fig5 = plot_changes_in_metrics_symbol(data) st.plotly_chart(fig5, use_container_width=True) st.subheader("Ownership Percent and Total Invested") st.markdown(""" **Explanation:** This chart compares the ownership percentage with the total amount invested in the stock. It helps in assessing the investment intensity relative to ownership stake. """) fig6 = plot_ownership_and_investment_symbol(data) st.plotly_chart(fig6, use_container_width=True) # Functions used in Page 4 def plot_investor_count_and_ownership(df): """ Plots investor count and ownership percentage. Args: df (pd.DataFrame): DataFrame containing symbol ownership data. Returns: plotly.graph_objects.Figure: Plotly figure object. """ fig = go.Figure() # Investors Holding Line fig.add_trace(go.Scatter( x=df["date"], y=df["investorsHolding"], mode='lines+markers', name="Investors Holding", yaxis="y1" )) # Ownership Percentage Line fig.add_trace(go.Scatter( x=df["date"], y=df["ownershipPercent"], mode='lines+markers', name="Ownership (%)", yaxis="y2" )) fig.update_layout( title="Investor Count and Ownership Percentage", xaxis_title="Date", yaxis=dict(title="Investors Holding"), yaxis2=dict(title="Ownership (%)", overlaying="y", side="right", tickformat=".2f"), legend=dict(title="Metrics"), height=500 ) return fig def plot_portfolio_value_and_change_symbol(df): """ Plots portfolio value and ownership percentage change. Args: df (pd.DataFrame): DataFrame containing symbol ownership data. Returns: plotly.graph_objects.Figure: Plotly figure object. """ fig = go.Figure() # Total Invested Line fig.add_trace(go.Scatter( x=df["date"], y=df["totalInvested"], mode='lines+markers', name="Total Invested", yaxis="y1" )) # Ownership Percentage Change Line fig.add_trace(go.Scatter( x=df["date"], y=df["ownershipPercentChange"], mode='lines+markers', name="Ownership Percent Change", yaxis="y2" )) fig.update_layout( title="Portfolio Value and Ownership Percentage Change", xaxis_title="Date", yaxis=dict(title="Total Invested ($)", tickformat="$,.0f"), yaxis2=dict(title="Ownership Percent Change (%)", overlaying="y", side="right", tickformat=".2f"), legend=dict(title="Metrics"), height=500 ) return fig def plot_positions_activity_symbol(df): """ Plots positions activity. Args: df (pd.DataFrame): DataFrame containing symbol ownership data. Returns: plotly.graph_objects.Figure: Plotly figure object. """ fig = go.Figure() # New and Closed Positions Lines fig.add_trace(go.Scatter( x=df["date"], y=df["newPositions"], mode='lines+markers', name="New Positions", yaxis="y1" )) fig.add_trace(go.Scatter( x=df["date"], y=df["closedPositions"], mode='lines+markers', name="Closed Positions", yaxis="y1" )) # Increased and Reduced Positions Lines fig.add_trace(go.Scatter( x=df["date"], y=df["increasedPositions"], mode='lines+markers', name="Increased Positions", yaxis="y2" )) fig.add_trace(go.Scatter( x=df["date"], y=df["reducedPositions"], mode='lines+markers', name="Reduced Positions", yaxis="y2" )) fig.update_layout( title="Positions Activity", xaxis_title="Date", yaxis=dict(title="New/Closed Positions"), yaxis2=dict(title="Increased/Reduced Positions", overlaying="y", side="right"), legend=dict(title="Metrics"), height=500 ) return fig def plot_derivative_activity_symbol(df): """ Plots derivative activity. Args: df (pd.DataFrame): DataFrame containing symbol ownership data. Returns: plotly.graph_objects.Figure: Plotly figure object. """ fig = go.Figure() # Total Calls and Puts Lines fig.add_trace(go.Scatter( x=df["date"], y=df["totalCalls"], mode='lines+markers', name="Total Calls", yaxis="y1" )) fig.add_trace(go.Scatter( x=df["date"], y=df["totalPuts"], mode='lines+markers', name="Total Puts", yaxis="y1" )) # Put/Call Ratio Line fig.add_trace(go.Scatter( x=df["date"], y=df["putCallRatio"], mode='lines+markers', name="Put/Call Ratio", yaxis="y2" )) fig.update_layout( title="Derivative Activity", xaxis_title="Date", yaxis=dict(title="Total Calls/Puts"), yaxis2=dict(title="Put/Call Ratio", overlaying="y", side="right", tickformat=".2f"), legend=dict(title="Metrics"), height=500 ) return fig def plot_changes_in_metrics_symbol(df): """ Plots changes in metrics. Args: df (pd.DataFrame): DataFrame containing symbol ownership data. Returns: plotly.graph_objects.Figure: Plotly figure object. """ fig = go.Figure() # Change in Number of 13F Shares fig.add_trace(go.Scatter( x=df["date"], y=df["numberOf13FsharesChange"], mode='lines+markers', name="Change in 13F Shares", yaxis="y1" )) # Change in Total Investment fig.add_trace(go.Scatter( x=df["date"], y=df["totalInvestedChange"], mode='lines+markers', name="Change in Total Investment", yaxis="y2" )) fig.update_layout( title="Changes in Metrics", xaxis_title="Date", yaxis=dict(title="Change in 13F Shares"), yaxis2=dict(title="Change in Total Investment ($)", overlaying="y", side="right", tickformat="$,.0f"), legend=dict(title="Metrics"), height=500 ) return fig def plot_ownership_and_investment_symbol(df): """ Plots ownership percent and total invested. Args: df (pd.DataFrame): DataFrame containing symbol ownership data. Returns: plotly.graph_objects.Figure: Plotly figure object. """ fig = go.Figure() # Ownership Percent fig.add_trace(go.Scatter( x=df["date"], y=df["ownershipPercent"], mode='lines+markers', name="Ownership Percent", yaxis="y1" )) # Total Invested fig.add_trace(go.Scatter( x=df["date"], y=df["totalInvested"], mode='lines+markers', name="Total Invested", yaxis="y2" )) fig.update_layout( title="Ownership Percent and Total Invested", xaxis_title="Date", yaxis=dict(title="Ownership Percent (%)"), yaxis2=dict(title="Total Invested ($)", overlaying="y", side="right", tickformat="$,.0f"), legend=dict(title="Metrics"), height=500 ) return fig # Function for Main Navigation def main(): st.sidebar.title("Input Parameters") with st.sidebar.expander("Navigation", expanded=True): page = st.radio("Go to", ["Portfolio Allocation", "Investor Performance", "Symbol Ownership","CIK List"]) if page == "CIK List": page1() elif page == "Portfolio Allocation": page2() elif page == "Investor Performance": page3() elif page == "Symbol Ownership": page4() if __name__ == "__main__": main() hide_streamlit_style = """ """ st.markdown(hide_streamlit_style, unsafe_allow_html=True)