aboutsummaryrefslogtreecommitdiff
path: root/services/data_service.py
diff options
context:
space:
mode:
Diffstat (limited to 'services/data_service.py')
-rw-r--r--services/data_service.py146
1 files changed, 139 insertions, 7 deletions
diff --git a/services/data_service.py b/services/data_service.py
index 73374df..156a33e 100644
--- a/services/data_service.py
+++ b/services/data_service.py
@@ -32,6 +32,60 @@ def get_company_info(ticker: str) -> dict:
@st.cache_data(ttl=300)
+def get_latest_price(ticker: str) -> float | None:
+ """Return latest close price from recent history, falling back to info."""
+ try:
+ t = yf.Ticker(ticker.upper())
+ hist = t.history(period="5d")
+ if hist is not None and not hist.empty:
+ close = hist["Close"].dropna()
+ if not close.empty:
+ return float(close.iloc[-1])
+ info = t.info or {}
+ for key in ("currentPrice", "regularMarketPrice", "previousClose"):
+ val = info.get(key)
+ if val is not None:
+ return float(val)
+ return None
+ except Exception:
+ return None
+
+
+@st.cache_data(ttl=300)
+def get_shares_outstanding(ticker: str) -> float | None:
+ """Return shares outstanding, preferring explicit shares fields."""
+ try:
+ t = yf.Ticker(ticker.upper())
+ info = t.info or {}
+ for key in ("sharesOutstanding", "impliedSharesOutstanding", "floatShares"):
+ val = info.get(key)
+ if val is not None:
+ return float(val)
+ return None
+ except Exception:
+ return None
+
+
+@st.cache_data(ttl=300)
+def get_market_cap_computed(ticker: str) -> float | None:
+ """Return market cap computed as latest price × shares outstanding.
+
+ Falls back to info['marketCap'] only when one of the computed inputs is unavailable.
+ """
+ price = get_latest_price(ticker)
+ shares = get_shares_outstanding(ticker)
+ if price is not None and shares is not None and price > 0 and shares > 0:
+ return float(price) * float(shares)
+
+ try:
+ info = get_company_info(ticker)
+ market_cap = info.get("marketCap")
+ return float(market_cap) if market_cap is not None else None
+ except Exception:
+ return None
+
+
+@st.cache_data(ttl=300)
def get_price_history(ticker: str, period: str = "1y") -> pd.DataFrame:
"""Return OHLCV price history."""
t = yf.Ticker(ticker.upper())
@@ -231,8 +285,8 @@ def compute_ttm_ratios(ticker: str) -> dict:
)
# ── Market data (live) ────────────────────────────────────────────────
- market_cap = info.get("marketCap")
- ev = info.get("enterpriseValue")
+ market_cap = get_market_cap_computed(ticker)
+ ev = None
# ── Profitability ─────────────────────────────────────────────────────
if revenue and revenue > 0:
@@ -266,7 +320,8 @@ def compute_ttm_ratios(ticker: str) -> dict:
if equity and equity > 0:
ratios["priceToBookRatioTTM"] = market_cap / equity
- if ev and ev > 0:
+ if market_cap and market_cap > 0:
+ ev = market_cap + (total_debt or 0.0) - cash
if revenue and revenue > 0:
ratios["evToSalesTTM"] = ev / revenue
if ebitda and ebitda > 0:
@@ -390,7 +445,7 @@ def get_historical_ratios_yfinance(ticker: str) -> list[dict]:
# One year of monthly price history per fiscal year going back 10 years
hist = t.history(period="10y", interval="1mo")
- shares = info.get("sharesOutstanding") or info.get("impliedSharesOutstanding")
+ shares = get_shares_outstanding(ticker)
rows: list[dict] = []
for date in income.columns:
@@ -469,10 +524,13 @@ def get_historical_ratios_yfinance(ticker: str) -> list[dict]:
if total_rev and total_rev > 0:
row["priceToSalesRatio"] = market_cap / total_rev
- # EV/EBITDA — approximate
- if ebitda_raw and ebitda_raw > 0 and total_debt is not None:
+ # EV/EBITDA — approximate. Skip if EBITDA is too small to be meaningful,
+ # which otherwise creates absurd multiples for some software names.
+ if ebitda_raw and ebitda_raw > 1e6 and total_debt is not None:
ev = market_cap + (total_debt or 0) - (total_cash or 0)
- row["enterpriseValueMultiple"] = ev / ebitda_raw
+ multiple = ev / ebitda_raw
+ if 0 < multiple < 500:
+ row["enterpriseValueMultiple"] = multiple
except Exception:
pass
@@ -485,6 +543,80 @@ def get_historical_ratios_yfinance(ticker: str) -> list[dict]:
@st.cache_data(ttl=3600)
+def get_balance_sheet_bridge_items(ticker: str) -> dict:
+ """Return debt/cash bridge inputs from the most recent balance sheet.
+
+ Uses the same raw balance-sheet rows shown in the Financials tab so DCF
+ equity-value bridging reconciles with the visible statements.
+ """
+ df = get_balance_sheet(ticker, quarterly=False)
+ if df is None or df.empty:
+ return {
+ "total_debt": 0.0,
+ "cash_and_equivalents": 0.0,
+ "preferred_equity": 0.0,
+ "minority_interest": 0.0,
+ "net_debt": 0.0,
+ "source_date": None,
+ }
+
+ latest_col = df.columns[0]
+
+ def pick(*labels):
+ for label in labels:
+ if label in df.index:
+ val = df.loc[label, latest_col]
+ if pd.notna(val):
+ return float(val)
+ return None
+
+ short_term_debt = pick(
+ "Current Debt",
+ "Current Debt And Capital Lease Obligation",
+ "Current Capital Lease Obligation",
+ "Commercial Paper",
+ "Other Current Borrowings",
+ ) or 0.0
+
+ long_term_debt = pick(
+ "Long Term Debt",
+ "Long Term Debt And Capital Lease Obligation",
+ "Long Term Capital Lease Obligation",
+ ) or 0.0
+
+ total_debt = pick("Total Debt")
+ if total_debt is None:
+ total_debt = short_term_debt + long_term_debt
+
+ cash_and_equivalents = (
+ pick(
+ "Cash And Cash Equivalents",
+ "Cash Cash Equivalents And Short Term Investments",
+ "Cash",
+ )
+ or 0.0
+ )
+
+ preferred_equity = pick("Preferred Stock") or 0.0
+ minority_interest = pick(
+ "Minority Interest",
+ "Minority Interests",
+ "Total Equity Gross Minority Interest",
+ ) or 0.0
+
+ net_debt = total_debt - cash_and_equivalents
+
+ return {
+ "total_debt": total_debt,
+ "cash_and_equivalents": cash_and_equivalents,
+ "preferred_equity": preferred_equity,
+ "minority_interest": minority_interest,
+ "net_debt": net_debt,
+ "source_date": str(latest_col)[:10],
+ }
+
+
+@st.cache_data(ttl=3600)
def get_free_cash_flow_series(ticker: str) -> pd.Series:
"""Return annual Free Cash Flow series (most recent first)."""
t = yf.Ticker(ticker.upper())