mirror of
https://github.com/Alkatrazz24/Funk-lab.git
synced 2026-07-08 04:44:43 +02:00
* feat(finlab): console Claude Code finance in-cluster + toolkit d'analyse Intègre finlab (ex-projet Projets/Finance) au lab comme une console Claude Code web spécialisée finance — l'esprit OpenAlice, mais c'est le vrai Claude Code sur l'abonnement (login persisté, pas d'API facturée), agentique, avec la boîte à outils finlab (Yahoo Finance) branchée en MCP. - tools/finlab/ : source finlab rapatriée + Dockerfile (Python 3.12 + Node + claude-code + ttyd) + persona workspace/CLAUDE.md + branchement MCP + entrypoint (seed du workspace no-clobber sur le PVC) - .github/workflows/build-finlab.yml : build GHCR funk-finlab + bump manifest (main) - k8s/apps/finlab/ : Deployment/Service/PVC/IngressRoute (finance.lab.local) + Middleware basicAuth (shell web protégé) ; PVC = HOME (login) + workspace - k8s/apps-of-apps/apps/finlab.yaml : Application ArgoCD - .mcp.json (racine) : outils finlab dans les sessions Claude Code du lab - admin/ia/finlab.md + READMEs + CLAUDE.md : doc + enregistrement Analyse/aide à la décision uniquement — aucun ordre réel (paper trading Alpaca fictif seul exécutable). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * fix(finlab): ttyd absent des dépôts bookworm → binaire statique GitHub Le build amont échouait (`E: Package 'ttyd' has no installation candidate`) : ttyd n'est pas packagé dans Debian bookworm. On récupère le binaire statique (musl, pin TTYD_VERSION=1.7.7) depuis les releases GitHub. Build complet validé en local (podman) : ttyd 1.7.7, claude-code 2.1.195, import finlab + seed OK. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
113 lines
3.7 KiB
Python
113 lines
3.7 KiB
Python
"""Couche d'accès aux données de marché (Yahoo Finance via yfinance).
|
|
|
|
Centralise les appels réseau, le cache devises et la lecture du portefeuille
|
|
pour que tous les outils partagent la même source.
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
import functools
|
|
import pickle
|
|
import time
|
|
from pathlib import Path
|
|
|
|
import pandas as pd
|
|
import yaml
|
|
import yfinance as yf
|
|
|
|
ROOT = Path(__file__).resolve().parent.parent
|
|
PORTFOLIO_FILE = ROOT / "portfolio.yaml"
|
|
CACHE_DIR = ROOT / ".cache"
|
|
|
|
# Durées de fraîcheur du cache disque (secondes)
|
|
TTL_HISTORY = 3600 # cours : 1h (suffisant pour des scans intraday espacés)
|
|
TTL_INFO = 7 * 86400 # fondamentaux/secteur : 1 semaine (change peu)
|
|
TTL_PRICE = 600 # dernier cours : 10 min
|
|
|
|
|
|
def _disk_cache(key: str, ttl: int, producer):
|
|
"""Renvoie l'objet caché si frais (< ttl), sinon le (re)calcule et le stocke."""
|
|
CACHE_DIR.mkdir(exist_ok=True)
|
|
path = CACHE_DIR / (key.replace("/", "_").replace("=", "_") + ".pkl")
|
|
if path.exists() and (time.time() - path.stat().st_mtime) < ttl:
|
|
try:
|
|
with open(path, "rb") as f:
|
|
return pickle.load(f)
|
|
except Exception:
|
|
pass # cache corrompu -> on recalcule
|
|
obj = producer()
|
|
with open(path, "wb") as f:
|
|
pickle.dump(obj, f)
|
|
return obj
|
|
|
|
|
|
def load_portfolio(path: Path | None = None) -> dict:
|
|
"""Charge portfolio.yaml."""
|
|
path = path or PORTFOLIO_FILE
|
|
with open(path, "r", encoding="utf-8") as f:
|
|
return yaml.safe_load(f)
|
|
|
|
|
|
@functools.lru_cache(maxsize=64)
|
|
def _ticker(symbol: str) -> yf.Ticker:
|
|
return yf.Ticker(symbol)
|
|
|
|
|
|
def history(symbol: str, period: str = "1y", interval: str = "1d", fresh: bool = False) -> pd.DataFrame:
|
|
"""Historique OHLCV (caché sur disque). fresh=True force un re-fetch."""
|
|
def fetch():
|
|
df = _ticker(symbol).history(period=period, interval=interval, auto_adjust=True)
|
|
if df.empty:
|
|
raise ValueError(f"Aucune donnée pour {symbol} (period={period})")
|
|
return df
|
|
|
|
ttl = 0 if fresh else TTL_HISTORY
|
|
return _disk_cache(f"hist_{symbol}_{period}_{interval}", ttl, fetch)
|
|
|
|
|
|
def last_price(symbol: str, fresh: bool = False) -> tuple[float, str]:
|
|
"""(dernier cours, devise) en devise native du titre (caché 10 min)."""
|
|
def fetch():
|
|
fi = _ticker(symbol).fast_info
|
|
return float(fi.last_price), fi.currency
|
|
|
|
return _disk_cache(f"price_{symbol}", 0 if fresh else TTL_PRICE, fetch)
|
|
|
|
|
|
def fx_rate(base: str, quote: str) -> float:
|
|
"""Taux de change : combien de `quote` pour 1 `base` (ex: EUR->USD ~ 1.07)."""
|
|
if base == quote:
|
|
return 1.0
|
|
|
|
def fetch():
|
|
df = _ticker(f"{base}{quote}=X").history(period="5d", auto_adjust=True)
|
|
if df.empty:
|
|
raise ValueError(f"Pas de taux {base}->{quote}")
|
|
return float(df["Close"].iloc[-1])
|
|
|
|
return _disk_cache(f"fx_{base}{quote}", TTL_PRICE, fetch)
|
|
|
|
|
|
def to_currency(amount: float, frm: str, to: str) -> float:
|
|
"""Convertit un montant d'une devise vers une autre."""
|
|
if frm == to:
|
|
return amount
|
|
# On passe par EUR->X pour limiter le nombre de paires interrogées.
|
|
try:
|
|
return amount * fx_rate(frm, to)
|
|
except ValueError:
|
|
return amount / fx_rate(to, frm)
|
|
|
|
|
|
def info(symbol: str) -> dict:
|
|
"""Métadonnées fondamentales (caché 1 semaine ; peut être lent / vide)."""
|
|
return _disk_cache(f"info_{symbol}", TTL_INFO, lambda: _ticker(symbol).get_info())
|
|
|
|
|
|
def clear_cache() -> int:
|
|
"""Vide le cache disque. Renvoie le nombre de fichiers supprimés."""
|
|
if not CACHE_DIR.exists():
|
|
return 0
|
|
files = list(CACHE_DIR.glob("*.pkl"))
|
|
for f in files:
|
|
f.unlink()
|
|
return len(files)
|