Funk-lab/tools/finlab/finlab/cli.py
ALI YESILKAYA 9d55cc803d
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>
2026-06-30 16:31:01 +02:00

118 lines
5 KiB
Python

"""CLI unifiée : python -m finlab.cli <commande>."""
from __future__ import annotations
import argparse
import pandas as pd
def main() -> None:
pd.set_option("display.max_columns", None, "display.max_colwidth", None, "display.width", 240)
parser = argparse.ArgumentParser(prog="finlab", description="Boîte à outils d'analyse de marché")
sub = parser.add_subparsers(dest="cmd", required=True)
sub.add_parser("portfolio", help="Suivi du portefeuille (valeur, P&L, secteurs)")
t = sub.add_parser("tech", help="Analyse technique")
t.add_argument("tickers", nargs="*", help="vide = tout le portefeuille")
t.add_argument("--period", default="1y")
f = sub.add_parser("fundamentals", help="Ratios fondamentaux")
f.add_argument("tickers", nargs="*", help="vide = tout le portefeuille")
s = sub.add_parser("scan", help="Scanner d'opportunités sur un thème")
s.add_argument("theme", nargs="?", default="all", help="thème de watchlists.yaml, 'all' ou 'portfolio'")
s.add_argument("--target", type=float, default=5.0, help="cible de perf hebdo en %%")
s.add_argument("--bullish", action="store_true", help="ne montrer que les setups haussiers")
d = sub.add_parser("digest", help="Digest compact (portefeuille + opportunités + alertes)")
d.add_argument("theme", nargs="?", default="all")
d.add_argument("--target", type=float, default=5.0)
a = sub.add_parser("alerts", help="Alertes déclenchées du jour")
a.add_argument("watch", nargs="?", default=None, help="thème, 'all' ou 'portfolio'")
p = sub.add_parser("plan", help="Plan de trade chiffré (entrée/stop/objectif/taille)")
p.add_argument("tickers", nargs="+")
p.add_argument("--capital", type=float, default=1427.0)
c = sub.add_parser("cache", help="Gestion du cache disque")
c.add_argument("action", choices=["clear", "info"], default="info", nargs="?")
ir = sub.add_parser("import-revolut", help="Reconstruit les positions depuis un relevé de compte Revolut")
ir.add_argument("csv", help="trading account statement Revolut (CSV, export sur toute la période)")
ir.add_argument("--account", default="Revolut", help="compte à mettre à jour dans portfolio.yaml")
ir.add_argument("--write", action="store_true", help="écrire dans portfolio.yaml (sinon aperçu)")
ir.add_argument("--no-verify", action="store_true", help="ne pas vérifier les cours Yahoo")
args = parser.parse_args()
if args.cmd == "portfolio":
from . import tracker
print(tracker.report())
elif args.cmd == "tech":
from . import technical
df = technical.scan(args.tickers, args.period) if args.tickers else technical.scan_portfolio(args.period)
print(df.to_string(index=False))
elif args.cmd == "fundamentals":
from . import fundamental
df = fundamental.compare(args.tickers) if args.tickers else fundamental.compare_portfolio()
print(df.to_string(index=False))
elif args.cmd == "scan":
from . import scanner
if args.theme == "portfolio":
df = scanner.scan_portfolio(args.target)
else:
df = scanner.scan_theme(args.theme, args.target, bullish_only=args.bullish)
print(df.to_string(index=False))
elif args.cmd == "digest":
from . import digest
path = digest.write(args.theme, args.target)
print(path.read_text(encoding="utf-8"))
print(f"\n[écrit dans {path}]")
elif args.cmd == "alerts":
from . import alerts
print(alerts.render(alerts.run(args.watch)))
elif args.cmd == "plan":
from . import plan
for tk in args.tickers:
try:
print(plan.render(plan.plan(tk, args.capital)))
except Exception as e:
print(f"{tk}: erreur {e}")
print()
elif args.cmd == "import-revolut":
from . import revolut
positions, warns = revolut.to_positions(
revolut.reconstruct(revolut.parse_statement(args.csv)), verify=not args.no_verify)
print(f"=== {len(positions)} positions (compte « {args.account} ») ===")
for p in positions:
print(f" {p['ticker']:9} {p['shares']:>14.8f} @ {p['avg_price']:>10.4f} {p['avg_currency']}")
for w in warns:
print(f"{w}")
if args.write:
revolut.update_portfolio(positions, account=args.account)
print(f"\n✅ portfolio.yaml mis à jour (compte « {args.account} »). Autres comptes/cash préservés.")
else:
print("\n(aperçu — ajoute --write pour écrire dans portfolio.yaml)")
elif args.cmd == "cache":
from . import data
if args.action == "clear":
n = data.clear_cache()
print(f"Cache vidé : {n} fichier(s).")
else:
files = list(data.CACHE_DIR.glob("*.pkl")) if data.CACHE_DIR.exists() else []
print(f"{len(files)} fichier(s) en cache dans {data.CACHE_DIR}")
if __name__ == "__main__":
main()