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