diff options
Diffstat (limited to 'components')
| -rw-r--r-- | components/valuation.py | 69 |
1 files changed, 49 insertions, 20 deletions
diff --git a/components/valuation.py b/components/valuation.py index 88ea889..863193b 100644 --- a/components/valuation.py +++ b/components/valuation.py @@ -4,7 +4,11 @@ import plotly.graph_objects as go import streamlit as st from services.data_service import ( get_company_info, + get_latest_price, + get_shares_outstanding, + get_market_cap_computed, get_free_cash_flow_series, + get_balance_sheet_bridge_items, get_analyst_price_targets, get_recommendations_summary, get_earnings_history, @@ -140,7 +144,7 @@ def _render_ratios(ticker: str): info = get_company_info(ticker) if not ratios and not info: - st.info("Ratio data unavailable. Check your FMP API key.") + st.info("Ratio data unavailable.") return def r(key, fmt=fmt_ratio): @@ -184,6 +188,10 @@ def _render_ratios(ticker: str): # ── DCF Model ──────────────────────────────────────────────────────────────── +def _net_debt_label(value: float) -> str: + return "Net Cash" if value < 0 else "Net Debt" + + def _render_dcf(ticker: str): info = get_company_info(ticker) @@ -198,17 +206,13 @@ def _render_dcf(ticker: str): ) return - shares = info.get("sharesOutstanding") or info.get("floatShares") - current_price = info.get("currentPrice") or info.get("regularMarketPrice") - total_debt = info.get("totalDebt") or 0.0 - cash_and_equivalents = ( - info.get("totalCash") - or info.get("cash") - or info.get("cashAndCashEquivalents") - or 0.0 - ) - preferred_equity = info.get("preferredStock") or 0.0 - minority_interest = info.get("minorityInterest") or 0.0 + shares = get_shares_outstanding(ticker) + current_price = get_latest_price(ticker) + bridge_items = get_balance_sheet_bridge_items(ticker) + total_debt = bridge_items["total_debt"] + cash_and_equivalents = bridge_items["cash_and_equivalents"] + preferred_equity = bridge_items["preferred_equity"] + minority_interest = bridge_items["minority_interest"] if not shares: st.info("Shares outstanding not available — DCF cannot be computed.") @@ -276,14 +280,34 @@ def _render_dcf(ticker: str): m3.metric("Upside / Downside", f"{upside * 100:+.1f}%", delta=f"{upside * 100:+.1f}%") m4.metric("FCF Growth Used", f"{result['growth_rate_used'] * 100:.1f}%") + source_date = bridge_items.get("source_date") st.caption( "DCF is modeled on firm-level free cash flow, so enterprise value is bridged to equity value " - "using cash and debt before calculating per-share value." + "using debt and cash from the most recent balance sheet before calculating per-share value." ) + if source_date: + st.caption(f"Balance-sheet bridge source date: **{source_date}**") + + with st.expander("Methodology & sources", expanded=False): + st.markdown( + "- **TTM ratios:** computed from raw quarterly financial statements where possible.\n" + "- **Enterprise Value:** computed as market cap + total debt - cash & equivalents.\n" + "- **Market cap:** computed as latest price × shares outstanding when available.\n" + "- **Shares outstanding:** pulled from yfinance shares fields.\n" + "- **DCF bridge:** uses the most recent annual balance sheet for debt, cash, preferred equity, and minority interest.\n" + "- **Historical ratios:** computed from annual statements plus price history, with guards against nonsensical EV/EBITDA values.\n" + "- **Forward metrics:** analyst-driven items such as Forward P/E and estimates still depend on vendor data." + ) + + bridge_a, bridge_b, bridge_c, bridge_d = st.columns(4) + bridge_a.metric("Total Debt", fmt_large(total_debt)) + bridge_b.metric("Cash & Equivalents", fmt_large(cash_and_equivalents)) + bridge_c.metric("Preferred Equity", fmt_large(preferred_equity)) + bridge_d.metric("Minority Interest", fmt_large(minority_interest)) bridge1, bridge2, bridge3, bridge4 = st.columns(4) bridge1.metric("Enterprise Value", fmt_large(result["enterprise_value"])) - bridge2.metric("Net Debt", fmt_large(result["net_debt"])) + bridge2.metric(_net_debt_label(result["net_debt"]), fmt_large(abs(result["net_debt"]))) bridge3.metric("Equity Value", fmt_large(result["equity_value"])) bridge4.metric("Terminal Value PV", fmt_large(result["terminal_value_pv"])) @@ -316,11 +340,14 @@ def _render_dcf(ticker: str): # Use income statement EBITDA — info["ebitda"] is unreliable in yfinance ebitda = get_ebitda_from_income_stmt(ticker) or info.get("ebitda") - total_debt = info.get("totalDebt") or 0.0 - total_cash = info.get("totalCash") or 0.0 + ev_bridge_items = get_balance_sheet_bridge_items(ticker) + total_debt = ev_bridge_items["total_debt"] + total_cash = ev_bridge_items["cash_and_equivalents"] - # Compute current EV/EBITDA from our own data, not the bad info dict value - ev_val = info.get("enterpriseValue") + market_cap = get_market_cap_computed(ticker) + ev_val = None + if market_cap and ebitda and ebitda > 0: + ev_val = float(market_cap) + float(total_debt or 0.0) - float(total_cash or 0.0) ev_ebitda_current = (ev_val / ebitda) if (ev_val and ebitda and ebitda > 0) else None if not ebitda or ebitda <= 0: @@ -367,9 +394,11 @@ def _render_dcf(ticker: str): ev_m4.metric("Implied EV", fmt_large(ev_result["implied_ev"])) st.caption( f"EBITDA: {fmt_large(ebitda)} · " - f"Net Debt: {fmt_large(ev_result['net_debt'])} · " + f"{_net_debt_label(ev_result['net_debt'])}: {fmt_large(abs(ev_result['net_debt']))} · " f"Equity Value: {fmt_large(ev_result['equity_value'])}" ) + if ev_bridge_items.get("source_date"): + st.caption(f"EV/EBITDA bridge source date: **{ev_bridge_items['source_date']}**") else: st.warning("Could not compute EV/EBITDA valuation.") @@ -741,7 +770,7 @@ def _render_forward_estimates(ticker: str): return info = get_company_info(ticker) - current_price = info.get("currentPrice") or info.get("regularMarketPrice") + current_price = get_latest_price(ticker) tab_ann, tab_qtr = st.tabs(["Annual", "Quarterly"]) |
