Space45 / app.py
QuantumLearner's picture
Update app.py
b447e02 verified
# 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}%<br>{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 = """
<style>
#MainMenu {visibility: hidden;}
footer {visibility: hidden;}
</style>
"""
st.markdown(hide_streamlit_style, unsafe_allow_html=True)