Funk-lab/tools/finlab/finlab/technical.py
alkatrazz 3b43025895 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>
2026-06-30 01:33:37 +02:00

78 lines
2.3 KiB
Python

"""Analyse technique : agrège les indicateurs en un état lisible par titre."""
from __future__ import annotations
import pandas as pd
from . import data, indicators as ind
def analyze(symbol: str, period: str = "1y") -> dict:
df = data.history(symbol, period=period)
close = df["Close"]
last = close.iloc[-1]
rsi = ind.rsi(close).iloc[-1]
macd = ind.macd(close)
macd_hist = macd["hist"].iloc[-1]
macd_cross = (
"haussier" if macd["macd"].iloc[-1] > macd["signal"].iloc[-1] else "baissier"
)
sma50 = ind.sma(close, 50).iloc[-1]
sma200 = ind.sma(close, 200).iloc[-1] if len(close) >= 200 else float("nan")
bb = ind.bollinger(close).iloc[-1]
atr = ind.atr(df).iloc[-1]
signals = []
if rsi >= 70:
signals.append("RSI en surachat (>70)")
elif rsi <= 30:
signals.append("RSI en survente (<30)")
if last > sma50:
signals.append("au-dessus MM50")
else:
signals.append("sous MM50")
if not pd.isna(sma200):
signals.append("tendance LT haussière" if last > sma200 else "tendance LT baissière")
signals.append(f"MACD {macd_cross}")
if last >= bb["upper"]:
signals.append("borne haute Bollinger")
elif last <= bb["lower"]:
signals.append("borne basse Bollinger")
return {
"ticker": symbol,
"cours": round(last, 2),
"RSI14": round(rsi, 1),
"MM50": round(sma50, 2),
"MM200": round(sma200, 2) if not pd.isna(sma200) else None,
"MACD_hist": round(macd_hist, 3),
"MACD": macd_cross,
"ATR14": round(atr, 2),
"vol_%": round(100 * atr / last, 2),
"signaux": signals,
}
def scan(symbols: list[str], period: str = "1y") -> pd.DataFrame:
rows = []
for s in symbols:
try:
a = analyze(s, period)
a["signaux"] = ", ".join(a["signaux"])
rows.append(a)
except Exception as e:
rows.append({"ticker": s, "signaux": f"erreur: {e}"})
return pd.DataFrame(rows)
def scan_portfolio(period: str = "1y") -> pd.DataFrame:
return scan(data.portfolio_tickers(), period)
if __name__ == "__main__":
import sys
syms = sys.argv[1:]
df = scan(syms) if syms else scan_portfolio()
pd.set_option("display.max_colwidth", None, "display.width", 200)
print(df.to_string(index=False))