From 99ff32158a3b62438cf8ed3fe226abc2602bfa6c Mon Sep 17 00:00:00 2001 From: ALI YESILKAYA Date: Tue, 30 Jun 2026 17:08:11 +0200 Subject: [PATCH] =?UTF-8?q?feat(finlab):=20dashboard=20fa=C3=A7on=20Revolu?= =?UTF-8?q?t=20+=20fiche=20action=20au=20clic=20(#73)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- tools/finlab/dashboard/server.py | 65 +- tools/finlab/dashboard/static/index.html | 799 +++++++++-------------- tools/finlab/finlab/data.py | 21 + 3 files changed, 378 insertions(+), 507 deletions(-) diff --git a/tools/finlab/dashboard/server.py b/tools/finlab/dashboard/server.py index 6d25a5e..c5e07cc 100644 --- a/tools/finlab/dashboard/server.py +++ b/tools/finlab/dashboard/server.py @@ -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") diff --git a/tools/finlab/dashboard/static/index.html b/tools/finlab/dashboard/static/index.html index 690a3a1..beb8ddc 100644 --- a/tools/finlab/dashboard/static/index.html +++ b/tools/finlab/dashboard/static/index.html @@ -3,164 +3,128 @@ -FinLab — Dashboard +FinLab
-
FinLab dashboard
-
-
Portefeuille
-
P&L
-
Cash
-
Concentration
- - ⌨ Console IA ↗ -
+
FinLab
+
+
+ + + ⌨ Console IA +
-
- +
+
-

Mes comptes - -

- -
-
Chargement…
-
-
-

Watchlists

-
- - - +
+
+
+
+
+
-
+
+
+
+ + +
+

Chaîne de valeur IA ⚡→🔧→🏢→🔌→☁️ · clic = fiche

+
- -
-
-

- Graphique - - - - - - -

-
-
- — MM50   — MM200 -
-
-
-
- - -
-
- -
-

Console IA - - - plein écran ↗ - -

- -
-
- - +

Actions rapides

@@ -172,11 +136,9 @@
-
Analyse / Plan / Fondamentaux → sur l'action sélectionnée (clique une ligne ou un titre). Le reste = global.
-

Alertes du jour

+

Alertes du jour

@@ -186,400 +148,239 @@
- -
-
-

Chaîne de valeur IA — par couche - de l'électron au logiciel   ⚡→🔧→🏢→🔌→☁️  ·  - haussier / baissier · clic = graphique -

-
Analyse des couches…
-
-
- - -