aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--components/valuation.py426
-rw-r--r--services/data_service.py24
-rw-r--r--services/valuation_service.py15
3 files changed, 189 insertions, 276 deletions
diff --git a/components/valuation.py b/components/valuation.py
index 89e7d49..24405d6 100644
--- a/components/valuation.py
+++ b/components/valuation.py
@@ -8,6 +8,7 @@ from services.data_service import (
get_shares_outstanding,
get_market_cap_computed,
get_free_cash_flow_series,
+ get_free_cash_flow_ttm,
get_revenue_ttm,
get_balance_sheet_bridge_items,
get_analyst_price_targets,
@@ -334,7 +335,7 @@ def _build_model_context(ticker: str) -> dict:
except Exception:
fcf_series = pd.Series(dtype=float)
- base_fcf = float(fcf_series.iloc[-1]) if not fcf_series.empty else None
+ base_fcf = _coerce_float(get_free_cash_flow_ttm(ticker))
hist_growth = compute_historical_growth_rate(fcf_series) if len(fcf_series) >= 2 else None
ebitda = _coerce_float(ratios_data.get("ebitdaTTM"))
revenue_ttm = _coerce_float(get_revenue_ttm(ticker))
@@ -391,12 +392,13 @@ def _build_model_context(ticker: str) -> dict:
ev_value = None
ev_ebitda_current = None
ev_revenue_current = None
+ other_claims = preferred_equity + minority_interest
if market_cap and market_cap > 0 and ebitda and ebitda > 0:
- ev_value = float(market_cap) + total_debt - cash_and_equivalents
+ ev_value = float(market_cap) + total_debt - cash_and_equivalents + other_claims
if ev_value > 0:
ev_ebitda_current = ev_value / ebitda
elif market_cap and market_cap > 0:
- ev_value = float(market_cap) + total_debt - cash_and_equivalents
+ ev_value = float(market_cap) + total_debt - cash_and_equivalents + other_claims
if ev_value and ev_value > 0 and revenue_ttm and revenue_ttm > 0:
ev_revenue_current = ev_value / revenue_ttm
@@ -530,6 +532,7 @@ def _render_dcf_model(ctx: dict):
cash_and_equivalents=ctx["cash_and_equivalents"],
preferred_equity=ctx["preferred_equity"],
minority_interest=ctx["minority_interest"],
+ base_fcf_override=ctx["base_fcf"],
)
if not result:
@@ -557,27 +560,6 @@ def _render_dcf_model(ctx: dict):
"adds a terminal value, and then bridges from enterprise value to equity value per share."
)
- m1, m2, m3, m4 = st.columns(4)
- m1.metric("Equity Value / Share", fmt_currency(iv))
- if current_price:
- upside = (iv - current_price) / current_price
- m2.metric("Current Price", fmt_currency(current_price))
- 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}%")
-
- if current_price and current_price > 0:
- valuation_gap = iv - current_price
- market_message = "above" if valuation_gap > 0 else "below"
- if abs(valuation_gap) < 0.005:
- market_message = "roughly in line with"
- implied_value = _escape_markdown_currency(fmt_currency(iv))
- gap_value = _escape_markdown_currency(fmt_currency(abs(valuation_gap)))
- current_value = _escape_markdown_currency(fmt_currency(current_price))
- st.markdown(
- f"The DCF implies **{implied_value} per share**, which is **{gap_value} "
- f"{market_message}** the current market price of **{current_value}**."
- )
-
calc_a, calc_b, calc_c, calc_d = st.columns(4)
calc_a.metric("Base FCF", fmt_large(result["base_fcf"]))
calc_b.metric("Forecast FCF PV", fmt_large(result["fcf_pv_sum"]))
@@ -592,103 +574,59 @@ def _render_dcf_model(ctx: dict):
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 quarterly 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."
- )
-
- st.markdown("**Enterprise Value To Equity Value Bridge**")
- bridge_a, bridge_b, bridge_c, bridge_d = st.columns(4)
- bridge_a.metric("Total Debt", fmt_large(ctx["total_debt"]))
- bridge_b.metric("Cash & Equivalents", fmt_large(ctx["cash_and_equivalents"]))
- bridge_c.metric("Preferred Equity", fmt_large(ctx["preferred_equity"]))
- bridge_d.metric("Minority Interest", fmt_large(ctx["minority_interest"]))
-
- bridge1, bridge2, bridge3, bridge4 = st.columns(4)
- bridge1.metric("Enterprise Value", fmt_large(result["enterprise_value"]))
- 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"]))
-
- bridge_labels = ["Enterprise Value"]
- bridge_measures = ["absolute"]
- bridge_values = [float(result["enterprise_value"])]
- bridge_text = [fmt_large(result["enterprise_value"])]
-
- if ctx["total_debt"]:
- bridge_labels.append("Add Debt")
- bridge_measures.append("relative")
- bridge_values.append(float(ctx["total_debt"]))
- bridge_text.append(fmt_large(ctx["total_debt"]))
- if ctx["cash_and_equivalents"]:
- bridge_labels.append("Less Cash")
- bridge_measures.append("relative")
- bridge_values.append(-float(ctx["cash_and_equivalents"]))
- bridge_text.append(fmt_large(-float(ctx["cash_and_equivalents"])))
- if ctx["preferred_equity"]:
- bridge_labels.append("Less Preferred")
- bridge_measures.append("relative")
- bridge_values.append(-float(ctx["preferred_equity"]))
- bridge_text.append(fmt_large(-float(ctx["preferred_equity"])))
- if ctx["minority_interest"]:
- bridge_labels.append("Less Minority")
- bridge_measures.append("relative")
- bridge_values.append(-float(ctx["minority_interest"]))
- bridge_text.append(fmt_large(-float(ctx["minority_interest"])))
-
- bridge_labels.append("Equity Value")
- bridge_measures.append("total")
- bridge_values.append(float(result["equity_value"]))
- bridge_text.append(fmt_large(result["equity_value"]))
+ years = [f"Year {y}" for y in result["years"]]
+ discounted = result["discounted_fcfs"]
+ terminal_pv = result["terminal_value_pv"]
- bridge_fig = go.Figure(
- go.Waterfall(
- x=bridge_labels,
- measure=bridge_measures,
- y=bridge_values,
- text=bridge_text,
- textposition="outside",
- connector={"line": {"color": "#6f7785"}},
- increasing={"marker": {"color": "#4F8EF7"}},
- decreasing={"marker": {"color": "#F76E6E"}},
- totals={"marker": {"color": "#2ecc71"}},
- )
- )
- bridge_fig.update_layout(
- title="Enterprise Value Bridge To Equity Value",
- yaxis_title="USD",
+ fig = go.Figure(go.Bar(
+ x=years + ["Terminal Value"],
+ y=[(v / 1e9) for v in discounted] + [terminal_pv / 1e9],
+ marker_color=["#4F8EF7"] * len(years) + ["#F7A24F"],
+ text=[f"${v / 1e9:.2f}B" for v in discounted] + [f"${terminal_pv / 1e9:.2f}B"],
+ textposition="outside",
+ ))
+ fig.update_layout(
+ title="Enterprise Value Build: PV of Forecast FCFs + Terminal Value (Billions)",
+ yaxis_title="USD (Billions)",
plot_bgcolor="rgba(0,0,0,0)",
paper_bgcolor="rgba(0,0,0,0)",
- margin=dict(l=0, r=0, t=44, b=0),
+ margin=dict(l=0, r=0, t=40, b=0),
height=360,
)
- st.plotly_chart(bridge_fig, use_container_width=True)
+ st.plotly_chart(fig, use_container_width=True)
+
+ st.markdown("**Enterprise Value To Equity Value Bridge**")
+ st.caption("Enterprise value is adjusted for net debt or net cash and other claims to arrive at equity value.")
+
+ bridge_a, bridge_b, bridge_c, bridge_d = st.columns(4)
+ bridge_a.metric("Enterprise Value", fmt_large(result["enterprise_value"]))
+ bridge_b.metric(_net_debt_label(result["net_debt"]), fmt_large(abs(result["net_debt"])))
+ bridge_c.metric("Other Claims", fmt_large(ctx["preferred_equity"] + ctx["minority_interest"]))
+ bridge_d.metric("Equity Value", fmt_large(result["equity_value"]))
+
+ detail_a, detail_b, detail_c = st.columns(3)
+ detail_a.metric("Total Debt", fmt_large(ctx["total_debt"]))
+ detail_b.metric("Cash & Equivalents", fmt_large(ctx["cash_and_equivalents"]))
+ detail_c.metric("Preferred + Minority", fmt_large(ctx["preferred_equity"] + ctx["minority_interest"]))
if market_cap and market_cap > 0:
- compare_a, compare_b, compare_c, compare_d = st.columns(4)
- compare_a.metric("Market Cap", fmt_large(market_cap))
+ st.markdown("**Market Comparison**")
+ compare_a, compare_b = st.columns(2)
if market_enterprise_value and market_enterprise_value > 0:
ev_delta = (result["enterprise_value"] - market_enterprise_value) / market_enterprise_value
- compare_b.metric(
+ compare_a.metric(
"Market Enterprise Value",
fmt_large(market_enterprise_value),
delta=f"{ev_delta * 100:+.1f}%",
)
equity_delta = (result["equity_value"] - market_cap) / market_cap
- compare_c.metric("DCF Equity Value", fmt_large(result["equity_value"]), delta=f"{equity_delta * 100:+.1f}%")
- compare_d.metric("Shares Outstanding", f"{ctx['shares'] / 1e6:,.1f}M")
+ compare_b.metric("Market Cap", fmt_large(market_cap), delta=f"{equity_delta * 100:+.1f}%")
summary_rows = [
{
"Step": "1. Start with base free cash flow",
"Value": fmt_large(result["base_fcf"]),
- "What it means": "Most recent annual free cash flow used as the starting point.",
+ "What it means": "Trailing-twelve-month free cash flow used as the starting point.",
},
{
"Step": "2. Project and discount forecast cash flows",
@@ -720,26 +658,46 @@ def _render_dcf_model(ctx: dict):
st.write("")
- years = [f"Year {y}" for y in result["years"]]
- discounted = result["discounted_fcfs"]
- terminal_pv = result["terminal_value_pv"]
+ st.markdown("**DCF Conclusion**")
+ conclusion_a, conclusion_b, conclusion_c, conclusion_d = st.columns(4)
+ conclusion_a.metric("Equity Value / Share", fmt_currency(iv))
+ if current_price:
+ upside = (iv - current_price) / current_price
+ conclusion_b.metric("Current Price", fmt_currency(current_price))
+ conclusion_c.metric("Upside / Downside", f"{upside * 100:+.1f}%", delta=f"{upside * 100:+.1f}%")
+ else:
+ conclusion_b.metric("FCF Growth Used", f"{result['growth_rate_used'] * 100:.1f}%")
+ conclusion_d.metric("Shares Outstanding", f"{ctx['shares'] / 1e6:,.1f}M")
- fig = go.Figure(go.Bar(
- x=years + ["Terminal Value"],
- y=[(v / 1e9) for v in discounted] + [terminal_pv / 1e9],
- marker_color=["#4F8EF7"] * len(years) + ["#F7A24F"],
- text=[f"${v / 1e9:.2f}B" for v in discounted] + [f"${terminal_pv / 1e9:.2f}B"],
- textposition="outside",
- ))
- fig.update_layout(
- title="Enterprise Value Build: PV of Forecast FCFs + Terminal Value (Billions)",
- yaxis_title="USD (Billions)",
- plot_bgcolor="rgba(0,0,0,0)",
- paper_bgcolor="rgba(0,0,0,0)",
- margin=dict(l=0, r=0, t=40, b=0),
- height=360,
- )
- st.plotly_chart(fig, use_container_width=True)
+ if current_price:
+ assumption_a, assumption_b = st.columns(2)
+ assumption_a.metric("FCF Growth Used", f"{result['growth_rate_used'] * 100:.1f}%")
+ assumption_b.metric("Equity Value", fmt_large(result["equity_value"]))
+
+ if current_price and current_price > 0:
+ valuation_gap = iv - current_price
+ market_message = "above" if valuation_gap > 0 else "below"
+ if abs(valuation_gap) < 0.005:
+ market_message = "roughly in line with"
+ implied_value = _escape_markdown_currency(fmt_currency(iv))
+ gap_value = _escape_markdown_currency(fmt_currency(abs(valuation_gap)))
+ current_value = _escape_markdown_currency(fmt_currency(current_price))
+ st.markdown(
+ f"The DCF implies **{implied_value} per share**, which is **{gap_value} "
+ f"{market_message}** the current market price of **{current_value}**."
+ )
+
+ 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 + preferred equity + minority interest.\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 quarterly balance sheet for debt, cash, preferred equity, and minority interest.\n"
+ "- **Base FCF:** computed as trailing-twelve-month free cash flow from the last four quarterly cash flow statements.\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."
+ )
def _render_ev_ebitda_model(ctx: dict):
st.markdown("**EV/EBITDA Valuation**")
@@ -768,6 +726,8 @@ def _render_ev_ebitda_model(ctx: dict):
ebitda=float(ctx["ebitda"]),
total_debt=ctx["total_debt"],
total_cash=ctx["cash_and_equivalents"],
+ preferred_equity=ctx["preferred_equity"],
+ minority_interest=ctx["minority_interest"],
shares_outstanding=float(ctx["shares"]),
target_multiple=target_multiple,
)
@@ -785,38 +745,14 @@ def _render_ev_ebitda_model(ctx: dict):
float(market_cap)
+ float(ctx["total_debt"])
- float(ctx["cash_and_equivalents"])
+ + float(ctx["preferred_equity"])
+ + float(ctx["minority_interest"])
)
st.caption(
"This model applies a target EV/EBITDA multiple to current EBITDA, then bridges from enterprise value to equity value per share."
)
- ev_m1, ev_m2, ev_m3, ev_m4 = st.columns(4)
- ev_m1.metric("Implied Price / Share", fmt_currency(imp_price))
- if current_price:
- ev_upside = (imp_price - current_price) / current_price
- ev_m2.metric("Current Price", fmt_currency(current_price))
- ev_m3.metric(
- "Upside / Downside",
- f"{ev_upside * 100:+.1f}%",
- delta=f"{ev_upside * 100:+.1f}%",
- )
- ev_m4.metric("Implied EV", fmt_large(ev_result["implied_ev"]))
-
- if current_price and current_price > 0:
- valuation_gap = imp_price - current_price
- market_message = "above" if valuation_gap > 0 else "below"
- if abs(valuation_gap) < 0.005:
- market_message = "roughly in line with"
- implied_value = _escape_markdown_currency(fmt_currency(imp_price))
- gap_value = _escape_markdown_currency(fmt_currency(abs(valuation_gap)))
- current_value = _escape_markdown_currency(fmt_currency(current_price))
- st.markdown(
- f"At **{target_multiple:.1f}x EBITDA**, the model implies **{implied_value} per share**, "
- f"which is **{gap_value} {market_message}** the current market price of "
- f"**{current_value}**."
- )
-
calc_a, calc_b, calc_c, calc_d = st.columns(4)
calc_a.metric("EBITDA Used", fmt_large(ctx["ebitda"]))
calc_b.metric("Target Multiple", f"{target_multiple:.1f}x")
@@ -826,69 +762,25 @@ def _render_ev_ebitda_model(ctx: dict):
st.caption(
f"EBITDA: {fmt_large(ctx['ebitda'])} · "
f"{_net_debt_label(ev_result['net_debt'])}: {fmt_large(abs(ev_result['net_debt']))} · "
+ f"Other claims: {fmt_large(ev_result['other_claims'])} · "
f"Equity Value: {fmt_large(ev_result['equity_value'])}"
)
source_date = ctx["bridge_items"].get("source_date")
if source_date:
st.caption(f"EV/EBITDA bridge source date: **{source_date}**")
- bridge_labels = ["Implied EV"]
- bridge_measures = ["absolute"]
- bridge_values = [float(ev_result["implied_ev"])]
- bridge_text = [fmt_large(ev_result["implied_ev"])]
-
- if ctx["total_debt"]:
- bridge_labels.append("Add Debt")
- bridge_measures.append("relative")
- bridge_values.append(float(ctx["total_debt"]))
- bridge_text.append(fmt_large(ctx["total_debt"]))
- if ctx["cash_and_equivalents"]:
- bridge_labels.append("Less Cash")
- bridge_measures.append("relative")
- bridge_values.append(-float(ctx["cash_and_equivalents"]))
- bridge_text.append(fmt_large(-float(ctx["cash_and_equivalents"])))
-
- bridge_labels.append("Implied Equity")
- bridge_measures.append("total")
- bridge_values.append(float(ev_result["equity_value"]))
- bridge_text.append(fmt_large(ev_result["equity_value"]))
-
- bridge_fig = go.Figure(
- go.Waterfall(
- x=bridge_labels,
- measure=bridge_measures,
- y=bridge_values,
- text=bridge_text,
- textposition="outside",
- connector={"line": {"color": "#6f7785"}},
- increasing={"marker": {"color": "#4F8EF7"}},
- decreasing={"marker": {"color": "#F76E6E"}},
- totals={"marker": {"color": "#2ecc71"}},
- )
- )
- bridge_fig.update_layout(
- title="EV/EBITDA Bridge To Equity Value",
- yaxis_title="USD",
- plot_bgcolor="rgba(0,0,0,0)",
- paper_bgcolor="rgba(0,0,0,0)",
- margin=dict(l=0, r=0, t=44, b=0),
- height=320,
- )
- st.plotly_chart(bridge_fig, use_container_width=True)
-
if market_cap and market_cap > 0:
- compare_a, compare_b, compare_c, compare_d = st.columns(4)
- compare_a.metric("Market Cap", fmt_large(market_cap))
+ st.markdown("**Market Comparison**")
+ compare_a, compare_b = st.columns(2)
if market_enterprise_value and market_enterprise_value > 0:
ev_delta = (ev_result["implied_ev"] - market_enterprise_value) / market_enterprise_value
- compare_b.metric(
+ compare_a.metric(
"Market Enterprise Value",
fmt_large(market_enterprise_value),
delta=f"{ev_delta * 100:+.1f}%",
)
equity_delta = (ev_result["equity_value"] - market_cap) / market_cap
- compare_c.metric("Model Equity Value", fmt_large(ev_result["equity_value"]), delta=f"{equity_delta * 100:+.1f}%")
- compare_d.metric("Shares Outstanding", f"{ctx['shares'] / 1e6:,.1f}M")
+ compare_b.metric("Market Cap", fmt_large(market_cap), delta=f"{equity_delta * 100:+.1f}%")
summary_rows = [
{
@@ -909,7 +801,7 @@ def _render_ev_ebitda_model(ctx: dict):
{
"Step": "4. Bridge to equity value",
"Value": fmt_large(ev_result["equity_value"]),
- "What it means": "Enterprise value less net debt.",
+ "What it means": "Enterprise value less net debt and other claims.",
},
{
"Step": "5. Convert to value per share",
@@ -919,6 +811,33 @@ def _render_ev_ebitda_model(ctx: dict):
]
st.dataframe(pd.DataFrame(summary_rows), use_container_width=True, hide_index=True)
+ st.markdown("**EV/EBITDA Conclusion**")
+ ev_m1, ev_m2, ev_m3, ev_m4 = st.columns(4)
+ ev_m1.metric("Implied Price / Share", fmt_currency(imp_price))
+ if current_price:
+ ev_upside = (imp_price - current_price) / current_price
+ ev_m2.metric("Current Price", fmt_currency(current_price))
+ ev_m3.metric(
+ "Upside / Downside",
+ f"{ev_upside * 100:+.1f}%",
+ delta=f"{ev_upside * 100:+.1f}%",
+ )
+ ev_m4.metric("Implied EV", fmt_large(ev_result["implied_ev"]))
+
+ if current_price and current_price > 0:
+ valuation_gap = imp_price - current_price
+ market_message = "above" if valuation_gap > 0 else "below"
+ if abs(valuation_gap) < 0.005:
+ market_message = "roughly in line with"
+ implied_value = _escape_markdown_currency(fmt_currency(imp_price))
+ gap_value = _escape_markdown_currency(fmt_currency(abs(valuation_gap)))
+ current_value = _escape_markdown_currency(fmt_currency(current_price))
+ st.markdown(
+ f"At **{target_multiple:.1f}x EBITDA**, the model implies **{implied_value} per share**, "
+ f"which is **{gap_value} {market_message}** the current market price of "
+ f"**{current_value}**."
+ )
+
def _render_ev_revenue_model(ctx: dict):
st.markdown("**EV/Revenue Valuation**")
@@ -947,6 +866,8 @@ def _render_ev_revenue_model(ctx: dict):
revenue=float(ctx["revenue_ttm"]),
total_debt=ctx["total_debt"],
total_cash=ctx["cash_and_equivalents"],
+ preferred_equity=ctx["preferred_equity"],
+ minority_interest=ctx["minority_interest"],
shares_outstanding=float(ctx["shares"]),
target_multiple=target_multiple,
)
@@ -964,38 +885,14 @@ def _render_ev_revenue_model(ctx: dict):
float(market_cap)
+ float(ctx["total_debt"])
- float(ctx["cash_and_equivalents"])
+ + float(ctx["preferred_equity"])
+ + float(ctx["minority_interest"])
)
st.caption(
"This model applies a target EV/Revenue multiple to TTM revenue, then bridges from enterprise value to equity value per share."
)
- evr_m1, evr_m2, evr_m3, evr_m4 = st.columns(4)
- evr_m1.metric("Implied Price / Share", fmt_currency(implied_price))
- if current_price:
- evr_upside = (implied_price - current_price) / current_price
- evr_m2.metric("Current Price", fmt_currency(current_price))
- evr_m3.metric(
- "Upside / Downside",
- f"{evr_upside * 100:+.1f}%",
- delta=f"{evr_upside * 100:+.1f}%",
- )
- evr_m4.metric("Implied EV", fmt_large(ev_revenue_result["implied_ev"]))
-
- if current_price and current_price > 0:
- valuation_gap = implied_price - current_price
- market_message = "above" if valuation_gap > 0 else "below"
- if abs(valuation_gap) < 0.005:
- market_message = "roughly in line with"
- implied_value = _escape_markdown_currency(fmt_currency(implied_price))
- gap_value = _escape_markdown_currency(fmt_currency(abs(valuation_gap)))
- current_value = _escape_markdown_currency(fmt_currency(current_price))
- st.markdown(
- f"At **{target_multiple:.1f}x revenue**, the model implies **{implied_value} per share**, "
- f"which is **{gap_value} {market_message}** the current market price of "
- f"**{current_value}**."
- )
-
calc_a, calc_b, calc_c, calc_d = st.columns(4)
calc_a.metric("Revenue Used", fmt_large(ctx["revenue_ttm"]))
calc_b.metric("Target Multiple", f"{target_multiple:.1f}x")
@@ -1005,69 +902,25 @@ def _render_ev_revenue_model(ctx: dict):
st.caption(
f"Revenue: {fmt_large(ctx['revenue_ttm'])} · "
f"{_net_debt_label(ev_revenue_result['net_debt'])}: {fmt_large(abs(ev_revenue_result['net_debt']))} · "
+ f"Other claims: {fmt_large(ev_revenue_result['other_claims'])} · "
f"Equity Value: {fmt_large(ev_revenue_result['equity_value'])}"
)
source_date = ctx["bridge_items"].get("source_date")
if source_date:
st.caption(f"EV/Revenue bridge source date: **{source_date}**")
- bridge_labels = ["Implied EV"]
- bridge_measures = ["absolute"]
- bridge_values = [float(ev_revenue_result["implied_ev"])]
- bridge_text = [fmt_large(ev_revenue_result["implied_ev"])]
-
- if ctx["total_debt"]:
- bridge_labels.append("Add Debt")
- bridge_measures.append("relative")
- bridge_values.append(float(ctx["total_debt"]))
- bridge_text.append(fmt_large(ctx["total_debt"]))
- if ctx["cash_and_equivalents"]:
- bridge_labels.append("Less Cash")
- bridge_measures.append("relative")
- bridge_values.append(-float(ctx["cash_and_equivalents"]))
- bridge_text.append(fmt_large(-float(ctx["cash_and_equivalents"])))
-
- bridge_labels.append("Implied Equity")
- bridge_measures.append("total")
- bridge_values.append(float(ev_revenue_result["equity_value"]))
- bridge_text.append(fmt_large(ev_revenue_result["equity_value"]))
-
- bridge_fig = go.Figure(
- go.Waterfall(
- x=bridge_labels,
- measure=bridge_measures,
- y=bridge_values,
- text=bridge_text,
- textposition="outside",
- connector={"line": {"color": "#6f7785"}},
- increasing={"marker": {"color": "#4F8EF7"}},
- decreasing={"marker": {"color": "#F76E6E"}},
- totals={"marker": {"color": "#2ecc71"}},
- )
- )
- bridge_fig.update_layout(
- title="EV/Revenue Bridge To Equity Value",
- yaxis_title="USD",
- plot_bgcolor="rgba(0,0,0,0)",
- paper_bgcolor="rgba(0,0,0,0)",
- margin=dict(l=0, r=0, t=44, b=0),
- height=320,
- )
- st.plotly_chart(bridge_fig, use_container_width=True)
-
if market_cap and market_cap > 0:
- compare_a, compare_b, compare_c, compare_d = st.columns(4)
- compare_a.metric("Market Cap", fmt_large(market_cap))
+ st.markdown("**Market Comparison**")
+ compare_a, compare_b = st.columns(2)
if market_enterprise_value and market_enterprise_value > 0:
ev_delta = (ev_revenue_result["implied_ev"] - market_enterprise_value) / market_enterprise_value
- compare_b.metric(
+ compare_a.metric(
"Market Enterprise Value",
fmt_large(market_enterprise_value),
delta=f"{ev_delta * 100:+.1f}%",
)
equity_delta = (ev_revenue_result["equity_value"] - market_cap) / market_cap
- compare_c.metric("Model Equity Value", fmt_large(ev_revenue_result["equity_value"]), delta=f"{equity_delta * 100:+.1f}%")
- compare_d.metric("Shares Outstanding", f"{ctx['shares'] / 1e6:,.1f}M")
+ compare_b.metric("Market Cap", fmt_large(market_cap), delta=f"{equity_delta * 100:+.1f}%")
summary_rows = [
{
@@ -1088,7 +941,7 @@ def _render_ev_revenue_model(ctx: dict):
{
"Step": "4. Bridge to equity value",
"Value": fmt_large(ev_revenue_result["equity_value"]),
- "What it means": "Enterprise value less net debt.",
+ "What it means": "Enterprise value less net debt and other claims.",
},
{
"Step": "5. Convert to value per share",
@@ -1098,6 +951,33 @@ def _render_ev_revenue_model(ctx: dict):
]
st.dataframe(pd.DataFrame(summary_rows), use_container_width=True, hide_index=True)
+ st.markdown("**EV/Revenue Conclusion**")
+ evr_m1, evr_m2, evr_m3, evr_m4 = st.columns(4)
+ evr_m1.metric("Implied Price / Share", fmt_currency(implied_price))
+ if current_price:
+ evr_upside = (implied_price - current_price) / current_price
+ evr_m2.metric("Current Price", fmt_currency(current_price))
+ evr_m3.metric(
+ "Upside / Downside",
+ f"{evr_upside * 100:+.1f}%",
+ delta=f"{evr_upside * 100:+.1f}%",
+ )
+ evr_m4.metric("Implied EV", fmt_large(ev_revenue_result["implied_ev"]))
+
+ if current_price and current_price > 0:
+ valuation_gap = implied_price - current_price
+ market_message = "above" if valuation_gap > 0 else "below"
+ if abs(valuation_gap) < 0.005:
+ market_message = "roughly in line with"
+ implied_value = _escape_markdown_currency(fmt_currency(implied_price))
+ gap_value = _escape_markdown_currency(fmt_currency(abs(valuation_gap)))
+ current_value = _escape_markdown_currency(fmt_currency(current_price))
+ st.markdown(
+ f"At **{target_multiple:.1f}x revenue**, the model implies **{implied_value} per share**, "
+ f"which is **{gap_value} {market_message}** the current market price of "
+ f"**{current_value}**."
+ )
+
def _render_price_to_book_model(ctx: dict):
st.markdown("**Price / Book Valuation**")
diff --git a/services/data_service.py b/services/data_service.py
index 67f2e7b..bfd1290 100644
--- a/services/data_service.py
+++ b/services/data_service.py
@@ -684,3 +684,27 @@ def get_free_cash_flow_series(ticker: str) -> pd.Series:
return (op + capex).dropna()
except KeyError:
return pd.Series(dtype=float)
+
+
+@st.cache_data(ttl=3600)
+def get_free_cash_flow_ttm(ticker: str) -> float | None:
+ """Return trailing-twelve-month free cash flow from quarterly cash flow statements."""
+ t = yf.Ticker(ticker.upper())
+ cf_q = t.quarterly_cashflow
+ if cf_q is None or cf_q.empty:
+ return None
+
+ if "Free Cash Flow" in cf_q.index:
+ vals = cf_q.loc["Free Cash Flow"].iloc[:4].dropna()
+ if len(vals) == 4:
+ return float(vals.sum())
+
+ try:
+ op = cf_q.loc["Operating Cash Flow"].iloc[:4].dropna()
+ capex = cf_q.loc["Capital Expenditure"].iloc[:4].dropna()
+ if len(op) == 4 and len(capex) == 4:
+ return float((op + capex).sum())
+ except KeyError:
+ return None
+
+ return None
diff --git a/services/valuation_service.py b/services/valuation_service.py
index 357c679..1230aa5 100644
--- a/services/valuation_service.py
+++ b/services/valuation_service.py
@@ -50,6 +50,7 @@ def run_dcf(
terminal_growth: float = 0.03,
projection_years: int = 5,
growth_rate_override: float | None = None,
+ base_fcf_override: float | None = None,
total_debt: float = 0.0,
cash_and_equivalents: float = 0.0,
preferred_equity: float = 0.0,
@@ -78,7 +79,7 @@ def run_dcf(
historical_growth = compute_historical_growth_rate(fcf_series)
growth_rate = historical_growth if historical_growth is not None else 0.05
- base_fcf = float(historical[-1])
+ base_fcf = float(base_fcf_override) if base_fcf_override is not None else float(historical[-1])
if base_fcf <= 0:
return {
"error": (
@@ -136,6 +137,8 @@ def run_ev_ebitda(
ebitda: float,
total_debt: float,
total_cash: float,
+ preferred_equity: float,
+ minority_interest: float,
shares_outstanding: float,
target_multiple: float,
) -> dict:
@@ -149,11 +152,13 @@ def run_ev_ebitda(
implied_ev = ebitda * target_multiple
net_debt = (total_debt or 0.0) - (total_cash or 0.0)
- equity_value = implied_ev - net_debt
+ other_claims = (preferred_equity or 0.0) + (minority_interest or 0.0)
+ equity_value = implied_ev - net_debt - other_claims
return {
"implied_ev": implied_ev,
"net_debt": net_debt,
+ "other_claims": other_claims,
"equity_value": equity_value,
"implied_price_per_share": equity_value / shares_outstanding,
"target_multiple_used": target_multiple,
@@ -164,6 +169,8 @@ def run_ev_revenue(
revenue: float,
total_debt: float,
total_cash: float,
+ preferred_equity: float,
+ minority_interest: float,
shares_outstanding: float,
target_multiple: float,
) -> dict:
@@ -177,11 +184,13 @@ def run_ev_revenue(
implied_ev = revenue * target_multiple
net_debt = (total_debt or 0.0) - (total_cash or 0.0)
- equity_value = implied_ev - net_debt
+ other_claims = (preferred_equity or 0.0) + (minority_interest or 0.0)
+ equity_value = implied_ev - net_debt - other_claims
return {
"implied_ev": implied_ev,
"net_debt": net_debt,
+ "other_claims": other_claims,
"equity_value": equity_value,
"implied_price_per_share": equity_value / shares_outstanding,
"target_multiple_used": target_multiple,