mirror of
https://github.com/Alkatrazz24/Funk-lab.git
synced 2026-07-08 05:34:43 +02:00
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>
127 lines
5 KiB
Python
127 lines
5 KiB
Python
"""Suivi de portefeuille multi-comptes : valorisation, P&L, secteurs, concentration."""
|
|
from __future__ import annotations
|
|
|
|
import pandas as pd
|
|
|
|
from . import data
|
|
|
|
|
|
def _sector(symbol: str) -> str:
|
|
try:
|
|
return data.info(symbol).get("sector") or "—"
|
|
except Exception:
|
|
return "—"
|
|
|
|
|
|
def _positions_df(positions: list[dict], base: str, with_sector: bool = True) -> pd.DataFrame:
|
|
"""Tableau valorisé d'une liste de positions (sans les poids — dépendent du total)."""
|
|
rows = []
|
|
for p in positions:
|
|
sym = p["ticker"]
|
|
price, cur = data.last_price(sym)
|
|
value = data.to_currency(price * p["shares"], cur, base)
|
|
cost = data.to_currency(p["avg_price"] * p["shares"], p["avg_currency"], base)
|
|
pnl = value - cost
|
|
rows.append({
|
|
"ticker": sym,
|
|
"secteur": _sector(sym) if with_sector else "—",
|
|
"qté": p["shares"],
|
|
"cours": round(price, 2),
|
|
"dev": cur,
|
|
f"valeur_{base}": round(value, 2),
|
|
f"PRU_{base}": round(cost, 2),
|
|
f"P&L_{base}": round(pnl, 2),
|
|
"P&L_%": round(100 * pnl / cost, 2) if cost else 0.0,
|
|
})
|
|
return pd.DataFrame(rows)
|
|
|
|
|
|
def _agg(df: pd.DataFrame, cash: float, base: str) -> dict:
|
|
"""Agrégats à partir d'un tableau de positions + liquidités (en devise de base)."""
|
|
invested = df[f"valeur_{base}"].sum() if len(df) else 0.0
|
|
total = invested + cash
|
|
if len(df) and total:
|
|
df["poids_%"] = (df[f"valeur_{base}"] / total * 100).round(2)
|
|
by_sector = (df.groupby("secteur")[f"valeur_{base}"].sum().sort_values(ascending=False)
|
|
/ total * 100).round(2)
|
|
pnl_total = df[f"P&L_{base}"].sum()
|
|
cost_total = df[f"PRU_{base}"].sum()
|
|
top_weight = df.nlargest(3, "poids_%")[["ticker", "poids_%"]]
|
|
else:
|
|
by_sector = pd.Series(dtype=float)
|
|
pnl_total = cost_total = 0.0
|
|
top_weight = pd.DataFrame(columns=["ticker", "poids_%"])
|
|
return {
|
|
"base": base,
|
|
"cash": round(cash, 2),
|
|
"invested": round(invested, 2),
|
|
"total": round(total, 2),
|
|
"pnl_total": round(pnl_total, 2),
|
|
"pnl_pct": round(100 * pnl_total / cost_total, 2) if cost_total else 0.0,
|
|
"by_sector": by_sector,
|
|
"top_weight": top_weight,
|
|
}
|
|
|
|
|
|
def build_account(acc: dict, base: str, with_sector: bool = True) -> tuple[pd.DataFrame, dict]:
|
|
"""(tableau, agrégats) d'UN compte. Poids relatifs au total de ce compte."""
|
|
df = _positions_df(acc["positions"], base, with_sector)
|
|
cash = data.to_currency(acc["cash"]["amount"], acc["cash"]["currency"], base)
|
|
agg = _agg(df, cash, base)
|
|
df = df.sort_values(f"valeur_{base}", ascending=False) if len(df) else df
|
|
return df, agg
|
|
|
|
|
|
def build_all(with_sector: bool = True) -> tuple[list[dict], dict]:
|
|
"""Tous les comptes (par compte) + l'agrégat global (poids relatifs au total global)."""
|
|
base = data.base_currency()
|
|
accounts = data.load_accounts()
|
|
per = []
|
|
all_positions, total_cash = [], 0.0
|
|
for acc in accounts:
|
|
df, agg = build_account(acc, base, with_sector)
|
|
per.append({"name": acc["name"], "type": acc.get("type", "courtier"), "df": df, "agg": agg})
|
|
all_positions += acc["positions"]
|
|
total_cash += data.to_currency(acc["cash"]["amount"], acc["cash"]["currency"], base)
|
|
gdf = _positions_df(all_positions, base, with_sector)
|
|
gagg = _agg(gdf, total_cash, base)
|
|
return per, gagg
|
|
|
|
|
|
def build(with_sector: bool = True) -> tuple[pd.DataFrame, dict]:
|
|
"""Vue GLOBALE (tous comptes fusionnés). Compat : digest/report/CLI."""
|
|
base = data.base_currency()
|
|
all_positions, total_cash = [], 0.0
|
|
for acc in data.load_accounts():
|
|
all_positions += acc["positions"]
|
|
total_cash += data.to_currency(acc["cash"]["amount"], acc["cash"]["currency"], base)
|
|
df = _positions_df(all_positions, base, with_sector)
|
|
agg = _agg(df, total_cash, base)
|
|
df = df.sort_values(f"valeur_{base}", ascending=False) if len(df) else df
|
|
return df, agg
|
|
|
|
|
|
def report() -> str:
|
|
per, agg = build_all()
|
|
b = agg["base"]
|
|
out = ["═" * 64, " PORTEFEUILLE (tous comptes)", "═" * 64]
|
|
for acc in per:
|
|
a = acc["agg"]
|
|
sign = "+" if a["pnl_total"] >= 0 else ""
|
|
out.append(f"\n▸ {acc['name']} ({acc['type']}) — {a['total']:,.2f} {b} "
|
|
f"| P&L {sign}{a['pnl_total']:,.2f} {b} ({sign}{a['pnl_pct']}%) | cash {a['cash']:,.2f} {b}")
|
|
if len(acc["df"]):
|
|
out.append(acc["df"].to_string(index=False))
|
|
sign = "+" if agg["pnl_total"] >= 0 else ""
|
|
out += ["", "─" * 64,
|
|
f"TOTAL GLOBAL : {agg['total']:>12,.2f} {b} "
|
|
f"(investi {agg['invested']:,.2f} + cash {agg['cash']:,.2f})",
|
|
f"P&L GLOBAL : {sign}{agg['pnl_total']:>11,.2f} {b} ({sign}{agg['pnl_pct']}%)"]
|
|
out.append("\nExposition sectorielle (globale) :")
|
|
for sec, w in agg["by_sector"].items():
|
|
out.append(f" {sec:<24} {w:>6.2f}% {'█' * int(w / 2)}")
|
|
return "\n".join(out)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
print(report())
|