Spaces:
Running
Running
import streamlit as st | |
import pandas as pd | |
import plotly.express as px | |
import plotly.graph_objects as go | |
from plotly.subplots import make_subplots | |
import requests | |
import os | |
# ------------------------------------------------------- | |
# GLOBAL CONFIG | |
# ------------------------------------------------------- | |
API_KEY = os.getenv("FMP_API_KEY") | |
st.set_page_config(page_title="Financial Statements", layout="wide") | |
# Initialize session state for caching | |
if 'data_cache' not in st.session_state: | |
st.session_state.data_cache = {} | |
# ------------------------------------------------------- | |
# CACHED FETCH FUNCTIONS | |
# ------------------------------------------------------- | |
def fetch_income_statement(symbol: str, period: str, api_key: str) -> pd.DataFrame: | |
url = f"https://financialmodelingprep.com/api/v3/income-statement/{symbol}?period={period}&apikey={api_key}" | |
r = requests.get(url) | |
r.raise_for_status() | |
data = r.json() if r.status_code == 200 else [] | |
df = pd.DataFrame(data) | |
if not df.empty and "date" in df.columns: | |
df["date"] = pd.to_datetime(df["date"], errors="coerce") | |
df.sort_values("date", inplace=True) | |
return df | |
def fetch_balance_sheet(symbol: str, period: str, api_key: str) -> pd.DataFrame: | |
url = f"https://financialmodelingprep.com/api/v3/balance-sheet-statement/{symbol}?period={period}&apikey={api_key}" | |
r = requests.get(url) | |
r.raise_for_status() | |
data = r.json() if r.status_code == 200 else [] | |
df = pd.DataFrame(data) | |
if not df.empty and "date" in df.columns: | |
df["date"] = pd.to_datetime(df["date"], errors="coerce") | |
df.sort_values("date", inplace=True) | |
return df | |
def fetch_cash_flow(symbol: str, period: str, api_key: str) -> pd.DataFrame: | |
url = f"https://financialmodelingprep.com/api/v3/cash-flow-statement/{symbol}?period={period}&apikey={api_key}" | |
r = requests.get(url) | |
r.raise_for_status() | |
data = r.json() if r.status_code == 200 else [] | |
df = pd.DataFrame(data) | |
if not df.empty and "date" in df.columns: | |
df["date"] = pd.to_datetime(df["date"], errors="coerce") | |
df.sort_values("date", inplace=True) | |
return df | |
# ------------------------------------------------------- | |
# HELPER: CREATE DUAL-AXIS SUBPLOT | |
# ------------------------------------------------------- | |
def create_dual_axis_figure(df: pd.DataFrame, vars_list: list[str], title: str, period: str) -> go.Figure: | |
shift_val = 1 if period == "annual" else 4 | |
df_local = df.copy() | |
for var in vars_list: | |
if var in df_local.columns: | |
df_local[var + "_yoy"] = ( | |
(df_local[var] - df_local[var].shift(shift_val)) | |
/ df_local[var].shift(shift_val) | |
) * 100 | |
else: | |
df_local[var + "_yoy"] = None | |
fig = make_subplots(specs=[[{"secondary_y": True}]]) | |
colors = px.colors.qualitative.Plotly | |
for idx, var in enumerate(vars_list): | |
color_idx = idx % len(colors) | |
base_color = colors[color_idx] | |
fig.add_trace( | |
go.Scatter( | |
x=df_local["date"], | |
y=df_local[var], | |
name=var, | |
mode="lines+markers", | |
line=dict(width=2, color=base_color), | |
hovertemplate=(f"<b>{var}</b><br>Date: %{{x}}<br>Value: %{{y:.2f}}<extra></extra>"), | |
), | |
secondary_y=False | |
) | |
yoy_col = var + "_yoy" | |
fig.add_trace( | |
go.Scatter( | |
x=df_local["date"], | |
y=df_local[yoy_col], | |
name=f"{var} YoY (%)", | |
mode="lines+markers", | |
line=dict(width=2, dash="dash", color=base_color), | |
opacity=0.3, | |
hovertemplate=(f"<b>{var} YoY</b><br>Date: %{{x}}<br>Change: %{{y:.2f}}%<extra></extra>"), | |
), | |
secondary_y=True | |
) | |
fig.update_layout( | |
title=title, | |
hovermode="closest", | |
legend=dict(x=0, y=-0.2, orientation="h", tracegroupgap=0), | |
) | |
fig.update_xaxes(title_text="Date") | |
fig.update_yaxes(title_text="Absolute Value", secondary_y=False) | |
fig.update_yaxes(title_text="YoY Change (%)", secondary_y=True) | |
return fig | |
# ------------------------------------------------------- | |
# HELPER: ENHANCED INTERPRETATION TEXT | |
# ------------------------------------------------------- | |
def interpret_financials(df: pd.DataFrame, metric_list: list[str], section_title: str, period: str) -> str: | |
existing_cols = [m for m in metric_list if m in df.columns] | |
if not existing_cols or df.empty: | |
return f"**{section_title}**: Data is not available for analysis." | |
df_valid = df[['date'] + existing_cols].dropna(subset=existing_cols, how='all') | |
if df_valid.empty: | |
return f"**{section_title}**: No valid data entries available." | |
df_valid = df_valid.sort_values("date") | |
latest_row = df_valid.iloc[-1] | |
latest_date = latest_row['date'] | |
shift = 1 if period == "annual" else 4 | |
period_type = "Year-over-Year" if period == "annual" else "Quarter-over-Quarter" | |
prior_row = df_valid.iloc[-1 - shift] if len(df_valid) > shift else None | |
prior_date = prior_row['date'] if prior_row is not None else None | |
values_only = df_valid[existing_cols].astype(float) | |
mean_vals = values_only.mean() | |
min_vals = values_only.min() | |
max_vals = values_only.max() | |
std_vals = values_only.std() | |
text = f"### {section_title}\n\n" | |
text += f"**Latest Data ({latest_date.date()}):** \n" | |
for col in existing_cols: | |
latest_val = latest_row[col] | |
text += f"- **{col.replace('_', ' ').title()}**: {latest_val:,.2f} \n" if pd.notna(latest_val) else f"- **{col.replace('_', ' ').title()}**: Data unavailable \n" | |
if prior_row is not None: | |
text += f"\n**{period_type} Change (vs. {prior_date.date()}):** \n" | |
for col in existing_cols: | |
latest_val = latest_row[col] | |
prior_val = prior_row[col] | |
if pd.notna(latest_val) and pd.notna(prior_val) and prior_val != 0: | |
pct_change = ((latest_val - prior_val) / abs(prior_val)) * 100 | |
diff = latest_val - prior_val | |
direction = "increased" if diff > 0 else "decreased" if diff < 0 else "unchanged" | |
text += f"- **{col.replace('_', ' ').title()}**: {direction.capitalize()} by {abs(diff):,.2f} ({pct_change:+.1f}%) \n" | |
else: | |
text += f"- **{col.replace('_', ' ').title()}**: Insufficient data for comparison \n" | |
text += "\n**Historical Trends:** \n" | |
for col in existing_cols: | |
text += (f"- **{col.replace('_', ' ').title()}**: Mean = {mean_vals[col]:,.2f}, " | |
f"Min = {min_vals[col]:,.2f}, Max = {max_vals[col]:,.2f}, " | |
f"Std Dev = {std_vals[col]:,.2f} \n") | |
text += "\n**Investor Insights:** \n" | |
if section_title == "Revenue & Gross Profit": | |
text += ( | |
"- Strong revenue growth paired with expanding gross profit margins signals operational efficiency and market strength. \n" | |
"- Declining trends may reflect competitive pressures or rising costs, impacting profitability. \n" | |
"- Volatility in these metrics could indicate cyclical demand or pricing instability. \n" | |
) | |
elif section_title == "Operating Expenses": | |
text += ( | |
"- Rising expenses with stable revenue may erode margins, suggesting inefficiencies or investment in growth. \n" | |
"- Controlled or declining expenses reflect disciplined cost management. \n" | |
"- High variability could point to inconsistent operational strategies. \n" | |
) | |
elif section_title == "Net Income & Operating Income": | |
text += ( | |
"- Consistent growth in operating and net income underscores sustainable earnings power. \n" | |
"- Divergence between operating income and net income may highlight tax or interest burdens. \n" | |
"- Sharp declines warrant investigation into cost structures or extraordinary items. \n" | |
) | |
elif section_title == "Earnings Per Share": | |
text += ( | |
"- Rising EPS reflects enhanced shareholder value. \n" | |
"- Stagnant or falling EPS may signal dilution or profitability challenges. \n" | |
"- Compare diluted vs. basic EPS to assess the impact of potential equity issuance. \n" | |
) | |
elif section_title == "Assets": | |
text += ( | |
"- Growth in total assets, especially liquid ones, indicates balance sheet strength and investment capacity. \n" | |
"- Declines may suggest asset sales or write-downs, potentially weakening financial flexibility. \n" | |
"- A balanced asset mix is key to supporting long-term growth. \n" | |
) | |
elif section_title == "Liabilities": | |
text += ( | |
"- Increasing liabilities with stable assets raise leverage concerns. \n" | |
"- Controlled liability growth supports a stable capital structure. \n" | |
"- High short-term liabilities relative to cash may pressure liquidity. \n" | |
) | |
elif section_title == "Stockholders' Equity": | |
text += ( | |
"- Rising equity reflects retained earnings growth or capital infusions. \n" | |
"- Declines may indicate losses or share repurchasing, affecting leverage ratios. \n" | |
"- Consistent equity growth enhances investor confidence. \n" | |
) | |
elif section_title == "Operating Activities": | |
text += ( | |
"- Strong cash flow from operations indicates robust core business health. \n" | |
"- Negative or declining trends may reflect working capital issues. \n" | |
"- High depreciation relative to net income suggests significant non-cash adjustments. \n" | |
) | |
elif section_title == "Investing Activities": | |
text += ( | |
"- Heavy investment in property or equipment signals long-term growth focus but may strain near-term cash. \n" | |
"- Positive cash from sales/maturities indicates strategic divestitures. \n" | |
"- Persistent negative flows suggest aggressive expansion. \n" | |
) | |
elif section_title == "Financing Activities": | |
text += ( | |
"- Debt repayment or dividend increases reflect confidence in cash flows. \n" | |
"- Significant stock repurchasing may signal undervaluation or reduced growth. \n" | |
"- High financing inflows could indicate reliance on external capital. \n" | |
) | |
text += "\n*Recommendation*: Cross-reference these insights with industry benchmarks and broader market conditions." | |
return text | |
# ------------------------------------------------------- | |
# PAGES | |
# ------------------------------------------------------- | |
def page_income_statement(symbol: str, period: str): | |
key = f"income_{symbol}_{period}" | |
if key not in st.session_state.data_cache: | |
st.session_state.data_cache[key] = fetch_income_statement(symbol, period, API_KEY) | |
df = st.session_state.data_cache[key] | |
if df.empty: | |
st.error("No income statement data returned. Check symbol or period.") | |
return | |
st.success("Income Statement data loaded successfully.") | |
st.write("Charts display absolute values and period-over-period changes.") | |
st.subheader("1. Revenue & Gross Profit") | |
rev_vars = ["revenue", "grossProfit"] | |
fig_rev = create_dual_axis_figure(df, rev_vars, "Revenue & Gross Profit", period) | |
st.plotly_chart(fig_rev, use_container_width=True) | |
with st.expander("Interpretation"): | |
st.markdown(interpret_financials(df, rev_vars, "Revenue & Gross Profit", period)) | |
st.subheader("2. Operating Expenses") | |
op_vars = ["researchAndDevelopmentExpenses", "sellingGeneralAndAdministrativeExpenses", "operatingExpenses"] | |
fig_op = create_dual_axis_figure(df, op_vars, "Operating Expenses", period) | |
st.plotly_chart(fig_op, use_container_width=True) | |
with st.expander("Interpretation"): | |
st.markdown(interpret_financials(df, op_vars, "Operating Expenses", period)) | |
st.subheader("3. Net Income & Operating Income") | |
net_vars = ["netIncome", "operatingIncome", "incomeBeforeTax"] | |
fig_net = create_dual_axis_figure(df, net_vars, "Net Income & Operating Income", period) | |
st.plotly_chart(fig_net, use_container_width=True) | |
with st.expander("Interpretation"): | |
st.markdown(interpret_financials(df, net_vars, "Net Income & Operating Income", period)) | |
st.subheader("4. Earnings Per Share") | |
eps_vars = ["eps", "epsdiluted"] | |
fig_eps = create_dual_axis_figure(df, eps_vars, "Earnings Per Share", period) | |
st.plotly_chart(fig_eps, use_container_width=True) | |
with st.expander("Interpretation"): | |
st.markdown(interpret_financials(df, eps_vars, "Earnings Per Share", period)) | |
st.subheader("Complete Income Statement Data") | |
with st.expander("Show Complete Data"): | |
st.dataframe(df) | |
def page_balance_sheet(symbol: str, period: str): | |
key = f"balance_{symbol}_{period}" | |
if key not in st.session_state.data_cache: | |
st.session_state.data_cache[key] = fetch_balance_sheet(symbol, period, API_KEY) | |
df = st.session_state.data_cache[key] | |
if df.empty: | |
st.error("No balance sheet data returned. Check symbol or period.") | |
return | |
st.success("Balance Sheet data loaded successfully.") | |
st.write("Charts display absolute values and period-over-period changes.") | |
st.subheader("1. Assets") | |
asset_vars = ["cashAndShortTermInvestments", "totalCurrentAssets", "totalNonCurrentAssets", "totalAssets"] | |
fig_a = create_dual_axis_figure(df, asset_vars, "Assets", period) | |
st.plotly_chart(fig_a, use_container_width=True) | |
with st.expander("Interpretation"): | |
st.markdown(interpret_financials(df, asset_vars, "Assets", period)) | |
st.subheader("2. Liabilities") | |
liability_vars = ["totalCurrentLiabilities", "totalNonCurrentLiabilities", "totalLiabilities"] | |
fig_l = create_dual_axis_figure(df, liability_vars, "Liabilities", period) | |
st.plotly_chart(fig_l, use_container_width=True) | |
with st.expander("Interpretation"): | |
st.markdown(interpret_financials(df, liability_vars, "Liabilities", period)) | |
st.subheader("3. Stockholders' Equity") | |
equity_vars = ["commonStock", "retainedEarnings", "accumulatedOtherComprehensiveIncomeLoss", "totalStockholdersEquity"] | |
fig_e = create_dual_axis_figure(df, equity_vars, "Stockholders' Equity", period) | |
st.plotly_chart(fig_e, use_container_width=True) | |
with st.expander("Interpretation"): | |
st.markdown(interpret_financials(df, equity_vars, "Stockholders' Equity", period)) | |
st.subheader("Complete Balance Sheet Data") | |
with st.expander("Show Complete Data"): | |
st.dataframe(df) | |
def page_cash_flow(symbol: str, period: str): | |
key = f"cash_{symbol}_{period}" | |
if key not in st.session_state.data_cache: | |
st.session_state.data_cache[key] = fetch_cash_flow(symbol, period, API_KEY) | |
df = st.session_state.data_cache[key] | |
if df.empty: | |
st.error("No cash flow data returned. Check symbol or period.") | |
return | |
st.success("Cash Flow data loaded successfully.") | |
st.write("Charts display absolute values and period-over-period changes.") | |
st.subheader("1. Operating Activities") | |
op_vars = ["netIncome", "depreciationAndAmortization", "changeInWorkingCapital", "netCashProvidedByOperatingActivities"] | |
fig_op = create_dual_axis_figure(df, op_vars, "Operating Activities", period) | |
st.plotly_chart(fig_op, use_container_width=True) | |
with st.expander("Interpretation"): | |
st.markdown(interpret_financials(df, op_vars, "Operating Activities", period)) | |
st.subheader("2. Investing Activities") | |
inv_vars = ["investmentsInPropertyPlantAndEquipment", "purchasesOfInvestments", "salesMaturitiesOfInvestments", "netCashUsedForInvestingActivites"] | |
fig_inv = create_dual_axis_figure(df, inv_vars, "Investing Activities", period) | |
st.plotly_chart(fig_inv, use_container_width=True) | |
with st.expander("Interpretation"): | |
st.markdown(interpret_financials(df, inv_vars, "Investing Activities", period)) | |
st.subheader("3. Financing Activities") | |
fin_vars = ["debtRepayment", "commonStockRepurchased", "dividendsPaid", "netCashUsedProvidedByFinancingActivities"] | |
fig_fin = create_dual_axis_figure(df, fin_vars, "Financing Activities", period) | |
st.plotly_chart(fig_fin, use_container_width=True) | |
with st.expander("Interpretation"): | |
st.markdown(interpret_financials(df, fin_vars, "Financing Activities", period)) | |
st.subheader("Complete Cash Flow Data") | |
with st.expander("Show Complete Data"): | |
st.dataframe(df) | |
# ------------------------------------------------------- | |
# MAIN | |
# ------------------------------------------------------- | |
st.title("Financial Statements Analysis") | |
st.markdown(""" | |
This tool presents key financial statements for your review. | |
It displays the Income Statement, Balance Sheet, and Cash Flow Statement. | |
Charts show absolute numbers on the left and changes over time on the right. | |
""") | |
# Sidebar: Navigation and Inputs | |
with st.sidebar.expander("Navigation", expanded=True): | |
selected_page = st.radio("Select Page", ["Income Statement", "Balance Sheet", "Cash Flow"], index=0) | |
st.session_state.page = selected_page | |
with st.sidebar.expander("Inputs", expanded=True): | |
symbol = st.text_input("Symbol or CIK", value="AAPL") | |
period = st.selectbox("Period", options=["annual", "quarter"]) | |
run_button = st.button("Run Analysis") | |
# When run is pressed, update symbol/period and refresh only the active page. | |
if run_button: | |
st.session_state.symbol = symbol | |
st.session_state.period = period | |
current_page = st.session_state.page | |
if current_page == "Income Statement": | |
st.session_state.data_cache[f"income_{symbol}_{period}"] = fetch_income_statement(symbol, period, API_KEY) | |
elif current_page == "Balance Sheet": | |
st.session_state.data_cache[f"balance_{symbol}_{period}"] = fetch_balance_sheet(symbol, period, API_KEY) | |
elif current_page == "Cash Flow": | |
st.session_state.data_cache[f"cash_{symbol}_{period}"] = fetch_cash_flow(symbol, period, API_KEY) | |
# Retrieve the latest inputs from session state. | |
symbol = st.session_state.get('symbol', 'AAPL') | |
period = st.session_state.get('period', 'annual') | |
current_page = st.session_state.get('page', 'Income Statement') | |
if current_page == "Income Statement": | |
page_income_statement(symbol, period) | |
elif current_page == "Balance Sheet": | |
page_balance_sheet(symbol, period) | |
elif current_page == "Cash Flow": | |
page_cash_flow(symbol, period) | |
# Hide default Streamlit style | |
st.markdown( | |
""" | |
<style> | |
#MainMenu {visibility: hidden;} | |
footer {visibility: hidden;} | |
</style> | |
""", | |
unsafe_allow_html=True | |
) | |