From c182bac1d231655c0afe50c9229d886d8db45eb6 Mon Sep 17 00:00:00 2001 From: ALI YESILKAYA Date: Tue, 30 Jun 2026 01:40:20 +0200 Subject: [PATCH] feat(finlab): portefeuilles multi-comptes (courtier/banque) + onglets dashboard (#67) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- admin/ia/finlab.md | 5 +- tools/finlab/dashboard/server.py | 33 ++++-- tools/finlab/dashboard/static/index.html | 80 ++++++++----- tools/finlab/finlab/alerts.py | 2 +- tools/finlab/finlab/data.py | 31 ++++- tools/finlab/finlab/scanner.py | 3 +- tools/finlab/finlab/technical.py | 3 +- tools/finlab/finlab/tracker.py | 137 ++++++++++++++--------- tools/finlab/portfolio.yaml | 102 +++++++---------- tools/finlab/workspace/CLAUDE.md | 8 +- 10 files changed, 246 insertions(+), 158 deletions(-) diff --git a/admin/ia/finlab.md b/admin/ia/finlab.md index 43f89b6..adfad81 100644 --- a/admin/ia/finlab.md +++ b/admin/ia/finlab.md @@ -47,8 +47,9 @@ le PVC (edits + login persistent ; une nouvelle image n'ajoute que les fichiers cible), `trade_plan`/`plan` (entrée/stop ATR/objectifs/taille/**R:R**), `alerts_today`, `price`, plus paper trading (`paper_account`/`paper_positions`/`paper_order`, clés Alpaca optionnelles). -Configs (workspace, éditables) : `portfolio.yaml` (positions Revolut), `watchlists.yaml` (thèmes -chaîne IA/datacenter), `alerts.yaml` (règles). +Configs (workspace, éditables) : `portfolio.yaml` (**multi-comptes** : `accounts` avec +name/type/cash/positions — ex. Revolut + BNP Paribas PEA ; agrégats globaux + par compte), +`watchlists.yaml` (thèmes chaîne IA/datacenter), `alerts.yaml` (règles). ## Onboarding (premier boot) diff --git a/tools/finlab/dashboard/server.py b/tools/finlab/dashboard/server.py index 17ab647..06706c8 100644 --- a/tools/finlab/dashboard/server.py +++ b/tools/finlab/dashboard/server.py @@ -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, }) diff --git a/tools/finlab/dashboard/static/index.html b/tools/finlab/dashboard/static/index.html index 3e12fd3..4d1342b 100644 --- a/tools/finlab/dashboard/static/index.html +++ b/tools/finlab/dashboard/static/index.html @@ -32,6 +32,11 @@ .row{display:flex;justify-content:space-between;gap:8px;padding:6px 8px;border-radius:6px;cursor:pointer;align-items:center} .row:hover{background:var(--panel2)} .row.sel{background:#1b2334;outline:1px solid var(--acc)} + .acct-tabs{display:flex;flex-wrap:wrap;gap:5px;margin-bottom:9px} + .atab{padding:3px 9px;border-radius:14px;border:1px solid var(--line);background:var(--panel2);font-size:12px;cursor:pointer;color:var(--mut)} + .atab:hover{border-color:var(--acc);color:#fff} + .atab.on{background:var(--acc);border-color:var(--acc);color:#fff} + .atag{font-size:11px;opacity:.85} .row .t{font-weight:600;color:#fff} .row .s{font-size:12px;color:var(--mut)} .mono{font-variant-numeric:tabular-nums} @@ -96,7 +101,8 @@
-

Mon portefeuille

+

Mes comptes

+
Chargement…
@@ -258,32 +264,54 @@ function setPeriod(p){ if(SEL) loadChart(SEL); } -// ── Portefeuille ─────────────────────────────────────────── +// ── Portefeuille multi-comptes ───────────────────────────── +let PF=null, ACC='*'; +const acctIcon = t => t==='banque'?'🏦':(t==='crypto'?'🪙':'📊'); + async function loadPortfolio(){ - try{ - const d = await api('/api/portfolio'); - CCY = d.base==='EUR'?'€':d.base; - document.getElementById('kTotal').textContent = fmt(d.total)+' '+CCY; - const pnlEl = document.getElementById('kPnl'); - pnlEl.textContent = sign(d.pnl_total)+' '+CCY+' ('+sign(d.pnl_pct)+'%)'; - pnlEl.className = 'v mono '+cls(d.pnl_total); - document.getElementById('kCash').textContent = fmt(d.cash)+' '+CCY; - const sectors = Object.entries(d.by_sector).sort((a,b)=>b[1]-a[1]); - document.getElementById('kConc').textContent = sectors.length?`${sectors[0][0]} ${fmt(sectors[0][1])}%`:'—'; - document.getElementById('pfCount').textContent = d.positions.length+' lignes'; - - document.getElementById('pfList').innerHTML = d.positions.map(p=>` -
-
${p.ticker}
${p.secteur} · ${fmt(p.poids,1)}%
-
${fmt(p.valeur)} ${CCY}
-
${sign(p.pnl_pct)}%
-
`).join(''); - - document.getElementById('sectors').innerHTML = sectors.map(([s,w])=>` -
-
${s}
${fmt(w,1)}%
`).join(''); - if(!SEL && d.positions.length) loadChart(d.positions[0].ticker); - }catch(e){ document.getElementById('pfList').innerHTML='
'+e.message+'
'; } + try{ PF = await api('/api/portfolio'); CCY = PF.base==='EUR'?'€':PF.base; + renderAccountTabs(); renderPortfolio(); } + catch(e){ document.getElementById('pfList').innerHTML='
'+e.message+'
'; } +} +function renderAccountTabs(){ + const tabs = [`Tous`] + .concat((PF.accounts||[]).map(a=>{ + const nm=a.name.replace(/'/g,"\\'"); + return `${acctIcon(a.type)} ${a.name}`; + })); + document.getElementById('acctTabs').innerHTML = tabs.join(''); +} +function selAcct(name){ ACC=name; renderAccountTabs(); renderPortfolio(); } +function scope(){ + if(ACC==='*') return {total:PF.total,cash:PF.cash,pnl_total:PF.pnl_total,pnl_pct:PF.pnl_pct, + by_sector:PF.by_sector, positions:(PF.accounts||[]).flatMap(a=>a.positions.map(p=>({...p,_type:a.type})))}; + const a=(PF.accounts||[]).find(x=>x.name===ACC)||PF.accounts[0]; + return {total:a.total,cash:a.cash,pnl_total:a.pnl_total,pnl_pct:a.pnl_pct, + by_sector:a.by_sector, positions:a.positions.map(p=>({...p,_type:a.type}))}; +} +function renderPortfolio(){ + const s=scope(); + document.getElementById('kTotal').textContent = fmt(s.total)+' '+CCY; + const pnlEl=document.getElementById('kPnl'); + pnlEl.textContent = sign(s.pnl_total)+' '+CCY+' ('+sign(s.pnl_pct)+'%)'; + pnlEl.className = 'v mono '+cls(s.pnl_total); + document.getElementById('kCash').textContent = fmt(s.cash)+' '+CCY; + const sectors = Object.entries(s.by_sector||{}).sort((a,b)=>b[1]-a[1]); + document.getElementById('kConc').textContent = sectors.length?`${sectors[0][0]} ${fmt(sectors[0][1])}%`:'—'; + document.getElementById('pfCount').textContent = s.positions.length+' lignes'; + const showAcct = ACC==='*'; + document.getElementById('pfList').innerHTML = s.positions.map(p=>{ + const poids = s.total ? (p.valeur/s.total*100) : 0; // poids relatif au périmètre courant + return `
+
${p.ticker}${showAcct?` ${acctIcon(p._type)}`:''}
+
${p.secteur} · ${fmt(poids,1)}%
+
${fmt(p.valeur)} ${CCY}
+
${sign(p.pnl_pct)}%
`; + }).join(''); + document.getElementById('sectors').innerHTML = sectors.map(([sec,w])=>` +
+
${sec}
${fmt(w,1)}%
`).join('') || '
'; + if(!SEL && s.positions.length) loadChart(s.positions[0].ticker); } // ── Watchlists / scan ────────────────────────────────────── diff --git a/tools/finlab/finlab/alerts.py b/tools/finlab/finlab/alerts.py index ea7ec42..787759a 100644 --- a/tools/finlab/finlab/alerts.py +++ b/tools/finlab/finlab/alerts.py @@ -56,7 +56,7 @@ def _matches(rule: dict, ev: dict) -> bool: def _watchlist(name: str) -> list[str]: if name == "portfolio": - return [p["ticker"] for p in data.load_portfolio()["positions"]] + return data.portfolio_tickers() # union de tous les comptes return scanner.load_theme(name) # 'all' ou un thème diff --git a/tools/finlab/finlab/data.py b/tools/finlab/finlab/data.py index b61bd0d..66679e4 100644 --- a/tools/finlab/finlab/data.py +++ b/tools/finlab/finlab/data.py @@ -45,12 +45,41 @@ def _disk_cache(key: str, ttl: int, producer): def load_portfolio(path: Path | None = None) -> dict: - """Charge portfolio.yaml.""" + """Charge portfolio.yaml (brut).""" path = path or PORTFOLIO_FILE with open(path, "r", encoding="utf-8") as f: return yaml.safe_load(f) +def base_currency() -> str: + return load_portfolio().get("base_currency", "EUR") + + +def load_accounts() -> list[dict]: + """Comptes normalisés [{name, type, cash, positions}]. + + Compat ascendante : un fichier à l'ancien format (cash/positions à la racine, + sans `accounts`) est présenté comme un compte unique « Principal ».""" + raw = load_portfolio() + if "accounts" in raw: + return raw["accounts"] + return [{ + "name": raw.get("name", "Principal"), + "type": raw.get("type", "courtier"), + "cash": raw.get("cash", {"amount": 0.0, "currency": raw.get("base_currency", "EUR")}), + "positions": raw.get("positions", []), + }] + + +def portfolio_tickers() -> list[str]: + """Union dédupliquée des tickers de tous les comptes (ordre stable).""" + seen: dict[str, None] = {} + for acc in load_accounts(): + for p in acc["positions"]: + seen.setdefault(p["ticker"], None) + return list(seen) + + @functools.lru_cache(maxsize=64) def _ticker(symbol: str) -> yf.Ticker: return yf.Ticker(symbol) diff --git a/tools/finlab/finlab/scanner.py b/tools/finlab/finlab/scanner.py index 1800e3e..13d30c9 100644 --- a/tools/finlab/finlab/scanner.py +++ b/tools/finlab/finlab/scanner.py @@ -100,8 +100,7 @@ def scan(symbols: list[str], target_pct: float = 5.0, sessions: int = SESSIONS_L def scan_portfolio(target_pct: float = 5.0, sessions: int = SESSIONS_LEFT_DEFAULT, extra: list[str] | None = None) -> pd.DataFrame: - pf = data.load_portfolio() - syms = [p["ticker"] for p in pf["positions"]] + (extra or []) + syms = data.portfolio_tickers() + (extra or []) return scan(syms, target_pct, sessions) diff --git a/tools/finlab/finlab/technical.py b/tools/finlab/finlab/technical.py index b3bce23..9578c5a 100644 --- a/tools/finlab/finlab/technical.py +++ b/tools/finlab/finlab/technical.py @@ -66,8 +66,7 @@ def scan(symbols: list[str], period: str = "1y") -> pd.DataFrame: def scan_portfolio(period: str = "1y") -> pd.DataFrame: - pf = data.load_portfolio() - return scan([p["ticker"] for p in pf["positions"]], period) + return scan(data.portfolio_tickers(), period) if __name__ == "__main__": diff --git a/tools/finlab/finlab/tracker.py b/tools/finlab/finlab/tracker.py index 4a86bb7..847cc24 100644 --- a/tools/finlab/finlab/tracker.py +++ b/tools/finlab/finlab/tracker.py @@ -1,4 +1,4 @@ -"""Suivi de portefeuille : valorisation, P&L, exposition sectorielle, concentration.""" +"""Suivi de portefeuille multi-comptes : valorisation, P&L, secteurs, concentration.""" from __future__ import annotations import pandas as pd @@ -13,76 +13,113 @@ def _sector(symbol: str) -> str: return "—" -def build(with_sector: bool = True) -> tuple[pd.DataFrame, dict]: - """Renvoie (tableau des positions, agrégats). Tout est exprimé en devise de base.""" - pf = data.load_portfolio() - base = pf["base_currency"] +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 pf["positions"]: + 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, - } - ) + 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) - df = pd.DataFrame(rows) - cash = data.to_currency(pf["cash"]["amount"], pf["cash"]["currency"], base) - invested = df[f"valeur_{base}"].sum() + +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 - - 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) - - agg = { + 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(df[f"P&L_{base}"].sum(), 2), - "pnl_pct": round(100 * df[f"P&L_{base}"].sum() / df[f"PRU_{base}"].sum(), 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": df.nlargest(3, "poids_%")[["ticker", "poids_%"]], + "top_weight": top_weight, } - return df.sort_values(f"valeur_{base}", ascending=False), agg + + +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: - df, agg = build() + per, agg = build_all() b = agg["base"] - out = ["═" * 64, " PORTEFEUILLE", "═" * 64] - out.append(df.to_string(index=False)) - out.append("") - out.append(f"Investi : {agg['invested']:>12,.2f} {b}") - out.append(f"Cash : {agg['cash']:>12,.2f} {b}") - out.append(f"TOTAL : {agg['total']:>12,.2f} {b}") + 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.append(f"P&L : {sign}{agg['pnl_total']:>11,.2f} {b} ({sign}{agg['pnl_pct']}%)") - out.append("\nExposition sectorielle :") + 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)}") - # Alerte concentration - top = agg["top_weight"] - out.append("\nConcentration (top 3) :") - for _, r in top.iterrows(): - out.append(f" {r['ticker']:<8} {r['poids_%']:>6.2f}%") return "\n".join(out) diff --git a/tools/finlab/portfolio.yaml b/tools/finlab/portfolio.yaml index 2c4c330..dcbb915 100644 --- a/tools/finlab/portfolio.yaml +++ b/tools/finlab/portfolio.yaml @@ -1,64 +1,44 @@ -# Portefeuille Revolut — Compte de courtage -# base_currency : devise de référence pour la valorisation/P&L +# Portefeuilles multi-comptes (courtiers / banques). +# base_currency : devise de référence pour la valorisation/P&L agrégés. +# Chaque compte : name, type (courtier|banque|pea|crypto…), cash, positions. +# ticker : symbole Yahoo Finance (suffixe .PA / .AS pour les places EU) +# shares : quantité détenue +# avg_price : prix de revient unitaire (PRU) +# avg_currency : devise du PRU +# +# (Compat : un fichier à l'ancien format — cash/positions au niveau racine, sans +# `accounts` — reste lu comme un compte unique « Principal ».) base_currency: EUR -# Liquidités -cash: - amount: 1248.44 - currency: EUR +accounts: + - name: Revolut + type: courtier + cash: + amount: 1248.44 + currency: EUR + positions: + - { ticker: NVDA, shares: 16.5959374, avg_price: 203.52, avg_currency: USD } # NVIDIA + - { ticker: ASML, shares: 1.5839474, avg_price: 1894.00, avg_currency: USD } # ASML (ADR, Nasdaq) + - { ticker: AMD, shares: 3.21341326, avg_price: 525.68, avg_currency: USD } # AMD + - { ticker: MU, shares: 1.44801383, avg_price: 994.62, avg_currency: USD } # Micron + - { ticker: STX, shares: 1.51530962, avg_price: 996.27, avg_currency: USD } # Seagate + - { ticker: ASML.AS, shares: 0.86432993, avg_price: 1550.60, avg_currency: EUR } # ASML (Amsterdam) + - { ticker: DELL, shares: 3.11943017, avg_price: 368.28, avg_currency: USD } # Dell + - { ticker: TTE.PA, shares: 17.74026299, avg_price: 73.14, avg_currency: EUR } # TotalEnergies + - { ticker: AAPL, shares: 3.39814436, avg_price: 294.28, avg_currency: USD } # Apple + - { ticker: HPE, shares: 13.32018017, avg_price: 43.45, avg_currency: USD } # HPE -# Positions -# ticker : symbole Yahoo Finance (suffixe .AS / .PA pour les places EU) -# shares : quantité détenue -# avg_price : prix de revient unitaire (Prix moyen Revolut) -# avg_currency: devise du prix de revient -positions: - - ticker: NVDA # NVIDIA - shares: 16.5959374 - avg_price: 203.52 - avg_currency: USD - - - ticker: ASML # ASML Holding (ADR, Nasdaq) - shares: 1.5839474 - avg_price: 1894.00 - avg_currency: USD - - - ticker: AMD # Advanced Micro Devices - shares: 3.21341326 - avg_price: 525.68 - avg_currency: USD - - - ticker: MU # Micron Technology - shares: 1.44801383 - avg_price: 994.62 - avg_currency: USD - - - ticker: STX # Seagate Technology - shares: 1.51530962 - avg_price: 996.27 - avg_currency: USD - - - ticker: ASML.AS # ASML Holding N.V. (Amsterdam, EUR) - shares: 0.86432993 - avg_price: 1550.60 - avg_currency: EUR - - - ticker: DELL # Dell Technologies - shares: 3.11943017 - avg_price: 368.28 - avg_currency: USD - - - ticker: TTE.PA # TotalEnergies SE (Paris, EUR) - shares: 17.74026299 - avg_price: 73.14 - avg_currency: EUR - - - ticker: AAPL # Apple - shares: 3.39814436 - avg_price: 294.28 - avg_currency: USD - - - ticker: HPE # Hewlett Packard Enterprise - shares: 13.32018017 - avg_price: 43.45 - avg_currency: USD + - name: BNP Paribas (PEA) + type: banque + cash: + amount: 46.34 + currency: EUR + positions: + - { ticker: AI.PA, shares: 1, avg_price: 170.73, avg_currency: EUR } # Air Liquide + - { ticker: PAEEM.PA, shares: 30, avg_price: 36.461, avg_currency: EUR } # Amundi PEA MSCI Emerging Markets + - { ticker: DCAM.PA, shares: 180, avg_price: 6.025, avg_currency: EUR } # Amundi PEA MSCI World + - { ticker: PCEU.PA, shares: 30, avg_price: 39.517, avg_currency: EUR } # Amundi PEA MSCI Europe + - { ticker: PSP5.PA, shares: 20, avg_price: 56.883, avg_currency: EUR } # Amundi PEA S&P 500 + - { ticker: BNP.PA, shares: 8, avg_price: 102.90, avg_currency: EUR } # BNP Paribas + - { ticker: ENGI.PA, shares: 20, avg_price: 27.08, avg_currency: EUR } # Engie + - { ticker: TTE.PA, shares: 5, avg_price: 70.26, avg_currency: EUR } # TotalEnergies diff --git a/tools/finlab/workspace/CLAUDE.md b/tools/finlab/workspace/CLAUDE.md index 62ed3e7..3e9895d 100644 --- a/tools/finlab/workspace/CLAUDE.md +++ b/tools/finlab/workspace/CLAUDE.md @@ -50,9 +50,11 @@ lancer en CLI : `python -m finlab.cli digest|portfolio|tech|fundamentals|scan|pl - `trade_plan` — plan chiffré (entrée / stop ATR / objectifs / taille / **R:R**). Le verdict R:R est central pour tout trade envisagé. -Fichiers du workspace, éditables ici (toi ou l'utilisateur) : **`portfolio.yaml`** (positions), -**`watchlists.yaml`** (thèmes), **`alerts.yaml`** (règles). Tu peux les modifier à la demande -(ex. « ajoute 5 actions Micron à mon portefeuille »), puis relancer les outils. +Fichiers du workspace, éditables ici (toi ou l'utilisateur) : **`portfolio.yaml`** +(**multi-comptes** : liste `accounts`, chacun avec `name`/`type`/`cash`/`positions` — ex. Revolut +courtier + BNP Paribas PEA), **`watchlists.yaml`** (thèmes), **`alerts.yaml`** (règles). Tu peux +les modifier à la demande (ex. « ajoute 5 actions Micron sur Revolut »), puis relancer les outils. +Les agrégats (`portfolio_summary`, alertes `portfolio`, scan) couvrent **tous les comptes**. ## Méthode de réponse