mirror of
https://github.com/Alkatrazz24/Funk-lab.git
synced 2026-07-08 14:24:42 +02:00
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:
parent
ba45ca198b
commit
3b43025895
10 changed files with 246 additions and 158 deletions
|
|
@ -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)
|
||||
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
})
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
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]);
|
||||
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(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 ──────────────────────────────────────
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -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__":
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue