| """ |
| Fundamentals — Fetch key fundamental data from yfinance. |
| Provides value, growth, and quality metrics for alpha scoring. |
| """ |
| import logging |
|
|
| import yfinance as yf |
|
|
| logger = logging.getLogger(__name__) |
|
|
|
|
| def fetch_fundamentals(ticker: str) -> dict: |
| """ |
| Fetch key fundamental metrics for a ticker. |
| Returns a dict with value, growth, and quality indicators. |
| """ |
| try: |
| stock = yf.Ticker(ticker) |
| info = stock.info or {} |
|
|
| fundamentals = { |
| "ticker": ticker, |
| |
| "pe_ratio": info.get("trailingPE") or info.get("forwardPE"), |
| "pb_ratio": info.get("priceToBook"), |
| "ps_ratio": info.get("priceToSalesTrailing12Months"), |
| "ev_ebitda": info.get("enterpriseToEbitda"), |
| "peg_ratio": info.get("pegRatio"), |
| |
| "revenue_growth": info.get("revenueGrowth"), |
| "earnings_growth": info.get("earningsGrowth"), |
| "earnings_quarterly_growth": info.get("earningsQuarterlyGrowth"), |
| |
| "profit_margin": info.get("profitMargins"), |
| "operating_margin": info.get("operatingMargins"), |
| "roe": info.get("returnOnEquity"), |
| "roa": info.get("returnOnAssets"), |
| |
| "dividend_yield": info.get("dividendYield"), |
| |
| "market_cap": info.get("marketCap"), |
| "avg_volume": info.get("averageVolume"), |
| "avg_volume_10d": info.get("averageDailyVolume10Day"), |
| |
| "target_mean": info.get("targetMeanPrice"), |
| "target_low": info.get("targetLowPrice"), |
| "target_high": info.get("targetHighPrice"), |
| "recommendation": info.get("recommendationKey"), |
| "num_analysts": info.get("numberOfAnalystOpinions"), |
| |
| "sector": info.get("sector", "Unknown"), |
| "industry": info.get("industry", "Unknown"), |
| "name": info.get("shortName", ticker), |
| "currency": info.get("currency", "USD"), |
| } |
|
|
| logger.info(f"{ticker}: Fetched fundamentals (PE={fundamentals['pe_ratio']}, sector={fundamentals['sector']})") |
| return fundamentals |
|
|
| except Exception as e: |
| logger.error(f"{ticker}: Failed to fetch fundamentals: {e}") |
| return {"ticker": ticker, "error": str(e)} |
|
|
|
|
| def compute_fundamental_score(fundamentals: dict) -> float: |
| """ |
| Compute a composite fundamental quality score (0-100). |
| Blends value, growth, and quality. |
| """ |
| score = 50.0 |
|
|
| |
| pe = fundamentals.get("pe_ratio") |
| if pe and pe > 0: |
| if pe < 15: |
| score += 10 |
| elif pe < 25: |
| score += 5 |
| elif pe > 50: |
| score -= 10 |
|
|
| |
| rev_growth = fundamentals.get("revenue_growth") |
| if rev_growth: |
| if rev_growth > 0.20: |
| score += 15 |
| elif rev_growth > 0.10: |
| score += 8 |
| elif rev_growth < 0: |
| score -= 10 |
|
|
| earn_growth = fundamentals.get("earnings_growth") |
| if earn_growth: |
| if earn_growth > 0.20: |
| score += 10 |
| elif earn_growth > 0.10: |
| score += 5 |
| elif earn_growth < -0.10: |
| score -= 10 |
|
|
| |
| roe = fundamentals.get("roe") |
| if roe: |
| if roe > 0.20: |
| score += 10 |
| elif roe > 0.15: |
| score += 5 |
| elif roe < 0: |
| score -= 15 |
|
|
| margin = fundamentals.get("profit_margin") |
| if margin: |
| if margin > 0.20: |
| score += 5 |
| elif margin < 0: |
| score -= 10 |
|
|
| |
| rec = fundamentals.get("recommendation") |
| if rec: |
| rec_scores = {"strongBuy": 10, "buy": 5, "hold": 0, "sell": -10, "strongSell": -15} |
| score += rec_scores.get(rec, 0) |
|
|
| return max(0.0, min(100.0, score)) |
|
|