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

@ -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`, 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). plus paper trading (`paper_account`/`paper_positions`/`paper_order`, clés Alpaca optionnelles).
Configs (workspace, éditables) : `portfolio.yaml` (positions Revolut), `watchlists.yaml` (thèmes Configs (workspace, éditables) : `portfolio.yaml` (**multi-comptes** : `accounts` avec
chaîne IA/datacenter), `alerts.yaml` (règles). 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) ## Onboarding (premier boot)

View file

@ -61,24 +61,37 @@ def healthz():
return {"ok": True} return {"ok": True}
@app.get("/api/portfolio") def _positions_of(df, base):
def api_portfolio(): return [
df, agg = tracker.build(with_sector=True)
base = agg["base"]
positions = [
{ {
"ticker": r["ticker"], "secteur": r["secteur"], "qte": r["qté"], "ticker": r["ticker"], "secteur": r["secteur"], "qte": r["qté"],
"cours": r["cours"], "dev": r["dev"], "cours": r["cours"], "dev": r["dev"],
"valeur": r[f"valeur_{base}"], "pru": r[f"PRU_{base}"], "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") 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({ return _clean({
"base": base, "total": agg["total"], "cash": agg["cash"], "base": base, "total": gagg["total"], "cash": gagg["cash"], "invested": gagg["invested"],
"invested": agg["invested"], "pnl_total": agg["pnl_total"], "pnl_pct": agg["pnl_pct"], "pnl_total": gagg["pnl_total"], "pnl_pct": gagg["pnl_pct"],
"by_sector": agg["by_sector"].to_dict(), "by_sector": gagg["by_sector"].to_dict(),
"positions": positions, "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{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:hover{background:var(--panel2)}
.row.sel{background:#1b2334;outline:1px solid var(--acc)} .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 .t{font-weight:600;color:#fff}
.row .s{font-size:12px;color:var(--mut)} .row .s{font-size:12px;color:var(--mut)}
.mono{font-variant-numeric:tabular-nums} .mono{font-variant-numeric:tabular-nums}
@ -96,7 +101,8 @@
<!-- Colonne gauche : portefeuille + watchlists --> <!-- Colonne gauche : portefeuille + watchlists -->
<div> <div>
<div class="panel"> <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 id="pfList"><div class="loading">Chargement…</div></div>
</div> </div>
<div class="panel"> <div class="panel">
@ -258,32 +264,54 @@ function setPeriod(p){
if(SEL) loadChart(SEL); if(SEL) loadChart(SEL);
} }
// ── Portefeuille ─────────────────────────────────────────── // ── Portefeuille multi-comptes ─────────────────────────────
let PF=null, ACC='*';
const acctIcon = t => t==='banque'?'🏦':(t==='crypto'?'🪙':'📊');
async function loadPortfolio(){ async function loadPortfolio(){
try{ try{ PF = await api('/api/portfolio'); CCY = PF.base==='EUR'?'€':PF.base;
const d = await api('/api/portfolio'); renderAccountTabs(); renderPortfolio(); }
CCY = d.base==='EUR'?'€':d.base; catch(e){ document.getElementById('pfList').innerHTML='<div class="err">'+e.message+'</div>'; }
document.getElementById('kTotal').textContent = fmt(d.total)+' '+CCY; }
const pnlEl = document.getElementById('kPnl'); function renderAccountTabs(){
pnlEl.textContent = sign(d.pnl_total)+' '+CCY+' ('+sign(d.pnl_pct)+'%)'; const tabs = [`<span class="atab${ACC==='*'?' on':''}" onclick="selAcct('*')">Tous</span>`]
pnlEl.className = 'v mono '+cls(d.pnl_total); .concat((PF.accounts||[]).map(a=>{
document.getElementById('kCash').textContent = fmt(d.cash)+' '+CCY; const nm=a.name.replace(/'/g,"\\'");
const sectors = Object.entries(d.by_sector).sort((a,b)=>b[1]-a[1]); return `<span class="atab${ACC===a.name?' on':''}" onclick="selAcct('${nm}')">${acctIcon(a.type)} ${a.name}</span>`;
document.getElementById('kConc').textContent = sectors.length?`${sectors[0][0]} ${fmt(sectors[0][1])}%`:'—'; }));
document.getElementById('pfCount').textContent = d.positions.length+' lignes'; document.getElementById('acctTabs').innerHTML = tabs.join('');
}
document.getElementById('pfList').innerHTML = d.positions.map(p=>` function selAcct(name){ ACC=name; renderAccountTabs(); renderPortfolio(); }
<div class="row" data-tk="${p.ticker}" onclick="loadChart('${p.ticker}')"> function scope(){
<div><div class="t">${p.ticker}</div><div class="s">${p.secteur} · ${fmt(p.poids,1)}%</div></div> if(ACC==='*') return {total:PF.total,cash:PF.cash,pnl_total:PF.pnl_total,pnl_pct:PF.pnl_pct,
<div style="text-align:right"><div class="mono">${fmt(p.valeur)} ${CCY}</div> by_sector:PF.by_sector, positions:(PF.accounts||[]).flatMap(a=>a.positions.map(p=>({...p,_type:a.type})))};
<div class="s mono ${cls(p.pnl)}">${sign(p.pnl_pct)}%</div></div> const a=(PF.accounts||[]).find(x=>x.name===ACC)||PF.accounts[0];
</div>`).join(''); 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}))};
document.getElementById('sectors').innerHTML = sectors.map(([s,w])=>` }
<div class="alert"><div class="dot" style="background:var(--acc)"></div> function renderPortfolio(){
<div style="flex:1">${s}</div><b class="mono">${fmt(w,1)}%</b></div>`).join(''); const s=scope();
if(!SEL && d.positions.length) loadChart(d.positions[0].ticker); document.getElementById('kTotal').textContent = fmt(s.total)+' '+CCY;
}catch(e){ document.getElementById('pfList').innerHTML='<div class="err">'+e.message+'</div>'; } 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 `<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(([sec,w])=>`
<div class="alert"><div class="dot" style="background:var(--acc)"></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 ────────────────────────────────────── // ── Watchlists / scan ──────────────────────────────────────

View file

@ -56,7 +56,7 @@ def _matches(rule: dict, ev: dict) -> bool:
def _watchlist(name: str) -> list[str]: def _watchlist(name: str) -> list[str]:
if name == "portfolio": 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 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: def load_portfolio(path: Path | None = None) -> dict:
"""Charge portfolio.yaml.""" """Charge portfolio.yaml (brut)."""
path = path or PORTFOLIO_FILE path = path or PORTFOLIO_FILE
with open(path, "r", encoding="utf-8") as f: with open(path, "r", encoding="utf-8") as f:
return yaml.safe_load(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) @functools.lru_cache(maxsize=64)
def _ticker(symbol: str) -> yf.Ticker: def _ticker(symbol: str) -> yf.Ticker:
return yf.Ticker(symbol) 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: 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 = data.portfolio_tickers() + (extra or [])
syms = [p["ticker"] for p in pf["positions"]] + (extra or [])
return scan(syms, target_pct, sessions) 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: def scan_portfolio(period: str = "1y") -> pd.DataFrame:
pf = data.load_portfolio() return scan(data.portfolio_tickers(), period)
return scan([p["ticker"] for p in pf["positions"]], period)
if __name__ == "__main__": 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 from __future__ import annotations
import pandas as pd import pandas as pd
@ -13,76 +13,113 @@ def _sector(symbol: str) -> str:
return "" return ""
def build(with_sector: bool = True) -> tuple[pd.DataFrame, dict]: def _positions_df(positions: list[dict], base: str, with_sector: bool = True) -> pd.DataFrame:
"""Renvoie (tableau des positions, agrégats). Tout est exprimé en devise de base.""" """Tableau valorisé d'une liste de positions (sans les poids — dépendent du total)."""
pf = data.load_portfolio()
base = pf["base_currency"]
rows = [] rows = []
for p in positions:
for p in pf["positions"]:
sym = p["ticker"] sym = p["ticker"]
price, cur = data.last_price(sym) price, cur = data.last_price(sym)
value = data.to_currency(price * p["shares"], cur, base) value = data.to_currency(price * p["shares"], cur, base)
cost = data.to_currency(p["avg_price"] * p["shares"], p["avg_currency"], base) cost = data.to_currency(p["avg_price"] * p["shares"], p["avg_currency"], base)
pnl = value - cost pnl = value - cost
rows.append( rows.append({
{ "ticker": sym,
"ticker": sym, "secteur": _sector(sym) if with_sector else "",
"secteur": _sector(sym) if with_sector else "", "qté": p["shares"],
"qté": p["shares"], "cours": round(price, 2),
"cours": round(price, 2), "dev": cur,
"dev": cur, f"valeur_{base}": round(value, 2),
f"valeur_{base}": round(value, 2), f"PRU_{base}": round(cost, 2),
f"PRU_{base}": round(cost, 2), f"P&L_{base}": round(pnl, 2),
f"P&L_{base}": round(pnl, 2), "P&L_%": round(100 * pnl / cost, 2) if cost else 0.0,
"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) def _agg(df: pd.DataFrame, cash: float, base: str) -> dict:
invested = df[f"valeur_{base}"].sum() """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 total = invested + cash
if len(df) and total:
df["poids_%"] = (df[f"valeur_{base}"] / total * 100).round(2) df["poids_%"] = (df[f"valeur_{base}"] / total * 100).round(2)
by_sector = (df.groupby("secteur")[f"valeur_{base}"].sum().sort_values(ascending=False)
by_sector = ( / total * 100).round(2)
df.groupby("secteur")[f"valeur_{base}"].sum().sort_values(ascending=False) pnl_total = df[f"P&L_{base}"].sum()
/ total * 100 cost_total = df[f"PRU_{base}"].sum()
).round(2) top_weight = df.nlargest(3, "poids_%")[["ticker", "poids_%"]]
else:
agg = { by_sector = pd.Series(dtype=float)
pnl_total = cost_total = 0.0
top_weight = pd.DataFrame(columns=["ticker", "poids_%"])
return {
"base": base, "base": base,
"cash": round(cash, 2), "cash": round(cash, 2),
"invested": round(invested, 2), "invested": round(invested, 2),
"total": round(total, 2), "total": round(total, 2),
"pnl_total": round(df[f"P&L_{base}"].sum(), 2), "pnl_total": round(pnl_total, 2),
"pnl_pct": round(100 * df[f"P&L_{base}"].sum() / df[f"PRU_{base}"].sum(), 2), "pnl_pct": round(100 * pnl_total / cost_total, 2) if cost_total else 0.0,
"by_sector": by_sector, "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: def report() -> str:
df, agg = build() per, agg = build_all()
b = agg["base"] b = agg["base"]
out = ["" * 64, " PORTEFEUILLE", "" * 64] out = ["" * 64, " PORTEFEUILLE (tous comptes)", "" * 64]
out.append(df.to_string(index=False)) for acc in per:
out.append("") a = acc["agg"]
out.append(f"Investi : {agg['invested']:>12,.2f} {b}") sign = "+" if a["pnl_total"] >= 0 else ""
out.append(f"Cash : {agg['cash']:>12,.2f} {b}") out.append(f"\n{acc['name']} ({acc['type']}) — {a['total']:,.2f} {b} "
out.append(f"TOTAL : {agg['total']:>12,.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 "" sign = "+" if agg["pnl_total"] >= 0 else ""
out.append(f"P&L : {sign}{agg['pnl_total']:>11,.2f} {b} ({sign}{agg['pnl_pct']}%)") out += ["", "" * 64,
out.append("\nExposition sectorielle :") 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(): for sec, w in agg["by_sector"].items():
out.append(f" {sec:<24} {w:>6.2f}% {'' * int(w / 2)}") 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) return "\n".join(out)

View file

@ -1,64 +1,44 @@
# Portefeuille Revolut — Compte de courtage # Portefeuilles multi-comptes (courtiers / banques).
# base_currency : devise de référence pour la valorisation/P&L # 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 base_currency: EUR
# Liquidités accounts:
cash: - name: Revolut
amount: 1248.44 type: courtier
currency: EUR 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 - name: BNP Paribas (PEA)
# ticker : symbole Yahoo Finance (suffixe .AS / .PA pour les places EU) type: banque
# shares : quantité détenue cash:
# avg_price : prix de revient unitaire (Prix moyen Revolut) amount: 46.34
# avg_currency: devise du prix de revient currency: EUR
positions: positions:
- ticker: NVDA # NVIDIA - { ticker: AI.PA, shares: 1, avg_price: 170.73, avg_currency: EUR } # Air Liquide
shares: 16.5959374 - { ticker: PAEEM.PA, shares: 30, avg_price: 36.461, avg_currency: EUR } # Amundi PEA MSCI Emerging Markets
avg_price: 203.52 - { ticker: DCAM.PA, shares: 180, avg_price: 6.025, avg_currency: EUR } # Amundi PEA MSCI World
avg_currency: USD - { 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: ASML # ASML Holding (ADR, Nasdaq) - { ticker: BNP.PA, shares: 8, avg_price: 102.90, avg_currency: EUR } # BNP Paribas
shares: 1.5839474 - { ticker: ENGI.PA, shares: 20, avg_price: 27.08, avg_currency: EUR } # Engie
avg_price: 1894.00 - { ticker: TTE.PA, shares: 5, avg_price: 70.26, avg_currency: EUR } # TotalEnergies
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

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 - `trade_plan` — plan chiffré (entrée / stop ATR / objectifs / taille / **R:R**). Le verdict
R:R est central pour tout trade envisagé. R:R est central pour tout trade envisagé.
Fichiers du workspace, éditables ici (toi ou l'utilisateur) : **`portfolio.yaml`** (positions), Fichiers du workspace, éditables ici (toi ou l'utilisateur) : **`portfolio.yaml`**
**`watchlists.yaml`** (thèmes), **`alerts.yaml`** (règles). Tu peux les modifier à la demande (**multi-comptes** : liste `accounts`, chacun avec `name`/`type`/`cash`/`positions` — ex. Revolut
(ex. « ajoute 5 actions Micron à mon portefeuille »), puis relancer les outils. 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 ## Méthode de réponse