diff options
Diffstat (limited to 'services/data_service.py')
| -rw-r--r-- | services/data_service.py | 146 |
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()) |
