feat(finlab): portefeuilles multi-comptes (courtier/banque) + onglets dashboard

Permet de suivre plusieurs comptes (courtiers/banques) côte à côte, avec vue par
compte et agrégée. Ajoute le PEA BNP Paribas à côté de Revolut.

- portfolio.yaml : nouveau format `accounts` (name/type/cash/positions par compte).
  Compat ascendante : ancien format (cash/positions à la racine) lu comme compte
  unique « Principal »
- data.py : load_accounts(), base_currency(), portfolio_tickers() (union dédupliquée)
- tracker.py : build_account / build_all (par compte + agrégat global) ; build()
  reste la vue globale (digest/report) ; report() multi-comptes
- scanner/technical/alerts : helpers « portfolio » → union de tous les comptes
- dashboard /api/portfolio : renvoie accounts[] (positions+agg par compte) + global
- dashboard front : onglets de comptes (Tous / Revolut / BNP Paribas), KPI + positions
  + secteurs qui suivent le périmètre sélectionné, badge de type par ligne en vue Tous
- BNP Paribas (PEA) saisi depuis la capture : AI/PAEEM/DCAM/PCEU/PSP5/BNP/ENGI/TTE .PA
  (tickers Yahoo vérifiés, cours conformes à la capture)

Vérifié : totaux conformes à la capture BNP (6 395,49 € ; P&L -39 €), global 22 444 € ;
bascule d'onglet OK au navigateur (Playwright) ; compile + JS + CLI report OK.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
alkatrazz 2026-06-30 01:33:37 +02:00
parent ba45ca198b
commit 3b43025895
10 changed files with 246 additions and 158 deletions

View file

@ -61,24 +61,37 @@ def healthz():
return {"ok": True}
@app.get("/api/portfolio")
def api_portfolio():
df, agg = tracker.build(with_sector=True)
base = agg["base"]
positions = [
def _positions_of(df, base):
return [
{
"ticker": r["ticker"], "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["poids_%"],
"pnl": r[f"P&L_{base}"], "pnl_pct": r["P&L_%"], "poids": r.get("poids_%"),
}
for r in df.to_dict(orient="records")
]
@app.get("/api/portfolio")
def api_portfolio():
per, gagg = tracker.build_all(with_sector=True)
base = gagg["base"]
accounts = [
{
"name": a["name"], "type": a["type"],
"total": a["agg"]["total"], "cash": a["agg"]["cash"], "invested": a["agg"]["invested"],
"pnl_total": a["agg"]["pnl_total"], "pnl_pct": a["agg"]["pnl_pct"],
"by_sector": a["agg"]["by_sector"].to_dict(),
"positions": _positions_of(a["df"], base),
}
for a in per
]
return _clean({
"base": base, "total": agg["total"], "cash": agg["cash"],
"invested": agg["invested"], "pnl_total": agg["pnl_total"], "pnl_pct": agg["pnl_pct"],
"by_sector": agg["by_sector"].to_dict(),
"positions": positions,
"base": base, "total": gagg["total"], "cash": gagg["cash"], "invested": gagg["invested"],
"pnl_total": gagg["pnl_total"], "pnl_pct": gagg["pnl_pct"],
"by_sector": gagg["by_sector"].to_dict(),
"accounts": accounts,
})