mirror of
https://github.com/Alkatrazz24/Funk-lab.git
synced 2026-07-10 01:24:43 +02:00
feat(finlab): import déterministe d'un relevé de compte Revolut → positions
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:
parent
1c4a9d647e
commit
efec4a5dfe
8 changed files with 324 additions and 29 deletions
|
|
@ -202,23 +202,57 @@ def api_analyze(ticker: str, capital: float = 1427.0):
|
|||
return _clean(out)
|
||||
|
||||
|
||||
# ── Import de relevés (image → analysée par la Console IA) ─────────────────────
|
||||
# ── Import de relevés ─────────────────────────────────────────────────────────
|
||||
# • Image → scannée par la Console IA (vision, abonnement).
|
||||
# • CSV Revolut (account statement) → reconstruit déterministe (aperçu + appliquer), sans IA.
|
||||
IMPORTS_DIR = data.ROOT / "imports"
|
||||
_IMG_EXT = {".png", ".jpg", ".jpeg", ".webp", ".gif"}
|
||||
_CSV_EXT = {".csv"}
|
||||
|
||||
|
||||
def _safe_import(name: str) -> Path:
|
||||
"""Résout un nom de fichier dans imports/ (refuse l'évasion de chemin)."""
|
||||
p = (IMPORTS_DIR / Path(name).name)
|
||||
if p.resolve().parent != IMPORTS_DIR.resolve():
|
||||
raise ValueError("chemin invalide")
|
||||
return p
|
||||
|
||||
|
||||
@app.post("/api/import")
|
||||
async def api_import(file: UploadFile = File(...)):
|
||||
"""Enregistre une capture de relevé dans le workspace (imports/) ; la Console IA la scanne
|
||||
ensuite pour mettre à jour portfolio.yaml (vision côté abonnement, pas de clé API ici)."""
|
||||
"""Enregistre un relevé dans le workspace (imports/). Image → Console IA ; CSV → import Revolut."""
|
||||
ext = Path(file.filename or "").suffix.lower()
|
||||
if ext not in _IMG_EXT:
|
||||
return JSONResponse(status_code=400, content={"error": f"format image non supporté: {ext or '?'}"})
|
||||
kind = "image" if ext in _IMG_EXT else ("csv" if ext in _CSV_EXT else None)
|
||||
if kind is None:
|
||||
return JSONResponse(status_code=400, content={"error": f"format non supporté: {ext or '?'} (image ou .csv)"})
|
||||
IMPORTS_DIR.mkdir(parents=True, exist_ok=True)
|
||||
ts = dt.datetime.now().strftime("%Y%m%d-%H%M%S")
|
||||
name = f"releve-{ts}{ext}"
|
||||
(IMPORTS_DIR / name).write_bytes(await file.read())
|
||||
return {"saved": name, "rel": f"imports/{name}"}
|
||||
_safe_import(name).write_bytes(await file.read())
|
||||
return {"saved": name, "rel": f"imports/{name}", "kind": kind}
|
||||
|
||||
|
||||
@app.get("/api/revolut/preview")
|
||||
def api_revolut_preview(file: str, account: str = "Revolut"):
|
||||
"""Aperçu des positions reconstruites depuis un relevé de compte Revolut (sans écrire)."""
|
||||
from finlab import revolut
|
||||
path = _safe_import(file)
|
||||
if not path.exists():
|
||||
return JSONResponse(status_code=404, content={"error": "fichier introuvable"})
|
||||
positions, warns = revolut.to_positions(revolut.reconstruct(revolut.parse_statement(path)))
|
||||
return _clean({"account": account, "positions": positions, "warnings": warns})
|
||||
|
||||
|
||||
@app.post("/api/revolut/apply")
|
||||
def api_revolut_apply(file: str, account: str = "Revolut"):
|
||||
"""Applique le relevé Revolut → met à jour le compte dans portfolio.yaml (autres comptes préservés)."""
|
||||
from finlab import revolut
|
||||
path = _safe_import(file)
|
||||
if not path.exists():
|
||||
return JSONResponse(status_code=404, content={"error": "fichier introuvable"})
|
||||
positions, warns = revolut.to_positions(revolut.reconstruct(revolut.parse_statement(path)))
|
||||
revolut.update_portfolio(positions, account=account)
|
||||
return _clean({"account": account, "positions": positions, "warnings": warns, "applied": True})
|
||||
|
||||
|
||||
@app.get("/api/imports")
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue