180 lines
5.8 KiB
Python
180 lines
5.8 KiB
Python
import pandas as pd
|
|
import yfinance as yf
|
|
|
|
from tradingagents.dataflows.y_finance import suppress_yfinance_warnings
|
|
from tradingagents.utils.logger import get_logger
|
|
|
|
logger = get_logger(__name__)
|
|
|
|
|
|
def calculate_altman_z_score(ticker: str) -> float:
|
|
"""
|
|
Calculate the Altman Z-Score for a manufacturing company.
|
|
If the company is non-manufacturing/service, a modified Z-score is often used,
|
|
but here we stick to the standard/modified approximation for simplicity.
|
|
Formula: Z = 1.2(X1) + 1.4(X2) + 3.3(X3) + 0.6(X4) + 1.0(X5)
|
|
X1 = Working Capital / Total Assets
|
|
X2 = Retained Earnings / Total Assets
|
|
X3 = EBIT / Total Assets
|
|
X4 = Market Value of Equity / Total Liabilities
|
|
X5 = Sales / Total Assets
|
|
Returns: float (Z-score). Values < 1.81 indicate distress.
|
|
"""
|
|
try:
|
|
with suppress_yfinance_warnings():
|
|
stock = yf.Ticker(ticker.upper())
|
|
bs = stock.balance_sheet
|
|
inc = stock.financials
|
|
info = stock.info
|
|
|
|
if bs.empty or inc.empty:
|
|
return None
|
|
|
|
# Get latest annual data
|
|
latest_bs = bs.iloc[:, 0]
|
|
latest_inc = inc.iloc[:, 0]
|
|
|
|
total_assets = latest_bs.get("Total Assets")
|
|
if not total_assets or total_assets == 0:
|
|
return None
|
|
|
|
total_liabilities = latest_bs.get(
|
|
"Total Liabilities Net Minority Interest", latest_bs.get("Total Liabilities")
|
|
)
|
|
current_assets = latest_bs.get("Current Assets", 0)
|
|
current_liabilities = latest_bs.get("Current Liabilities", 0)
|
|
retained_earnings = latest_bs.get("Retained Earnings", 0)
|
|
|
|
ebit = latest_inc.get("EBIT")
|
|
if pd.isna(ebit):
|
|
ebit = latest_inc.get("Operating Income", 0)
|
|
|
|
sales = latest_inc.get("Total Revenue", 0)
|
|
|
|
market_cap = info.get("marketCap", 0)
|
|
|
|
# Handle NaNs
|
|
total_liabilities = 0 if pd.isna(total_liabilities) else total_liabilities
|
|
retained_earnings = 0 if pd.isna(retained_earnings) else retained_earnings
|
|
|
|
working_capital = current_assets - current_liabilities
|
|
|
|
x1 = working_capital / total_assets
|
|
x2 = retained_earnings / total_assets
|
|
x3 = ebit / total_assets
|
|
x4 = market_cap / total_liabilities if total_liabilities > 0 else 0
|
|
x5 = sales / total_assets
|
|
|
|
z_score = 1.2 * x1 + 1.4 * x2 + 3.3 * x3 + 0.6 * x4 + 1.0 * x5
|
|
return round(z_score, 2)
|
|
except Exception as e:
|
|
logger.debug(f"Error calculating Altman Z-Score for {ticker}: {e}")
|
|
return None
|
|
|
|
|
|
def calculate_piotroski_f_score(ticker: str) -> int:
|
|
"""
|
|
Calculate the Piotroski F-Score (0-9).
|
|
High score (7-9) indicates strong value/health.
|
|
Low score (0-3) indicates poor financial health.
|
|
"""
|
|
try:
|
|
with suppress_yfinance_warnings():
|
|
stock = yf.Ticker(ticker.upper())
|
|
bs = stock.balance_sheet
|
|
inc = stock.financials
|
|
cf = stock.cashflow
|
|
|
|
if (
|
|
bs.empty
|
|
or inc.empty
|
|
or cf.empty
|
|
or len(bs.columns) < 2
|
|
or len(inc.columns) < 2
|
|
or len(cf.columns) < 1
|
|
):
|
|
return None
|
|
|
|
latest_bs = bs.iloc[:, 0]
|
|
prev_bs = bs.iloc[:, 1]
|
|
|
|
latest_inc = inc.iloc[:, 0]
|
|
prev_inc = inc.iloc[:, 1]
|
|
|
|
latest_cf = cf.iloc[:, 0]
|
|
|
|
total_assets = latest_bs.get("Total Assets")
|
|
prev_total_assets = prev_bs.get("Total Assets")
|
|
|
|
if not total_assets or not prev_total_assets or total_assets == 0 or prev_total_assets == 0:
|
|
return None
|
|
|
|
score = 0
|
|
|
|
# Profitability
|
|
# 1. ROA > 0
|
|
net_income = latest_inc.get("Net Income", 0)
|
|
roa = net_income / total_assets
|
|
if roa > 0:
|
|
score += 1
|
|
|
|
# 2. Operating Cash Flow > 0
|
|
cfo = latest_cf.get("Operating Cash Flow", 0)
|
|
if pd.isna(cfo):
|
|
cfo = latest_cf.get("Total Cash From Operating Activities", 0)
|
|
if cfo > 0:
|
|
score += 1
|
|
|
|
# 3. Change in ROA > 0
|
|
prev_net_income = prev_inc.get("Net Income", 0)
|
|
prev_roa = prev_net_income / prev_total_assets
|
|
if roa > prev_roa:
|
|
score += 1
|
|
|
|
# 4. CFO > Net Income
|
|
if cfo > net_income:
|
|
score += 1
|
|
|
|
# Leverage, Liquidity
|
|
# 5. Change in Leverage (Long-Term Debt / Assets) < 0
|
|
ltd = latest_bs.get("Long Term Debt", 0)
|
|
prev_ltd = prev_bs.get("Long Term Debt", 0)
|
|
if (ltd / total_assets) < (prev_ltd / prev_total_assets):
|
|
score += 1
|
|
|
|
# 6. Change in Current Ratio > 0
|
|
ca = latest_bs.get("Current Assets", 0)
|
|
cl = latest_bs.get("Current Liabilities", 1) # avoid div by zero
|
|
prev_ca = prev_bs.get("Current Assets", 0)
|
|
prev_cl = prev_bs.get("Current Liabilities", 1)
|
|
cr = ca / cl
|
|
prev_cr = prev_ca / prev_cl
|
|
if cr > prev_cr:
|
|
score += 1
|
|
|
|
# 7. Change in Shares Outstanding < 0 (or constant)
|
|
shares = latest_bs.get("Ordinary Shares Number", 0)
|
|
prev_shares = prev_bs.get("Ordinary Shares Number", 0)
|
|
if shares <= prev_shares:
|
|
score += 1
|
|
|
|
# Operating Efficiency
|
|
# 8. Change in Gross Margin > 0
|
|
gp = latest_inc.get("Gross Profit", 0)
|
|
prev_gp = prev_inc.get("Gross Profit", 0)
|
|
rev = latest_inc.get("Total Revenue", 1)
|
|
prev_rev = prev_inc.get("Total Revenue", 1)
|
|
if (gp / rev) > (prev_gp / prev_rev):
|
|
score += 1
|
|
|
|
# 9. Change in Asset Turnover > 0
|
|
ato = rev / total_assets
|
|
prev_ato = prev_rev / prev_total_assets
|
|
if ato > prev_ato:
|
|
score += 1
|
|
|
|
return score
|
|
except Exception as e:
|
|
logger.debug(f"Error calculating Piotroski F-Score for {ticker}: {e}")
|
|
return None
|