aboutsummaryrefslogtreecommitdiff
path: root/components
diff options
context:
space:
mode:
authorOpenclaw <openclaw@mail.tylerhoang.xyz>2026-03-29 01:12:24 -0700
committerOpenclaw <openclaw@mail.tylerhoang.xyz>2026-03-29 01:12:24 -0700
commit547997cbd069e9b958b12a8da38b3a4a257e29e5 (patch)
treedbae519a7c6c8f2d803e58e9a77f9a9db73da969 /components
parentad6b0b59c2a4f557d6d9d7fe9810c2ba7627580d (diff)
Fix valuation methodology and documentation
Diffstat (limited to 'components')
-rw-r--r--components/news.py13
-rw-r--r--components/valuation.py33
2 files changed, 37 insertions, 9 deletions
diff --git a/components/news.py b/components/news.py
index cea678e..522826c 100644
--- a/components/news.py
+++ b/components/news.py
@@ -7,11 +7,11 @@ from services.fmp_service import get_company_news as get_fmp_news
def _sentiment_badge(sentiment: str) -> str:
badges = {
- "bullish": "🟢 Bullish",
- "bearish": "🔴 Bearish",
- "neutral": "⚪ Neutral",
+ "bullish": "🟢 Bullish (heuristic)",
+ "bearish": "🔴 Bearish (heuristic)",
+ "neutral": "⚪ Neutral (heuristic)",
}
- return badges.get(sentiment.lower(), "⚪ Neutral")
+ return badges.get(sentiment.lower(), "⚪ Neutral (heuristic)")
def _classify_sentiment(article: dict) -> str:
@@ -52,8 +52,9 @@ def render_news(ticker: str):
col1.metric("Articles (7d)", buzz.get("articlesInLastWeek", "—"))
bull_pct = score.get("bullishPercent")
bear_pct = score.get("bearishPercent")
- col2.metric("Bullish %", f"{bull_pct * 100:.1f}%" if bull_pct else "—")
- col3.metric("Bearish %", f"{bear_pct * 100:.1f}%" if bear_pct else "—")
+ col2.metric("Bullish %", f"{bull_pct * 100:.1f}%" if bull_pct is not None else "—")
+ col3.metric("Bearish %", f"{bear_pct * 100:.1f}%" if bear_pct is not None else "—")
+ st.caption("Sentiment tags below are rule-based headline heuristics, not model-scored article sentiment.")
st.divider()
# Fetch articles — Finnhub first, FMP as fallback
diff --git a/components/valuation.py b/components/valuation.py
index 62ee1e3..82e0f0d 100644
--- a/components/valuation.py
+++ b/components/valuation.py
@@ -61,7 +61,7 @@ def _render_ratios(ticker: str):
rows = [
("Valuation", [
("P/E (TTM)", r("peRatioTTM", "trailingPE")),
- ("Forward P/E", r("priceEarningsRatioTTM", "forwardPE")),
+ ("Forward P/E", fmt_ratio(info.get("forwardPE")) if info.get("forwardPE") is not None else "—"),
("P/S (TTM)", r("priceToSalesRatioTTM", "priceToSalesTrailing12Months")),
("P/B", r("priceToBookRatioTTM", "priceToBook")),
("EV/EBITDA", r("enterpriseValueMultipleTTM", "enterpriseToEbitda")),
@@ -99,6 +99,15 @@ def _render_dcf(ticker: str):
info = get_company_info(ticker)
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
if not shares:
st.info("Shares outstanding not available — DCF cannot be computed.")
@@ -143,22 +152,40 @@ def _render_dcf(ticker: str):
terminal_growth=terminal_growth,
projection_years=projection_years,
growth_rate_override=fcf_growth_pct / 100,
+ total_debt=total_debt,
+ cash_and_equivalents=cash_and_equivalents,
+ preferred_equity=preferred_equity,
+ minority_interest=minority_interest,
)
if not result:
st.warning("Insufficient data to run DCF model.")
return
+ if result.get("error"):
+ st.warning(result["error"])
+ return
iv = result["intrinsic_value_per_share"]
m1, m2, m3, m4 = st.columns(4)
- m1.metric("Intrinsic Value / Share", fmt_currency(iv))
+ 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}%")
+ 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."
+ )
+
+ 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"]))
+ bridge3.metric("Equity Value", fmt_large(result["equity_value"]))
+ bridge4.metric("Terminal Value PV", fmt_large(result["terminal_value_pv"]))
+
st.write("")
years = [f"Year {y}" for y in result["years"]]
@@ -173,7 +200,7 @@ def _render_dcf(ticker: str):
textposition="outside",
))
fig.update_layout(
- title="PV of Projected FCFs + Terminal Value (Billions)",
+ 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)",