Funk-lab/tools/finlab/finlab/fundamental.py
ALI YESILKAYA 73e2ad30db
feat(finlab): boutons d'analyse rapide (digest/analyse/plan/fondamental/opport./alertes) (#70)
Panneau « Actions rapides » sur le dashboard : 6 boutons one-click qui renvoient
une analyse finlab instantanée (déterministe, gratuite, sans LLM), dans une modale.

- 📊 Analyse (action sélectionnée) : technique (RSI/MACD/MM/signaux) + fondamental
  (PER/PEG/ROE/marges/dette/reco/cible) + plan R:R, combinés en une fiche
- 📐 Plan R:R, 📈 Fondamentaux (action sélectionnée) ; 📋 Digest, 🔭 Opportunités,
  🔔 Alertes (global)
- Backend : endpoints /api/digest, /api/fundamentals, /api/analyze
- Fix : fundamental.compare_portfolio() utilisait l'ancien schéma mono-compte
  (pf["positions"]) → cassé par le multi-comptes ; passe par data.portfolio_tickers()

Vérifié : endpoints OK (TestClient), rendu modale au navigateur (Playwright) —
fiche Analyse NVDA complète (technique + fondamental + plan). JS node --check OK.

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-30 11:51:40 +02:00

65 lines
1.9 KiB
Python

"""Analyse fondamentale : ratios clés et résultats via yfinance."""
from __future__ import annotations
import pandas as pd
from . import data
def _b(x: float | None) -> str:
"""Formate un grand nombre en milliards."""
return f"{x/1e9:.1f} Md" if isinstance(x, (int, float)) else ""
def snapshot(symbol: str) -> dict:
i = data.info(symbol)
def g(*keys):
for k in keys:
v = i.get(k)
if v is not None:
return v
return None
return {
"ticker": symbol,
"nom": g("shortName", "longName"),
"secteur": g("sector"),
"cours": g("currentPrice", "regularMarketPrice"),
"PER": g("trailingPE"),
"PER_fwd": g("forwardPE"),
"PEG": g("pegRatio"),
"P/S": g("priceToSalesTrailing12Months"),
"P/B": g("priceToBook"),
"marge_nette_%": round(g("profitMargins") * 100, 1) if g("profitMargins") else None,
"ROE_%": round(g("returnOnEquity") * 100, 1) if g("returnOnEquity") else None,
"croiss_CA_%": round(g("revenueGrowth") * 100, 1) if g("revenueGrowth") else None,
"dette/FP": g("debtToEquity"),
"div_%": round(g("dividendYield") * 100, 2) if g("dividendYield") else None,
"capi": _b(g("marketCap")),
"reco": g("recommendationKey"),
"cible_moy": g("targetMeanPrice"),
}
def compare(symbols: list[str]) -> pd.DataFrame:
rows = []
for s in symbols:
try:
rows.append(snapshot(s))
except Exception as e:
rows.append({"ticker": s, "nom": f"erreur: {e}"})
return pd.DataFrame(rows)
def compare_portfolio() -> pd.DataFrame:
return compare(data.portfolio_tickers())
if __name__ == "__main__":
import sys
syms = sys.argv[1:]
df = compare(syms) if syms else compare_portfolio()
pd.set_option("display.max_columns", None, "display.width", 240)
print(df.to_string(index=False))