import streamlit as st import requests import pandas as pd import numpy as np import yfinance as yf import plotly.graph_objects as go import plotly.figure_factory as ff from datetime import datetime, date from dateutil.relativedelta import relativedelta import datetime as dt import warnings warnings.filterwarnings("ignore") import os from scipy.optimize import fsolve from scipy.stats import norm ############################################################################### # SET WIDE LAYOUT AND PAGE TITLE ############################################################################### st.set_page_config(page_title="Default Risk Estimation", layout="wide") ############################################################################### # GLOBALS & SESSION STATE ############################################################################### FMP_API_KEY = os.getenv("FMP_API_KEY") if "altman_results" not in st.session_state: st.session_state["altman_results"] = None if "dtd_results" not in st.session_state: st.session_state["dtd_results"] = None ############################################################################### # HELPER FUNCTIONS (Altman Z) ############################################################################### def get_fmp_json(url): """ Retrieves JSON from the specified URL and returns as a list. Omits direct mention of data source in any error messages. """ r = requests.get(url) try: data = r.json() if not isinstance(data, list): return [] return data except Exception: return [] def fetch_fmp_annual(endpoint): """ Fetches annual data from the endpoint, sorts by date if present. """ data = get_fmp_json(endpoint) df = pd.DataFrame(data) if not df.empty and 'date' in df.columns: df['date'] = pd.to_datetime(df['date']) df.sort_values('date', inplace=True) return df ############################################################################### # HELPER FUNCTIONS (Distance-to-Default) ############################################################################### def solve_merton(E, sigma_E, D, T, r): """ Merton model solver: E = A * N(d1) - D * exp(-rT) * N(d2) sigma_E = (A / E) * N(d1) * sigma_A """ def equations(vars_): A_, sigmaA_ = vars_ d1_ = (np.log(A_ / D) + (r + 0.5 * sigmaA_**2) * T) / (sigmaA_ * np.sqrt(T)) d2_ = d1_ - sigmaA_ * np.sqrt(T) eq1 = A_ * norm.cdf(d1_) - D * np.exp(-r * T) * norm.cdf(d2_) - E eq2 = sigma_E - (A_ / E) * norm.cdf(d1_) * sigmaA_ return (eq1, eq2) A_guess = E + D sigmaA_guess = sigma_E * (E / (E + D)) A_star, sigmaA_star = fsolve(equations, [A_guess, sigmaA_guess], maxfev=3000) return A_star, sigmaA_star def distance_to_default(A, D, T, r, sigmaA): """ Merton distance to default (d2): d2 = [ln(A/D) + (r - 0.5*sigmaA^2)*T] / (sigmaA * sqrt(T)) """ return (np.log(A / D) + (r - 0.5 * sigmaA**2) * T) / (sigmaA * np.sqrt(T)) ############################################################################### # ALTMAN Z-SCORE EXECUTION (From Provided Code) ############################################################################### def run_altman_zscore_calculations(ticker, years_back): """ Uses the original user-provided Altman Z code to fetch and compute partials. Returns the final DataFrame with partials and total Z-scores. """ # 1) FETCH ANNUAL STATEMENTS income_url = f"https://financialmodelingprep.com/api/v3/income-statement/{ticker}?period=annual&limit=100&apikey={FMP_API_KEY}" balance_url = f"https://financialmodelingprep.com/api/v3/balance-sheet-statement/{ticker}?period=annual&limit=100&apikey={FMP_API_KEY}" income_df = fetch_fmp_annual(income_url) balance_df = fetch_fmp_annual(balance_url) merged_bi = pd.merge(balance_df, income_df, on='date', how='inner', suffixes=('_bal','_inc')) merged_bi.sort_values('date', inplace=True) if merged_bi.empty: st.warning("No statements to analyze for this ticker/date range.") return pd.DataFrame() # 2) FILTER TO LAST X YEARS end_date = pd.Timestamp.today() start_date = end_date - relativedelta(years=years_back) merged_bi = merged_bi[(merged_bi['date'] >= start_date) & (merged_bi['date'] <= end_date)] merged_bi.sort_values('date', inplace=True) if merged_bi.empty: st.warning("No financial statements found in the chosen range.") return pd.DataFrame() # 3) FETCH HISTORICAL MARKET CAP mktcap_df = pd.DataFrame() iterations = (years_back // 5) + (1 if years_back % 5 != 0 else 0) for i in range(iterations): period_end_date = end_date - relativedelta(years=i * 5) period_start_date = period_end_date - relativedelta(years=5) if period_start_date < start_date: period_start_date = start_date mktcap_url = ( f"https://financialmodelingprep.com/api/v3/historical-market-capitalization/{ticker}" f"?from={period_start_date.date()}&to={period_end_date.date()}&apikey={FMP_API_KEY}" ) mktcap_data = get_fmp_json(mktcap_url) mktcap_period_df = pd.DataFrame(mktcap_data) if not mktcap_period_df.empty and 'date' in mktcap_period_df.columns: mktcap_period_df['date'] = pd.to_datetime(mktcap_period_df['date']) mktcap_period_df.rename(columns={'marketCap': 'historical_market_cap'}, inplace=True) mktcap_df = pd.concat([mktcap_df, mktcap_period_df], ignore_index=True) mktcap_df = mktcap_df.sort_values('date').drop_duplicates(subset=['date']) if not mktcap_df.empty and 'date' in mktcap_df.columns: mktcap_df['date'] = pd.to_datetime(mktcap_df['date']) mktcap_df = mktcap_df[(mktcap_df['date'] >= start_date) & (mktcap_df['date'] <= end_date)] mktcap_df.sort_values('date', inplace=True) else: mktcap_df = pd.DataFrame(columns=['date','historical_market_cap']) if not merged_bi.empty and not mktcap_df.empty: merged_bi = pd.merge_asof( merged_bi.sort_values('date'), mktcap_df.sort_values('date'), on='date', direction='nearest' ) else: merged_bi['historical_market_cap'] = np.nan # 4) COMPUTE PARTIAL CONTRIBUTIONS z_rows = [] for _, row in merged_bi.iterrows(): ta = row.get('totalAssets', np.nan) tl = row.get('totalLiabilities', np.nan) if pd.isnull(ta) or pd.isnull(tl) or ta == 0 or tl == 0: continue rev = row.get('revenue', 0) hist_mcap = row.get('historical_market_cap', np.nan) if pd.isnull(hist_mcap): continue tca = row.get('totalCurrentAssets', np.nan) tcl = row.get('totalCurrentLiabilities', np.nan) if pd.isnull(tca) or pd.isnull(tcl): continue wc = (tca - tcl) re = row.get('retainedEarnings', 0) ebit = row.get('operatingIncome', np.nan) if pd.isnull(ebit): ebit = row.get('ebitda', 0) X1 = wc / ta X2 = re / ta X3 = ebit / ta X4 = hist_mcap / tl X5 = rev / ta if ta != 0 else 0 # Original Z o_part1 = 1.2 * X1 o_part2 = 1.4 * X2 o_part3 = 3.3 * X3 o_part4 = 0.6 * X4 o_part5 = 1.0 * X5 z_original = o_part1 + o_part2 + o_part3 + o_part4 + o_part5 # Z'' d_part1 = 6.56 * X1 d_part2 = 3.26 * X2 d_part3 = 6.72 * X3 d_part4 = 1.05 * X4 z_double_prime = d_part1 + d_part2 + d_part3 + d_part4 # Z''' t_part1 = 3.25 * X1 t_part2 = 2.85 * X2 t_part3 = 4.15 * X3 t_part4 = 0.95 * X4 z_triple_prime_service = t_part1 + t_part2 + t_part3 + t_part4 z_rows.append({ 'date': row['date'], # Original partials 'o_part1': o_part1, 'o_part2': o_part2, 'o_part3': o_part3, 'o_part4': o_part4, 'o_part5': o_part5, 'z_original': z_original, # Z'' partials 'd_part1': d_part1, 'd_part2': d_part2, 'd_part3': d_part3, 'd_part4': d_part4, 'z_double_prime': z_double_prime, # Z''' partials 't_part1': t_part1, 't_part2': t_part2, 't_part3': t_part3, 't_part4': t_part4, 'z_triple_prime_service': z_triple_prime_service }) z_df = pd.DataFrame(z_rows) z_df.sort_values('date', inplace=True) return z_df ############################################################################### # DTD EXECUTION (From Provided Code) ############################################################################### def calculate_yearly_distance_to_default( symbol="AAPL", years_back=10, debt_method="TOTAL", risk_free_ticker="^TNX", apikey="YOUR_FMP_API_KEY", ): """ Fetches up to `years_back` years of annual data, merges market cap, debt, and risk-free yields. Then computes Merton Distance to Default for each year. Returns a DataFrame. """ end_date = date.today() start_date = end_date - dt.timedelta(days=365 * years_back) # Market cap df_mcap = pd.DataFrame() iterations = (years_back // 5) + (1 if years_back % 5 != 0 else 0) for i in range(iterations): period_end_date = end_date - dt.timedelta(days=365 * i * 5) period_start_date = period_end_date - dt.timedelta(days=365 * 5) url_mcap = ( f"https://financialmodelingprep.com/api/v3/historical-market-capitalization/" f"{symbol}?from={period_start_date}&to={period_end_date}&apikey={apikey}" ) resp_mcap = requests.get(url_mcap) data_mcap = resp_mcap.json() if resp_mcap.status_code == 200 else [] df_mcap_period = pd.DataFrame(data_mcap) df_mcap = pd.concat([df_mcap, df_mcap_period], ignore_index=True) if df_mcap.empty or "date" not in df_mcap.columns: raise ValueError("No market cap data returned. Check your inputs.") df_mcap["year"] = pd.to_datetime(df_mcap["date"]).dt.year df_mcap = ( df_mcap.groupby("year", as_index=False) .agg({"marketCap": "mean"}) .sort_values("year", ascending=False) ) # Balance Sheet url_bs = f"https://financialmodelingprep.com/api/v3/balance-sheet-statement/{symbol}?period=annual&apikey={apikey}" resp_bs = requests.get(url_bs) data_bs = resp_bs.json() if resp_bs.status_code == 200 else [] df_bs = pd.DataFrame(data_bs) if df_bs.empty or "date" not in df_bs.columns: raise ValueError("No balance sheet data returned. Check your inputs.") df_bs["year"] = pd.to_datetime(df_bs["date"]).dt.year keep_cols = ["year", "shortTermDebt", "longTermDebt", "totalDebt", "date"] df_bs = df_bs[keep_cols].sort_values("year", ascending=False) # Risk-free from yfinance rf_ticker_obj = yf.Ticker(risk_free_ticker) rf_data = rf_ticker_obj.history(start=start_date, end=end_date, auto_adjust=False) if rf_data.empty or "Close" not in rf_data.columns: raise ValueError("No valid risk-free rate data found. Check your inputs.") rf_data = rf_data.reset_index() rf_data["year"] = rf_data["Date"].dt.year rf_data = rf_data[["year", "Close"]] rf_yearly = rf_data.groupby("year", as_index=False)["Close"].mean() rf_yearly.rename(columns={"Close": "rf_yield"}, inplace=True) rf_yearly["rf_yield"] = rf_yearly["rf_yield"] / 100.0 # decimal # Merge df_all = pd.merge(df_mcap, df_bs, on="year", how="left") df_all = pd.merge(df_all, rf_yearly, on="year", how="left") # Merton each year results = [] for _, row in df_all.iterrows(): yr = row["year"] E = row["marketCap"] if pd.isna(E) or E <= 0: continue shortD = row.get("shortTermDebt", 0) or 0 longD = row.get("longTermDebt", 0) or 0 totalD = row.get("totalDebt", 0) or 0 if debt_method.upper() == "STPLUSLT": D = shortD + longD elif debt_method.upper() == "STPLUSHALFLT": D = shortD + 0.5 * longD else: D = totalD if not D or D <= 0: D = np.nan r_val = row.get("rf_yield", 0.03) from_dt = f"{yr}-01-01" to_dt = f"{yr}-12-31" url_hist = ( f"https://financialmodelingprep.com/api/v3/historical-price-full/{symbol}" f"?from={from_dt}&to={to_dt}&apikey={apikey}" ) resp_hist = requests.get(url_hist) data_hist = resp_hist.json() if resp_hist.status_code == 200 else {} daily_prices = data_hist.get("historical", []) if not daily_prices: sigma_E = 0.30 else: df_prices = pd.DataFrame(daily_prices) df_prices.sort_values("date", inplace=True) close_vals = df_prices["close"].values log_rets = np.diff(np.log(close_vals)) daily_vol = np.std(log_rets) sigma_E = daily_vol * np.sqrt(252) if sigma_E < 1e-4: sigma_E = 0.30 T = 1.0 if not np.isnan(D): try: A_star, sigmaA_star = solve_merton(E, sigma_E, D, T, r_val) dtd_value = distance_to_default(A_star, D, T, r_val, sigmaA_star) except: A_star, sigmaA_star, dtd_value = np.nan, np.nan, np.nan else: A_star, sigmaA_star, dtd_value = np.nan, np.nan, np.nan results.append({ "year": yr, "marketCap": E, "shortTermDebt": shortD, "longTermDebt": longD, "totalDebt": totalD, "chosenDebt": D, "rf": r_val, "sigma_E": sigma_E, "A_star": A_star, "sigmaA_star": sigmaA_star, "DTD": dtd_value }) result_df = pd.DataFrame(results).sort_values("year") return result_df ############################################################################### # PLOTTING HELPERS ############################################################################### def plot_zscore_figure(df, date_col, partial_cols, total_col, partial_names, total_name, title_text, zones, ticker): """ Creates stacked bar for partial contributions plus a line for the total Z. Draws shading for distress/gray/safe zones. Full width. """ fig = go.Figure() x_min = df[date_col].min() x_max = df[date_col].max() total_max = df[total_col].max() partial_sum_max = df[partial_cols].sum(axis=1).max() if not df[partial_cols].empty else 0 y_max = max(total_max, partial_sum_max, 0) * 1.2 y_min = min(df[total_col].min(), 0) * 1.2 if df[total_col].min() < 0 else 0 # Distress fig.add_shape( type="rect", x0=x_min, x1=x_max, y0=y_min, y1=zones['distress'], fillcolor="red", opacity=0.2, layer="below", line=dict(width=0) ) # Gray fig.add_shape( type="rect", x0=x_min, x1=x_max, y0=zones['gray_lower'], y1=zones['gray_upper'], fillcolor="gray", opacity=0.2, layer="below", line=dict(width=0) ) # Safe fig.add_shape( type="rect", x0=x_min, x1=x_max, y0=zones['safe'], y1=y_max, fillcolor="green", opacity=0.2, layer="below", line=dict(width=0) ) # Stacked bars for col, name, color in partial_names: fig.add_trace(go.Bar( x=df[date_col], y=df[col], name=name, marker_color=color )) # Line fig.add_trace(go.Scatter( x=df[date_col], y=df[total_col], mode='lines+markers+text', text=df[total_col].round(2), textposition='top center', textfont=dict(size=16), name=total_name, line=dict(color='white', width=2) )) fig.update_layout( title=dict( text=f"{title_text} for {ticker}", font=dict(size=26, color="white") ), legend=dict( font=dict(color="white", size=18) ), barmode="stack", template="plotly_dark", paper_bgcolor="#0e1117", plot_bgcolor="#0e1117", xaxis=dict( title="Year", tickangle=45, tickformat="%Y", dtick="M12", showgrid=True, gridcolor="rgba(255, 255, 255, 0.1)" ), yaxis=dict( title="Z-Score Contribution", showgrid=True, gridcolor="rgba(255, 255, 255, 0.1)" ), margin=dict(l=40, r=40, t=80, b=80), height=700 ) st.plotly_chart(fig, use_container_width=True) ############################################################################### # STREAMLIT APP ############################################################################### st.title("Bankruptcy Risk Estimation") #st.write("## Overview") st.write("This tool assesses a firm's bankruptcy and default risk using two widely recognized models:") st.write("1) **Altman Z-Score**: A financial distress predictor based on accounting ratios.") st.write("2) **Merton Distance-to-Default (DTD)**: A market-based risk measure derived from option pricing theory.") #st.write("Select a page from the sidebar to explore each model’s estimates and methodology.") # Sidebar for user inputs with st.sidebar: st.write("## Input Parameters") # Page selector in an open-by-default expander with st.expander("Page Selector", expanded=True): page = st.radio("Select Page:", ["Altman Z Score", "Distance-to-Default"]) with st.expander("General Settings", expanded=True): ticker = st.text_input("Ticker", value="AAPL", help="Enter a valid stock ticker") years_back = st.number_input("Years back", min_value=1, max_value=30, value=10, step=1, help="How many years of data to retrieve?") run_button = st.button("Run Analysis", help="Fetch data and compute metrics") # If user clicks to run, fetch data if run_button: # Altman Z z_data = run_altman_zscore_calculations(ticker, years_back) st.session_state["altman_results"] = z_data # DTD try: dtd_df = calculate_yearly_distance_to_default( symbol=ticker, years_back=years_back, debt_method="TOTAL", risk_free_ticker="^TNX", apikey=FMP_API_KEY ) st.session_state["dtd_results"] = dtd_df except ValueError: st.warning("No valid data was returned. Check your inputs.") ############################################################################### # PAGE 1: ALTMAN Z ############################################################################### if page == "Altman Z Score": z_df = st.session_state.get("altman_results", None) if z_df is None or z_df.empty: st.info("Select Page, input the paramters and click 'Run Analysis' on the sidebar.") else: # Original #st.subheader("Original Altman Z-Score (1968)") with st.expander("Methodology: Original Altman Z-Score (1968)", expanded=False): st.write("The **Altman Z-Score** is a financial distress prediction model developed by Edward Altman in 1968. It combines five financial ratios to assess the likelihood of corporate bankruptcy.") # Formula st.latex(r"Z = 1.2 \times X_1 + 1.4 \times X_2 + 3.3 \times X_3 + 0.6 \times X_4 + 1.0 \times X_5") # Definitions of variables st.latex(r"X_1 = \frac{\text{Working Capital}}{\text{Total Assets}}") st.write("**Liquidity (X₁)**: Measures short-term financial health by comparing working capital to total assets. Higher values suggest better liquidity and lower default risk.") st.latex(r"X_2 = \frac{\text{Retained Earnings}}{\text{Total Assets}}") st.write("**Accumulated Profitability (X₂)**: Indicates the proportion of assets financed through retained earnings. Firms with strong retained earnings are less dependent on external financing.") st.latex(r"X_3 = \frac{\text{EBIT}}{\text{Total Assets}}") st.write("**Earnings Strength (X₃)**: EBIT (Earnings Before Interest and Taxes) relative to total assets reflects operating profitability and efficiency.") st.latex(r"X_4 = \frac{\text{Market Value of Equity}}{\text{Total Liabilities}}") st.write("**Leverage (X₄)**: Compares a firm's market capitalization to its total liabilities. A higher ratio suggests lower financial risk, as equity holders have a stronger claim.") st.latex(r"X_5 = \frac{\text{Revenue}}{\text{Total Assets}}") st.write("**Asset Turnover (X₅)**: Assesses how efficiently a company generates revenue from its assets. High turnover suggests better asset utilization.") # Academic Justification st.write("##### Academic Justification") st.write( "The Altman Z-Score was developed using **discriminant analysis** on a dataset of manufacturing firms. " "The Altman Z-Score was found to correctly predict bankruptcy **72-80%** of the time in original studies, typically with a **one-year lead time** before actual default." "The model’s strength lies in its ability to quantify financial health across multiple dimensions—liquidity, profitability, leverage, and efficiency." ) # Interpretation st.write("##### Interpretation") st.write( "**Z > 2.99**: Company is considered financially healthy (Low risk of bankruptcy). \n" "**1.81 ≤ Z ≤ 2.99**: 'Gray Area' where financial stability is uncertain. \n" "**Z < 1.81**: High financial distress, indicating potential bankruptcy risk." ) # Downsides / Limitations st.write("##### Limitations") st.write( "- Developed using **only manufacturing firms**, which limits its applicability to other industries.\n" "- Uses **historical accounting data**, which may not reflect current market conditions.\n" "- Market Value of Equity (X₄) makes the score **sensitive to stock price volatility**.\n" "- Does not incorporate forward-looking indicators such as market sentiment or macroeconomic risks." ) orig_partial_names = [ ('o_part1', "1.2 × (WC/TA)", 'blue'), ('o_part2', "1.4 × (RE/TA)", 'orange'), ('o_part3', "3.3 × (EBIT/TA)", 'green'), ('o_part4', "0.6 × (MktCap/TL)", 'red'), ('o_part5', "1.0 × (Rev/TA)", 'purple'), ] orig_zones = { 'distress': 1.81, 'gray_lower': 1.81, 'gray_upper': 2.99, 'safe': 2.99 } plot_zscore_figure( df=z_df, date_col='date', partial_cols=['o_part1','o_part2','o_part3','o_part4','o_part5'], total_col='z_original', partial_names=orig_partial_names, total_name="Original Z (Total)", title_text="Original Altman Z-Score (1968)", zones=orig_zones, ticker=ticker ) with st.expander("Interpretation", expanded=False): # EXACT TEXT from user code (Original Z) latest_z = z_df['z_original'].iloc[-1] # For time-series logic: first_val = z_df['z_original'].iloc[0] if latest_z > first_val: trend = "increased" elif latest_z < first_val: trend = "decreased" else: trend = "remained the same" min_val = z_df['z_original'].min() max_val = z_df['z_original'].max() min_idx = z_df['z_original'].idxmin() max_idx = z_df['z_original'].idxmax() min_year = z_df.loc[min_idx, 'date'].year max_year = z_df.loc[max_idx, 'date'].year st.write("**--- Interpretation for Original Z-Score ---") st.write(f"Over the entire time series, the Z-Score has {trend}.") st.write(f"The lowest value was {min_val:.2f} in {min_year}.") st.write(f"The highest value was {max_val:.2f} in {max_year}.") if latest_z < orig_zones['distress']: st.write("Current reading is in distress zone. This suggests high financial risk.") elif latest_z < orig_zones['gray_upper']: st.write("Current reading is in the gray area. This signals mixed financial stability.") else: st.write("Current reading is in the safe zone. This implies a stronger financial condition.") latest_data = z_df.iloc[-1] orig_partials = { 'o_part1': latest_data['o_part1'], 'o_part2': latest_data['o_part2'], 'o_part3': latest_data['o_part3'], 'o_part4': latest_data['o_part4'], 'o_part5': latest_data['o_part5'] } key_driver = max(orig_partials, key=orig_partials.get) if key_driver == 'o_part1': st.write("The most significant factor is Working Capital. This suggests the company's ability to cover short-term obligations with current assets. ") st.write("A high contribution from Working Capital means strong liquidity, but too much could indicate inefficient capital allocation. ") st.write("If the company holds excess current assets, it may not be deploying resources efficiently for growth.") elif key_driver == 'o_part2': st.write("The most significant factor is Retained Earnings. This reflects the company's history of profitability and reinvestment. ") st.write("A high retained earnings contribution indicates that past profits have been reinvested rather than paid out as dividends. ") st.write("This can be a positive sign of financial stability, but if earnings retention is excessive, investors may question the company’s capital allocation strategy.") elif key_driver == 'o_part3': st.write("The most significant factor is EBIT (Earnings Before Interest and Taxes). This underscores the company’s ability to generate profits from operations. ") st.write("A high EBIT contribution suggests that core business activities are profitable and drive financial health. ") st.write("However, if EBIT dominates the Z-Score, it may mean the company is heavily reliant on operational earnings, making it vulnerable to downturns in revenue.") elif key_driver == 'o_part4': st.write("The most significant factor is Market Cap to Liabilities. This reflects investor confidence in the company’s future performance relative to its debt burden. ") st.write("A strong market cap contribution means investors perceive the company as having high equity value compared to liabilities, reducing bankruptcy risk. ") st.write("However, if this is the dominant driver, financial stability may be tied to market sentiment, which can be volatile.") elif key_driver == 'o_part5': st.write("The most significant factor is Revenue. This indicates that top-line growth is a major driver of financial stability. ") st.write("A high revenue contribution is positive if it translates to strong margins, but if costs are rising at the same pace, profitability may not improve. ") st.write("If revenue dominates the Z-Score, the company must ensure sustainable cost management and profitability to maintain financial strength.") st.write("If management seeks to lower this Z-Score, they might reduce liquidity or raise liabilities.") st.write("A higher liability base or weaker earnings can press the score downward.") # Z'' #st.subheader("Z'' (1993, Non-Manufacturing)") with st.expander("Methodology: Z'' (1993, Non-Manufacturing)", expanded=False): st.write("The **Z''-Score (1993)** is an adaptation of the original Altman Z-Score, developed to assess financial distress in **non-manufacturing firms**, particularly service and retail sectors. It removes the revenue-based efficiency metric (X₅) and adjusts weightings to better fit firms with different asset structures.") # Formula st.latex(r"Z'' = 6.56 \times X_1 + 3.26 \times X_2 + 6.72 \times X_3 + 1.05 \times X_4") # Definitions of variables st.latex(r"X_1 = \frac{\text{Working Capital}}{\text{Total Assets}}") st.write("**Liquidity (X₁)**: Measures short-term financial flexibility. Firms with higher working capital relative to assets are better positioned to meet short-term obligations.") st.latex(r"X_2 = \frac{\text{Retained Earnings}}{\text{Total Assets}}") st.write("**Cumulative Profitability (X₂)**: Higher retained earnings relative to total assets suggest long-term profitability and financial resilience.") st.latex(r"X_3 = \frac{\text{EBIT}}{\text{Total Assets}}") st.write("**Operating Profitability (X₃)**: Measures how efficiently a company generates profit from its assets, reflecting core business strength.") st.latex(r"X_4 = \frac{\text{Market Value of Equity}}{\text{Total Liabilities}}") st.write("**Leverage (X₄)**: A firm's ability to cover its liabilities with market value equity. A lower ratio suggests greater financial risk.") # Academic Justification st.write("##### Academic Justification") st.write( "The original Z-Score was optimized for **manufacturing firms**, making it less effective for firms with fewer tangible assets. " "Z'' (1993) improves bankruptcy prediction for **service and retail firms**, as it excludes the revenue turnover component (X₅) " "and places greater emphasis on profitability and liquidity. Empirical studies found Z'' to be **better suited for firms with lower capital intensity**." ) # Interpretation st.write("##### Interpretation") st.write( "**Z'' > 2.60**: Firm is financially stable, with low bankruptcy risk. \n" "**1.10 ≤ Z'' ≤ 2.60**: 'Gray Area'—financial condition is uncertain. \n" "**Z'' < 1.10**: Firm is in financial distress, at a higher risk of default." ) # Downsides / Limitations st.write("##### Limitations") st.write( "- Developed for **non-manufacturing firms**, but may not be applicable to banks or financial institutions.\n" "- Still **relies on historical accounting data**, which may not fully capture real-time financial conditions.\n" "- Market-based variable (X₄) makes the score **sensitive to stock market fluctuations**.\n" "- Does not consider external macroeconomic risks or qualitative factors like management decisions." ) double_partial_names = [ ('d_part1', "6.56 × (WC/TA)", 'blue'), ('d_part2', "3.26 × (RE/TA)", 'orange'), ('d_part3', "6.72 × (EBIT/TA)", 'green'), ('d_part4', "1.05 × (MktCap/TL)", 'red'), ] double_zones = { 'distress': 1.1, 'gray_lower': 1.1, 'gray_upper': 2.6, 'safe': 2.6 } plot_zscore_figure( df=z_df, date_col='date', partial_cols=['d_part1','d_part2','d_part3','d_part4'], total_col='z_double_prime', partial_names=double_partial_names, total_name="Z'' (Total)", title_text="Z'' (1993, Non-Manufacturing)", zones=double_zones, ticker=ticker ) with st.expander("Interpretation", expanded=False): latest_z_double = z_df['z_double_prime'].iloc[-1] first_val = z_df['z_double_prime'].iloc[0] if latest_z_double > first_val: trend_d = "increased" elif latest_z_double < first_val: trend_d = "decreased" else: trend_d = "remained the same" min_val_d = z_df['z_double_prime'].min() max_val_d = z_df['z_double_prime'].max() min_idx_d = z_df['z_double_prime'].idxmin() max_idx_d = z_df['z_double_prime'].idxmax() min_year_d = z_df.loc[min_idx_d, 'date'].year max_year_d = z_df.loc[max_idx_d, 'date'].year st.write("**--- Interpretation for Z'' (Non-Manufacturing) ---**") st.write(f"Over the chosen period, the Z-Score has {trend_d}.") st.write(f"Lowest: {min_val_d:.2f} in {min_year_d}.") st.write(f"Highest: {max_val_d:.2f} in {max_year_d}.") if latest_z_double < double_zones['distress']: st.write("Current reading is in distress zone. Financial risk is elevated.") elif latest_z_double < double_zones['gray_upper']: st.write("Current reading is in the gray zone. Financial signals are not clear.") else: st.write("Current reading is in the safe zone. Financial picture seems stable.") latest_data_double = z_df.iloc[-1] double_partials = { 'd_part1': latest_data_double['d_part1'], 'd_part2': latest_data_double['d_part2'], 'd_part3': latest_data_double['d_part3'], 'd_part4': latest_data_double['d_part4'] } key_driver_double = max(double_partials, key=double_partials.get) if key_driver_double == 'd_part1': st.write("The key factor is Working Capital. This measures the company’s ability to cover short-term liabilities with current assets.") st.write("A strong working capital contribution means the company has a healthy liquidity buffer, reducing short-term financial risk.") st.write("However, excessive working capital can signal inefficient capital deployment, where too much cash is tied up in receivables or inventory.") elif key_driver_double == 'd_part2': st.write("The key factor is Retained Earnings. This represents accumulated profits that have been reinvested rather than distributed as dividends.") st.write("A high retained earnings contribution suggests financial discipline and the ability to self-finance operations, reducing reliance on external funding.") st.write("However, if retained earnings are excessive, investors may question whether the company is efficiently reinvesting in growth opportunities or hoarding cash.") elif key_driver_double == 'd_part3': st.write("The key factor is EBIT (Earnings Before Interest and Taxes). This highlights the strength of the company’s core operations in driving profitability.") st.write("A high EBIT contribution is a strong indicator of financial health, as it suggests the company generates consistent earnings before financing costs.") st.write("However, if EBIT is the dominant driver, the company may be vulnerable to economic downturns or market shifts that impact its ability to sustain margins.") elif key_driver_double == 'd_part4': st.write("The key factor is Market Cap vs. Liabilities. This shows how the market values the company relative to its total debt obligations.") st.write("A strong contribution from this metric suggests investor confidence in the company’s financial future, lowering perceived bankruptcy risk.") st.write("However, if market sentiment is the main driver, the company could be vulnerable to stock price fluctuations rather than underlying business fundamentals.") st.write("To decrease this score, raising debt or reducing EBIT can cause the drop.") st.write("An increase in liabilities often pulls down the ratio.") # Z''' #st.subheader("Z''' (2023, Service/Tech)") with st.expander("Methodology: Z''' (2023, Service/Tech)", expanded=False): st.write("The **Z'''-Score (2023)** is a further refinement of the Altman Z models, designed to assess financial distress in **modern service and technology firms**. This version accounts for the **intangible asset-heavy nature** of these companies, where traditional balance sheet metrics may not fully capture financial health.") # Formula st.latex(r"Z''' = 3.25 \times X_1 + 2.85 \times X_2 + 4.15 \times X_3 + 0.95 \times X_4") # Definitions of variables st.latex(r"X_1 = \frac{\text{Working Capital}}{\text{Total Assets}}") st.write("**Liquidity (X₁)**: Measures short-term financial flexibility. A strong working capital position helps firms cover immediate liabilities.") st.latex(r"X_2 = \frac{\text{Retained Earnings}}{\text{Total Assets}}") st.write("**Accumulated Profitability (X₂)**: Indicates the extent to which a firm’s assets are funded by retained earnings rather than external debt or equity.") st.latex(r"X_3 = \frac{\text{EBIT}}{\text{Total Assets}}") st.write("**Core Earnings Strength (X₃)**: Measures profitability before interest and taxes, reflecting operational efficiency.") st.latex(r"X_4 = \frac{\text{Market Value of Equity}}{\text{Total Liabilities}}") st.write("**Market Confidence (X₄)**: Assesses how the market values the firm relative to its total liabilities. Higher values suggest lower financial risk.") # Academic Justification st.write("##### Academic Justification") st.write( "Unlike traditional manufacturing firms, **service and tech firms rely heavily on intangible assets** (e.g., software, R&D, brand equity), " "which are often not reflected on the balance sheet. **Z''' (2023) adjusts for this** by rebalancing weightings to better account for profitability " "and market valuation. It provides a more relevant measure for industries where physical assets play a reduced role in financial stability." ) # Interpretation st.write("##### Interpretation") st.write( "**Z''' > 2.90**: Firm is financially stable, with a low probability of distress. \n" "**1.50 ≤ Z''' ≤ 2.90**: 'Gray Area'—financial condition is uncertain. \n" "**Z''' < 1.50**: Firm is in financial distress, with an elevated bankruptcy risk." ) # Downsides / Limitations st.write("##### Limitations") st.write( "- Developed for **service and tech firms**, but may not generalize well to capital-intensive industries.\n" "- **Still based on historical financial data**, which may lag behind real-time market shifts.\n" "- Market value component (X₄) **introduces volatility**, making results sensitive to stock price swings.\n" "- Does not explicitly factor in **R&D investment or future revenue potential**, which are key in tech sectors." ) triple_partial_names = [ ('t_part1', "3.25 × (WC/TA)", 'blue'), ('t_part2', "2.85 × (RE/TA)", 'orange'), ('t_part3', "4.15 × (EBIT/TA)", 'green'), ('t_part4', "0.95 × (MktCap/TL)", 'red'), ] triple_zones = { 'distress': 1.5, 'gray_lower': 1.5, 'gray_upper': 2.9, 'safe': 2.9 } plot_zscore_figure( df=z_df, date_col='date', partial_cols=['t_part1','t_part2','t_part3','t_part4'], total_col='z_triple_prime_service', partial_names=triple_partial_names, total_name="Z''' (Total)", title_text="Z''' (2023, Service/Tech)", zones=triple_zones, ticker=ticker ) with st.expander("Interpretation", expanded=False): latest_z_triple = z_df['z_triple_prime_service'].iloc[-1] first_val_t = z_df['z_triple_prime_service'].iloc[0] if latest_z_triple > first_val_t: trend_t = "increased" elif latest_z_triple < first_val_t: trend_t = "decreased" else: trend_t = "remained the same" min_val_t = z_df['z_triple_prime_service'].min() max_val_t = z_df['z_triple_prime_service'].max() min_idx_t = z_df['z_triple_prime_service'].idxmin() max_idx_t = z_df['z_triple_prime_service'].idxmax() min_year_t = z_df.loc[min_idx_t, 'date'].year max_year_t = z_df.loc[max_idx_t, 'date'].year st.write("**--- Interpretation for Z''' (Service/Tech) ---**") st.write(f"Across the selected years, this Z-Score has {trend_t}.") st.write(f"Minimum was {min_val_t:.2f} in {min_year_t}.") st.write(f"Maximum was {max_val_t:.2f} in {max_year_t}.") if latest_z_triple < triple_zones['distress']: st.write("Current reading is in the distress zone. This indicates possible financial strain.") elif latest_z_triple < triple_zones['gray_upper']: st.write("Current reading is in the gray range. This means uncertain financial signals.") else: st.write("Current reading is in the safe zone. Financial health looks positive.") latest_data_triple = z_df.iloc[-1] triple_partials = { 't_part1': latest_data_triple['t_part1'], 't_part2': latest_data_triple['t_part2'], 't_part3': latest_data_triple['t_part3'], 't_part4': latest_data_triple['t_part4'] } key_driver_triple = max(triple_partials, key=triple_partials.get) if key_driver_triple == 't_part1': st.write("Working Capital stands out as the main influence, emphasizing the company's short-term financial flexibility.") st.write("A strong working capital contribution indicates a well-managed balance between current assets and liabilities, reducing liquidity risk.") st.write("However, if too much capital is tied up in cash or inventory, it may suggest inefficiency in deploying assets for growth.") elif key_driver_triple == 't_part2': st.write("Retained Earnings plays the biggest role, highlighting the company's ability to reinvest past profits into future growth.") st.write("A high retained earnings contribution suggests the company has a history of profitability and financial discipline, reducing reliance on external financing.") st.write("However, if retained earnings dominate, it raises questions about whether capital is allocated effectively.") elif key_driver_triple == 't_part3': st.write("EBIT is the dominant factor, meaning the company’s operational efficiency is the primary driver of financial stability.") st.write("A strong EBIT contribution indicates that core business activities are profitable. This supports the firm's financial health.") st.write("But if EBIT is the largest driver, the company may be heavily dependent on margins, making it vulnerable to cost pressures.") elif key_driver_triple == 't_part4': st.write("Market Cap vs. Liabilities leads, suggesting that investor confidence and market valuation are key drivers of financial stability.") st.write("A high contribution from this metric means the company’s equity is valued significantly higher than its liabilities.") st.write("Reliance on market sentiment can expose the firm to stock price volatility.") st.write("If the goal is to reduce this Z-Score, rising debt or shrinking EBIT will push it downward.") st.write("Lower liquidity or lower equity value can also move the score lower.") # Show raw data with st.expander("Raw Altman Z Data", expanded=False): st.dataframe(z_df) ############################################################################### # PAGE 2: DISTANCE TO DEFAULT ############################################################################### if page == "Distance-to-Default": dtd_df = st.session_state.get("dtd_results", None) if dtd_df is None or dtd_df.empty: st.info("Select Page, input the paramters and click 'Run Analysis' on the sidebar.") else: valid_df = dtd_df.dropna(subset=["chosenDebt", "A_star", "sigmaA_star", "DTD"]) if valid_df.empty: st.warning("No valid rows for Merton calculations in the chosen range.") else: with st.expander("Methodology: Merton Distance-to-Default (DTD)", expanded=False): st.write( "The **Distance-to-Default (DTD)** is a structural credit risk model based on Merton's (1974) option pricing theory. " "It estimates the likelihood that a firm's asset value will fall below its debt obligations, triggering default." ) # Merton Model Core Equations st.latex(r"V_t = S_t + D_t") st.write("**Firm Value (Vₜ)**: The total market value of the firm, consisting of equity (Sₜ) and debt (Dₜ).") st.latex(r"\sigma_V = \frac{S_t}{V_t} \sigma_S") st.write("**Asset Volatility (σ_V)**: Derived from the observed equity volatility (σ_S), using the Merton model.") st.latex(r"d_1 = \frac{\ln{\left(\frac{V_t}{D_t}\right)} + \left( r - \frac{1}{2} \sigma_V^2 \right)T}{\sigma_V \sqrt{T}}") st.latex(r"d_2 = d_1 - \sigma_V \sqrt{T}") st.write("**Merton's d₁ and d₂**: Standardized metrics capturing the firm's asset dynamics relative to debt.") st.latex(r"\text{DTD} = d_2 = \frac{\ln{\left(\frac{V_t}{D_t}\right)} + \left( r - \frac{1}{2} \sigma_V^2 \right)T}{\sigma_V \sqrt{T}}") st.write("**Distance-to-Default (DTD)**: Measures how many standard deviations the firm's asset value is from the default threshold (Dₜ).") # Academic Justification st.write("##### Academic Justification") st.write( "Merton's model treats a firm's equity as a **call option** on its assets, where default occurs if asset value (Vₜ) " "falls below debt (Dₜ) at time T. **DTD quantifies this probability** by measuring how far the firm is from this threshold, " "adjusting for volatility. Studies show that **lower DTD values correlate with higher default probabilities**, making it " "a key metric for credit risk analysis in corporate finance and banking." ) # Interpretation st.write("##### Interpretation") st.write( "**DTD > 2.0**: Low probability of default (strong financial health). \n" "**1.0 ≤ DTD ≤ 2.0**: Moderate risk—firm is financially stable but should be monitored. \n" "**DTD < 1.0**: High default risk—firm is approaching financial distress. \n" "**DTD < 0.0**: Extreme risk—firm’s asset value is below its debt obligations." ) # Downsides / Limitations st.write("##### Limitations") st.write( "- **Assumes market efficiency**, meaning it relies heavily on accurate stock price movements.\n" "- **Volatility estimates impact accuracy**, as market fluctuations can distort results.\n" "- **Ignores liquidity constraints**—a firm may default due to cash flow problems, even if assets exceed liabilities.\n" "- **Not designed for financial institutions**, where leverage and risk dynamics differ significantly.\n" "- **Short-term focused**, making it less predictive for long-term financial health." ) #st.subheader("Annual Distance to Default (Merton Model)") # Chart 1 fig_time = go.Figure() fig_time.add_trace( go.Scatter( x=dtd_df["year"], y=dtd_df["DTD"], mode="lines+markers", name="Distance to Default" ) ) fig_time.update_layout( title=f"{ticker} Annual Distance to Default (Merton Model)", title_font=dict(size=26, color="white"), xaxis_title="Year", yaxis_title="Distance to Default (d2)", template="plotly_dark", paper_bgcolor="#0e1117", plot_bgcolor="#0e1117", xaxis=dict( showgrid=True, gridcolor="rgba(255, 255, 255, 0.1)" ), yaxis=dict( showgrid=True, gridcolor="rgba(255, 255, 255, 0.1)" ) ) st.plotly_chart(fig_time, use_container_width=True) # --- Dynamic Interpretation for Chart 1 (Exact user code) --- with st.expander("Interpretation", expanded=False): dtd_series = dtd_df["DTD"].dropna() if len(dtd_series) > 1: first_val = dtd_series.iloc[0] last_val = dtd_series.iloc[-1] trend_str = "increased" if last_val > first_val else "decreased" if last_val < first_val else "remained stable" min_val = dtd_series.min() max_val = dtd_series.max() min_yr = dtd_df.loc[dtd_series.idxmin(), "year"] max_yr = dtd_df.loc[dtd_series.idxmax(), "year"] st.write("Dynamic Interpretation for Annual Distance to Default:") st.write(f"**1) The time series shows that DTD has {trend_str} from {first_val:.2f} to {last_val:.2f}.**") st.write(f"**2) The lowest DTD was {min_val:.2f} in {min_yr}, and the highest was {max_val:.2f} in {max_yr}.**") if last_val < 0: st.write(" Current DTD is negative. The firm may be in distress territory, implying higher default risk.") elif last_val < 1: st.write(" Current DTD is below 1. This suggests caution, as default risk is higher than comfortable.") elif last_val < 2: st.write(" Current DTD is between 1 and 2. This is moderate territory. Risk is not extreme but warrants monitoring.") else: st.write(" Current DTD is above 2. This generally indicates safer conditions and lower default probability.") else: st.write("DTD time series is insufficient for a dynamic interpretation.") # Chart 2: Distribution #st.subheader("Distribution of Simulated Distance-to-Default") latest_data = valid_df.iloc[-1] A_star = latest_data["A_star"] sigmaA_star = latest_data["sigmaA_star"] D = latest_data["chosenDebt"] r = latest_data["rf"] T = 1.0 dtd_value = latest_data["DTD"] num_simulations = 10000 A_simulated = np.random.normal(A_star, sigmaA_star * A_star, num_simulations) A_simulated = np.where(A_simulated > 0, A_simulated, np.nan) DTD_simulated = (np.log(A_simulated / D) + (r - 0.5 * sigmaA_star**2) * T) / (sigmaA_star * np.sqrt(T)) DTD_simulated = DTD_simulated[~np.isnan(DTD_simulated)] fig_hist = ff.create_distplot( [DTD_simulated], ["Simulated DTD"], show_hist=True, show_rug=False, curve_type='kde' ) fig_hist.add_vline( x=dtd_value, line=dict(color="red", dash="dash"), annotation_text=f"Actual DTD = {dtd_value:.2f}" ) fig_hist.update_layout( title=f"{ticker} Distribution of Simulated Distance-to-Default (DTD)", title_font=dict(size=26, color="white"), xaxis_title="Distance-to-Default (DTD)", yaxis_title="Frequency", template="plotly_dark", paper_bgcolor="#0e1117", plot_bgcolor="#0e1117", xaxis=dict( showgrid=True, gridcolor="rgba(255, 255, 255, 0.1)" ), yaxis=dict( showgrid=True, gridcolor="rgba(255, 255, 255, 0.1)" ) ) st.plotly_chart(fig_hist, use_container_width=True) # --- Dynamic Interpretation for Chart 2 (Exact user code) --- with st.expander("Interpretation", expanded=False): mean_sim = np.mean(DTD_simulated) median_sim = np.median(DTD_simulated) st.write("\n--- Dynamic Interpretation for DTD Distribution ---") st.write(f"**1) The mean simulated Distance-to-Default (DTD) is {mean_sim:.2f}, while the median is {median_sim:.2f}.**") if mean_sim < 0: st.write(" On average, the simulations suggest the firm is in distress. A negative mean DTD implies that, in many scenarios, asset value falls below debt obligations.") st.write(" This significantly raises default risk, indicating a high probability of financial distress under typical market conditions.") elif mean_sim < 1: st.write(" A large portion of simulations yield a DTD below 1, signaling heightened risk. The firm’s financial cushion against default is thin.") st.write(" Companies in this range often face higher borrowing costs and investor skepticism, as they are perceived as more vulnerable to downturns.") elif mean_sim < 2: st.write(" The majority of simulations fall between 1 and 2, meaning the firm is not in immediate danger but isn’t fully secure either.") st.write(" This suggests moderate financial health. While not at crisis levels, management should remain cautious about leverage and volatility.") else: st.write(" The distribution is mostly above 2, implying that, under most scenarios, the firm maintains a strong buffer against default.") st.write(" Companies in this range generally enjoy greater financial stability, better credit ratings, and lower risk premiums.") if dtd_value < mean_sim: st.write(f"**2) The actual DTD ({dtd_value:.2f}) is below the simulation average ({mean_sim:.2f}).**") st.write(" This suggests that the real-world financial position of the company is weaker than the average simulated outcome.") st.write(" It may imply that recent market conditions or company-specific factors have increased risk beyond what the model predicts.") st.write(" Management might need to reinforce liquidity or reassess capital structure to avoid sliding into higher-risk territory.") else: st.write(f"2) The actual DTD ({dtd_value:.2f}) is above the simulation average ({mean_sim:.2f}).") st.write(" This is a positive signal, suggesting that real-world financial conditions are better than the typical simulated scenario.") st.write(" The firm may have a stronger-than-expected balance sheet or be benefiting from favorable market conditions.") st.write(" While this is reassuring, it is important to monitor whether this stability is due to structural financial strength or short-term market factors.") # Chart 3: Sensitivity of DTD to Asset Value #st.subheader("Sensitivity of DTD to Asset Value") asset_range = np.linspace(D, 1.1 * A_star, 200) dtd_asset = (np.log(asset_range / D) + (r - 0.5 * sigmaA_star**2) * T) / (sigmaA_star * np.sqrt(T)) fig_asset = go.Figure() fig_asset.add_trace( go.Scatter( x=asset_range, y=dtd_asset, mode='lines', name="DTD vs. Asset Value", line=dict(color="blue") ) ) fig_asset.add_vline( x=A_star, line=dict(color="red", dash="dash"), annotation_text=f"Estimated A = {A_star:,.2f}" ) fig_asset.update_layout( title=f"{ticker} Sensitivity of DTD to Variation in Asset Value", title_font=dict(size=26, color="white"), xaxis_title="Asset Value (A)", yaxis_title="Distance-to-Default (d2)", xaxis_type="log", template="plotly_dark", paper_bgcolor="#0e1117", plot_bgcolor="#0e1117", xaxis=dict( showgrid=True, gridcolor="rgba(255, 255, 255, 0.1)" ), yaxis=dict( showgrid=True, gridcolor="rgba(255, 255, 255, 0.1)" ) ) st.plotly_chart(fig_asset, use_container_width=True) # --- Dynamic Interpretation for Chart 3 (Exact user code) --- with st.expander("Interpretation", expanded=False): dtd_lowA = dtd_asset[0] dtd_highA = dtd_asset[-1] st.write("\nDynamic Interpretation for Asset Value Sensitivity:") st.write(f"**1) At the lower bound (A = {asset_range[0]:,.2f}), DTD is {dtd_lowA:.2f}.**") st.write(f"**2) At the higher bound (A = {asset_range[-1]:,.2f}), DTD rises to {dtd_highA:.2f}.**") if dtd_highA > 2: st.write(" If asset value grows, the firm gains a comfortable buffer against default.") else: st.write(" Even at higher asset values, default risk remains moderate. Growth alone may not guarantee safety.") # Chart 4: Sensitivity of DTD to Debt Variation #st.subheader("Sensitivity of DTD to Debt Variation") debt_range = np.linspace(0.1 * D, 1.2 * A_star, 300) dtd_debt = (np.log(A_star / debt_range) + (r - 0.5 * sigmaA_star**2) * T) / (sigmaA_star * np.sqrt(T)) fig_debt = go.Figure() fig_debt.add_trace( go.Scatter( x=debt_range, y=dtd_debt, mode='lines', name="DTD vs. Debt", line=dict(color="green") ) ) fig_debt.add_vline( x=D, line=dict(color="red", dash="dash"), annotation_text=f"Estimated D = {D:,.2f}" ) fig_debt.update_layout( title=f"{ticker} Sensitivity of DTD to Variation in Debt (Extended Range)", title_font=dict(size=26, color="white"), xaxis_title="Debt (D)", yaxis_title="Distance-to-Default (d2)", xaxis_type="log", template="plotly_dark", paper_bgcolor="#0e1117", plot_bgcolor="#0e1117", xaxis=dict( showgrid=True, gridcolor="rgba(255, 255, 255, 0.1)" ), yaxis=dict( showgrid=True, gridcolor="rgba(255, 255, 255, 0.1)" ) ) st.plotly_chart(fig_debt, use_container_width=True) # --- Dynamic Interpretation for Chart 4 (Exact user code) --- with st.expander("Interpretation", expanded=False): dtd_lowD = dtd_debt[0] dtd_highD = dtd_debt[-1] st.write("\n--- Dynamic Interpretation for Debt Variation ---") st.write(f"**1) At lower debt levels (D ≈ {debt_range[0]:,.2f}), the estimated Distance-to-Default (DTD) is {dtd_lowD:.2f}.**") st.write(f"**2) At higher debt levels (D ≈ {debt_range[-1]:,.2f}), the estimated DTD drops to {dtd_highD:.2f}.**") if dtd_lowD > 2: st.write(" With lower debt, the firm has a strong financial cushion. A DTD above 2 typically indicates low default risk.") st.write(" This suggests the company could sustain economic downturns or earnings declines without significantly increasing its probability of distress.") st.write(" In this range, the firm may enjoy better credit ratings, lower borrowing costs, and greater investor confidence.") elif 1 < dtd_lowD <= 2: st.write(" Even with reduced debt, the firm remains in a moderate risk zone. While the probability of default is not alarming, it isn't fully secure.") st.write(" This suggests that other financial pressures—such as earnings volatility or low asset returns—might be limiting the risk buffer.") st.write(" Maintaining a balanced capital structure with prudent debt management will be key to ensuring financial stability.") else: st.write(" Despite lowering debt, the firm remains in a high-risk category. This indicates that other financial weaknesses, such as low asset returns or high volatility, are still dominant.") st.write(" The company may need a more aggressive strategy to strengthen its financial position, such as improving earnings stability or reducing operational risks.") if dtd_highD < 0: st.write(" At significantly higher debt levels, the model suggests a **negative DTD**, which signals extreme financial distress.") st.write(" This implies that, under this scenario, the company's total asset value would likely fall below its debt obligations.") st.write(" If this situation were to materialize, the company would be seen as highly vulnerable, potentially leading to credit downgrades or refinancing difficulties.") elif 0 <= dtd_highD < 1: st.write(" With higher debt, DTD drops to below 1, meaning the firm is dangerously close to default.") st.write(" A DTD below 1 indicates that even small negative shocks to asset value could push the firm into financial distress.") st.write(" This could lead to increased borrowing costs, investor concerns, and potential restrictions on raising further capital.") elif 1 <= dtd_highD < 2: st.write(" The firm’s risk profile worsens with higher debt, but it remains in the moderate zone. The probability of distress increases but is not immediately alarming.") st.write(" Companies in this range often need to manage debt maturities carefully and ensure steady cash flow generation to avoid further deterioration.") else: st.write(" Even at a higher debt level, the firm maintains a strong buffer (DTD > 2).") st.write(" This suggests the company has **enough asset value or earnings strength to comfortably manage the additional leverage**.") st.write(" However, increasing debt too aggressively, even in a safe zone, could reduce financial flexibility in downturns.") # Chart 5: Asset Value vs. Debt #st.subheader("Asset Value vs. Default Point (Debt)") fig_bar = go.Figure() fig_bar.add_trace( go.Bar( x=["Asset Value (A)", "Debt (D)"], y=[A_star, D], marker=dict(color=["blue", "orange"]) ) ) fig_bar.update_layout( title=f"{ticker} Asset Value vs. Default Point", title_font=dict(size=26, color="white"), yaxis_title="Value (USD)", template="plotly_dark", paper_bgcolor="#0e1117", plot_bgcolor="#0e1117", xaxis=dict( showgrid=True, gridcolor="rgba(255, 255, 255, 0.1)" ), yaxis=dict( showgrid=True, gridcolor="rgba(255, 255, 255, 0.1)" ) ) st.plotly_chart(fig_bar, use_container_width=True) # --- Dynamic Interpretation for Chart 5 (Exact user code) --- with st.expander("Interpretation", expanded=False): st.write("\n--- Dynamic Interpretation for Asset Value vs. Debt ---") if A_star > D: st.write(f"**1) The estimated asset value ({A_star:,.2f}) exceeds total debt ({D:,.2f}), providing a financial buffer.**") asset_debt_ratio = A_star / D if asset_debt_ratio > 2: st.write(" The asset-to-debt ratio is above 2, meaning the firm holds **more than double the assets compared to its debt obligations**.") st.write(" This implies a highly secure financial position, with a strong ability to absorb economic downturns or revenue declines.") elif 1.5 <= asset_debt_ratio <= 2: st.write(" The asset-to-debt ratio is between 1.5 and 2, which is considered **moderately strong**.") st.write(" While there is a solid financial cushion, **prudent debt management is still necessary** to maintain stability.") else: st.write(" The asset-to-debt ratio is between 1 and 1.5, meaning the firm has a **narrower but still positive buffer.**") st.write(" This level is acceptable, but **a small decline in asset value could quickly increase financial risk.**") else: st.write(f"1) The estimated asset value ({A_star:,.2f}) is **less than or close to total debt** ({D:,.2f}).") st.write(" This signals **a limited financial cushion**, increasing the probability of distress in unfavorable conditions.") if A_star < D: st.write(" **Warning:** The company’s total assets are lower than its total debt.") st.write(" This implies that if the company were to liquidate its assets today, it would still **not be able to fully cover its obligations**.") st.write(" Such a position increases the likelihood of credit downgrades and difficulty in securing additional financing.") elif A_star / D < 1.1: st.write(" The asset buffer is extremely thin. A minor shock in earnings or asset valuation could put the firm in distress.") st.write(" The company should consider **reducing leverage or improving asset utilization** to reinforce financial stability.") gap = A_star - D if gap > 0.5 * D: st.write("**2) The firm has a **comfortable margin** between assets and debt. Even with some decline in asset value, financial stability is not immediately at risk.**") elif 0.2 * D < gap <= 0.5 * D: st.write("2) The firm has **a moderate cushion**, but there is some vulnerability to financial shocks.") st.write(" If debt levels increase or asset values decline, risk could rise quickly.") else: st.write("2) The asset buffer is **very narrow**, making the firm susceptible to external risks such as declining revenues, rising interest rates, or asset write-downs.") st.write(" A **small misstep in financial strategy could significantly increase default probability.**") with st.expander("Raw Distance-to-Default Data", expanded=False): st.dataframe(dtd_df) # Hide default Streamlit style st.markdown( """ """, unsafe_allow_html=True )