aboutsummaryrefslogtreecommitdiff
path: root/components/valuation.py
diff options
context:
space:
mode:
authorOpenclaw <openclaw@mail.tylerhoang.xyz>2026-04-01 23:32:01 -0700
committerOpenclaw <openclaw@mail.tylerhoang.xyz>2026-04-01 23:32:01 -0700
commit3806bd3b4d69917f3f5312acfa57bc4ee2886a49 (patch)
tree7fca5c5c8aacc5365d2db33e40da2e251e3c67b0 /components/valuation.py
parent96b27f1d00ae8110273de973053c3d6bfc4f3662 (diff)
Harden valuation edge cases
Diffstat (limited to 'components/valuation.py')
-rw-r--r--components/valuation.py206
1 files changed, 168 insertions, 38 deletions
diff --git a/components/valuation.py b/components/valuation.py
index a72d177..72c8001 100644
--- a/components/valuation.py
+++ b/components/valuation.py
@@ -146,35 +146,138 @@ def _render_ratios(ticker: str):
st.info("Ratio data unavailable.")
return
- def r(key, fmt=fmt_ratio):
+ def _normalized_label(label: str) -> str:
+ return " ".join(str(label).replace("/", " ").replace("-", " ").split()).strip().lower()
+
+ def _display_value(key: str, fmt=fmt_ratio):
val = ratios.get(key) if ratios else None
return fmt(val) if val is not None else "—"
+ def _company_context() -> dict:
+ return info or {}
+
+ def _display_reasoned_metric(key: str, fmt=fmt_ratio) -> str:
+ val = ratios.get(key) if ratios else None
+ if val is not None:
+ return fmt(val)
+
+ ctx = _company_context()
+
+ if key == "peRatioTTM":
+ trailing_pe = ctx.get("trailingPE")
+ if trailing_pe is not None:
+ return fmt_ratio(trailing_pe)
+ if ratios and ratios.get("netProfitMarginTTM") is not None and ratios.get("netProfitMarginTTM") < 0:
+ return "N/M (neg. TTM earnings)"
+ trailing_eps = ctx.get("trailingEps")
+ if trailing_eps is not None:
+ try:
+ if float(trailing_eps) <= 0:
+ return "N/M (neg. TTM earnings)"
+ except (TypeError, ValueError):
+ pass
+ return "—"
+
+ if key == "priceToBookRatioTTM":
+ book_value = ctx.get("bookValue")
+ if book_value is not None:
+ try:
+ if float(book_value) <= 0:
+ return "N/M (neg. equity)"
+ except (TypeError, ValueError):
+ pass
+ return "—"
+
+ if key == "enterpriseValueMultipleTTM":
+ ebitda = ratios.get("ebitdaTTM") if ratios else None
+ if ebitda is not None:
+ try:
+ if float(ebitda) <= 0:
+ return "N/M (neg. EBITDA)"
+ except (TypeError, ValueError):
+ pass
+ return "—"
+
+ if key == "dividendPayoutRatioTTM":
+ payout_ratio = ctx.get("payoutRatio")
+ if payout_ratio is not None:
+ try:
+ if float(payout_ratio) <= 0:
+ return "—"
+ except (TypeError, ValueError):
+ pass
+ if ratios and ratios.get("netProfitMarginTTM") is not None and ratios.get("netProfitMarginTTM") < 0:
+ return "N/M (neg. earnings)"
+ return "—"
+
+ if key == "returnOnEquityTTM":
+ book_value = ctx.get("bookValue")
+ if book_value is not None:
+ try:
+ if float(book_value) <= 0:
+ return "N/M (neg. equity)"
+ except (TypeError, ValueError):
+ pass
+ return "—"
+
+ if key == "debtToEquityRatioTTM":
+ book_value = ctx.get("bookValue")
+ if book_value is not None:
+ try:
+ if float(book_value) <= 0:
+ return "N/M (neg. equity)"
+ except (TypeError, ValueError):
+ pass
+ return "—"
+
+ if key == "interestCoverageRatioTTM":
+ operating_margins = ctx.get("operatingMargins")
+ if operating_margins is not None:
+ try:
+ if float(operating_margins) <= 0:
+ return "N/M (neg. EBIT)"
+ except (TypeError, ValueError):
+ pass
+ return "—"
+
+ return "—"
+
+ def _dedupe_metrics(metrics: list[tuple[str, str]]) -> list[tuple[str, str]]:
+ deduped: list[tuple[str, str]] = []
+ seen_labels: set[str] = set()
+ for label, val in metrics:
+ norm = _normalized_label(label)
+ if norm in seen_labels:
+ continue
+ seen_labels.add(norm)
+ deduped.append((label, val))
+ return deduped
+
rows = [
- ("Valuation", [
- ("P/E (TTM)", r("peRatioTTM")),
- ("Forward P/E", r("forwardPE")),
- ("P/S (TTM)", r("priceToSalesRatioTTM")),
- ("P/B", r("priceToBookRatioTTM")),
- ("EV/EBITDA", r("enterpriseValueMultipleTTM")),
- ("EV/Revenue", r("evToSalesTTM")),
- ]),
- ("Profitability", [
- ("Gross Margin", r("grossProfitMarginTTM", fmt=fmt_pct)),
- ("Operating Margin", r("operatingProfitMarginTTM", fmt=fmt_pct)),
- ("Net Margin", r("netProfitMarginTTM", fmt=fmt_pct)),
- ("ROE", r("returnOnEquityTTM", fmt=fmt_pct)),
- ("ROA", r("returnOnAssetsTTM", fmt=fmt_pct)),
- ("ROIC", r("returnOnInvestedCapitalTTM", fmt=fmt_pct)),
- ]),
- ("Leverage & Liquidity", [
- ("Debt/Equity", r("debtToEquityRatioTTM")),
- ("Current Ratio", r("currentRatioTTM")),
- ("Quick Ratio", r("quickRatioTTM")),
- ("Interest Coverage", r("interestCoverageRatioTTM")),
- ("Dividend Yield", r("dividendYieldTTM", fmt=fmt_pct)),
- ("Payout Ratio", r("dividendPayoutRatioTTM", fmt=fmt_pct)),
- ]),
+ ("Valuation", _dedupe_metrics([
+ ("P/E (TTM)", _display_reasoned_metric("peRatioTTM")),
+ ("Forward P/E", _display_value("forwardPE")),
+ ("P/S (TTM)", _display_value("priceToSalesRatioTTM")),
+ ("P/B", _display_reasoned_metric("priceToBookRatioTTM")),
+ ("EV/EBITDA", _display_reasoned_metric("enterpriseValueMultipleTTM")),
+ ("EV/Revenue", _display_value("evToSalesTTM")),
+ ])),
+ ("Profitability", _dedupe_metrics([
+ ("Gross Margin", _display_value("grossProfitMarginTTM", fmt=fmt_pct)),
+ ("Operating Margin", _display_value("operatingProfitMarginTTM", fmt=fmt_pct)),
+ ("Net Margin", _display_value("netProfitMarginTTM", fmt=fmt_pct)),
+ ("ROE", _display_reasoned_metric("returnOnEquityTTM", fmt=fmt_pct)),
+ ("ROA", _display_value("returnOnAssetsTTM", fmt=fmt_pct)),
+ ("ROIC", _display_value("returnOnInvestedCapitalTTM", fmt=fmt_pct)),
+ ])),
+ ("Leverage & Liquidity", _dedupe_metrics([
+ ("Debt/Equity", _display_reasoned_metric("debtToEquityRatioTTM")),
+ ("Current Ratio", _display_value("currentRatioTTM")),
+ ("Quick Ratio", _display_value("quickRatioTTM")),
+ ("Interest Coverage", _display_reasoned_metric("interestCoverageRatioTTM")),
+ ("Dividend Yield", _display_value("dividendYieldTTM", fmt=fmt_pct)),
+ ("Payout Ratio", _display_reasoned_metric("dividendPayoutRatioTTM", fmt=fmt_pct)),
+ ])),
]
for section_name, metrics in rows:
@@ -463,14 +566,30 @@ def _render_comps(ticker: str):
available = [c for c in ["symbol", "peRatioTTM", "priceToSalesRatioTTM", "priceToBookRatioTTM", "enterpriseValueMultipleTTM", "netProfitMarginTTM", "returnOnEquityTTM", "debtToEquityRatioTTM"] if c in df.columns]
df = df[available].rename(columns=display_cols)
- pct_cols = {"Net Margin", "ROE"}
+ def _format_comp_value(column: str, value):
+ if value is None:
+ return "—"
+ try:
+ v = float(value)
+ except (TypeError, ValueError):
+ return "—"
+
+ if column == "P/E":
+ return fmt_ratio(v) if v > 0 else "N/M (neg. earnings)"
+ if column == "P/B":
+ return fmt_ratio(v) if v > 0 else "N/M (neg. equity)"
+ if column == "EV/EBITDA":
+ return fmt_ratio(v) if v > 0 else "N/M (neg. EBITDA)"
+ if column == "D/E":
+ return fmt_ratio(v) if v >= 0 else "N/M (neg. equity)"
+ if column in {"Net Margin", "ROE"}:
+ return fmt_pct(v)
+ return fmt_ratio(v) if v > 0 else "—"
+
for col in df.columns:
if col == "Ticker":
continue
- if col in pct_cols:
- df[col] = df[col].apply(lambda v: fmt_pct(v) if v is not None else "—")
- else:
- df[col] = df[col].apply(lambda v: fmt_ratio(v) if v is not None else "—")
+ df[col] = df[col].apply(lambda v, c=col: _format_comp_value(c, v))
def highlight_subject(row):
if row["Ticker"] == ticker.upper():
@@ -737,19 +856,30 @@ def _render_historical_ratios(ticker: str):
primary, alt, fmt = _HIST_RATIO_OPTIONS[label]
display_cols[label] = (primary, alt, fmt)
+ def _format_hist_value(label: str, value, fmt: str | None) -> str:
+ if value is None:
+ return "—"
+ try:
+ v = float(value)
+ except (TypeError, ValueError):
+ return "—"
+
+ if fmt == "pct":
+ return f"{v * 100:.2f}%"
+ if label == "P/E":
+ return f"{v:.2f}x" if v > 0 else "N/M (neg. earnings)"
+ if label == "EV/EBITDA":
+ return f"{v:.2f}x" if v > 0 else "N/M (neg. EBITDA)"
+ if label in {"P/B", "Debt/Equity"}:
+ return f"{v:.2f}x" if v > 0 else "N/M (neg. equity)"
+ return f"{v:.2f}x" if v > 0 else "—"
+
table_rows = []
for row in merged_rows:
r: dict = {"Year": str(row.get("date", ""))[:4]}
for label, (primary, alt, fmt) in display_cols.items():
val = row.get(primary) or (row.get(alt) if alt else None)
- if val is not None:
- try:
- v = float(val)
- r[label] = f"{v * 100:.2f}%" if fmt == "pct" else f"{v:.2f}x"
- except (TypeError, ValueError):
- r[label] = "—"
- else:
- r[label] = "—"
+ r[label] = _format_hist_value(label, val, fmt)
table_rows.append(r)
if table_rows: