mirror of
https://github.com/Alkatrazz24/Funk-lab.git
synced 2026-07-10 05:44:42 +02:00
feat(finlab): dashboard façon Revolut + fiche action au clic (#73)
Rebasé sur main (post-#72). Refonte de l'interface inspirée de Revolut : - En-tête : valeur totale + variation sur la période (1J/1S/1M/6M/1A/Tous) + courbe de valeur du portefeuille (area chart) - Tableau de positions style Revolut par compte (avatar, nom+ticker, quantité, prix moyen, cours, valeur, P&L €, taux %, variation jour, répartition, ▲▼) - Onglets de comptes (Tous/Revolut/BNP) en barre du haut - Clic sur une ligne → fiche action : chandeliers (MM50+volume), tabs période, chips techniques, boutons Analyse/Plan/Fondamentaux - Sidebar (actions rapides, alertes, secteurs) + couches IA ; import image/CSV conservé Backend : /api/portfolio-history (courbe valeur, alignée date US/EU, « Tous » 2 ans), /api/portfolio enrichi (nom société + variation jour), data.day_change/company_name. Validé navigateur (Playwright) + endpoints (TestClient) ; JS node --check OK. Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
parent
c6680a983a
commit
99ff32158a
3 changed files with 378 additions and 507 deletions
|
|
@ -65,15 +65,64 @@ def healthz():
|
|||
|
||||
|
||||
def _positions_of(df, base):
|
||||
return [
|
||||
{
|
||||
"ticker": r["ticker"], "secteur": r["secteur"], "qte": r["qté"],
|
||||
"cours": r["cours"], "dev": r["dev"],
|
||||
out = []
|
||||
for r in df.to_dict(orient="records"):
|
||||
tk = r["ticker"]
|
||||
out.append({
|
||||
"ticker": tk, "name": data.company_name(tk), "secteur": r["secteur"],
|
||||
"qte": r["qté"], "cours": r["cours"], "dev": r["dev"],
|
||||
"valeur": r[f"valeur_{base}"], "pru": r[f"PRU_{base}"],
|
||||
"pnl": r[f"P&L_{base}"], "pnl_pct": r["P&L_%"], "poids": r.get("poids_%"),
|
||||
}
|
||||
for r in df.to_dict(orient="records")
|
||||
]
|
||||
"pnl": r[f"P&L_{base}"], "pnl_pct": r["P&L_%"],
|
||||
"day_pct": data.day_change(tk), "poids": r.get("poids_%"),
|
||||
})
|
||||
return out
|
||||
|
||||
|
||||
# Périodes Revolut → (period, interval) yfinance pour la courbe de valeur.
|
||||
_HIST_RANGES = {
|
||||
"1J": ("1d", "5m"), "1S": ("5d", "60m"), "1M": ("1mo", "1d"),
|
||||
"6M": ("6mo", "1d"), "1A": ("1y", "1d"), "Tous": ("2y", "1wk"),
|
||||
}
|
||||
|
||||
|
||||
@app.get("/api/portfolio-history")
|
||||
def api_portfolio_history(range: str = "1M", account: str = "*"):
|
||||
"""Courbe de valeur du portefeuille (somme positions × clôture, converti en devise de base)."""
|
||||
period, interval = _HIST_RANGES.get(range, ("1mo", "1d"))
|
||||
daily = not interval.endswith(("m", "h"))
|
||||
base = data.base_currency()
|
||||
cols, cash = {}, 0.0
|
||||
for a in data.load_accounts():
|
||||
if account not in ("*", a["name"]):
|
||||
continue
|
||||
cash += data.to_currency(a["cash"]["amount"], a["cash"]["currency"], base)
|
||||
for i, p in enumerate(a["positions"]):
|
||||
try:
|
||||
h = data.history(p["ticker"], period=period, interval=interval)["Close"]
|
||||
if daily: # aligner par date calendaire (sinon désalignement tz US/EU)
|
||||
if h.index.tz is not None:
|
||||
h.index = h.index.tz_localize(None)
|
||||
h.index = h.index.normalize()
|
||||
h = h[~h.index.duplicated(keep="last")]
|
||||
cur = data.last_price(p["ticker"])[1]
|
||||
rate = 1.0 if cur == base else data.fx_rate(cur, base)
|
||||
cols[f"{p['ticker']}_{a['name']}_{i}"] = h * p["shares"] * rate
|
||||
except Exception:
|
||||
continue
|
||||
if not cols:
|
||||
return {"range": range, "points": [], "change": 0.0, "change_pct": 0.0}
|
||||
# ffill (jours manquants) puis bfill (NaN de tête dus au désalignement US/EU) → pas de
|
||||
# sous-estimation au début. Chaque colonne est ainsi définie sur tout l'index.
|
||||
dfh = pd.DataFrame(cols).sort_index().ffill().bfill()
|
||||
total = dfh.sum(axis=1) + cash
|
||||
fmt = "%Y-%m-%d %H:%M" if interval.endswith(("m", "h")) else "%Y-%m-%d"
|
||||
points = [{"time": idx.strftime(fmt), "value": round(float(v), 2)} for idx, v in total.items()]
|
||||
first, last = (points[0]["value"], points[-1]["value"]) if points else (0, 0)
|
||||
return _clean({
|
||||
"range": range, "points": points,
|
||||
"change": round(last - first, 2),
|
||||
"change_pct": round((last - first) / first * 100, 2) if first else 0.0,
|
||||
})
|
||||
|
||||
|
||||
@app.get("/api/portfolio")
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue