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

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:
ALI YESILKAYA 2026-06-30 01:40:20 +02:00 committed by GitHub
parent ba45ca198b
commit c182bac1d2
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
10 changed files with 246 additions and 158 deletions

View file

@ -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)

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,
})

View file

@ -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 @@
<!-- Colonne gauche : portefeuille + watchlists -->
<div>
<div class="panel">
<h3>Mon portefeuille <span class="muted" id="pfCount"></span></h3>
<h3>Mes comptes <span class="muted" id="pfCount"></span></h3>
<div class="acct-tabs" id="acctTabs"></div>
<div id="pfList"><div class="loading">Chargement…</div></div>
</div>
<div class="panel">
@ -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;
try{ PF = await api('/api/portfolio'); CCY = PF.base==='EUR'?'€':PF.base;
renderAccountTabs(); renderPortfolio(); }
catch(e){ document.getElementById('pfList').innerHTML='<div class="err">'+e.message+'</div>'; }
}
function renderAccountTabs(){
const tabs = [`<span class="atab${ACC==='*'?' on':''}" onclick="selAcct('*')">Tous</span>`]
.concat((PF.accounts||[]).map(a=>{
const nm=a.name.replace(/'/g,"\\'");
return `<span class="atab${ACC===a.name?' on':''}" onclick="selAcct('${nm}')">${acctIcon(a.type)} ${a.name}</span>`;
}));
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(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]);
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 = d.positions.length+' lignes';
document.getElementById('pfList').innerHTML = d.positions.map(p=>`
<div class="row" data-tk="${p.ticker}" onclick="loadChart('${p.ticker}')">
<div><div class="t">${p.ticker}</div><div class="s">${p.secteur} · ${fmt(p.poids,1)}%</div></div>
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 `<div class="row" data-tk="${p.ticker}" onclick="loadChart('${p.ticker}')">
<div><div class="t">${p.ticker}${showAcct?` <span class="atag">${acctIcon(p._type)}</span>`:''}</div>
<div class="s">${p.secteur} · ${fmt(poids,1)}%</div></div>
<div style="text-align:right"><div class="mono">${fmt(p.valeur)} ${CCY}</div>
<div class="s mono ${cls(p.pnl)}">${sign(p.pnl_pct)}%</div></div>
</div>`).join('');
document.getElementById('sectors').innerHTML = sectors.map(([s,w])=>`
<div class="s mono ${cls(p.pnl)}">${sign(p.pnl_pct)}%</div></div></div>`;
}).join('');
document.getElementById('sectors').innerHTML = sectors.map(([sec,w])=>`
<div class="alert"><div class="dot" style="background:var(--acc)"></div>
<div style="flex:1">${s}</div><b class="mono">${fmt(w,1)}%</b></div>`).join('');
if(!SEL && d.positions.length) loadChart(d.positions[0].ticker);
}catch(e){ document.getElementById('pfList').innerHTML='<div class="err">'+e.message+'</div>'; }
<div style="flex:1">${sec}</div><b class="mono">${fmt(w,1)}%</b></div>`).join('') || '<div class="muted"></div>';
if(!SEL && s.positions.length) loadChart(s.positions[0].ticker);
}
// ── Watchlists / scan ──────────────────────────────────────

View file

@ -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

View file

@ -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)

View file

@ -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)

View file

@ -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__":

View file

@ -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,20 +13,16 @@ 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(
{
rows.append({
"ticker": sym,
"secteur": _sector(sym) if with_sector else "",
"qté": p["shares"],
@ -36,53 +32,94 @@ def build(with_sector: bool = True) -> tuple[pd.DataFrame, dict]:
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
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)
agg = {
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)

View file

@ -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
accounts:
- name: Revolut
type: courtier
cash:
amount: 1248.44
currency: EUR
# 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: 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
- 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

View file

@ -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