Spaces:
Sleeping
Sleeping
Create app.py
Browse files
app.py
ADDED
|
@@ -0,0 +1,1341 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import streamlit as st
|
| 2 |
+
import requests
|
| 3 |
+
import pandas as pd
|
| 4 |
+
import numpy as np
|
| 5 |
+
import yfinance as yf
|
| 6 |
+
import plotly.graph_objects as go
|
| 7 |
+
import plotly.figure_factory as ff
|
| 8 |
+
from datetime import datetime, date
|
| 9 |
+
from dateutil.relativedelta import relativedelta
|
| 10 |
+
import datetime as dt
|
| 11 |
+
import warnings
|
| 12 |
+
warnings.filterwarnings("ignore")
|
| 13 |
+
import os
|
| 14 |
+
from scipy.optimize import fsolve
|
| 15 |
+
from scipy.stats import norm
|
| 16 |
+
|
| 17 |
+
###############################################################################
|
| 18 |
+
# SET WIDE LAYOUT AND PAGE TITLE
|
| 19 |
+
###############################################################################
|
| 20 |
+
st.set_page_config(page_title="Default Risk Estimation", layout="wide")
|
| 21 |
+
|
| 22 |
+
###############################################################################
|
| 23 |
+
# GLOBALS & SESSION STATE
|
| 24 |
+
###############################################################################
|
| 25 |
+
FMP_API_KEY = os.getenv("FMP_API_KEY")
|
| 26 |
+
|
| 27 |
+
if "altman_results" not in st.session_state:
|
| 28 |
+
st.session_state["altman_results"] = None
|
| 29 |
+
|
| 30 |
+
if "dtd_results" not in st.session_state:
|
| 31 |
+
st.session_state["dtd_results"] = None
|
| 32 |
+
|
| 33 |
+
###############################################################################
|
| 34 |
+
# HELPER FUNCTIONS (Altman Z)
|
| 35 |
+
###############################################################################
|
| 36 |
+
def get_fmp_json(url):
|
| 37 |
+
"""
|
| 38 |
+
Retrieves JSON from the specified URL and returns as a list.
|
| 39 |
+
Omits direct mention of data source in any error messages.
|
| 40 |
+
"""
|
| 41 |
+
r = requests.get(url)
|
| 42 |
+
try:
|
| 43 |
+
data = r.json()
|
| 44 |
+
if not isinstance(data, list):
|
| 45 |
+
return []
|
| 46 |
+
return data
|
| 47 |
+
except Exception:
|
| 48 |
+
return []
|
| 49 |
+
|
| 50 |
+
def fetch_fmp_annual(endpoint):
|
| 51 |
+
"""
|
| 52 |
+
Fetches annual data from the endpoint, sorts by date if present.
|
| 53 |
+
"""
|
| 54 |
+
data = get_fmp_json(endpoint)
|
| 55 |
+
df = pd.DataFrame(data)
|
| 56 |
+
if not df.empty and 'date' in df.columns:
|
| 57 |
+
df['date'] = pd.to_datetime(df['date'])
|
| 58 |
+
df.sort_values('date', inplace=True)
|
| 59 |
+
return df
|
| 60 |
+
|
| 61 |
+
###############################################################################
|
| 62 |
+
# HELPER FUNCTIONS (Distance-to-Default)
|
| 63 |
+
###############################################################################
|
| 64 |
+
def solve_merton(E, sigma_E, D, T, r):
|
| 65 |
+
"""
|
| 66 |
+
Merton model solver:
|
| 67 |
+
E = A * N(d1) - D * exp(-rT) * N(d2)
|
| 68 |
+
sigma_E = (A / E) * N(d1) * sigma_A
|
| 69 |
+
"""
|
| 70 |
+
def equations(vars_):
|
| 71 |
+
A_, sigmaA_ = vars_
|
| 72 |
+
d1_ = (np.log(A_ / D) + (r + 0.5 * sigmaA_**2) * T) / (sigmaA_ * np.sqrt(T))
|
| 73 |
+
d2_ = d1_ - sigmaA_ * np.sqrt(T)
|
| 74 |
+
eq1 = A_ * norm.cdf(d1_) - D * np.exp(-r * T) * norm.cdf(d2_) - E
|
| 75 |
+
eq2 = sigma_E - (A_ / E) * norm.cdf(d1_) * sigmaA_
|
| 76 |
+
return (eq1, eq2)
|
| 77 |
+
|
| 78 |
+
A_guess = E + D
|
| 79 |
+
sigmaA_guess = sigma_E * (E / (E + D))
|
| 80 |
+
A_star, sigmaA_star = fsolve(equations, [A_guess, sigmaA_guess], maxfev=3000)
|
| 81 |
+
return A_star, sigmaA_star
|
| 82 |
+
|
| 83 |
+
def distance_to_default(A, D, T, r, sigmaA):
|
| 84 |
+
"""
|
| 85 |
+
Merton distance to default (d2):
|
| 86 |
+
d2 = [ln(A/D) + (r - 0.5*sigmaA^2)*T] / (sigmaA * sqrt(T))
|
| 87 |
+
"""
|
| 88 |
+
return (np.log(A / D) + (r - 0.5 * sigmaA**2) * T) / (sigmaA * np.sqrt(T))
|
| 89 |
+
|
| 90 |
+
###############################################################################
|
| 91 |
+
# ALTMAN Z-SCORE EXECUTION (From Provided Code)
|
| 92 |
+
###############################################################################
|
| 93 |
+
def run_altman_zscore_calculations(ticker, years_back):
|
| 94 |
+
"""
|
| 95 |
+
Uses the original user-provided Altman Z code to fetch and compute partials.
|
| 96 |
+
Returns the final DataFrame with partials and total Z-scores.
|
| 97 |
+
"""
|
| 98 |
+
# 1) FETCH ANNUAL STATEMENTS
|
| 99 |
+
income_url = f"https://financialmodelingprep.com/api/v3/income-statement/{ticker}?period=annual&limit=100&apikey={FMP_API_KEY}"
|
| 100 |
+
balance_url = f"https://financialmodelingprep.com/api/v3/balance-sheet-statement/{ticker}?period=annual&limit=100&apikey={FMP_API_KEY}"
|
| 101 |
+
|
| 102 |
+
income_df = fetch_fmp_annual(income_url)
|
| 103 |
+
balance_df = fetch_fmp_annual(balance_url)
|
| 104 |
+
|
| 105 |
+
merged_bi = pd.merge(balance_df, income_df, on='date', how='inner', suffixes=('_bal','_inc'))
|
| 106 |
+
merged_bi.sort_values('date', inplace=True)
|
| 107 |
+
|
| 108 |
+
if merged_bi.empty:
|
| 109 |
+
st.warning("No statements to analyze for this ticker/date range.")
|
| 110 |
+
return pd.DataFrame()
|
| 111 |
+
|
| 112 |
+
# 2) FILTER TO LAST X YEARS
|
| 113 |
+
end_date = pd.Timestamp.today()
|
| 114 |
+
start_date = end_date - relativedelta(years=years_back)
|
| 115 |
+
|
| 116 |
+
merged_bi = merged_bi[(merged_bi['date'] >= start_date) & (merged_bi['date'] <= end_date)]
|
| 117 |
+
merged_bi.sort_values('date', inplace=True)
|
| 118 |
+
|
| 119 |
+
if merged_bi.empty:
|
| 120 |
+
st.warning("No financial statements found in the chosen range.")
|
| 121 |
+
return pd.DataFrame()
|
| 122 |
+
|
| 123 |
+
# 3) FETCH HISTORICAL MARKET CAP
|
| 124 |
+
mktcap_df = pd.DataFrame()
|
| 125 |
+
iterations = (years_back // 5) + (1 if years_back % 5 != 0 else 0)
|
| 126 |
+
|
| 127 |
+
for i in range(iterations):
|
| 128 |
+
period_end_date = end_date - relativedelta(years=i * 5)
|
| 129 |
+
period_start_date = period_end_date - relativedelta(years=5)
|
| 130 |
+
|
| 131 |
+
if period_start_date < start_date:
|
| 132 |
+
period_start_date = start_date
|
| 133 |
+
|
| 134 |
+
mktcap_url = (
|
| 135 |
+
f"https://financialmodelingprep.com/api/v3/historical-market-capitalization/{ticker}"
|
| 136 |
+
f"?from={period_start_date.date()}&to={period_end_date.date()}&apikey={FMP_API_KEY}"
|
| 137 |
+
)
|
| 138 |
+
mktcap_data = get_fmp_json(mktcap_url)
|
| 139 |
+
mktcap_period_df = pd.DataFrame(mktcap_data)
|
| 140 |
+
|
| 141 |
+
if not mktcap_period_df.empty and 'date' in mktcap_period_df.columns:
|
| 142 |
+
mktcap_period_df['date'] = pd.to_datetime(mktcap_period_df['date'])
|
| 143 |
+
mktcap_period_df.rename(columns={'marketCap': 'historical_market_cap'}, inplace=True)
|
| 144 |
+
mktcap_df = pd.concat([mktcap_df, mktcap_period_df], ignore_index=True)
|
| 145 |
+
|
| 146 |
+
mktcap_df = mktcap_df.sort_values('date').drop_duplicates(subset=['date'])
|
| 147 |
+
if not mktcap_df.empty and 'date' in mktcap_df.columns:
|
| 148 |
+
mktcap_df['date'] = pd.to_datetime(mktcap_df['date'])
|
| 149 |
+
mktcap_df = mktcap_df[(mktcap_df['date'] >= start_date) & (mktcap_df['date'] <= end_date)]
|
| 150 |
+
mktcap_df.sort_values('date', inplace=True)
|
| 151 |
+
else:
|
| 152 |
+
mktcap_df = pd.DataFrame(columns=['date','historical_market_cap'])
|
| 153 |
+
|
| 154 |
+
if not merged_bi.empty and not mktcap_df.empty:
|
| 155 |
+
merged_bi = pd.merge_asof(
|
| 156 |
+
merged_bi.sort_values('date'),
|
| 157 |
+
mktcap_df.sort_values('date'),
|
| 158 |
+
on='date',
|
| 159 |
+
direction='nearest'
|
| 160 |
+
)
|
| 161 |
+
else:
|
| 162 |
+
merged_bi['historical_market_cap'] = np.nan
|
| 163 |
+
|
| 164 |
+
# 4) COMPUTE PARTIAL CONTRIBUTIONS
|
| 165 |
+
z_rows = []
|
| 166 |
+
for _, row in merged_bi.iterrows():
|
| 167 |
+
ta = row.get('totalAssets', np.nan)
|
| 168 |
+
tl = row.get('totalLiabilities', np.nan)
|
| 169 |
+
if pd.isnull(ta) or pd.isnull(tl) or ta == 0 or tl == 0:
|
| 170 |
+
continue
|
| 171 |
+
|
| 172 |
+
rev = row.get('revenue', 0)
|
| 173 |
+
hist_mcap = row.get('historical_market_cap', np.nan)
|
| 174 |
+
if pd.isnull(hist_mcap):
|
| 175 |
+
continue
|
| 176 |
+
|
| 177 |
+
tca = row.get('totalCurrentAssets', np.nan)
|
| 178 |
+
tcl = row.get('totalCurrentLiabilities', np.nan)
|
| 179 |
+
if pd.isnull(tca) or pd.isnull(tcl):
|
| 180 |
+
continue
|
| 181 |
+
|
| 182 |
+
wc = (tca - tcl)
|
| 183 |
+
re = row.get('retainedEarnings', 0)
|
| 184 |
+
ebit = row.get('operatingIncome', np.nan)
|
| 185 |
+
if pd.isnull(ebit):
|
| 186 |
+
ebit = row.get('ebitda', 0)
|
| 187 |
+
|
| 188 |
+
X1 = wc / ta
|
| 189 |
+
X2 = re / ta
|
| 190 |
+
X3 = ebit / ta
|
| 191 |
+
X4 = hist_mcap / tl
|
| 192 |
+
X5 = rev / ta if ta != 0 else 0
|
| 193 |
+
|
| 194 |
+
# Original Z
|
| 195 |
+
o_part1 = 1.2 * X1
|
| 196 |
+
o_part2 = 1.4 * X2
|
| 197 |
+
o_part3 = 3.3 * X3
|
| 198 |
+
o_part4 = 0.6 * X4
|
| 199 |
+
o_part5 = 1.0 * X5
|
| 200 |
+
z_original = o_part1 + o_part2 + o_part3 + o_part4 + o_part5
|
| 201 |
+
|
| 202 |
+
# Z''
|
| 203 |
+
d_part1 = 6.56 * X1
|
| 204 |
+
d_part2 = 3.26 * X2
|
| 205 |
+
d_part3 = 6.72 * X3
|
| 206 |
+
d_part4 = 1.05 * X4
|
| 207 |
+
z_double_prime = d_part1 + d_part2 + d_part3 + d_part4
|
| 208 |
+
|
| 209 |
+
# Z'''
|
| 210 |
+
t_part1 = 3.25 * X1
|
| 211 |
+
t_part2 = 2.85 * X2
|
| 212 |
+
t_part3 = 4.15 * X3
|
| 213 |
+
t_part4 = 0.95 * X4
|
| 214 |
+
z_triple_prime_service = t_part1 + t_part2 + t_part3 + t_part4
|
| 215 |
+
|
| 216 |
+
z_rows.append({
|
| 217 |
+
'date': row['date'],
|
| 218 |
+
|
| 219 |
+
# Original partials
|
| 220 |
+
'o_part1': o_part1,
|
| 221 |
+
'o_part2': o_part2,
|
| 222 |
+
'o_part3': o_part3,
|
| 223 |
+
'o_part4': o_part4,
|
| 224 |
+
'o_part5': o_part5,
|
| 225 |
+
'z_original': z_original,
|
| 226 |
+
|
| 227 |
+
# Z'' partials
|
| 228 |
+
'd_part1': d_part1,
|
| 229 |
+
'd_part2': d_part2,
|
| 230 |
+
'd_part3': d_part3,
|
| 231 |
+
'd_part4': d_part4,
|
| 232 |
+
'z_double_prime': z_double_prime,
|
| 233 |
+
|
| 234 |
+
# Z''' partials
|
| 235 |
+
't_part1': t_part1,
|
| 236 |
+
't_part2': t_part2,
|
| 237 |
+
't_part3': t_part3,
|
| 238 |
+
't_part4': t_part4,
|
| 239 |
+
'z_triple_prime_service': z_triple_prime_service
|
| 240 |
+
})
|
| 241 |
+
|
| 242 |
+
z_df = pd.DataFrame(z_rows)
|
| 243 |
+
z_df.sort_values('date', inplace=True)
|
| 244 |
+
return z_df
|
| 245 |
+
|
| 246 |
+
###############################################################################
|
| 247 |
+
# DTD EXECUTION (From Provided Code)
|
| 248 |
+
###############################################################################
|
| 249 |
+
def calculate_yearly_distance_to_default(
|
| 250 |
+
symbol="AAPL",
|
| 251 |
+
years_back=10,
|
| 252 |
+
debt_method="TOTAL",
|
| 253 |
+
risk_free_ticker="^TNX",
|
| 254 |
+
apikey="YOUR_FMP_API_KEY",
|
| 255 |
+
):
|
| 256 |
+
"""
|
| 257 |
+
Fetches up to `years_back` years of annual data, merges market cap, debt,
|
| 258 |
+
and risk-free yields. Then computes Merton Distance to Default for each year.
|
| 259 |
+
Returns a DataFrame.
|
| 260 |
+
"""
|
| 261 |
+
end_date = date.today()
|
| 262 |
+
start_date = end_date - dt.timedelta(days=365 * years_back)
|
| 263 |
+
|
| 264 |
+
# Market cap
|
| 265 |
+
df_mcap = pd.DataFrame()
|
| 266 |
+
iterations = (years_back // 5) + (1 if years_back % 5 != 0 else 0)
|
| 267 |
+
for i in range(iterations):
|
| 268 |
+
period_end_date = end_date - dt.timedelta(days=365 * i * 5)
|
| 269 |
+
period_start_date = period_end_date - dt.timedelta(days=365 * 5)
|
| 270 |
+
url_mcap = (
|
| 271 |
+
f"https://financialmodelingprep.com/api/v3/historical-market-capitalization/"
|
| 272 |
+
f"{symbol}?from={period_start_date}&to={period_end_date}&apikey={apikey}"
|
| 273 |
+
)
|
| 274 |
+
resp_mcap = requests.get(url_mcap)
|
| 275 |
+
data_mcap = resp_mcap.json() if resp_mcap.status_code == 200 else []
|
| 276 |
+
df_mcap_period = pd.DataFrame(data_mcap)
|
| 277 |
+
df_mcap = pd.concat([df_mcap, df_mcap_period], ignore_index=True)
|
| 278 |
+
|
| 279 |
+
if df_mcap.empty or "date" not in df_mcap.columns:
|
| 280 |
+
raise ValueError("No market cap data returned. Check your inputs.")
|
| 281 |
+
df_mcap["year"] = pd.to_datetime(df_mcap["date"]).dt.year
|
| 282 |
+
df_mcap = (
|
| 283 |
+
df_mcap.groupby("year", as_index=False)
|
| 284 |
+
.agg({"marketCap": "mean"})
|
| 285 |
+
.sort_values("year", ascending=False)
|
| 286 |
+
)
|
| 287 |
+
|
| 288 |
+
# Balance Sheet
|
| 289 |
+
url_bs = f"https://financialmodelingprep.com/api/v3/balance-sheet-statement/{symbol}?period=annual&apikey={apikey}"
|
| 290 |
+
resp_bs = requests.get(url_bs)
|
| 291 |
+
data_bs = resp_bs.json() if resp_bs.status_code == 200 else []
|
| 292 |
+
df_bs = pd.DataFrame(data_bs)
|
| 293 |
+
if df_bs.empty or "date" not in df_bs.columns:
|
| 294 |
+
raise ValueError("No balance sheet data returned. Check your inputs.")
|
| 295 |
+
df_bs["year"] = pd.to_datetime(df_bs["date"]).dt.year
|
| 296 |
+
keep_cols = ["year", "shortTermDebt", "longTermDebt", "totalDebt", "date"]
|
| 297 |
+
df_bs = df_bs[keep_cols].sort_values("year", ascending=False)
|
| 298 |
+
|
| 299 |
+
# Risk-free from yfinance
|
| 300 |
+
rf_ticker_obj = yf.Ticker(risk_free_ticker)
|
| 301 |
+
rf_data = rf_ticker_obj.history(start=start_date, end=end_date, auto_adjust=False)
|
| 302 |
+
if rf_data.empty or "Close" not in rf_data.columns:
|
| 303 |
+
raise ValueError("No valid risk-free rate data found. Check your inputs.")
|
| 304 |
+
rf_data = rf_data.reset_index()
|
| 305 |
+
rf_data["year"] = rf_data["Date"].dt.year
|
| 306 |
+
rf_data = rf_data[["year", "Close"]]
|
| 307 |
+
rf_yearly = rf_data.groupby("year", as_index=False)["Close"].mean()
|
| 308 |
+
rf_yearly.rename(columns={"Close": "rf_yield"}, inplace=True)
|
| 309 |
+
rf_yearly["rf_yield"] = rf_yearly["rf_yield"] / 100.0 # decimal
|
| 310 |
+
|
| 311 |
+
# Merge
|
| 312 |
+
df_all = pd.merge(df_mcap, df_bs, on="year", how="left")
|
| 313 |
+
df_all = pd.merge(df_all, rf_yearly, on="year", how="left")
|
| 314 |
+
|
| 315 |
+
# Merton each year
|
| 316 |
+
results = []
|
| 317 |
+
for _, row in df_all.iterrows():
|
| 318 |
+
yr = row["year"]
|
| 319 |
+
E = row["marketCap"]
|
| 320 |
+
if pd.isna(E) or E <= 0:
|
| 321 |
+
continue
|
| 322 |
+
|
| 323 |
+
shortD = row.get("shortTermDebt", 0) or 0
|
| 324 |
+
longD = row.get("longTermDebt", 0) or 0
|
| 325 |
+
totalD = row.get("totalDebt", 0) or 0
|
| 326 |
+
if debt_method.upper() == "STPLUSLT":
|
| 327 |
+
D = shortD + longD
|
| 328 |
+
elif debt_method.upper() == "STPLUSHALFLT":
|
| 329 |
+
D = shortD + 0.5 * longD
|
| 330 |
+
else:
|
| 331 |
+
D = totalD
|
| 332 |
+
if not D or D <= 0:
|
| 333 |
+
D = np.nan
|
| 334 |
+
|
| 335 |
+
r_val = row.get("rf_yield", 0.03)
|
| 336 |
+
|
| 337 |
+
from_dt = f"{yr}-01-01"
|
| 338 |
+
to_dt = f"{yr}-12-31"
|
| 339 |
+
url_hist = (
|
| 340 |
+
f"https://financialmodelingprep.com/api/v3/historical-price-full/{symbol}"
|
| 341 |
+
f"?from={from_dt}&to={to_dt}&apikey={apikey}"
|
| 342 |
+
)
|
| 343 |
+
resp_hist = requests.get(url_hist)
|
| 344 |
+
data_hist = resp_hist.json() if resp_hist.status_code == 200 else {}
|
| 345 |
+
daily_prices = data_hist.get("historical", [])
|
| 346 |
+
|
| 347 |
+
if not daily_prices:
|
| 348 |
+
sigma_E = 0.30
|
| 349 |
+
else:
|
| 350 |
+
df_prices = pd.DataFrame(daily_prices)
|
| 351 |
+
df_prices.sort_values("date", inplace=True)
|
| 352 |
+
close_vals = df_prices["close"].values
|
| 353 |
+
log_rets = np.diff(np.log(close_vals))
|
| 354 |
+
daily_vol = np.std(log_rets)
|
| 355 |
+
sigma_E = daily_vol * np.sqrt(252)
|
| 356 |
+
if sigma_E < 1e-4:
|
| 357 |
+
sigma_E = 0.30
|
| 358 |
+
|
| 359 |
+
T = 1.0
|
| 360 |
+
if not np.isnan(D):
|
| 361 |
+
try:
|
| 362 |
+
A_star, sigmaA_star = solve_merton(E, sigma_E, D, T, r_val)
|
| 363 |
+
dtd_value = distance_to_default(A_star, D, T, r_val, sigmaA_star)
|
| 364 |
+
except:
|
| 365 |
+
A_star, sigmaA_star, dtd_value = np.nan, np.nan, np.nan
|
| 366 |
+
else:
|
| 367 |
+
A_star, sigmaA_star, dtd_value = np.nan, np.nan, np.nan
|
| 368 |
+
|
| 369 |
+
results.append({
|
| 370 |
+
"year": yr,
|
| 371 |
+
"marketCap": E,
|
| 372 |
+
"shortTermDebt": shortD,
|
| 373 |
+
"longTermDebt": longD,
|
| 374 |
+
"totalDebt": totalD,
|
| 375 |
+
"chosenDebt": D,
|
| 376 |
+
"rf": r_val,
|
| 377 |
+
"sigma_E": sigma_E,
|
| 378 |
+
"A_star": A_star,
|
| 379 |
+
"sigmaA_star": sigmaA_star,
|
| 380 |
+
"DTD": dtd_value
|
| 381 |
+
})
|
| 382 |
+
|
| 383 |
+
result_df = pd.DataFrame(results).sort_values("year")
|
| 384 |
+
return result_df
|
| 385 |
+
|
| 386 |
+
###############################################################################
|
| 387 |
+
# PLOTTING HELPERS
|
| 388 |
+
###############################################################################
|
| 389 |
+
def plot_zscore_figure(df, date_col, partial_cols, total_col, partial_names, total_name, title_text, zones, ticker):
|
| 390 |
+
"""
|
| 391 |
+
Creates stacked bar for partial contributions plus a line for the total Z.
|
| 392 |
+
Draws shading for distress/gray/safe zones. Full width.
|
| 393 |
+
"""
|
| 394 |
+
fig = go.Figure()
|
| 395 |
+
|
| 396 |
+
x_min = df[date_col].min()
|
| 397 |
+
x_max = df[date_col].max()
|
| 398 |
+
total_max = df[total_col].max()
|
| 399 |
+
partial_sum_max = df[partial_cols].sum(axis=1).max() if not df[partial_cols].empty else 0
|
| 400 |
+
y_max = max(total_max, partial_sum_max, 0) * 1.2
|
| 401 |
+
y_min = min(df[total_col].min(), 0) * 1.2 if df[total_col].min() < 0 else 0
|
| 402 |
+
|
| 403 |
+
# Distress
|
| 404 |
+
fig.add_shape(
|
| 405 |
+
type="rect",
|
| 406 |
+
x0=x_min, x1=x_max,
|
| 407 |
+
y0=y_min, y1=zones['distress'],
|
| 408 |
+
fillcolor="red",
|
| 409 |
+
opacity=0.2,
|
| 410 |
+
layer="below",
|
| 411 |
+
line=dict(width=0)
|
| 412 |
+
)
|
| 413 |
+
# Gray
|
| 414 |
+
fig.add_shape(
|
| 415 |
+
type="rect",
|
| 416 |
+
x0=x_min, x1=x_max,
|
| 417 |
+
y0=zones['gray_lower'], y1=zones['gray_upper'],
|
| 418 |
+
fillcolor="gray",
|
| 419 |
+
opacity=0.2,
|
| 420 |
+
layer="below",
|
| 421 |
+
line=dict(width=0)
|
| 422 |
+
)
|
| 423 |
+
# Safe
|
| 424 |
+
fig.add_shape(
|
| 425 |
+
type="rect",
|
| 426 |
+
x0=x_min, x1=x_max,
|
| 427 |
+
y0=zones['safe'], y1=y_max,
|
| 428 |
+
fillcolor="green",
|
| 429 |
+
opacity=0.2,
|
| 430 |
+
layer="below",
|
| 431 |
+
line=dict(width=0)
|
| 432 |
+
)
|
| 433 |
+
|
| 434 |
+
# Stacked bars
|
| 435 |
+
for col, name, color in partial_names:
|
| 436 |
+
fig.add_trace(go.Bar(
|
| 437 |
+
x=df[date_col],
|
| 438 |
+
y=df[col],
|
| 439 |
+
name=name,
|
| 440 |
+
marker_color=color
|
| 441 |
+
))
|
| 442 |
+
|
| 443 |
+
# Line
|
| 444 |
+
fig.add_trace(go.Scatter(
|
| 445 |
+
x=df[date_col],
|
| 446 |
+
y=df[total_col],
|
| 447 |
+
mode='lines+markers+text',
|
| 448 |
+
text=df[total_col].round(2),
|
| 449 |
+
textposition='top center',
|
| 450 |
+
textfont=dict(size=16),
|
| 451 |
+
name=total_name,
|
| 452 |
+
line=dict(color='white', width=2)
|
| 453 |
+
))
|
| 454 |
+
|
| 455 |
+
fig.update_layout(
|
| 456 |
+
title=dict(
|
| 457 |
+
text=f"{title_text} for {ticker}",
|
| 458 |
+
font=dict(size=26, color="white")
|
| 459 |
+
),
|
| 460 |
+
|
| 461 |
+
legend=dict(
|
| 462 |
+
font=dict(color="white", size=18)
|
| 463 |
+
),
|
| 464 |
+
barmode="stack",
|
| 465 |
+
template="plotly_dark",
|
| 466 |
+
paper_bgcolor="#0e1117",
|
| 467 |
+
plot_bgcolor="#0e1117",
|
| 468 |
+
xaxis=dict(
|
| 469 |
+
title="Year",
|
| 470 |
+
tickangle=45,
|
| 471 |
+
tickformat="%Y",
|
| 472 |
+
dtick="M12",
|
| 473 |
+
showgrid=True,
|
| 474 |
+
gridcolor="rgba(255, 255, 255, 0.1)"
|
| 475 |
+
),
|
| 476 |
+
yaxis=dict(
|
| 477 |
+
title="Z-Score Contribution",
|
| 478 |
+
showgrid=True,
|
| 479 |
+
gridcolor="rgba(255, 255, 255, 0.1)"
|
| 480 |
+
),
|
| 481 |
+
margin=dict(l=40, r=40, t=80, b=80),
|
| 482 |
+
height=700
|
| 483 |
+
)
|
| 484 |
+
st.plotly_chart(fig, use_container_width=True)
|
| 485 |
+
|
| 486 |
+
###############################################################################
|
| 487 |
+
# STREAMLIT APP
|
| 488 |
+
###############################################################################
|
| 489 |
+
st.title("Default Risk Estimation")
|
| 490 |
+
|
| 491 |
+
#st.write("## Overview")
|
| 492 |
+
st.write("This tool assesses a firm's default risk using two widely recognized models:")
|
| 493 |
+
st.write("1) **Altman Z-Score**: A financial distress predictor based on accounting ratios.")
|
| 494 |
+
st.write("2) **Merton Distance-to-Default (DTD)**: A market-based risk measure derived from option pricing theory.")
|
| 495 |
+
#st.write("Select a page from the sidebar to explore each model’s estimates and methodology.")
|
| 496 |
+
|
| 497 |
+
# Sidebar for user inputs
|
| 498 |
+
with st.sidebar:
|
| 499 |
+
st.write("## Input Parameters")
|
| 500 |
+
|
| 501 |
+
# Page selector in an open-by-default expander
|
| 502 |
+
with st.expander("Page Selector", expanded=True):
|
| 503 |
+
page = st.radio("Select Page:", ["Altman Z Score", "Distance-to-Default"])
|
| 504 |
+
|
| 505 |
+
with st.expander("General Settings", expanded=True):
|
| 506 |
+
ticker = st.text_input("Ticker", value="AAPL", help="Enter a valid stock ticker")
|
| 507 |
+
years_back = st.number_input("Years back", min_value=1, max_value=30, value=10, step=1,
|
| 508 |
+
help="How many years of data to retrieve?")
|
| 509 |
+
run_button = st.button("Run Analysis", help="Fetch data and compute metrics")
|
| 510 |
+
|
| 511 |
+
# If user clicks to run, fetch data
|
| 512 |
+
if run_button:
|
| 513 |
+
# Altman Z
|
| 514 |
+
z_data = run_altman_zscore_calculations(ticker, years_back)
|
| 515 |
+
st.session_state["altman_results"] = z_data
|
| 516 |
+
|
| 517 |
+
# DTD
|
| 518 |
+
try:
|
| 519 |
+
dtd_df = calculate_yearly_distance_to_default(
|
| 520 |
+
symbol=ticker,
|
| 521 |
+
years_back=years_back,
|
| 522 |
+
debt_method="TOTAL",
|
| 523 |
+
risk_free_ticker="^TNX",
|
| 524 |
+
apikey=FMP_API_KEY
|
| 525 |
+
)
|
| 526 |
+
st.session_state["dtd_results"] = dtd_df
|
| 527 |
+
except ValueError:
|
| 528 |
+
st.warning("No valid data was returned. Check your inputs.")
|
| 529 |
+
|
| 530 |
+
###############################################################################
|
| 531 |
+
# PAGE 1: ALTMAN Z
|
| 532 |
+
###############################################################################
|
| 533 |
+
if page == "Altman Z Score":
|
| 534 |
+
z_df = st.session_state.get("altman_results", None)
|
| 535 |
+
if z_df is None or z_df.empty:
|
| 536 |
+
st.info("Select Page, input the paramters and click 'Run Analysis' on the sidebar.")
|
| 537 |
+
else:
|
| 538 |
+
# Original
|
| 539 |
+
#st.subheader("Original Altman Z-Score (1968)")
|
| 540 |
+
|
| 541 |
+
with st.expander("Methodology: Original Altman Z-Score (1968)", expanded=False):
|
| 542 |
+
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.")
|
| 543 |
+
|
| 544 |
+
# Formula
|
| 545 |
+
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")
|
| 546 |
+
|
| 547 |
+
# Definitions of variables
|
| 548 |
+
st.latex(r"X_1 = \frac{\text{Working Capital}}{\text{Total Assets}}")
|
| 549 |
+
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.")
|
| 550 |
+
|
| 551 |
+
st.latex(r"X_2 = \frac{\text{Retained Earnings}}{\text{Total Assets}}")
|
| 552 |
+
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.")
|
| 553 |
+
|
| 554 |
+
st.latex(r"X_3 = \frac{\text{EBIT}}{\text{Total Assets}}")
|
| 555 |
+
st.write("**Earnings Strength (X₃)**: EBIT (Earnings Before Interest and Taxes) relative to total assets reflects operating profitability and efficiency.")
|
| 556 |
+
|
| 557 |
+
st.latex(r"X_4 = \frac{\text{Market Value of Equity}}{\text{Total Liabilities}}")
|
| 558 |
+
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.")
|
| 559 |
+
|
| 560 |
+
st.latex(r"X_5 = \frac{\text{Revenue}}{\text{Total Assets}}")
|
| 561 |
+
st.write("**Asset Turnover (X₅)**: Assesses how efficiently a company generates revenue from its assets. High turnover suggests better asset utilization.")
|
| 562 |
+
|
| 563 |
+
# Academic Justification
|
| 564 |
+
st.write("##### Academic Justification")
|
| 565 |
+
st.write(
|
| 566 |
+
"The Altman Z-Score was developed using **discriminant analysis** on a dataset of manufacturing firms. "
|
| 567 |
+
"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."
|
| 568 |
+
"The model’s strength lies in its ability to quantify financial health across multiple dimensions—liquidity, profitability, leverage, and efficiency."
|
| 569 |
+
)
|
| 570 |
+
|
| 571 |
+
# Interpretation
|
| 572 |
+
st.write("##### Interpretation")
|
| 573 |
+
st.write(
|
| 574 |
+
"**Z > 2.99**: Company is considered financially healthy (Low risk of bankruptcy). \n"
|
| 575 |
+
"**1.81 ≤ Z ≤ 2.99**: 'Gray Area' where financial stability is uncertain. \n"
|
| 576 |
+
"**Z < 1.81**: High financial distress, indicating potential bankruptcy risk."
|
| 577 |
+
)
|
| 578 |
+
|
| 579 |
+
# Downsides / Limitations
|
| 580 |
+
st.write("##### Limitations")
|
| 581 |
+
st.write(
|
| 582 |
+
"- Developed using **only manufacturing firms**, which limits its applicability to other industries.\n"
|
| 583 |
+
"- Uses **historical accounting data**, which may not reflect current market conditions.\n"
|
| 584 |
+
"- Market Value of Equity (X₄) makes the score **sensitive to stock price volatility**.\n"
|
| 585 |
+
"- Does not incorporate forward-looking indicators such as market sentiment or macroeconomic risks."
|
| 586 |
+
)
|
| 587 |
+
|
| 588 |
+
|
| 589 |
+
orig_partial_names = [
|
| 590 |
+
('o_part1', "1.2 × (WC/TA)", 'blue'),
|
| 591 |
+
('o_part2', "1.4 × (RE/TA)", 'orange'),
|
| 592 |
+
('o_part3', "3.3 × (EBIT/TA)", 'green'),
|
| 593 |
+
('o_part4', "0.6 × (MktCap/TL)", 'red'),
|
| 594 |
+
('o_part5', "1.0 × (Rev/TA)", 'purple'),
|
| 595 |
+
]
|
| 596 |
+
orig_zones = {
|
| 597 |
+
'distress': 1.81,
|
| 598 |
+
'gray_lower': 1.81,
|
| 599 |
+
'gray_upper': 2.99,
|
| 600 |
+
'safe': 2.99
|
| 601 |
+
}
|
| 602 |
+
plot_zscore_figure(
|
| 603 |
+
df=z_df,
|
| 604 |
+
date_col='date',
|
| 605 |
+
partial_cols=['o_part1','o_part2','o_part3','o_part4','o_part5'],
|
| 606 |
+
total_col='z_original',
|
| 607 |
+
partial_names=orig_partial_names,
|
| 608 |
+
total_name="Original Z (Total)",
|
| 609 |
+
title_text="Original Altman Z-Score (1968)",
|
| 610 |
+
zones=orig_zones,
|
| 611 |
+
ticker=ticker
|
| 612 |
+
)
|
| 613 |
+
|
| 614 |
+
|
| 615 |
+
with st.expander("Interpretation", expanded=False):
|
| 616 |
+
# EXACT TEXT from user code (Original Z)
|
| 617 |
+
latest_z = z_df['z_original'].iloc[-1]
|
| 618 |
+
# For time-series logic:
|
| 619 |
+
first_val = z_df['z_original'].iloc[0]
|
| 620 |
+
if latest_z > first_val:
|
| 621 |
+
trend = "increased"
|
| 622 |
+
elif latest_z < first_val:
|
| 623 |
+
trend = "decreased"
|
| 624 |
+
else:
|
| 625 |
+
trend = "remained the same"
|
| 626 |
+
min_val = z_df['z_original'].min()
|
| 627 |
+
max_val = z_df['z_original'].max()
|
| 628 |
+
min_idx = z_df['z_original'].idxmin()
|
| 629 |
+
max_idx = z_df['z_original'].idxmax()
|
| 630 |
+
min_year = z_df.loc[min_idx, 'date'].year
|
| 631 |
+
max_year = z_df.loc[max_idx, 'date'].year
|
| 632 |
+
|
| 633 |
+
st.write("**--- Rich Interpretation for Original Z-Score ---")
|
| 634 |
+
st.write(f"Over the entire time series, the Z-Score has {trend}.")
|
| 635 |
+
st.write(f"The lowest value was {min_val:.2f} in {min_year}.")
|
| 636 |
+
st.write(f"The highest value was {max_val:.2f} in {max_year}.")
|
| 637 |
+
|
| 638 |
+
if latest_z < orig_zones['distress']:
|
| 639 |
+
st.write("Current reading is in distress zone. This suggests high financial risk.")
|
| 640 |
+
elif latest_z < orig_zones['gray_upper']:
|
| 641 |
+
st.write("Current reading is in the gray area. This signals mixed financial stability.")
|
| 642 |
+
else:
|
| 643 |
+
st.write("Current reading is in the safe zone. This implies a stronger financial condition.")
|
| 644 |
+
|
| 645 |
+
latest_data = z_df.iloc[-1]
|
| 646 |
+
orig_partials = {
|
| 647 |
+
'o_part1': latest_data['o_part1'],
|
| 648 |
+
'o_part2': latest_data['o_part2'],
|
| 649 |
+
'o_part3': latest_data['o_part3'],
|
| 650 |
+
'o_part4': latest_data['o_part4'],
|
| 651 |
+
'o_part5': latest_data['o_part5']
|
| 652 |
+
}
|
| 653 |
+
key_driver = max(orig_partials, key=orig_partials.get)
|
| 654 |
+
if key_driver == 'o_part1':
|
| 655 |
+
st.write("The most significant factor is Working Capital. This suggests the company's ability to cover short-term obligations with current assets. ")
|
| 656 |
+
st.write("A high contribution from Working Capital means strong liquidity, but too much could indicate inefficient capital allocation. ")
|
| 657 |
+
st.write("If the company holds excess current assets, it may not be deploying resources efficiently for growth.")
|
| 658 |
+
elif key_driver == 'o_part2':
|
| 659 |
+
st.write("The most significant factor is Retained Earnings. This reflects the company's history of profitability and reinvestment. ")
|
| 660 |
+
st.write("A high retained earnings contribution indicates that past profits have been reinvested rather than paid out as dividends. ")
|
| 661 |
+
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.")
|
| 662 |
+
elif key_driver == 'o_part3':
|
| 663 |
+
st.write("The most significant factor is EBIT (Earnings Before Interest and Taxes). This underscores the company’s ability to generate profits from operations. ")
|
| 664 |
+
st.write("A high EBIT contribution suggests that core business activities are profitable and drive financial health. ")
|
| 665 |
+
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.")
|
| 666 |
+
elif key_driver == 'o_part4':
|
| 667 |
+
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. ")
|
| 668 |
+
st.write("A strong market cap contribution means investors perceive the company as having high equity value compared to liabilities, reducing bankruptcy risk. ")
|
| 669 |
+
st.write("However, if this is the dominant driver, financial stability may be tied to market sentiment, which can be volatile.")
|
| 670 |
+
elif key_driver == 'o_part5':
|
| 671 |
+
st.write("The most significant factor is Revenue. This indicates that top-line growth is a major driver of financial stability. ")
|
| 672 |
+
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. ")
|
| 673 |
+
st.write("If revenue dominates the Z-Score, the company must ensure sustainable cost management and profitability to maintain financial strength.")
|
| 674 |
+
|
| 675 |
+
st.write("If management seeks to lower this Z-Score, they might reduce liquidity or raise liabilities.")
|
| 676 |
+
st.write("A higher liability base or weaker earnings can press the score downward.")
|
| 677 |
+
|
| 678 |
+
# Z''
|
| 679 |
+
#st.subheader("Z'' (1993, Non-Manufacturing)")
|
| 680 |
+
|
| 681 |
+
with st.expander("Methodology: Z'' (1993, Non-Manufacturing)", expanded=False):
|
| 682 |
+
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.")
|
| 683 |
+
|
| 684 |
+
# Formula
|
| 685 |
+
st.latex(r"Z'' = 6.56 \times X_1 + 3.26 \times X_2 + 6.72 \times X_3 + 1.05 \times X_4")
|
| 686 |
+
|
| 687 |
+
# Definitions of variables
|
| 688 |
+
st.latex(r"X_1 = \frac{\text{Working Capital}}{\text{Total Assets}}")
|
| 689 |
+
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.")
|
| 690 |
+
|
| 691 |
+
st.latex(r"X_2 = \frac{\text{Retained Earnings}}{\text{Total Assets}}")
|
| 692 |
+
st.write("**Cumulative Profitability (X₂)**: Higher retained earnings relative to total assets suggest long-term profitability and financial resilience.")
|
| 693 |
+
|
| 694 |
+
st.latex(r"X_3 = \frac{\text{EBIT}}{\text{Total Assets}}")
|
| 695 |
+
st.write("**Operating Profitability (X₃)**: Measures how efficiently a company generates profit from its assets, reflecting core business strength.")
|
| 696 |
+
|
| 697 |
+
st.latex(r"X_4 = \frac{\text{Market Value of Equity}}{\text{Total Liabilities}}")
|
| 698 |
+
st.write("**Leverage (X₄)**: A firm's ability to cover its liabilities with market value equity. A lower ratio suggests greater financial risk.")
|
| 699 |
+
|
| 700 |
+
# Academic Justification
|
| 701 |
+
st.write("##### Academic Justification")
|
| 702 |
+
st.write(
|
| 703 |
+
"The original Z-Score was optimized for **manufacturing firms**, making it less effective for firms with fewer tangible assets. "
|
| 704 |
+
"Z'' (1993) improves bankruptcy prediction for **service and retail firms**, as it excludes the revenue turnover component (X₅) "
|
| 705 |
+
"and places greater emphasis on profitability and liquidity. Empirical studies found Z'' to be **better suited for firms with lower capital intensity**."
|
| 706 |
+
)
|
| 707 |
+
|
| 708 |
+
# Interpretation
|
| 709 |
+
st.write("##### Interpretation")
|
| 710 |
+
st.write(
|
| 711 |
+
"**Z'' > 2.60**: Firm is financially stable, with low bankruptcy risk. \n"
|
| 712 |
+
"**1.10 ≤ Z'' ≤ 2.60**: 'Gray Area'—financial condition is uncertain. \n"
|
| 713 |
+
"**Z'' < 1.10**: Firm is in financial distress, at a higher risk of default."
|
| 714 |
+
)
|
| 715 |
+
|
| 716 |
+
# Downsides / Limitations
|
| 717 |
+
st.write("##### Limitations")
|
| 718 |
+
st.write(
|
| 719 |
+
"- Developed for **non-manufacturing firms**, but may not be applicable to banks or financial institutions.\n"
|
| 720 |
+
"- Still **relies on historical accounting data**, which may not fully capture real-time financial conditions.\n"
|
| 721 |
+
"- Market-based variable (X₄) makes the score **sensitive to stock market fluctuations**.\n"
|
| 722 |
+
"- Does not consider external macroeconomic risks or qualitative factors like management decisions."
|
| 723 |
+
)
|
| 724 |
+
|
| 725 |
+
|
| 726 |
+
double_partial_names = [
|
| 727 |
+
('d_part1', "6.56 × (WC/TA)", 'blue'),
|
| 728 |
+
('d_part2', "3.26 × (RE/TA)", 'orange'),
|
| 729 |
+
('d_part3', "6.72 × (EBIT/TA)", 'green'),
|
| 730 |
+
('d_part4', "1.05 × (MktCap/TL)", 'red'),
|
| 731 |
+
]
|
| 732 |
+
double_zones = {
|
| 733 |
+
'distress': 1.1,
|
| 734 |
+
'gray_lower': 1.1,
|
| 735 |
+
'gray_upper': 2.6,
|
| 736 |
+
'safe': 2.6
|
| 737 |
+
}
|
| 738 |
+
plot_zscore_figure(
|
| 739 |
+
df=z_df,
|
| 740 |
+
date_col='date',
|
| 741 |
+
partial_cols=['d_part1','d_part2','d_part3','d_part4'],
|
| 742 |
+
total_col='z_double_prime',
|
| 743 |
+
partial_names=double_partial_names,
|
| 744 |
+
total_name="Z'' (Total)",
|
| 745 |
+
title_text="Z'' (1993, Non-Manufacturing)",
|
| 746 |
+
zones=double_zones,
|
| 747 |
+
ticker=ticker
|
| 748 |
+
)
|
| 749 |
+
|
| 750 |
+
with st.expander("Interpretation", expanded=False):
|
| 751 |
+
latest_z_double = z_df['z_double_prime'].iloc[-1]
|
| 752 |
+
first_val = z_df['z_double_prime'].iloc[0]
|
| 753 |
+
if latest_z_double > first_val:
|
| 754 |
+
trend_d = "increased"
|
| 755 |
+
elif latest_z_double < first_val:
|
| 756 |
+
trend_d = "decreased"
|
| 757 |
+
else:
|
| 758 |
+
trend_d = "remained the same"
|
| 759 |
+
|
| 760 |
+
min_val_d = z_df['z_double_prime'].min()
|
| 761 |
+
max_val_d = z_df['z_double_prime'].max()
|
| 762 |
+
min_idx_d = z_df['z_double_prime'].idxmin()
|
| 763 |
+
max_idx_d = z_df['z_double_prime'].idxmax()
|
| 764 |
+
min_year_d = z_df.loc[min_idx_d, 'date'].year
|
| 765 |
+
max_year_d = z_df.loc[max_idx_d, 'date'].year
|
| 766 |
+
|
| 767 |
+
st.write("**--- Rich Interpretation for Z'' (Non-Manufacturing) ---**")
|
| 768 |
+
st.write(f"Over the chosen period, the Z-Score has {trend_d}.")
|
| 769 |
+
st.write(f"Lowest: {min_val_d:.2f} in {min_year_d}.")
|
| 770 |
+
st.write(f"Highest: {max_val_d:.2f} in {max_year_d}.")
|
| 771 |
+
|
| 772 |
+
if latest_z_double < double_zones['distress']:
|
| 773 |
+
st.write("Current reading is in distress zone. Financial risk is elevated.")
|
| 774 |
+
elif latest_z_double < double_zones['gray_upper']:
|
| 775 |
+
st.write("Current reading is in the gray zone. Financial signals are not clear.")
|
| 776 |
+
else:
|
| 777 |
+
st.write("Current reading is in the safe zone. Financial picture seems stable.")
|
| 778 |
+
|
| 779 |
+
latest_data_double = z_df.iloc[-1]
|
| 780 |
+
double_partials = {
|
| 781 |
+
'd_part1': latest_data_double['d_part1'],
|
| 782 |
+
'd_part2': latest_data_double['d_part2'],
|
| 783 |
+
'd_part3': latest_data_double['d_part3'],
|
| 784 |
+
'd_part4': latest_data_double['d_part4']
|
| 785 |
+
}
|
| 786 |
+
key_driver_double = max(double_partials, key=double_partials.get)
|
| 787 |
+
|
| 788 |
+
if key_driver_double == 'd_part1':
|
| 789 |
+
st.write("The key factor is Working Capital. This measures the company’s ability to cover short-term liabilities with current assets.")
|
| 790 |
+
st.write("A strong working capital contribution means the company has a healthy liquidity buffer, reducing short-term financial risk.")
|
| 791 |
+
st.write("However, excessive working capital can signal inefficient capital deployment, where too much cash is tied up in receivables or inventory.")
|
| 792 |
+
elif key_driver_double == 'd_part2':
|
| 793 |
+
st.write("The key factor is Retained Earnings. This represents accumulated profits that have been reinvested rather than distributed as dividends.")
|
| 794 |
+
st.write("A high retained earnings contribution suggests financial discipline and the ability to self-finance operations, reducing reliance on external funding.")
|
| 795 |
+
st.write("However, if retained earnings are excessive, investors may question whether the company is efficiently reinvesting in growth opportunities or hoarding cash.")
|
| 796 |
+
elif key_driver_double == 'd_part3':
|
| 797 |
+
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.")
|
| 798 |
+
st.write("A high EBIT contribution is a strong indicator of financial health, as it suggests the company generates consistent earnings before financing costs.")
|
| 799 |
+
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.")
|
| 800 |
+
elif key_driver_double == 'd_part4':
|
| 801 |
+
st.write("The key factor is Market Cap vs. Liabilities. This shows how the market values the company relative to its total debt obligations.")
|
| 802 |
+
st.write("A strong contribution from this metric suggests investor confidence in the company’s financial future, lowering perceived bankruptcy risk.")
|
| 803 |
+
st.write("However, if market sentiment is the main driver, the company could be vulnerable to stock price fluctuations rather than underlying business fundamentals.")
|
| 804 |
+
|
| 805 |
+
st.write("To decrease this score, raising debt or reducing EBIT can cause the drop.")
|
| 806 |
+
st.write("An increase in liabilities often pulls down the ratio.")
|
| 807 |
+
|
| 808 |
+
# Z'''
|
| 809 |
+
#st.subheader("Z''' (2023, Service/Tech)")
|
| 810 |
+
|
| 811 |
+
with st.expander("Methodology: Z''' (2023, Service/Tech)", expanded=False):
|
| 812 |
+
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.")
|
| 813 |
+
|
| 814 |
+
# Formula
|
| 815 |
+
st.latex(r"Z''' = 3.25 \times X_1 + 2.85 \times X_2 + 4.15 \times X_3 + 0.95 \times X_4")
|
| 816 |
+
|
| 817 |
+
# Definitions of variables
|
| 818 |
+
st.latex(r"X_1 = \frac{\text{Working Capital}}{\text{Total Assets}}")
|
| 819 |
+
st.write("**Liquidity (X₁)**: Measures short-term financial flexibility. A strong working capital position helps firms cover immediate liabilities.")
|
| 820 |
+
|
| 821 |
+
st.latex(r"X_2 = \frac{\text{Retained Earnings}}{\text{Total Assets}}")
|
| 822 |
+
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.")
|
| 823 |
+
|
| 824 |
+
st.latex(r"X_3 = \frac{\text{EBIT}}{\text{Total Assets}}")
|
| 825 |
+
st.write("**Core Earnings Strength (X₃)**: Measures profitability before interest and taxes, reflecting operational efficiency.")
|
| 826 |
+
|
| 827 |
+
st.latex(r"X_4 = \frac{\text{Market Value of Equity}}{\text{Total Liabilities}}")
|
| 828 |
+
st.write("**Market Confidence (X₄)**: Assesses how the market values the firm relative to its total liabilities. Higher values suggest lower financial risk.")
|
| 829 |
+
|
| 830 |
+
# Academic Justification
|
| 831 |
+
st.write("##### Academic Justification")
|
| 832 |
+
st.write(
|
| 833 |
+
"Unlike traditional manufacturing firms, **service and tech firms rely heavily on intangible assets** (e.g., software, R&D, brand equity), "
|
| 834 |
+
"which are often not reflected on the balance sheet. **Z''' (2023) adjusts for this** by rebalancing weightings to better account for profitability "
|
| 835 |
+
"and market valuation. It provides a more relevant measure for industries where physical assets play a reduced role in financial stability."
|
| 836 |
+
)
|
| 837 |
+
|
| 838 |
+
# Interpretation
|
| 839 |
+
st.write("##### Interpretation")
|
| 840 |
+
st.write(
|
| 841 |
+
"**Z''' > 2.90**: Firm is financially stable, with a low probability of distress. \n"
|
| 842 |
+
"**1.50 ≤ Z''' ≤ 2.90**: 'Gray Area'—financial condition is uncertain. \n"
|
| 843 |
+
"**Z''' < 1.50**: Firm is in financial distress, with an elevated bankruptcy risk."
|
| 844 |
+
)
|
| 845 |
+
|
| 846 |
+
# Downsides / Limitations
|
| 847 |
+
st.write("##### Limitations")
|
| 848 |
+
st.write(
|
| 849 |
+
"- Developed for **service and tech firms**, but may not generalize well to capital-intensive industries.\n"
|
| 850 |
+
"- **Still based on historical financial data**, which may lag behind real-time market shifts.\n"
|
| 851 |
+
"- Market value component (X₄) **introduces volatility**, making results sensitive to stock price swings.\n"
|
| 852 |
+
"- Does not explicitly factor in **R&D investment or future revenue potential**, which are key in tech sectors."
|
| 853 |
+
)
|
| 854 |
+
|
| 855 |
+
|
| 856 |
+
triple_partial_names = [
|
| 857 |
+
('t_part1', "3.25 × (WC/TA)", 'blue'),
|
| 858 |
+
('t_part2', "2.85 × (RE/TA)", 'orange'),
|
| 859 |
+
('t_part3', "4.15 × (EBIT/TA)", 'green'),
|
| 860 |
+
('t_part4', "0.95 × (MktCap/TL)", 'red'),
|
| 861 |
+
]
|
| 862 |
+
triple_zones = {
|
| 863 |
+
'distress': 1.5,
|
| 864 |
+
'gray_lower': 1.5,
|
| 865 |
+
'gray_upper': 2.9,
|
| 866 |
+
'safe': 2.9
|
| 867 |
+
}
|
| 868 |
+
plot_zscore_figure(
|
| 869 |
+
df=z_df,
|
| 870 |
+
date_col='date',
|
| 871 |
+
partial_cols=['t_part1','t_part2','t_part3','t_part4'],
|
| 872 |
+
total_col='z_triple_prime_service',
|
| 873 |
+
partial_names=triple_partial_names,
|
| 874 |
+
total_name="Z''' (Total)",
|
| 875 |
+
title_text="Z''' (2023, Service/Tech)",
|
| 876 |
+
zones=triple_zones,
|
| 877 |
+
ticker=ticker
|
| 878 |
+
)
|
| 879 |
+
|
| 880 |
+
with st.expander("Interpretation", expanded=False):
|
| 881 |
+
latest_z_triple = z_df['z_triple_prime_service'].iloc[-1]
|
| 882 |
+
first_val_t = z_df['z_triple_prime_service'].iloc[0]
|
| 883 |
+
if latest_z_triple > first_val_t:
|
| 884 |
+
trend_t = "increased"
|
| 885 |
+
elif latest_z_triple < first_val_t:
|
| 886 |
+
trend_t = "decreased"
|
| 887 |
+
else:
|
| 888 |
+
trend_t = "remained the same"
|
| 889 |
+
|
| 890 |
+
min_val_t = z_df['z_triple_prime_service'].min()
|
| 891 |
+
max_val_t = z_df['z_triple_prime_service'].max()
|
| 892 |
+
min_idx_t = z_df['z_triple_prime_service'].idxmin()
|
| 893 |
+
max_idx_t = z_df['z_triple_prime_service'].idxmax()
|
| 894 |
+
min_year_t = z_df.loc[min_idx_t, 'date'].year
|
| 895 |
+
max_year_t = z_df.loc[max_idx_t, 'date'].year
|
| 896 |
+
|
| 897 |
+
st.write("**--- Rich Interpretation for Z''' (Service/Tech) ---**")
|
| 898 |
+
st.write(f"Across the selected years, this Z-Score has {trend_t}.")
|
| 899 |
+
st.write(f"Minimum was {min_val_t:.2f} in {min_year_t}.")
|
| 900 |
+
st.write(f"Maximum was {max_val_t:.2f} in {max_year_t}.")
|
| 901 |
+
|
| 902 |
+
if latest_z_triple < triple_zones['distress']:
|
| 903 |
+
st.write("Current reading is in the distress zone. This indicates possible financial strain.")
|
| 904 |
+
elif latest_z_triple < triple_zones['gray_upper']:
|
| 905 |
+
st.write("Current reading is in the gray range. This means uncertain financial signals.")
|
| 906 |
+
else:
|
| 907 |
+
st.write("Current reading is in the safe zone. Financial health looks positive.")
|
| 908 |
+
|
| 909 |
+
latest_data_triple = z_df.iloc[-1]
|
| 910 |
+
triple_partials = {
|
| 911 |
+
't_part1': latest_data_triple['t_part1'],
|
| 912 |
+
't_part2': latest_data_triple['t_part2'],
|
| 913 |
+
't_part3': latest_data_triple['t_part3'],
|
| 914 |
+
't_part4': latest_data_triple['t_part4']
|
| 915 |
+
}
|
| 916 |
+
key_driver_triple = max(triple_partials, key=triple_partials.get)
|
| 917 |
+
if key_driver_triple == 't_part1':
|
| 918 |
+
st.write("Working Capital stands out as the main influence, emphasizing the company's short-term financial flexibility.")
|
| 919 |
+
st.write("A strong working capital contribution indicates a well-managed balance between current assets and liabilities, reducing liquidity risk.")
|
| 920 |
+
st.write("However, if too much capital is tied up in cash or inventory, it may suggest inefficiency in deploying assets for growth.")
|
| 921 |
+
elif key_driver_triple == 't_part2':
|
| 922 |
+
st.write("Retained Earnings plays the biggest role, highlighting the company's ability to reinvest past profits into future growth.")
|
| 923 |
+
st.write("A high retained earnings contribution suggests the company has a history of profitability and financial discipline, reducing reliance on external financing.")
|
| 924 |
+
st.write("However, if retained earnings dominate, it raises questions about whether capital is allocated effectively.")
|
| 925 |
+
elif key_driver_triple == 't_part3':
|
| 926 |
+
st.write("EBIT is the dominant factor, meaning the company’s operational efficiency is the primary driver of financial stability.")
|
| 927 |
+
st.write("A strong EBIT contribution indicates that core business activities are profitable. This supports the firm's financial health.")
|
| 928 |
+
st.write("But if EBIT is the largest driver, the company may be heavily dependent on margins, making it vulnerable to cost pressures.")
|
| 929 |
+
elif key_driver_triple == 't_part4':
|
| 930 |
+
st.write("Market Cap vs. Liabilities leads, suggesting that investor confidence and market valuation are key drivers of financial stability.")
|
| 931 |
+
st.write("A high contribution from this metric means the company’s equity is valued significantly higher than its liabilities.")
|
| 932 |
+
st.write("Reliance on market sentiment can expose the firm to stock price volatility.")
|
| 933 |
+
|
| 934 |
+
st.write("If the goal is to reduce this Z-Score, rising debt or shrinking EBIT will push it downward.")
|
| 935 |
+
st.write("Lower liquidity or lower equity value can also move the score lower.")
|
| 936 |
+
|
| 937 |
+
# Show raw data
|
| 938 |
+
with st.expander("Raw Altman Z Data", expanded=False):
|
| 939 |
+
st.dataframe(z_df)
|
| 940 |
+
|
| 941 |
+
###############################################################################
|
| 942 |
+
# PAGE 2: DISTANCE TO DEFAULT
|
| 943 |
+
###############################################################################
|
| 944 |
+
if page == "Distance-to-Default":
|
| 945 |
+
|
| 946 |
+
dtd_df = st.session_state.get("dtd_results", None)
|
| 947 |
+
if dtd_df is None or dtd_df.empty:
|
| 948 |
+
st.info("Select Page, input the paramters and click 'Run Analysis' on the sidebar.")
|
| 949 |
+
else:
|
| 950 |
+
valid_df = dtd_df.dropna(subset=["chosenDebt", "A_star", "sigmaA_star", "DTD"])
|
| 951 |
+
if valid_df.empty:
|
| 952 |
+
st.warning("No valid rows for Merton calculations in the chosen range.")
|
| 953 |
+
else:
|
| 954 |
+
with st.expander("Methodology: Merton Distance-to-Default (DTD)", expanded=False):
|
| 955 |
+
st.write(
|
| 956 |
+
"The **Distance-to-Default (DTD)** is a structural credit risk model based on Merton's (1974) option pricing theory. "
|
| 957 |
+
"It estimates the likelihood that a firm's asset value will fall below its debt obligations, triggering default."
|
| 958 |
+
)
|
| 959 |
+
|
| 960 |
+
# Merton Model Core Equations
|
| 961 |
+
st.latex(r"V_t = S_t + D_t")
|
| 962 |
+
st.write("**Firm Value (Vₜ)**: The total market value of the firm, consisting of equity (Sₜ) and debt (Dₜ).")
|
| 963 |
+
|
| 964 |
+
st.latex(r"\sigma_V = \frac{S_t}{V_t} \sigma_S")
|
| 965 |
+
st.write("**Asset Volatility (σ_V)**: Derived from the observed equity volatility (σ_S), using the Merton model.")
|
| 966 |
+
|
| 967 |
+
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}}")
|
| 968 |
+
st.latex(r"d_2 = d_1 - \sigma_V \sqrt{T}")
|
| 969 |
+
st.write("**Merton's d₁ and d₂**: Standardized metrics capturing the firm's asset dynamics relative to debt.")
|
| 970 |
+
|
| 971 |
+
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}}")
|
| 972 |
+
st.write("**Distance-to-Default (DTD)**: Measures how many standard deviations the firm's asset value is from the default threshold (Dₜ).")
|
| 973 |
+
|
| 974 |
+
# Academic Justification
|
| 975 |
+
st.write("##### Academic Justification")
|
| 976 |
+
st.write(
|
| 977 |
+
"Merton's model treats a firm's equity as a **call option** on its assets, where default occurs if asset value (Vₜ) "
|
| 978 |
+
"falls below debt (Dₜ) at time T. **DTD quantifies this probability** by measuring how far the firm is from this threshold, "
|
| 979 |
+
"adjusting for volatility. Studies show that **lower DTD values correlate with higher default probabilities**, making it "
|
| 980 |
+
"a key metric for credit risk analysis in corporate finance and banking."
|
| 981 |
+
)
|
| 982 |
+
|
| 983 |
+
# Interpretation
|
| 984 |
+
st.write("##### Interpretation")
|
| 985 |
+
st.write(
|
| 986 |
+
"**DTD > 2.0**: Low probability of default (strong financial health). \n"
|
| 987 |
+
"**1.0 ≤ DTD ≤ 2.0**: Moderate risk—firm is financially stable but should be monitored. \n"
|
| 988 |
+
"**DTD < 1.0**: High default risk—firm is approaching financial distress. \n"
|
| 989 |
+
"**DTD < 0.0**: Extreme risk—firm’s asset value is below its debt obligations."
|
| 990 |
+
)
|
| 991 |
+
|
| 992 |
+
# Downsides / Limitations
|
| 993 |
+
st.write("##### Limitations")
|
| 994 |
+
st.write(
|
| 995 |
+
"- **Assumes market efficiency**, meaning it relies heavily on accurate stock price movements.\n"
|
| 996 |
+
"- **Volatility estimates impact accuracy**, as market fluctuations can distort results.\n"
|
| 997 |
+
"- **Ignores liquidity constraints**—a firm may default due to cash flow problems, even if assets exceed liabilities.\n"
|
| 998 |
+
"- **Not designed for financial institutions**, where leverage and risk dynamics differ significantly.\n"
|
| 999 |
+
"- **Short-term focused**, making it less predictive for long-term financial health."
|
| 1000 |
+
)
|
| 1001 |
+
|
| 1002 |
+
#st.subheader("Annual Distance to Default (Merton Model)")
|
| 1003 |
+
|
| 1004 |
+
# Chart 1
|
| 1005 |
+
fig_time = go.Figure()
|
| 1006 |
+
fig_time.add_trace(
|
| 1007 |
+
go.Scatter(
|
| 1008 |
+
x=dtd_df["year"],
|
| 1009 |
+
y=dtd_df["DTD"],
|
| 1010 |
+
mode="lines+markers",
|
| 1011 |
+
name="Distance to Default"
|
| 1012 |
+
)
|
| 1013 |
+
)
|
| 1014 |
+
fig_time.update_layout(
|
| 1015 |
+
title=f"{ticker} Annual Distance to Default (Merton Model)",
|
| 1016 |
+
title_font=dict(size=26, color="white"),
|
| 1017 |
+
xaxis_title="Year",
|
| 1018 |
+
yaxis_title="Distance to Default (d2)",
|
| 1019 |
+
template="plotly_dark",
|
| 1020 |
+
paper_bgcolor="#0e1117",
|
| 1021 |
+
plot_bgcolor="#0e1117",
|
| 1022 |
+
xaxis=dict(
|
| 1023 |
+
showgrid=True,
|
| 1024 |
+
gridcolor="rgba(255, 255, 255, 0.1)"
|
| 1025 |
+
),
|
| 1026 |
+
yaxis=dict(
|
| 1027 |
+
showgrid=True,
|
| 1028 |
+
gridcolor="rgba(255, 255, 255, 0.1)"
|
| 1029 |
+
)
|
| 1030 |
+
)
|
| 1031 |
+
st.plotly_chart(fig_time, use_container_width=True)
|
| 1032 |
+
|
| 1033 |
+
# --- Dynamic Interpretation for Chart 1 (Exact user code) ---
|
| 1034 |
+
with st.expander("Interpretation", expanded=False):
|
| 1035 |
+
dtd_series = dtd_df["DTD"].dropna()
|
| 1036 |
+
if len(dtd_series) > 1:
|
| 1037 |
+
first_val = dtd_series.iloc[0]
|
| 1038 |
+
last_val = dtd_series.iloc[-1]
|
| 1039 |
+
trend_str = "increased" if last_val > first_val else "decreased" if last_val < first_val else "remained stable"
|
| 1040 |
+
min_val = dtd_series.min()
|
| 1041 |
+
max_val = dtd_series.max()
|
| 1042 |
+
min_yr = dtd_df.loc[dtd_series.idxmin(), "year"]
|
| 1043 |
+
max_yr = dtd_df.loc[dtd_series.idxmax(), "year"]
|
| 1044 |
+
|
| 1045 |
+
st.write("Dynamic Interpretation for Annual Distance to Default:")
|
| 1046 |
+
st.write(f"**1) The time series shows that DTD has {trend_str} from {first_val:.2f} to {last_val:.2f}.**")
|
| 1047 |
+
st.write(f"**2) The lowest DTD was {min_val:.2f} in {min_yr}, and the highest was {max_val:.2f} in {max_yr}.**")
|
| 1048 |
+
if last_val < 0:
|
| 1049 |
+
st.write(" Current DTD is negative. The firm may be in distress territory, implying higher default risk.")
|
| 1050 |
+
elif last_val < 1:
|
| 1051 |
+
st.write(" Current DTD is below 1. This suggests caution, as default risk is higher than comfortable.")
|
| 1052 |
+
elif last_val < 2:
|
| 1053 |
+
st.write(" Current DTD is between 1 and 2. This is moderate territory. Risk is not extreme but warrants monitoring.")
|
| 1054 |
+
else:
|
| 1055 |
+
st.write(" Current DTD is above 2. This generally indicates safer conditions and lower default probability.")
|
| 1056 |
+
else:
|
| 1057 |
+
st.write("DTD time series is insufficient for a dynamic interpretation.")
|
| 1058 |
+
|
| 1059 |
+
# Chart 2: Distribution
|
| 1060 |
+
#st.subheader("Distribution of Simulated Distance-to-Default")
|
| 1061 |
+
latest_data = valid_df.iloc[-1]
|
| 1062 |
+
A_star = latest_data["A_star"]
|
| 1063 |
+
sigmaA_star = latest_data["sigmaA_star"]
|
| 1064 |
+
D = latest_data["chosenDebt"]
|
| 1065 |
+
r = latest_data["rf"]
|
| 1066 |
+
T = 1.0
|
| 1067 |
+
dtd_value = latest_data["DTD"]
|
| 1068 |
+
|
| 1069 |
+
num_simulations = 10000
|
| 1070 |
+
A_simulated = np.random.normal(A_star, sigmaA_star * A_star, num_simulations)
|
| 1071 |
+
A_simulated = np.where(A_simulated > 0, A_simulated, np.nan)
|
| 1072 |
+
DTD_simulated = (np.log(A_simulated / D) + (r - 0.5 * sigmaA_star**2) * T) / (sigmaA_star * np.sqrt(T))
|
| 1073 |
+
DTD_simulated = DTD_simulated[~np.isnan(DTD_simulated)]
|
| 1074 |
+
|
| 1075 |
+
fig_hist = ff.create_distplot(
|
| 1076 |
+
[DTD_simulated],
|
| 1077 |
+
["Simulated DTD"],
|
| 1078 |
+
show_hist=True,
|
| 1079 |
+
show_rug=False,
|
| 1080 |
+
curve_type='kde'
|
| 1081 |
+
)
|
| 1082 |
+
fig_hist.add_vline(
|
| 1083 |
+
x=dtd_value,
|
| 1084 |
+
line=dict(color="red", dash="dash"),
|
| 1085 |
+
annotation_text=f"Actual DTD = {dtd_value:.2f}"
|
| 1086 |
+
)
|
| 1087 |
+
fig_hist.update_layout(
|
| 1088 |
+
title=f"{ticker} Distribution of Simulated Distance-to-Default (DTD)",
|
| 1089 |
+
title_font=dict(size=26, color="white"),
|
| 1090 |
+
xaxis_title="Distance-to-Default (DTD)",
|
| 1091 |
+
yaxis_title="Frequency",
|
| 1092 |
+
template="plotly_dark",
|
| 1093 |
+
paper_bgcolor="#0e1117",
|
| 1094 |
+
plot_bgcolor="#0e1117",
|
| 1095 |
+
xaxis=dict(
|
| 1096 |
+
showgrid=True,
|
| 1097 |
+
gridcolor="rgba(255, 255, 255, 0.1)"
|
| 1098 |
+
),
|
| 1099 |
+
yaxis=dict(
|
| 1100 |
+
showgrid=True,
|
| 1101 |
+
gridcolor="rgba(255, 255, 255, 0.1)"
|
| 1102 |
+
)
|
| 1103 |
+
)
|
| 1104 |
+
st.plotly_chart(fig_hist, use_container_width=True)
|
| 1105 |
+
|
| 1106 |
+
# --- Dynamic Interpretation for Chart 2 (Exact user code) ---
|
| 1107 |
+
with st.expander("Interpretation", expanded=False):
|
| 1108 |
+
mean_sim = np.mean(DTD_simulated)
|
| 1109 |
+
median_sim = np.median(DTD_simulated)
|
| 1110 |
+
st.write("\n--- Dynamic Interpretation for DTD Distribution ---")
|
| 1111 |
+
st.write(f"**1) The mean simulated Distance-to-Default (DTD) is {mean_sim:.2f}, while the median is {median_sim:.2f}.**")
|
| 1112 |
+
if mean_sim < 0:
|
| 1113 |
+
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.")
|
| 1114 |
+
st.write(" This significantly raises default risk, indicating a high probability of financial distress under typical market conditions.")
|
| 1115 |
+
elif mean_sim < 1:
|
| 1116 |
+
st.write(" A large portion of simulations yield a DTD below 1, signaling heightened risk. The firm’s financial cushion against default is thin.")
|
| 1117 |
+
st.write(" Companies in this range often face higher borrowing costs and investor skepticism, as they are perceived as more vulnerable to downturns.")
|
| 1118 |
+
elif mean_sim < 2:
|
| 1119 |
+
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.")
|
| 1120 |
+
st.write(" This suggests moderate financial health. While not at crisis levels, management should remain cautious about leverage and volatility.")
|
| 1121 |
+
else:
|
| 1122 |
+
st.write(" The distribution is mostly above 2, implying that, under most scenarios, the firm maintains a strong buffer against default.")
|
| 1123 |
+
st.write(" Companies in this range generally enjoy greater financial stability, better credit ratings, and lower risk premiums.")
|
| 1124 |
+
|
| 1125 |
+
if dtd_value < mean_sim:
|
| 1126 |
+
st.write(f"**2) The actual DTD ({dtd_value:.2f}) is below the simulation average ({mean_sim:.2f}).**")
|
| 1127 |
+
st.write(" This suggests that the real-world financial position of the company is weaker than the average simulated outcome.")
|
| 1128 |
+
st.write(" It may imply that recent market conditions or company-specific factors have increased risk beyond what the model predicts.")
|
| 1129 |
+
st.write(" Management might need to reinforce liquidity or reassess capital structure to avoid sliding into higher-risk territory.")
|
| 1130 |
+
else:
|
| 1131 |
+
st.write(f"2) The actual DTD ({dtd_value:.2f}) is above the simulation average ({mean_sim:.2f}).")
|
| 1132 |
+
st.write(" This is a positive signal, suggesting that real-world financial conditions are better than the typical simulated scenario.")
|
| 1133 |
+
st.write(" The firm may have a stronger-than-expected balance sheet or be benefiting from favorable market conditions.")
|
| 1134 |
+
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.")
|
| 1135 |
+
|
| 1136 |
+
# Chart 3: Sensitivity of DTD to Asset Value
|
| 1137 |
+
#st.subheader("Sensitivity of DTD to Asset Value")
|
| 1138 |
+
asset_range = np.linspace(D, 1.1 * A_star, 200)
|
| 1139 |
+
dtd_asset = (np.log(asset_range / D) + (r - 0.5 * sigmaA_star**2) * T) / (sigmaA_star * np.sqrt(T))
|
| 1140 |
+
|
| 1141 |
+
fig_asset = go.Figure()
|
| 1142 |
+
fig_asset.add_trace(
|
| 1143 |
+
go.Scatter(
|
| 1144 |
+
x=asset_range,
|
| 1145 |
+
y=dtd_asset,
|
| 1146 |
+
mode='lines',
|
| 1147 |
+
name="DTD vs. Asset Value",
|
| 1148 |
+
line=dict(color="blue")
|
| 1149 |
+
)
|
| 1150 |
+
)
|
| 1151 |
+
fig_asset.add_vline(
|
| 1152 |
+
x=A_star,
|
| 1153 |
+
line=dict(color="red", dash="dash"),
|
| 1154 |
+
annotation_text=f"Estimated A = {A_star:,.2f}"
|
| 1155 |
+
)
|
| 1156 |
+
fig_asset.update_layout(
|
| 1157 |
+
title=f"{ticker} Sensitivity of DTD to Variation in Asset Value",
|
| 1158 |
+
title_font=dict(size=26, color="white"),
|
| 1159 |
+
xaxis_title="Asset Value (A)",
|
| 1160 |
+
yaxis_title="Distance-to-Default (d2)",
|
| 1161 |
+
xaxis_type="log",
|
| 1162 |
+
template="plotly_dark",
|
| 1163 |
+
paper_bgcolor="#0e1117",
|
| 1164 |
+
plot_bgcolor="#0e1117",
|
| 1165 |
+
xaxis=dict(
|
| 1166 |
+
showgrid=True,
|
| 1167 |
+
gridcolor="rgba(255, 255, 255, 0.1)"
|
| 1168 |
+
),
|
| 1169 |
+
yaxis=dict(
|
| 1170 |
+
showgrid=True,
|
| 1171 |
+
gridcolor="rgba(255, 255, 255, 0.1)"
|
| 1172 |
+
)
|
| 1173 |
+
)
|
| 1174 |
+
st.plotly_chart(fig_asset, use_container_width=True)
|
| 1175 |
+
|
| 1176 |
+
# --- Dynamic Interpretation for Chart 3 (Exact user code) ---
|
| 1177 |
+
with st.expander("Interpretation", expanded=False):
|
| 1178 |
+
dtd_lowA = dtd_asset[0]
|
| 1179 |
+
dtd_highA = dtd_asset[-1]
|
| 1180 |
+
st.write("\nDynamic Interpretation for Asset Value Sensitivity:")
|
| 1181 |
+
st.write(f"**1) At the lower bound (A = {asset_range[0]:,.2f}), DTD is {dtd_lowA:.2f}.**")
|
| 1182 |
+
st.write(f"**2) At the higher bound (A = {asset_range[-1]:,.2f}), DTD rises to {dtd_highA:.2f}.**")
|
| 1183 |
+
if dtd_highA > 2:
|
| 1184 |
+
st.write(" If asset value grows, the firm gains a comfortable buffer against default.")
|
| 1185 |
+
else:
|
| 1186 |
+
st.write(" Even at higher asset values, default risk remains moderate. Growth alone may not guarantee safety.")
|
| 1187 |
+
|
| 1188 |
+
# Chart 4: Sensitivity of DTD to Debt Variation
|
| 1189 |
+
#st.subheader("Sensitivity of DTD to Debt Variation")
|
| 1190 |
+
debt_range = np.linspace(0.1 * D, 1.2 * A_star, 300)
|
| 1191 |
+
dtd_debt = (np.log(A_star / debt_range) + (r - 0.5 * sigmaA_star**2) * T) / (sigmaA_star * np.sqrt(T))
|
| 1192 |
+
|
| 1193 |
+
fig_debt = go.Figure()
|
| 1194 |
+
fig_debt.add_trace(
|
| 1195 |
+
go.Scatter(
|
| 1196 |
+
x=debt_range,
|
| 1197 |
+
y=dtd_debt,
|
| 1198 |
+
mode='lines',
|
| 1199 |
+
name="DTD vs. Debt",
|
| 1200 |
+
line=dict(color="green")
|
| 1201 |
+
)
|
| 1202 |
+
)
|
| 1203 |
+
fig_debt.add_vline(
|
| 1204 |
+
x=D,
|
| 1205 |
+
line=dict(color="red", dash="dash"),
|
| 1206 |
+
annotation_text=f"Estimated D = {D:,.2f}"
|
| 1207 |
+
)
|
| 1208 |
+
fig_debt.update_layout(
|
| 1209 |
+
title=f"{ticker} Sensitivity of DTD to Variation in Debt (Extended Range)",
|
| 1210 |
+
title_font=dict(size=26, color="white"),
|
| 1211 |
+
xaxis_title="Debt (D)",
|
| 1212 |
+
yaxis_title="Distance-to-Default (d2)",
|
| 1213 |
+
xaxis_type="log",
|
| 1214 |
+
template="plotly_dark",
|
| 1215 |
+
paper_bgcolor="#0e1117",
|
| 1216 |
+
plot_bgcolor="#0e1117",
|
| 1217 |
+
xaxis=dict(
|
| 1218 |
+
showgrid=True,
|
| 1219 |
+
gridcolor="rgba(255, 255, 255, 0.1)"
|
| 1220 |
+
),
|
| 1221 |
+
yaxis=dict(
|
| 1222 |
+
showgrid=True,
|
| 1223 |
+
gridcolor="rgba(255, 255, 255, 0.1)"
|
| 1224 |
+
)
|
| 1225 |
+
)
|
| 1226 |
+
st.plotly_chart(fig_debt, use_container_width=True)
|
| 1227 |
+
|
| 1228 |
+
# --- Dynamic Interpretation for Chart 4 (Exact user code) ---
|
| 1229 |
+
with st.expander("Interpretation", expanded=False):
|
| 1230 |
+
dtd_lowD = dtd_debt[0]
|
| 1231 |
+
dtd_highD = dtd_debt[-1]
|
| 1232 |
+
st.write("\n--- Dynamic Interpretation for Debt Variation ---")
|
| 1233 |
+
st.write(f"**1) At lower debt levels (D ≈ {debt_range[0]:,.2f}), the estimated Distance-to-Default (DTD) is {dtd_lowD:.2f}.**")
|
| 1234 |
+
st.write(f"**2) At higher debt levels (D ≈ {debt_range[-1]:,.2f}), the estimated DTD drops to {dtd_highD:.2f}.**")
|
| 1235 |
+
|
| 1236 |
+
if dtd_lowD > 2:
|
| 1237 |
+
st.write(" With lower debt, the firm has a strong financial cushion. A DTD above 2 typically indicates low default risk.")
|
| 1238 |
+
st.write(" This suggests the company could sustain economic downturns or earnings declines without significantly increasing its probability of distress.")
|
| 1239 |
+
st.write(" In this range, the firm may enjoy better credit ratings, lower borrowing costs, and greater investor confidence.")
|
| 1240 |
+
elif 1 < dtd_lowD <= 2:
|
| 1241 |
+
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.")
|
| 1242 |
+
st.write(" This suggests that other financial pressures—such as earnings volatility or low asset returns—might be limiting the risk buffer.")
|
| 1243 |
+
st.write(" Maintaining a balanced capital structure with prudent debt management will be key to ensuring financial stability.")
|
| 1244 |
+
else:
|
| 1245 |
+
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.")
|
| 1246 |
+
st.write(" The company may need a more aggressive strategy to strengthen its financial position, such as improving earnings stability or reducing operational risks.")
|
| 1247 |
+
|
| 1248 |
+
if dtd_highD < 0:
|
| 1249 |
+
st.write(" At significantly higher debt levels, the model suggests a **negative DTD**, which signals extreme financial distress.")
|
| 1250 |
+
st.write(" This implies that, under this scenario, the company's total asset value would likely fall below its debt obligations.")
|
| 1251 |
+
st.write(" If this situation were to materialize, the company would be seen as highly vulnerable, potentially leading to credit downgrades or refinancing difficulties.")
|
| 1252 |
+
elif 0 <= dtd_highD < 1:
|
| 1253 |
+
st.write(" With higher debt, DTD drops to below 1, meaning the firm is dangerously close to default.")
|
| 1254 |
+
st.write(" A DTD below 1 indicates that even small negative shocks to asset value could push the firm into financial distress.")
|
| 1255 |
+
st.write(" This could lead to increased borrowing costs, investor concerns, and potential restrictions on raising further capital.")
|
| 1256 |
+
elif 1 <= dtd_highD < 2:
|
| 1257 |
+
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.")
|
| 1258 |
+
st.write(" Companies in this range often need to manage debt maturities carefully and ensure steady cash flow generation to avoid further deterioration.")
|
| 1259 |
+
else:
|
| 1260 |
+
st.write(" Even at a higher debt level, the firm maintains a strong buffer (DTD > 2).")
|
| 1261 |
+
st.write(" This suggests the company has **enough asset value or earnings strength to comfortably manage the additional leverage**.")
|
| 1262 |
+
st.write(" However, increasing debt too aggressively, even in a safe zone, could reduce financial flexibility in downturns.")
|
| 1263 |
+
|
| 1264 |
+
# Chart 5: Asset Value vs. Debt
|
| 1265 |
+
#st.subheader("Asset Value vs. Default Point (Debt)")
|
| 1266 |
+
fig_bar = go.Figure()
|
| 1267 |
+
fig_bar.add_trace(
|
| 1268 |
+
go.Bar(
|
| 1269 |
+
x=["Asset Value (A)", "Debt (D)"],
|
| 1270 |
+
y=[A_star, D],
|
| 1271 |
+
marker=dict(color=["blue", "orange"])
|
| 1272 |
+
)
|
| 1273 |
+
)
|
| 1274 |
+
fig_bar.update_layout(
|
| 1275 |
+
title=f"{ticker} Asset Value vs. Default Point",
|
| 1276 |
+
title_font=dict(size=26, color="white"),
|
| 1277 |
+
yaxis_title="Value (USD)",
|
| 1278 |
+
template="plotly_dark",
|
| 1279 |
+
paper_bgcolor="#0e1117",
|
| 1280 |
+
plot_bgcolor="#0e1117",
|
| 1281 |
+
xaxis=dict(
|
| 1282 |
+
showgrid=True,
|
| 1283 |
+
gridcolor="rgba(255, 255, 255, 0.1)"
|
| 1284 |
+
),
|
| 1285 |
+
yaxis=dict(
|
| 1286 |
+
showgrid=True,
|
| 1287 |
+
gridcolor="rgba(255, 255, 255, 0.1)"
|
| 1288 |
+
)
|
| 1289 |
+
)
|
| 1290 |
+
st.plotly_chart(fig_bar, use_container_width=True)
|
| 1291 |
+
|
| 1292 |
+
# --- Dynamic Interpretation for Chart 5 (Exact user code) ---
|
| 1293 |
+
with st.expander("Interpretation", expanded=False):
|
| 1294 |
+
st.write("\n--- Dynamic Interpretation for Asset Value vs. Debt ---")
|
| 1295 |
+
if A_star > D:
|
| 1296 |
+
st.write(f"**1) The estimated asset value ({A_star:,.2f}) exceeds total debt ({D:,.2f}), providing a financial buffer.**")
|
| 1297 |
+
asset_debt_ratio = A_star / D
|
| 1298 |
+
if asset_debt_ratio > 2:
|
| 1299 |
+
st.write(" The asset-to-debt ratio is above 2, meaning the firm holds **more than double the assets compared to its debt obligations**.")
|
| 1300 |
+
st.write(" This implies a highly secure financial position, with a strong ability to absorb economic downturns or revenue declines.")
|
| 1301 |
+
elif 1.5 <= asset_debt_ratio <= 2:
|
| 1302 |
+
st.write(" The asset-to-debt ratio is between 1.5 and 2, which is considered **moderately strong**.")
|
| 1303 |
+
st.write(" While there is a solid financial cushion, **prudent debt management is still necessary** to maintain stability.")
|
| 1304 |
+
else:
|
| 1305 |
+
st.write(" The asset-to-debt ratio is between 1 and 1.5, meaning the firm has a **narrower but still positive buffer.**")
|
| 1306 |
+
st.write(" This level is acceptable, but **a small decline in asset value could quickly increase financial risk.**")
|
| 1307 |
+
else:
|
| 1308 |
+
st.write(f"1) The estimated asset value ({A_star:,.2f}) is **less than or close to total debt** ({D:,.2f}).")
|
| 1309 |
+
st.write(" This signals **a limited financial cushion**, increasing the probability of distress in unfavorable conditions.")
|
| 1310 |
+
if A_star < D:
|
| 1311 |
+
st.write(" **Warning:** The company’s total assets are lower than its total debt.")
|
| 1312 |
+
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**.")
|
| 1313 |
+
st.write(" Such a position increases the likelihood of credit downgrades and difficulty in securing additional financing.")
|
| 1314 |
+
elif A_star / D < 1.1:
|
| 1315 |
+
st.write(" The asset buffer is extremely thin. A minor shock in earnings or asset valuation could put the firm in distress.")
|
| 1316 |
+
st.write(" The company should consider **reducing leverage or improving asset utilization** to reinforce financial stability.")
|
| 1317 |
+
|
| 1318 |
+
gap = A_star - D
|
| 1319 |
+
if gap > 0.5 * D:
|
| 1320 |
+
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.**")
|
| 1321 |
+
elif 0.2 * D < gap <= 0.5 * D:
|
| 1322 |
+
st.write("2) The firm has **a moderate cushion**, but there is some vulnerability to financial shocks.")
|
| 1323 |
+
st.write(" If debt levels increase or asset values decline, risk could rise quickly.")
|
| 1324 |
+
else:
|
| 1325 |
+
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.")
|
| 1326 |
+
st.write(" A **small misstep in financial strategy could significantly increase default probability.**")
|
| 1327 |
+
|
| 1328 |
+
with st.expander("Raw Distance-to-Default Data", expanded=False):
|
| 1329 |
+
st.dataframe(dtd_df)
|
| 1330 |
+
|
| 1331 |
+
|
| 1332 |
+
# Hide default Streamlit style
|
| 1333 |
+
st.markdown(
|
| 1334 |
+
"""
|
| 1335 |
+
<style>
|
| 1336 |
+
#MainMenu {visibility: hidden;}
|
| 1337 |
+
footer {visibility: hidden;}
|
| 1338 |
+
</style>
|
| 1339 |
+
""",
|
| 1340 |
+
unsafe_allow_html=True
|
| 1341 |
+
)
|