import os import requests import pandas as pd import plotly.graph_objects as go import streamlit as st st.set_page_config(layout="wide") # you need to sign up at FMP API to get the API key, then put it in the respective environment variable apikey = os.environ["FMP_API_KEY"] def parse_json(url): resp = requests.get(url) resp.raise_for_status() return pd.DataFrame(resp.json()) def fmt(val): if abs(val) >= 1e9: return f"${val/1e9:.1f}B" if abs(val) >= 1e6: return f"${val/1e6:.1f}M" if abs(val) >= 1e3: return f"${val/1e3:.0f}K" return f"${val:.0f}" def draw_balance_sankey(balance_sheet, symbol, height, font_size): # define all Sankey flows (left is source, right is target) flows = [ # Current Assets ("Cash and Cash Equivalents", "Total Current Assets", balance_sheet["cashAndCashEquivalents"]), ("Short-Term Investments", "Total Current Assets", balance_sheet["shortTermInvestments"]), ("Net Receivables", "Total Current Assets", balance_sheet["netReceivables"]), ("Inventory", "Total Current Assets", balance_sheet["inventory"]), ("Prepaids", "Total Current Assets", balance_sheet["prepaids"]), # NOTE: Other Current Assets may sometimes overlap with some of the above lines ("Other Current Assets", "Total Current Assets", balance_sheet["otherCurrentAssets"]), # Non-Current Assets ("Property, Plant and Equipment, Net", "Total Non-Current Assets", balance_sheet["propertyPlantEquipmentNet"]), ("Goodwill", "Total Non-Current Assets", balance_sheet["goodwill"]), ("Intangible Assets", "Total Non-Current Assets", balance_sheet["intangibleAssets"]), ("Long-Term Investments", "Total Non-Current Assets", balance_sheet["longTermInvestments"]), ("Tax Assets", "Total Non-Current Assets", balance_sheet["taxAssets"]), ("Other Non-Current Assets", "Total Non-Current Assets", balance_sheet["otherNonCurrentAssets"]), # ... to Total Assets ("Total Current Assets", "Total Assets", balance_sheet["totalCurrentAssets"]), ("Total Non-Current Assets", "Total Assets", balance_sheet["totalNonCurrentAssets"]), # Total Assets to ... ("Total Assets", "Total Liabilities", balance_sheet["totalLiabilities"]), ("Total Assets", "Total Stockholders' Equity", balance_sheet["totalStockholdersEquity"]), # Current Liabilities ("Total Liabilities", "Total Current Liabilities", balance_sheet["totalCurrentLiabilities"]), ("Total Current Liabilities", "Tax Payables", balance_sheet["taxPayables"]), ("Total Current Liabilities", "Short-Term Debt", balance_sheet["shortTermDebt"]), ("Total Current Liabilities", "Capital Lease Obligations (Current)", balance_sheet["capitalLeaseObligationsCurrent"]), ("Total Current Liabilities", "Deferred Revenue (Current)", balance_sheet["deferredRevenue"]), ("Total Current Liabilities", "Other Current Liabilities", balance_sheet["otherCurrentLiabilities"]), ("Total Current Liabilities", "Accounts Payable", balance_sheet["accountPayables"]), ("Total Current Liabilities", "Other Payables", balance_sheet["otherPayables"]), ("Total Current Liabilities", "Accrued Expenses", balance_sheet["accruedExpenses"]), # Non-Current Liabilities ("Total Liabilities", "Total Non-Current Liabilities", balance_sheet["totalNonCurrentLiabilities"]), ("Total Non-Current Liabilities", "Long-Term Debt", balance_sheet["longTermDebt"]), ("Total Non-Current Liabilities", "Capital Lease Obligations (Non-Current)", balance_sheet["capitalLeaseObligationsNonCurrent"]), ("Total Non-Current Liabilities", "Deferred Revenue (Non-Current)", balance_sheet["deferredRevenueNonCurrent"]), ("Total Non-Current Liabilities", "Deferred Tax Liabilities (Non-Current)", balance_sheet["deferredTaxLiabilitiesNonCurrent"]), ("Total Non-Current Liabilities", "Other Non-Current Liabilities", balance_sheet["otherNonCurrentLiabilities"]), # Equity ("Total Stockholders' Equity", "Common Stock", balance_sheet["commonStock"]), ("Total Stockholders' Equity", "Retained Earnings", balance_sheet["retainedEarnings"]), ("Total Stockholders' Equity", "Accumulated Other Comprehensive Income (Loss)", balance_sheet["accumulatedOtherComprehensiveIncomeLoss"]), ("Total Stockholders' Equity", "Additional Paid-In Capital", balance_sheet["additionalPaidInCapital"]), ("Total Stockholders' Equity", "Other Stockholders' Equity", balance_sheet["otherTotalStockholdersEquity"]), ] # need to adjust flow to make negative values easier to read adjusted_flows = [] for src, tgt, val in flows: if val >= 0: # positive: keep direction, color green adjusted_flows.append((src, tgt, val, 'rgba(50,200,50,0.6)')) else: # negative: reverse direction, color red adjusted_flows.append((tgt, src, -val, 'rgba(200,50,50,0.6)')) # for labelling later, first we store the source and target names labels = [] for src, tgt, _ in flows: if src not in labels: labels.append(src) if tgt not in labels: labels.append(tgt) # map label to its actual balance‐sheet value for annotation node_values = { "Cash and Cash Equivalents": balance_sheet["cashAndCashEquivalents"], "Short-Term Investments": balance_sheet["shortTermInvestments"], "Net Receivables": balance_sheet["netReceivables"], "Inventory": balance_sheet["inventory"], "Prepaids": balance_sheet["prepaids"], "Other Current Assets": balance_sheet["otherCurrentAssets"], "Total Current Assets": balance_sheet["totalCurrentAssets"], "Property, Plant and Equipment, Net": balance_sheet["propertyPlantEquipmentNet"], "Goodwill": balance_sheet["goodwill"], "Intangible Assets": balance_sheet["intangibleAssets"], "Long-Term Investments": balance_sheet["longTermInvestments"], "Tax Assets": balance_sheet["taxAssets"], "Other Non-Current Assets": balance_sheet["otherNonCurrentAssets"], "Total Non-Current Assets": balance_sheet["totalNonCurrentAssets"], "Total Assets": balance_sheet["totalAssets"], "Total Liabilities": balance_sheet["totalLiabilities"], "Total Current Liabilities": balance_sheet["totalCurrentLiabilities"], "Tax Payables": balance_sheet["taxPayables"], "Short-Term Debt": balance_sheet["shortTermDebt"], "Capital Lease Obligations (Current)": balance_sheet["capitalLeaseObligationsCurrent"], "Deferred Revenue (Current)": balance_sheet["deferredRevenue"], "Other Current Liabilities": balance_sheet["otherCurrentLiabilities"], "Accounts Payable": balance_sheet["accountPayables"], "Other Payables": balance_sheet["otherPayables"], "Accrued Expenses": balance_sheet["accruedExpenses"], "Total Non-Current Liabilities": balance_sheet["totalNonCurrentLiabilities"], "Long-Term Debt": balance_sheet["longTermDebt"], "Capital Lease Obligations (Non-Current)": balance_sheet["capitalLeaseObligationsNonCurrent"], "Deferred Revenue (Non-Current)": balance_sheet["deferredRevenueNonCurrent"], "Deferred Tax Liabilities (Non-Current)": balance_sheet["deferredTaxLiabilitiesNonCurrent"], "Other Non-Current Liabilities": balance_sheet["otherNonCurrentLiabilities"], "Total Stockholders' Equity": balance_sheet["totalStockholdersEquity"], "Common Stock": balance_sheet["commonStock"], "Retained Earnings": balance_sheet["retainedEarnings"], "Accumulated Other Comprehensive Income (Loss)": balance_sheet["accumulatedOtherComprehensiveIncomeLoss"], "Additional Paid-In Capital": balance_sheet["additionalPaidInCapital"], "Other Stockholders' Equity": balance_sheet["otherTotalStockholdersEquity"], } # for formatting, annotate labels with $ amounts and take care of billions, millions, thousands def fmt(val): if abs(val) >= 1e9: return f"${val/1e9:.1f}B" if abs(val) >= 1e6: return f"${val/1e6:.1f}M" if abs(val) >= 1e3: return f"${val/1e3:.0f}K" return f"${val:.0f}" # put the sorce and target values in labels labels = [] for s, t, _, _ in adjusted_flows: if s not in labels: labels.append(s) if t not in labels: labels.append(t) idx = {label:i for i,label in enumerate(labels)} source = [ idx[s] for s, t, _, _ in adjusted_flows ] # index of sources for sankey input target = [ idx[t] for s, t, _, _ in adjusted_flows ] # index of target for sankey input value = [ v for _, _, v, _ in adjusted_flows ] colors = [ c for _, _, _, c in adjusted_flows ] label_with_values = [] for label in labels: val = node_values[label] base = label.replace(" (Current)", "")\ .replace(" (Non-Current)", "") # saves some printing space if val < 0: base += " [NEGATIVE]" # just to make negatives more obvious in the label label_with_values.append(f"{base} ({fmt(val)})") fig = go.Figure(go.Sankey( arrangement="snap", node = dict(label=label_with_values, pad=15, thickness=20), link = dict(source=source, target=target, value=value, color=colors) )) fig.update_layout( title_text=f"Balance Sheet Sankey — {symbol}", height=height, font_size=font_size ) return fig def draw_income_sankey(income_statement, symbol, height, font_size): flows = [ # Revenue─ ("Revenue", "Cost of Revenue", income_statement["costOfRevenue"]), ("Revenue", "Gross Profit", income_statement["grossProfit"]), # Gross Profit ("Gross Profit", "Operating Income", income_statement["operatingIncome"]), ("Gross Profit", "Operating Expenses", income_statement["operatingExpenses"]), # Operating Expenses ("Operating Expenses", "Research & Development Expenses", income_statement["researchAndDevelopmentExpenses"]), # ("Operating Expenses", "General & Administrative Expenses", income_statement["generalAndAdministrativeExpenses"]), # already in SG&A # ("Operating Expenses", "Selling & Marketing Expenses", income_statement["sellingAndMarketingExpenses"]), # already in SG&A ("Operating Expenses", "SG&A Expenses", income_statement["sellingGeneralAndAdministrativeExpenses"]), ("Operating Expenses", "Other Operating Expenses", income_statement["otherExpenses"]), # Pretax Income ("Pretax Income", "Income Tax Expense", income_statement["incomeTaxExpense"]), ("Pretax Income", "Net Income", income_statement["netIncome"]), ("Pretax Income", "Interest Expense", income_statement["interestExpense"]), # this value is recorded as negative in API, but we do not need to reverse the flow like in balance sheet # because it decreases the pretax income so we put it together at the same side with all the tax expenses ("Pretax Income", "Non-Operating Income Excl. Interest", -income_statement["nonOperatingIncomeExcludingInterest"]), ("Pretax Income", "Total Other Income & Expenses Net", income_statement["totalOtherIncomeExpensesNet"]), ("Pretax Income", "Other Adjustments to Net Income", income_statement["otherAdjustmentsToNetIncome"]), # Other Income that goes into Pretax Income ("Operating Income", "Pretax Income", income_statement["operatingIncome"]), ("Net Interest Income", "Pretax Income", income_statement["netInterestIncome"]), ("Interest Income", "Pretax Income", income_statement["interestIncome"]), ] # need to adjust flow to make negative values easier to read adjusted_flows = [] for src, tgt, val in flows: if val >= 0: # positive: keep direction, color green adjusted_flows.append((src, tgt, val, 'rgba(50,200,50,0.6)')) else: # negative: reverse direction, color red adjusted_flows.append((tgt, src, -val, 'rgba(200,50,50,0.6)')) # for labelling later, first we store the source and target names labels = [] for src, tgt, _ in flows: if src not in labels: labels.append(src) if tgt not in labels: labels.append(tgt) # map label to its actual balance‐sheet value for annotation node_values = { "Revenue": income_statement["revenue"], "Cost of Revenue": income_statement["costOfRevenue"], "Gross Profit": income_statement["grossProfit"], "Operating Income": income_statement["operatingIncome"], "Operating Expenses": income_statement["operatingExpenses"], "Research & Development Expenses": income_statement["researchAndDevelopmentExpenses"], #"General & Administrative Expenses": income_statement["generalAndAdministrativeExpenses"], # already in SG&A #"Selling & Marketing Expenses": income_statement["sellingAndMarketingExpenses"], # already in SG&A "SG&A Expenses": income_statement["sellingGeneralAndAdministrativeExpenses"], "Other Operating Expenses": income_statement["otherExpenses"], "Net Interest Income": income_statement["netInterestIncome"], "Interest Income": income_statement["interestIncome"], "Interest Expense": income_statement["interestExpense"], "Non-Operating Income Excl. Interest":-income_statement["nonOperatingIncomeExcludingInterest"], "Total Other Income & Expenses Net": income_statement["totalOtherIncomeExpensesNet"], "Pretax Income": income_statement["incomeBeforeTax"], "Income Tax Expense": income_statement["incomeTaxExpense"], "Net Income": income_statement["netIncome"], "Other Adjustments to Net Income": income_statement["otherAdjustmentsToNetIncome"], "Bottom Line Net Income": income_statement["bottomLineNetIncome"], } # for formatting, annotate labels with $ amounts and take care of billions, millions, thousands def fmt(val): if abs(val) >= 1e9: return f"${val/1e9:.1f}B" if abs(val) >= 1e6: return f"${val/1e6:.1f}M" if abs(val) >= 1e3: return f"${val/1e3:.0f}K" return f"${val:.0f}" # put the sorce and target values in labels labels = [] for s, t, _, _ in adjusted_flows: if s not in labels: labels.append(s) if t not in labels: labels.append(t) idx = {label:i for i,label in enumerate(labels)} source = [ idx[s] for s, t, _, _ in adjusted_flows ] # index of sources for sankey input target = [ idx[t] for s, t, _, _ in adjusted_flows ] # index of target for sankey input value = [ v for _, _, v, _ in adjusted_flows ] colors = [ c for _, _, _, c in adjusted_flows ] label_with_values = [] for label in labels: val = node_values[label] base = label.replace(" (Current)", "")\ .replace(" (Non-Current)", "") # saves some printing space if val < 0: base += " [NEGATIVE]" # just to make negatives more obvious in the label label_with_values.append(f"{base} ({fmt(val)})") fig = go.Figure(go.Sankey( arrangement="snap", node = dict(label=label_with_values, pad=15, thickness=20), link = dict(source=source, target=target, value=value, color=colors) )) fig.update_layout( title_text=f"Income Statement Sankey — {symbol}", height=height, font_size=font_size ) return fig def draw_cashflow_sankey(cash_flow, symbol, height, font_size): flows = [ # Operating Activities (Inflow) ("Net Income", "Operating Activities", cash_flow["netIncome"]), ("Depreciation & Amortization", "Operating Activities", cash_flow["depreciationAndAmortization"]), ("Deferred Income Tax", "Operating Activities", cash_flow["deferredIncomeTax"]), ("Stock-Based Compensation", "Operating Activities", cash_flow["stockBasedCompensation"]), ("Change in Working Capital", "Operating Activities", cash_flow["changeInWorkingCapital"]), ("Accounts Receivables Δ", "Operating Activities", cash_flow["accountsReceivables"]), ("Inventory Δ", "Operating Activities", cash_flow["inventory"]), ("Accounts Payable Δ", "Operating Activities", cash_flow["accountsPayables"]), #("Other Working Capital Δ", "Operating Activities", cash_flow["otherWorkingCapital"]), # overlap with some lines ("Other Non-Cash Items", "Operating Activities", cash_flow["otherNonCashItems"]), ("Operating Activities", "Net Cash Inflow (Operating)", cash_flow["netCashProvidedByOperatingActivities"]), # Investing Activities (Outflow) ("Investments in PP&E", "Investing Activities", cash_flow["investmentsInPropertyPlantAndEquipment"]), ("Acquisitions, Net", "Investing Activities", cash_flow["acquisitionsNet"]), ("Purchases of Investments", "Investing Activities", cash_flow["purchasesOfInvestments"]), ("Sales/Maturities of Investments", "Investing Activities", cash_flow["salesMaturitiesOfInvestments"]), ("Other Investing Activities", "Investing Activities", cash_flow["otherInvestingActivities"]), ("Investing Activities", "Net Cash Outflow (Investing)", cash_flow["netCashProvidedByInvestingActivities"]), # Financing Activities (Outflow) ("Net Debt Issuance", "Financing Activities", cash_flow["netDebtIssuance"]), #("Long-Term Net Debt Issuance", "Financing Activities", cash_flow["longTermNetDebtIssuance"]), # already under net debt #("Short-Term Net Debt Issuance", "Financing Activities", cash_flow["shortTermNetDebtIssuance"]), # already under net debt #("Net Stock Issuance", "Financing Activities", cash_flow["netStockIssuance"]), ("Net Common Stock Issuance", "Financing Activities", cash_flow["netCommonStockIssuance"]), #("Common Stock Issuance", "Financing Activities", cash_flow["commonStockIssuance"]), # already under net common stock issuance #("Common Stock Repurchased", "Financing Activities", cash_flow["commonStockRepurchased"]), # already under net common stock issuance ("Net Preferred Stock Issuance", "Financing Activities", cash_flow["netPreferredStockIssuance"]), ("Net Dividends Paid", "Financing Activities", cash_flow["netDividendsPaid"]), #("Common Dividends Paid", "Financing Activities", cash_flow["commonDividendsPaid"]), # already under net dividends paid #("Preferred Dividends Paid", "Financing Activities", cash_flow["preferredDividendsPaid"]), # already under net dividends paid ("Other Financing Activities", "Financing Activities", cash_flow["otherFinancingActivities"]), ("Financing Activities", "Net Cash Outflow (Financing)", cash_flow["netCashProvidedByFinancingActivities"]), # Combine In and Out Flows ("Net Cash Inflow (Operating)", "Net Change in Cash", cash_flow["netCashProvidedByOperatingActivities"]), ("Net Cash Outflow (Investing)", "Net Change in Cash", cash_flow["netCashProvidedByInvestingActivities"]), ("Net Cash Outflow (Financing)", "Net Change in Cash", cash_flow["netCashProvidedByFinancingActivities"]), ("Effect of Forex on Cash", "Net Change in Cash", cash_flow["effectOfForexChangesOnCash"]), # Beginning & Change → Ending Balance ("Cash at Beginning of Period", "Cash at End of Period", cash_flow["cashAtBeginningOfPeriod"]), ("Net Change in Cash", "Cash at End of Period", cash_flow["netChangeInCash"]), ] adjusted_flows = [] for src, tgt, val in flows: if val >= 0: adjusted_flows.append((src, tgt, val, 'rgba(50,200,50,0.6)')) else: # reverse direction for readability adjusted_flows.append((tgt, src, -val, 'rgba(200,50,50,0.6)')) labels = [] for s, t, _, _ in adjusted_flows: if s not in labels: labels.append(s) if t not in labels: labels.append(t) node_values = { lbl: cash_flow[ { "Net Income": "netIncome", "Depreciation & Amortization": "depreciationAndAmortization", "Deferred Income Tax": "deferredIncomeTax", "Stock-Based Compensation": "stockBasedCompensation", "Change in Working Capital": "changeInWorkingCapital", "Accounts Receivables Δ": "accountsReceivables", "Inventory Δ": "inventory", "Accounts Payable Δ": "accountsPayables", "Other Working Capital Δ": "otherWorkingCapital", "Other Non-Cash Items": "otherNonCashItems", "Operating Activities": "netCashProvidedByOperatingActivities", "Investments in PP&E": "investmentsInPropertyPlantAndEquipment", "Acquisitions, Net": "acquisitionsNet", "Purchases of Investments": "purchasesOfInvestments", "Sales/Maturities of Investments": "salesMaturitiesOfInvestments", "Other Investing Activities": "otherInvestingActivities", "Investing Activities": "netCashProvidedByInvestingActivities", "Net Debt Issuance": "netDebtIssuance", "Long-Term Net Debt Issuance": "longTermNetDebtIssuance", "Short-Term Net Debt Issuance": "shortTermNetDebtIssuance", "Net Stock Issuance": "netStockIssuance", "Net Common Stock Issuance": "netCommonStockIssuance", "Common Stock Issuance": "commonStockIssuance", "Common Stock Repurchased": "commonStockRepurchased", "Net Preferred Stock Issuance": "netPreferredStockIssuance", "Net Dividends Paid": "netDividendsPaid", "Common Dividends Paid": "commonDividendsPaid", "Preferred Dividends Paid": "preferredDividendsPaid", "Other Financing Activities": "otherFinancingActivities", "Financing Activities": "netCashProvidedByFinancingActivities", "Net Cash Inflow (Operating)": "netCashProvidedByOperatingActivities", "Net Cash Outflow (Investing)": "netCashProvidedByInvestingActivities", "Net Cash Outflow (Financing)": "netCashProvidedByFinancingActivities", "Effect of Forex on Cash": "effectOfForexChangesOnCash", "Net Change in Cash": "netChangeInCash", "Cash at Beginning of Period": "cashAtBeginningOfPeriod", "Cash at End of Period": "cashAtEndOfPeriod", }[lbl] ] for lbl in labels } def fmt(val): if abs(val) >= 1e9: return f"${val/1e9:.1f}B" if abs(val) >= 1e6: return f"${val/1e6:.1f}M" if abs(val) >= 1e3: return f"${val/1e3:.0f}K" return f"${val:.0f}" idx = { lbl:i for i,lbl in enumerate(labels) } source = [ idx[s] for s, t, _, _ in adjusted_flows ] target = [ idx[t] for s, t, _, _ in adjusted_flows ] value = [ v for _, _, v, _ in adjusted_flows ] colors = [ c for _, _, _, c in adjusted_flows ] label_with_values = [] for lbl in labels: val = node_values[lbl] base = lbl if val < 0: base += " [NEGATIVE]" label_with_values.append(f"{base} ({fmt(val)})") fig = go.Figure(go.Sankey( arrangement="snap", node = dict(label=label_with_values, pad=15, thickness=20), link = dict(source=source, target=target, value=value, color=colors) )) fig.update_layout( title_text=f"Cash Flow Statement Sankey — {symbol}", height=height, font_size=font_size ) return fig st.title("Financial Sankeys") symbol = st.sidebar.text_input("Ticker symbol", "AMZN").upper() # sidebar controls bs_height = st.sidebar.slider("Balance Sheet height", 500, 1500, 800) bs_font = st.sidebar.slider("Balance Sheet font size", 5, 15, 10) is_height = st.sidebar.slider("Income Statement height", 500, 1500, 600) is_font = st.sidebar.slider("Income Statement font size", 5, 15, 10) cf_height = st.sidebar.slider("Cash Flow Statement height", 500, 1500, 800) cf_font = st.sidebar.slider("Cash Flow Statement font size", 5, 15, 10) # where the data came from st.sidebar.markdown("## [Financial Modeling Prep API](https://site.financialmodelingprep.com/?utm_source=medium&utm_medium=medium&utm_campaign=damian8)\ \n\nFinancial statements are obtained from the FinancialModelingPrep API, feel free to sign up\ [here](https://site.financialmodelingprep.com/?utm_source=medium&utm_medium=medium&utm_campaign=damian8)\ if you wish.") if symbol: # Balance Sheet st.header(f"Balance Sheet — {symbol}") try: df_bs = parse_json(f"https://financialmodelingprep.com/stable/balance-sheet-statement?symbol={symbol}&apikey={apikey}") balance_sheet = df_bs.iloc[0] fig_bs = draw_balance_sankey(balance_sheet, symbol.upper(), bs_height, bs_font) st.plotly_chart(fig_bs, use_container_width=True) except Exception as e: st.error(f"{e}") # Income Statement st.header(f"Income Statement — {symbol}") try: df_is = parse_json(f"https://financialmodelingprep.com/stable/income-statement?symbol={symbol}&apikey={apikey}") income_statement = df_is.iloc[0] fig_is = draw_income_sankey(income_statement, symbol.upper(), is_height, is_font) st.plotly_chart(fig_is, use_container_width=True) except Exception as e: st.error(f"{e}") # Cash Flow Statement st.header(f"Cash Flow Statement — {symbol}") try: df_cf = parse_json(f"https://financialmodelingprep.com/stable/cash-flow-statement?symbol={symbol}&apikey={apikey}") cash_flow = df_cf.iloc[0] fig_cf = draw_cashflow_sankey(cash_flow, symbol.upper(), cf_height, cf_font) st.plotly_chart(fig_cf, use_container_width=True) except Exception as e: st.error(f"{e}")