feat(finlab): import déterministe d'un relevé de compte Revolut → positions (#72)

Procédure de mise à jour du portefeuille « à chaque trade » à partir des relevés
Revolut, sans IA (exact).

- finlab/revolut.py : reconstruit les positions depuis le « trading account
  statement » (toutes transactions) par COÛT MOYEN (achats − ventes, PRU pondéré),
  mappe les tickers Revolut→Yahoo (ASME→ASML.AS, TOTB→TTE.PA, BNP→BNP.PA, AXA→CS.PA,
  FTE→ORA.PA, AIR1→AI.PA) et vérifie les cours. Idempotent, ne touche qu'un compte,
  préserve les autres comptes/cash/commentaires (round-trip ruamel.yaml)
- CLI : python -m finlab.cli import-revolut <csv> [--write] [--account]
- Dashboard : le bouton « Importer un relevé » accepte les CSV → endpoints
  /api/revolut/preview (aperçu) + /api/revolut/apply (écrit), avec garde anti-évasion
  de chemin ; le front affiche les positions + bouton « Appliquer »
- portfolio.yaml : compte Revolut reconstruit depuis l'historique complet → ajoute
  AMAT (achetée le 29/06, manquait) ; valeurs alignées au centime
- requirements : + ruamel.yaml ; persona console + admin/ia/finlab.md documentés

Validé sur l'historique réel : 11 positions reconstruites = portfolio.yaml existant
(cours vérifiés, 0 warning) + AMAT. Flux dashboard upload→preview→apply testé
(TestClient), JS OK.

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
ALI YESILKAYA 2026-06-30 16:31:01 +02:00 committed by GitHub
parent 1c4a9d647e
commit 9d55cc803d
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
8 changed files with 324 additions and 29 deletions

View file

@ -108,9 +108,9 @@
<div>
<div class="panel">
<h3>Mes comptes <span class="muted" id="pfCount"></span>
<button class="btn perbtn" style="font-size:11px" title="Importer une capture de relevé (la Console IA la scanne)" onclick="document.getElementById('importFile').click()">📥 Importer un relevé</button>
<button class="btn perbtn" style="font-size:11px" title="Image (scannée par l'IA) ou CSV Revolut (import déterministe)" onclick="document.getElementById('importFile').click()">📥 Importer un relevé</button>
</h3>
<input type="file" id="importFile" accept="image/*" style="display:none" onchange="uploadImport(this.files[0])">
<input type="file" id="importFile" accept="image/*,.csv" style="display:none" onchange="uploadImport(this.files[0])">
<div class="acct-tabs" id="acctTabs"></div>
<div id="pfList"><div class="loading">Chargement…</div></div>
</div>
@ -439,7 +439,8 @@ async function uploadImport(f){
const r=await fetch('/api/import',{method:'POST',body:fd});
const j=await r.json();
if(j.error) throw new Error(j.error);
out.innerHTML = `<p style="color:var(--up)">✅ Relevé enregistré : <code>${j.rel}</code></p>
if(j.kind==='csv') return revolutPreview(j.rel); // relevé Revolut → import déterministe
out.innerHTML = `<p style="color:var(--up)">✅ Capture enregistrée : <code>${j.rel}</code></p>
<p>Ouvre la <b>Console IA</b> et demande&nbsp;:</p>
<pre>importe le relevé ${j.rel} et mets à jour mon portefeuille</pre>
<p class="muted" style="font-size:12px">Je lirai l'image, je mapperai chaque ligne vers son ticker Yahoo (en vérifiant le cours), puis je mettrai à jour <code>portfolio.yaml</code> <b>après ta confirmation</b>.</p>
@ -447,6 +448,33 @@ async function uploadImport(f){
}catch(e){ out.innerHTML='<div class="err">Erreur: '+e.message+'</div>'; }
finally{ inp.value=''; }
}
// Relevé de compte Revolut (CSV) : aperçu déterministe des positions, puis « Appliquer ».
async function revolutPreview(rel){
const out=document.getElementById('importOut');
out.innerHTML='<div class="loading">Reconstruction des positions Revolut…</div>';
try{
const d=await api('/api/revolut/preview?file='+encodeURIComponent(rel));
out.innerHTML = `<p style="color:var(--up)">📄 Relevé Revolut — <b>${d.positions.length} positions</b> reconstruites (compte « ${d.account} ») :</p>`
+ '<table><thead><tr><th>Ticker</th><th>Quantité</th><th>PRU</th><th>Dev</th></tr></thead><tbody>'
+ d.positions.map(p=>`<tr><td class="t">${p.ticker}</td><td class="mono">${p.shares}</td><td class="mono">${p.avg_price}</td><td>${p.avg_currency}</td></tr>`).join('')
+ '</tbody></table>'
+ (d.warnings.length?`<div class="err" style="margin-top:6px">⚠ ${d.warnings.join('<br>⚠ ')}</div>`:'')
+ `<div class="controls" style="margin-top:12px">
<button class="btn acc" onclick="revolutApply('${rel}')">✅ Appliquer au compte Revolut</button>
</div>
<p class="muted" style="font-size:11px">Remplace les positions du compte Revolut dans portfolio.yaml (cash + autres comptes préservés). Coût moyen. Re-déposer un relevé à jour à chaque trade.</p>`;
}catch(e){ out.innerHTML='<div class="err">Erreur: '+e.message+'</div>'; }
}
async function revolutApply(rel){
const out=document.getElementById('importOut');
out.innerHTML='<div class="loading">Mise à jour de portfolio.yaml…</div>';
try{
const r=await fetch('/api/revolut/apply?file='+encodeURIComponent(rel),{method:'POST'});
const d=await r.json(); if(d.error) throw new Error(d.error);
out.innerHTML=`<p style="color:var(--up)">✅ Compte Revolut mis à jour (${d.positions.length} positions). Le dashboard se recharge…</p>`;
setTimeout(()=>{ document.getElementById('importModal').classList.remove('on'); loadPortfolio(); }, 1200);
}catch(e){ out.innerHTML='<div class="err">Erreur: '+e.message+'</div>'; }
}
// ── Actions rapides (finlab pur, one-click) ────────────────
const escapeHtml = s => (s||'').replace(/[&<>]/g,c=>({'&':'&amp;','<':'&lt;','>':'&gt;'}[c]));