mirror of
https://github.com/shawnkim1997/All-in-one-Financial-Analysis.git
synced 2026-08-18 04:48:08 +00:00
- Add missing numpy, scipy, dbnomics to requirements.txt (fixes ImportError on fresh install) - Sync claude.md with actual codebase: §3 file structure (37 services, 21 routers), §5 API endpoints (92 routes), §6 frontend pages (12), §13 TODO status - Update README.md with current architecture (92 API routes, 21 routers, 37 services), multi-asset overview, research grid, macro dashboard, screener+backtest, multi-jurisdiction filings, and 2026-03-26 changelog entry - Add new routers: dart, edinet, fmp, macro, research - Add new services: cache, dart_fetcher, dart_filing_service, economic_calendar, ecos_fetcher, edinet_filing_service, fmp_client, global_macro_quadrant, kpi_history_service, macro_cycle, macro_fetcher, oecd_cycle, peer_comparison_service, research_dashboard, smart_money_service, yield_fx_service - Add new frontend: macro page, screener+backtest, research grid components, overview (Equity/ETF/Commodity), filings (SEC/DART/EDINET), error boundaries - Remove 6 unused services: copilot_context, crypto_fetcher, fx_fetcher, gemini_analysis, market_data, technical_analysis - Remove obsolete docs: .agent/, AGENT.md, ATLAS_EVALUATION.md, docs/ Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
116 lines
3.8 KiB
Python
116 lines
3.8 KiB
Python
"""Peer valuation multiples for overview (yfinance, no LLM)."""
|
|
|
|
from __future__ import annotations
|
|
|
|
from typing import Any, Dict, List, Optional, Tuple
|
|
|
|
import yfinance as yf
|
|
|
|
from server.utils.ticker_utils import SECTORS
|
|
from server.utils.safe_float import _safe_float
|
|
|
|
# Extra keyword → bucket name (must match keys in SECTORS)
|
|
_BUCKET_KEYWORDS: List[Tuple[str, List[str]]] = [
|
|
("Semiconductors & Hardware", ["semiconductor", "semiconductors", "semi ", "hardware"]),
|
|
("Software & Cloud", ["software", "cloud", "saas", "internet content"]),
|
|
("Consumer Retail", ["retail", "consumer", "restaurant", "specialty retail"]),
|
|
("Financial Services", ["financial", "bank", "insurance", "capital market"]),
|
|
("Healthcare", ["health", "drug", "biotech", "medical"]),
|
|
]
|
|
|
|
|
|
def _match_bucket(sector: str, industry: str) -> Optional[str]:
|
|
text = f"{sector} {industry}".lower()
|
|
for bucket, kws in _BUCKET_KEYWORDS:
|
|
if any(kw in text for kw in kws):
|
|
return bucket
|
|
for bucket_name in SECTORS:
|
|
parts = bucket_name.lower().replace("&", " ").split()
|
|
if any(p in text for p in parts if len(p) > 3):
|
|
return bucket_name
|
|
return None
|
|
|
|
|
|
def _fallback_large_caps(sector: str) -> List[str]:
|
|
s = (sector or "").lower()
|
|
if "technology" in s or "tech" in s:
|
|
return ["MSFT", "AAPL", "GOOGL", "META", "NVDA"]
|
|
if "financial" in s or "financials" in s:
|
|
return ["JPM", "BAC", "GS", "MS", "V"]
|
|
if "health" in s:
|
|
return ["UNH", "JNJ", "LLY", "ABBV", "MRK"]
|
|
if "consumer" in s:
|
|
return ["AMZN", "WMT", "HD", "MCD", "SBUX"]
|
|
return ["MSFT", "AAPL", "GOOGL", "AMZN", "JPM"]
|
|
|
|
|
|
def _peer_symbols(ticker: str, sector: str, industry: str) -> List[str]:
|
|
t = ticker.upper().strip()
|
|
bucket = _match_bucket(sector, industry)
|
|
if bucket and bucket in SECTORS:
|
|
syms = list(SECTORS[bucket])
|
|
else:
|
|
syms = _fallback_large_caps(sector)
|
|
if t not in syms:
|
|
syms = [t] + [x for x in syms if x != t]
|
|
# unique preserve order, cap 8
|
|
seen: set[str] = set()
|
|
out: List[str] = []
|
|
for s in syms:
|
|
u = s.upper()
|
|
if u not in seen:
|
|
seen.add(u)
|
|
out.append(u)
|
|
if len(out) >= 8:
|
|
break
|
|
return out
|
|
|
|
|
|
def _peer_row(sym: str) -> Dict[str, Any]:
|
|
info = (yf.Ticker(sym).info) or {}
|
|
return {
|
|
"ticker": sym,
|
|
"name": str(info.get("shortName") or info.get("longName") or sym)[:80],
|
|
"market_cap": _safe_float(info.get("marketCap")),
|
|
"pe": _safe_float(info.get("trailingPE")) or _safe_float(info.get("forwardPE")),
|
|
"pb": _safe_float(info.get("priceToBook")),
|
|
"ps": _safe_float(info.get("priceToSalesTrailing12Months")),
|
|
"ev_ebitda": _safe_float(info.get("enterpriseToEbitda")),
|
|
}
|
|
|
|
|
|
def _avg(vals: List[Optional[float]]) -> Optional[float]:
|
|
nums = [v for v in vals if v is not None]
|
|
if not nums:
|
|
return None
|
|
return round(sum(nums) / len(nums), 2)
|
|
|
|
|
|
def build_peer_comparison(ticker: str) -> Dict[str, Any]:
|
|
"""Return payload matching PeerComparisonData on the frontend."""
|
|
t = ticker.upper().strip()
|
|
try:
|
|
info = (yf.Ticker(t).info) or {}
|
|
except Exception:
|
|
info = {}
|
|
sector = str(info.get("sector") or "")
|
|
industry = str(info.get("industry") or "")
|
|
syms = _peer_symbols(t, sector, industry)
|
|
peers = [_peer_row(s) for s in syms]
|
|
pes = [p["pe"] for p in peers]
|
|
pbs = [p["pb"] for p in peers]
|
|
pss = [p["ps"] for p in peers]
|
|
evs = [p["ev_ebitda"] for p in peers]
|
|
return {
|
|
"ticker": t,
|
|
"sector": sector or "—",
|
|
"industry": industry or "—",
|
|
"averages": {
|
|
"pe": _avg(pes),
|
|
"pb": _avg(pbs),
|
|
"ps": _avg(pss),
|
|
"ev_ebitda": _avg(evs),
|
|
},
|
|
"peers": peers,
|
|
}
|