diff --git a/CLAUDE.md b/CLAUDE.md index 96b5d04..a409773 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -173,7 +173,7 @@ k8s/ │ ├── searxng/ # namespace: ai — méta-moteur recherche web (outil web_search d'Asa) │ ├── ghostfolio/ # namespace: ai — suivi de portefeuille (ghostfolio.lab.local) │ ├── openalice/ # namespace: ai — agent IA financier (openalice.lab.local, back-end finance d'Asa) — ⏸️ SUSPENDU -│ ├── finlab/ # namespace: ai — console Claude Code finance (finance.lab.local) + toolkit d'analyse de marché +│ ├── finlab/ # namespace: ai — dashboard finance + console Claude Code (finance.lab.local, /console) + toolkit │ └── {external-services}/ # ExternalName/Endpoints pointant vers storage-01 ou gpu-01 └── infra/ # Helm releases (HelmRelease + values.yaml) ├── monitoring/ # kube-prometheus-stack 85.0.2 diff --git a/admin/ia/finlab.md b/admin/ia/finlab.md index 528ac22..43f89b6 100644 --- a/admin/ia/finlab.md +++ b/admin/ia/finlab.md @@ -1,10 +1,16 @@ -# FinLab — console Claude Code finance + toolkit d'analyse de marché +# FinLab — dashboard finance + console Claude Code + toolkit d'analyse **Quoi** : boîte à outils d'analyse de marché maison (`finlab`, données Yahoo Finance gratuites) -+ une **console Claude Code web** spécialisée finance, déployée in-cluster. Remplace l'approche -OpenAlice abandonnée (cf. `admin/ia/openalice.md`) par quelque chose de plus simple et plus -puissant : **ton vrai Claude Code** (sur ton abonnement, pas l'API au compteur), agentique, avec -les outils finlab branchés. +déployée in-cluster sous **deux faces** (une seule image) : un **dashboard web** (graphiques, +portefeuille, watchlists, actions) et une **console Claude Code** finance. Remplace l'approche +OpenAlice abandonnée (cf. `admin/ia/openalice.md`) par plus simple et plus puissant : **ton vrai +Claude Code** (sur ton abonnement, pas l'API au compteur) + une UI graphique gratuite. + +- **Dashboard** (`finance.lab.local/`) — graphiques chandeliers (MM50/200 + volume), portefeuille + (P&L/poids/secteurs), watchlists thématiques scannées, alertes du jour, plan de trade R:R. + FastAPI `dashboard/server.py`, **aucun LLM/clé** (analyse technique pure). +- **Console** (`finance.lab.local/console/`) — terminal web (`ttyd`) → `claude` agentique, + persona + outils finlab en MCP, login abonnement persisté. **Où** : - Source : `tools/finlab/` (code finlab + Dockerfile + persona `workspace/CLAUDE.md`). @@ -21,14 +27,18 @@ trader concentré tech/semis, philosophie R:R/concentration/discipline). | Composant | Détail | |---|---| -| Console | `ttyd` (terminal web :7681) → CLI `claude` (Claude Code) dans `/home/finlab/workspace` | -| Cerveau | **Abonnement Claude Code** (login OAuth persisté dans `~/.claude` sur le PVC) — pas de clé API facturée | -| Outils | finlab en **MCP** (`finlab.mcp_server`, stdio) + CLI `python -m finlab.cli ...` | -| PVC `finlab-data` | 5Gi NFS → `/home/finlab` : login + workspace (configs éditables + `.cache`) | -| Auth | basicAuth Traefik (`finlab-auth`, middleware) — un shell web ne s'expose pas nu | +| initContainer `seed` | Peuple le workspace (`cp -rn`), robuste au piège NFS (fsGroup non honoré → recrée le dossier s'il est root/non-inscriptible). Tourne avant les conteneurs | +| container `dashboard` | `uvicorn dashboard.server:app` :8800 — UI + API JSON finlab | +| container `console` | `ttyd -b /console` :7681 → `claude` dans le workspace | +| Cerveau (console) | **Abonnement Claude Code** (login OAuth persisté dans `~/.claude` sur le PVC) — pas de clé API | +| Code vs data | Code (`finlab` + `dashboard`) dans l'image `/opt/app` (maj avec l'image) ; configs éditables via **`FINLAB_HOME=/home/finlab/workspace`** (PVC) | +| PVC `finlab-data` | 5Gi NFS → `/home/finlab` : login + workspace (configs + `.cache`) | +| Auth | basicAuth Traefik (`finlab-auth`) sur les deux routes — un shell web ne s'expose pas nu | +| Routes | `/console` → 7681 · reste → 8800 | -Le workspace est **seedé depuis l'image au 1er boot** (`cp -rn` no-clobber) puis vit sur le PVC : -edits (portefeuille) et login persistent ; rebuild d'image = seuls les fichiers manquants ajoutés. +**Découplage code/data** : `finlab` lit ses configs depuis `FINLAB_HOME` si défini (sinon repli +sur le dossier du package → dév local inchangé). Le workspace est seedé au 1er boot puis vit sur +le PVC (edits + login persistent ; une nouvelle image n'ajoute que les fichiers manquants). ## Outils finlab diff --git a/k8s/apps/finlab/README.md b/k8s/apps/finlab/README.md index c3953f0..0ee2973 100644 --- a/k8s/apps/finlab/README.md +++ b/k8s/apps/finlab/README.md @@ -1,32 +1,34 @@ -# FinLab — console Claude Code finance (in-cluster) +# FinLab — dashboard + console Claude Code (in-cluster) -Une **vraie session Claude Code** (sur l'**abonnement** d'Alkatrazz, login persisté) servie -dans un **terminal web** (`ttyd`), spécialisée analyse de marché via la persona `CLAUDE.md` et -la boîte à outils **finlab** (Yahoo Finance, exposée en **MCP**). Esprit OpenAlice, mais c'est -Claude Code + le toolkit maison — **aucune facturation API**, **aucun broker**, analyse seule. +Deux usages, **une image**, un workspace partagé : -> ⚠️ La console expose un **shell web** → protégée par **basicAuth Traefik**. Analyse et aide à -> la décision uniquement : **aucun ordre réel** (seul le paper trading Alpaca fictif existe). +- **Dashboard** (`finance.lab.local/`) — interface web : graphiques chandeliers (MM50/200 + + volume), portefeuille (P&L, poids, secteurs), watchlists thématiques scannées, alertes du + jour, actions (plan de trade R:R). Données finlab (Yahoo Finance) — **aucun LLM, aucune clé**. +- **Console** (`finance.lab.local/console/`) — vraie session **Claude Code** dans un terminal + web (`ttyd`), sur ton **abonnement** (login persisté), persona finance + outils finlab en MCP. -## Image +> ⚠️ La console expose un **shell web** → tout est derrière **basicAuth Traefik**. Analyse et +> aide à la décision uniquement : **aucun ordre réel** (paper trading Alpaca fictif seul). -Pas d'image publique : la CI (`.github/workflows/build-finlab.yml`) build depuis -`tools/finlab/` (notre code) → `ghcr.io/alkatrazz24/funk-finlab`, tag `sha-`, bump du -manifest sur `main`. +## Image & code -## Architecture du déploiement +CI (`.github/workflows/build-finlab.yml`) build depuis `tools/finlab/` → `ghcr.io/alkatrazz24/funk-finlab`. +Le **code** (package `finlab` + backend `dashboard`) vit dans l'image (`/opt/app`) et se met à +jour avec elle. Les **configs éditables** (portefeuille/watchlists/alertes) + la persona + le +login vivent dans le **workspace persistant** (`FINLAB_HOME=/home/finlab/workspace`, sur le PVC). -| Composant | Détail | +## Architecture du pod + +| Élément | Rôle | |---|---| -| Deployment | 1 réplica, `Recreate` (volume RWO), PodSecurity *restricted* (uid 1000, `fsGroup` 1000) | -| PVC `finlab-data` | 5Gi NFS → `/home/finlab` : `~/.claude` (login abonnement) + workspace (code finlab seedé, `portfolio.yaml`/`watchlists.yaml`/`alerts.yaml` éditables, `.cache`) | -| Service `finlab` | `web:7681` (ttyd) | -| Middleware `finlab-auth` | basicAuth Traefik (secret `finlab-auth`, htpasswd) | -| IngressRoute | `finance.lab.local` → 7681 (via `finlab-auth`) | - -Le workspace est **seedé depuis l'image au 1er boot** (`cp -rn`, no-clobber) puis vit sur le -PVC : tes edits (portefeuille, etc.) et ton login persistent ; une nouvelle image n'apporte que -les fichiers manquants. +| initContainer `seed` | Peuple le workspace depuis l'image (`cp -rn`), robuste au piège NFS (recrée le dossier si root/non-inscriptible). Tourne **avant** les conteneurs | +| container `console` | `ttyd -b /console` → `claude` dans le workspace (port 7681) | +| container `dashboard` | `uvicorn dashboard.server:app` (port 8800), lit `FINLAB_HOME` | +| PVC `finlab-data` (5Gi NFS) | `/home/finlab` : `~/.claude` (login) + workspace (configs + `.cache`) | +| Service `finlab` | `web:8800` + `console:7681` | +| Middleware `finlab-auth` | basicAuth Traefik (secret htpasswd) | +| IngressRoute | `/console` → 7681 · reste → 8800 (les deux derrière `finlab-auth`) | ## Pré-requis manuels (une fois) @@ -34,35 +36,21 @@ les fichiers manquants. ```bash kubectl -n ai create secret docker-registry ghcr-pull \ - --docker-server=ghcr.io \ - --docker-username=Alkatrazz24 \ + --docker-server=ghcr.io --docker-username=Alkatrazz24 \ --docker-password= ``` -### 2. Secret basicAuth de la console (`finlab-auth`) +### 2. Secret basicAuth `finlab-auth` ```bash -# htpasswd (paquet apache2-utils). Remplace /. -htpasswd -nbB '' > /tmp/finlab.htpasswd kubectl -n ai create secret generic finlab-auth \ - --from-file=users=/tmp/finlab.htpasswd -rm -f /tmp/finlab.htpasswd + --from-literal=users="alkatrazz:$(openssl passwd -apr1 'MOT_DE_PASSE')" ``` -### 3. Premier boot — login Claude Code (abonnement) +### 3. Premier boot — login Claude Code (pour la console) -1. Ouvrir **http://finance.lab.local** (basicAuth → identifiants de l'étape 2). -2. Dans le terminal, lancer `claude`, puis `/login` → suivre l'OAuth (ouvrir l'URL sur ta - machine, autoriser, coller le code). Persisté dans `~/.claude` sur le PVC. -3. À l'invite, FinLab a déjà sa persona (`CLAUDE.md`) et les outils finlab (MCP `finlab`, - auto-activé). Essaie : *« résume mon portefeuille »*, *« plan de trade pour Micron »*. +1. Ouvrir **http://finance.lab.local/console/** (basicAuth), lancer `claude`, puis `/login`. +2. Le dashboard est sur **http://finance.lab.local/** (rien à configurer). -> Le workspace `/home/finlab/workspace` contient `portfolio.yaml` etc. — éditables directement -> (par toi ou par Claude à ta demande). Pour une vue rapide hors console : -> `kubectl -n ai exec deploy/finlab -- python -m finlab.cli digest`. - -## Accès depuis le terminal (sans navigateur) - -```bash -kubectl -n ai exec -it deploy/finlab -- bash -lc 'cd "$WORKSPACE" && claude' -``` +> Configs éditables dans le workspace (`portfolio.yaml`…) : depuis la console, ou +> `kubectl -n ai exec deploy/finlab -c console -- python -m finlab.cli digest`. diff --git a/k8s/apps/finlab/deployment.yaml b/k8s/apps/finlab/deployment.yaml index 3e0caec..cb1084a 100644 --- a/k8s/apps/finlab/deployment.yaml +++ b/k8s/apps/finlab/deployment.yaml @@ -16,12 +16,9 @@ spec: labels: app: finlab spec: - # Image privée sur GHCR → secret de pull créé manuellement dans le ns ai - # (cf. README : kubectl create secret docker-registry ghcr-pull ...). imagePullSecrets: - name: ghcr-pull - # Conformité PodSecurity « restricted » (namespace ai). fsGroup 1000 → le PVC NFS monté - # en /home/finlab (HOME : ~/.claude du login abonnement + workspace) est inscriptible. + # PodSecurity « restricted » (ns ai). fsGroup 1000 → PVC NFS monté en /home/finlab. securityContext: runAsNonRoot: true runAsUser: 1000 @@ -29,40 +26,72 @@ spec: fsGroup: 1000 seccompProfile: type: RuntimeDefault - containers: - - name: finlab - # Tag bumpé par la CI (build-finlab.yml) sur main. + # initContainer : prépare le workspace (seed robuste NFS) AVANT console + dashboard, + # pour qu'il soit peuplé et inscriptible quand les deux conteneurs démarrent. + initContainers: + - name: seed image: ghcr.io/alkatrazz24/funk-finlab:sha-54f6f7c + command: ["/usr/local/bin/entrypoint-seed.sh"] + securityContext: + allowPrivilegeEscalation: false + capabilities: + drop: ["ALL"] + volumeMounts: + - name: home + mountPath: /home/finlab + containers: + # ── CONSOLE : Claude Code dans un terminal web (ttyd), sous /console ── + - name: console + image: ghcr.io/alkatrazz24/funk-finlab:sha-54f6f7c + command: ["/usr/local/bin/entrypoint-console.sh"] + securityContext: + allowPrivilegeEscalation: false + capabilities: + drop: ["ALL"] + ports: + - name: console + containerPort: 7681 + readinessProbe: + tcpSocket: { port: 7681 } + initialDelaySeconds: 10 + periodSeconds: 15 + livenessProbe: + tcpSocket: { port: 7681 } + initialDelaySeconds: 30 + periodSeconds: 30 + resources: + requests: { cpu: 150m, memory: 512Mi } + limits: { cpu: "1", memory: 1536Mi } + volumeMounts: + - name: home + mountPath: /home/finlab + # ── DASHBOARD : interface web (FastAPI), graphiques + données finlab ── + - name: dashboard + image: ghcr.io/alkatrazz24/funk-finlab:sha-54f6f7c + command: + - /opt/venv/bin/uvicorn + - dashboard.server:app + - --host=0.0.0.0 + - --port=8800 securityContext: allowPrivilegeEscalation: false capabilities: drop: ["ALL"] ports: - name: web - containerPort: 7681 - env: - - name: TZ - value: "Europe/Paris" - # ttyd écoute en TCP : sonde TCP (pas de endpoint HTTP de santé dédié). Démarrage - # rapide, mais on laisse de la marge (seed du workspace au 1er boot). + containerPort: 8800 + # FINLAB_HOME/PYTHONPATH viennent de l'image ; FINLAB_HOME = workspace (configs live). readinessProbe: - tcpSocket: - port: 7681 - initialDelaySeconds: 10 + httpGet: { path: /healthz, port: 8800 } + initialDelaySeconds: 8 periodSeconds: 15 livenessProbe: - tcpSocket: - port: 7681 - initialDelaySeconds: 30 + httpGet: { path: /healthz, port: 8800 } + initialDelaySeconds: 20 periodSeconds: 30 - # Node + Claude Code peuvent consommer ; nœuds 8 Go (OOM actif) → limite raisonnable. resources: - requests: - cpu: 200m - memory: 512Mi - limits: - cpu: "1" - memory: 1536Mi + requests: { cpu: 100m, memory: 256Mi } + limits: { cpu: 500m, memory: 768Mi } volumeMounts: - name: home mountPath: /home/finlab diff --git a/k8s/apps/finlab/ingress.yaml b/k8s/apps/finlab/ingress.yaml index 6e348d9..426034b 100644 --- a/k8s/apps/finlab/ingress.yaml +++ b/k8s/apps/finlab/ingress.yaml @@ -7,11 +7,21 @@ spec: entryPoints: - web routes: - - match: Host(`finance.lab.local`) + # /console → console Claude Code (ttyd sert sous ce préfixe via -b /console). + - match: Host(`finance.lab.local`) && PathPrefix(`/console`) kind: Rule + priority: 20 middlewares: - # basicAuth : un shell web ne doit pas être ouvert sans auth (cf. middleware.yaml). - name: finlab-auth services: - name: finlab port: 7681 + # Tout le reste → dashboard web (graphiques, portefeuille, watchlists, actions). + - match: Host(`finance.lab.local`) + kind: Rule + priority: 10 + middlewares: + - name: finlab-auth + services: + - name: finlab + port: 8800 diff --git a/k8s/apps/finlab/service.yaml b/k8s/apps/finlab/service.yaml index 7f69122..12e0968 100644 --- a/k8s/apps/finlab/service.yaml +++ b/k8s/apps/finlab/service.yaml @@ -8,5 +8,8 @@ spec: app: finlab ports: - name: web + port: 8800 + targetPort: 8800 + - name: console port: 7681 targetPort: 7681 diff --git a/tools/finlab/Dockerfile b/tools/finlab/Dockerfile index 81d3363..31fe1aa 100644 --- a/tools/finlab/Dockerfile +++ b/tools/finlab/Dockerfile @@ -1,13 +1,11 @@ -# funk-finlab — console Claude Code web, spécialisée finance. -# -# Une vraie session Claude Code (sur l'ABONNEMENT de l'utilisateur, login persisté sur le -# PVC) servie dans un terminal web (ttyd), dans un workspace où vit la boîte à outils finlab -# (exposée en MCP). C'est l'esprit OpenAlice, mais c'est Claude Code + le toolkit maison — -# aucune facturation API, aucun broker, lecture/analyse uniquement. +# funk-finlab — image double usage (un seul build, deux conteneurs en déploiement) : +# • CONSOLE : session Claude Code dans un terminal web (ttyd), abonnement persisté. +# • DASHBOARD : interface web (graphiques marché / portefeuille / watchlists / actions). +# Les deux partagent le workspace persistant (PVC) où vivent les CONFIGS éditables (portefeuille, +# watchlists, alertes) ; le CODE finlab vit dans l'image (/opt/app) et se met à jour avec elle. FROM python:3.12-slim-bookworm -# Node (pour le CLI claude-code) + git + tini (PID 1 propre). ttyd n'est PAS dans les dépôts -# Debian bookworm → on récupère le binaire statique (musl) depuis les releases GitHub (pin). +# Node (CLI claude-code) + git + tini. ttyd n'est pas packagé sur bookworm → binaire statique. ARG TTYD_VERSION=1.7.7 RUN apt-get update && apt-get install -y --no-install-recommends \ curl ca-certificates git tini procps \ @@ -21,35 +19,39 @@ RUN apt-get update && apt-get install -y --no-install-recommends \ && npm cache clean --force \ && apt-get clean && rm -rf /var/lib/apt/lists/* -# Dépendances Python de finlab dans un venv dédié (séparé du workspace, donc non altérable -# depuis la session). finlab lui-même est importé depuis le workspace (PYTHONPATH au runtime). +# Dépendances Python (venv dédié, hors du workspace). COPY requirements.txt /opt/finlab/requirements.txt RUN python -m venv /opt/venv \ && /opt/venv/bin/pip install --no-cache-dir -r /opt/finlab/requirements.txt -# « Seed » du workspace : copié dans le PVC au premier boot (sans écraser les edits ensuite). -# Contient le code finlab, les configs (portefeuille/watchlists/alertes), la persona Claude -# (CLAUDE.md), le branchement MCP finlab et les réglages d'autorisation. -COPY finlab/ /opt/seed/finlab/ -COPY webapp/ /opt/seed/webapp/ +# CODE applicatif (mis à jour avec l'image) : package finlab + backend dashboard + webapp (option). +COPY finlab/ /opt/app/finlab/ +COPY dashboard/ /opt/app/dashboard/ +COPY webapp/ /opt/app/webapp/ + +# « Seed » du workspace : UNIQUEMENT ce que l'utilisateur édite/possède (pas de code). +# Configs finlab + persona Claude + branchement MCP + autorisations. Copié dans le PVC au +# 1er boot par l'initContainer (cp -rn no-clobber → edits préservés ensuite). COPY portfolio.yaml watchlists.yaml alerts.yaml /opt/seed/ COPY workspace/CLAUDE.md /opt/seed/CLAUDE.md COPY workspace/.mcp.json /opt/seed/.mcp.json COPY workspace/.claude/ /opt/seed/.claude/ -COPY entrypoint.sh /usr/local/bin/entrypoint.sh -RUN chmod +x /usr/local/bin/entrypoint.sh +COPY entrypoint-seed.sh entrypoint-console.sh /usr/local/bin/ +RUN chmod +x /usr/local/bin/entrypoint-seed.sh /usr/local/bin/entrypoint-console.sh -# uid non-root (PodSecurity « restricted » du namespace ai). HOME et workspace vivent sur le -# PVC monté en /home/finlab : ainsi ~/.claude (login abonnement) ET le workspace persistent. +# uid non-root (PodSecurity « restricted »). HOME = PVC (login abonnement + workspace). +# WORKDIR reste sur /opt/app (PAS sur le volume) : sinon le runtime crée le dossier du workspace +# en root sur le NFS avant le process → seed cassé (bug corrigé). Le seed/console font le cd. RUN useradd -m -u 1000 -s /bin/bash finlab ENV HOME=/home/finlab \ WORKSPACE=/home/finlab/workspace \ + FINLAB_HOME=/home/finlab/workspace \ PATH=/opt/venv/bin:/usr/local/bin:/usr/bin:/bin \ - PYTHONPATH=/home/finlab/workspace \ + PYTHONPATH=/opt/app \ PYTHONUNBUFFERED=1 \ TZ=Europe/Paris USER 1000 -WORKDIR /home/finlab/workspace -EXPOSE 7681 -ENTRYPOINT ["tini", "--", "/usr/local/bin/entrypoint.sh"] +WORKDIR /opt/app +# Pas d'ENTRYPOINT unique : le Deployment fixe la commande par conteneur +# (entrypoint-seed.sh en initContainer, entrypoint-console.sh + uvicorn en conteneurs). diff --git a/tools/finlab/README.md b/tools/finlab/README.md index f060db4..cce5493 100644 --- a/tools/finlab/README.md +++ b/tools/finlab/README.md @@ -18,14 +18,20 @@ Code finance** déployée in-cluster. Paper trading optionnel via **Alpaca** (ar Glu : `data` (fetch + cache), `scanner` (watchlists), `digest` (rapport compact), `alerts`, `plan` (plan de trade R:R), `cli`, `mcp_server`. -## Trois façons de l'utiliser +## Quatre façons de l'utiliser -1. **Console Claude Code web** (déploiement) — `finance.lab.local` : vraie session Claude Code - spécialisée finance, outils finlab en MCP. Voir `k8s/apps/finlab/README.md`. -2. **CLI** — `python -m finlab.cli digest|portfolio|tech|fundamentals|scan|plan|alerts`. -3. **MCP** — serveur stdio `finlab.mcp_server`, branché dans le `.mcp.json` du repo lab → outils +1. **Dashboard web** (déploiement) — `finance.lab.local/` : graphiques chandeliers, portefeuille, + watchlists, alertes, plan de trade. Backend `dashboard/server.py` (FastAPI, sans LLM). +2. **Console Claude Code web** (déploiement) — `finance.lab.local/console/` : vraie session Claude + Code spécialisée finance, outils finlab en MCP. Voir `k8s/apps/finlab/README.md`. +3. **CLI** — `python -m finlab.cli digest|portfolio|tech|fundamentals|scan|plan|alerts`. +4. **MCP** — serveur stdio `finlab.mcp_server`, branché dans le `.mcp.json` du repo lab → outils finlab dans les sessions Claude Code du lab. +> Le code lit ses configs depuis `FINLAB_HOME` si défini (déploiement : workspace persistant), +> sinon depuis le dossier du package (dév local). Le dashboard se lance en local : +> `FINLAB_HOME=$PWD PYTHONPATH=$PWD .venv/bin/uvicorn dashboard.server:app --port 8800`. + Le portefeuille se modifie dans **`portfolio.yaml`**, les watchlists dans **`watchlists.yaml`** (thèmes : `energy_power`, `chips`, `cables_optical_network`, `software_cloud`, `datacenter_infra`), les règles d'alerte dans **`alerts.yaml`**. diff --git a/tools/finlab/dashboard/__init__.py b/tools/finlab/dashboard/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tools/finlab/dashboard/server.py b/tools/finlab/dashboard/server.py new file mode 100644 index 0000000..1fd6f29 --- /dev/null +++ b/tools/finlab/dashboard/server.py @@ -0,0 +1,150 @@ +"""FinLab Dashboard — interface web (graphiques marché, portefeuille, watchlists, actions). + +Backend FastAPI exposant les données finlab en JSON. **Aucun LLM, aucune clé API** : c'est de +la donnée et de l'analyse technique pure. La partie conversationnelle/agentique vit dans la +console Claude Code (bouton « Console IA » → /console). + +Lancement : uvicorn dashboard.server:app --host 0.0.0.0 --port 8800 +""" +from __future__ import annotations + +import math +from pathlib import Path + +import pandas as pd +import yaml +from fastapi import FastAPI, Request +from fastapi.responses import FileResponse, JSONResponse +from fastapi.staticfiles import StaticFiles + +from finlab import ( + alerts, + data, + indicators as ind, + plan as plan_mod, + scanner, + technical, + tracker, +) + +app = FastAPI(title="FinLab Dashboard") +STATIC = Path(__file__).resolve().parent / "static" + + +# ── Helpers ─────────────────────────────────────────────────────────────────── +def _clean(obj): + """Rend un objet JSON-safe (NaN/inf → None, types numpy → natifs).""" + if isinstance(obj, float): + return None if (math.isnan(obj) or math.isinf(obj)) else obj + if isinstance(obj, dict): + return {k: _clean(v) for k, v in obj.items()} + if isinstance(obj, (list, tuple)): + return [_clean(v) for v in obj] + if hasattr(obj, "item"): # numpy scalar + return _clean(obj.item()) + return obj + + +def _records(df: pd.DataFrame): + return _clean(df.where(pd.notna(df), None).to_dict(orient="records")) + + +@app.exception_handler(Exception) +async def _on_error(request: Request, exc: Exception): + # Un échec ponctuel (ticker introuvable, Yahoo bridé...) ne doit pas casser le front. + return JSONResponse(status_code=500, content={"error": str(exc)}) + + +# ── API ─────────────────────────────────────────────────────────────────────── +@app.get("/healthz") +def healthz(): + return {"ok": True} + + +@app.get("/api/portfolio") +def api_portfolio(): + df, agg = tracker.build(with_sector=True) + base = agg["base"] + positions = [ + { + "ticker": r["ticker"], "secteur": r["secteur"], "qte": r["qté"], + "cours": r["cours"], "dev": r["dev"], + "valeur": r[f"valeur_{base}"], "pru": r[f"PRU_{base}"], + "pnl": r[f"P&L_{base}"], "pnl_pct": r["P&L_%"], "poids": r["poids_%"], + } + for r in df.to_dict(orient="records") + ] + return _clean({ + "base": base, "total": agg["total"], "cash": agg["cash"], + "invested": agg["invested"], "pnl_total": agg["pnl_total"], "pnl_pct": agg["pnl_pct"], + "by_sector": agg["by_sector"].to_dict(), + "positions": positions, + }) + + +@app.get("/api/themes") +def api_themes(): + themes = yaml.safe_load(open(scanner.WATCHLISTS_FILE, encoding="utf-8"))["themes"] + return {"themes": themes} + + +@app.get("/api/scan") +def api_scan(theme: str = "all", target: float = 5.0, bullish: bool = False): + df = scanner.scan_theme(theme, target, bullish_only=bullish) + return {"theme": theme, "target": target, "rows": _records(df)} + + +@app.get("/api/ohlc") +def api_ohlc(ticker: str, period: str = "6mo"): + df = data.history(ticker, period=period) + idx = [d.strftime("%Y-%m-%d") for d in df.index] + candles = [ + {"time": t, "open": round(float(o), 2), "high": round(float(h), 2), + "low": round(float(l), 2), "close": round(float(c), 2)} + for t, o, h, l, c in zip(idx, df["Open"], df["High"], df["Low"], df["Close"]) + ] + volume = [ + {"time": t, "value": float(v), "color": "#2a9d8f88" if c >= o else "#e76f5188"} + for t, v, o, c in zip(idx, df["Volume"], df["Open"], df["Close"]) + ] + + def line(series): + return [ + {"time": t, "value": round(float(x), 2)} + for t, x in zip(idx, series) if pd.notna(x) + ] + + try: + tech = technical.analyze(ticker, period="1y") + except Exception: + tech = None + return _clean({ + "ticker": ticker, "period": period, "candles": candles, "volume": volume, + "ma50": line(ind.sma(df["Close"], 50)), "ma200": line(ind.sma(df["Close"], 200)), + "technical": tech, + }) + + +@app.get("/api/alerts") +def api_alerts(watch: str = "all"): + return {"watch": watch, "hits": _clean(alerts.run(watch))} + + +@app.get("/api/plan") +def api_plan(ticker: str, capital: float = 1427.0): + p = plan_mod.plan(ticker, capital) + return _clean({"plan": p, "render": plan_mod.render(p)}) + + +# ── Statique ────────────────────────────────────────────────────────────────── +@app.get("/") +def index(): + return FileResponse(STATIC / "index.html") + + +app.mount("/static", StaticFiles(directory=STATIC), name="static") + + +if __name__ == "__main__": + import uvicorn + uvicorn.run(app, host="0.0.0.0", port=8800) diff --git a/tools/finlab/dashboard/static/index.html b/tools/finlab/dashboard/static/index.html new file mode 100644 index 0000000..2bc716c --- /dev/null +++ b/tools/finlab/dashboard/static/index.html @@ -0,0 +1,329 @@ + + + + + +FinLab — Dashboard + + + + +
+
FinLab dashboard
+
+
Portefeuille
+
P&L
+
Cash
+
Concentration
+ + ⌨ Console IA ↗ +
+
+ +
+ +
+
+

Mon portefeuille

+
Chargement…
+
+
+

Watchlists

+
+ + + +
+
+
+
+ + +
+
+

+ Graphique + + + + + + +

+
+
+ — MM50   — MM200 +
+
+
+
+ + +
+
+
+ + +
+
+

Alertes du jour

+
+
+
+

Exposition sectorielle

+
+
+
+
+ + + + + + + diff --git a/tools/finlab/entrypoint-console.sh b/tools/finlab/entrypoint-console.sh new file mode 100644 index 0000000..58da35e --- /dev/null +++ b/tools/finlab/entrypoint-console.sh @@ -0,0 +1,25 @@ +#!/usr/bin/env bash +# Conteneur CONSOLE : terminal web (ttyd) → Claude Code dans le workspace. +# Le workspace est déjà seedé par l'initContainer ; on se contente d'y entrer. +set -euo pipefail + +WS="${WORKSPACE:-/home/finlab/workspace}" +cd "$WS" + +# Mot d'accueil du shell de la console. +cat > "$HOME/.bash_motd" <<'MOTD' +──────────────────────────────────────────────────────────── + FinLab — console Claude Code (analyste de marché) + • Lance l'assistant : claude (1ʳᵉ fois : /login dans claude) + • Outils finlab (MCP) + CLI : python -m finlab.cli digest + • Dashboard graphique : retire /console de l'URL + • Tes configs (éditables) : portfolio.yaml · watchlists.yaml · alerts.yaml + ⚠ Analyse uniquement — aucun ordre réel. +──────────────────────────────────────────────────────────── +MOTD +grep -q bash_motd "$HOME/.bashrc" 2>/dev/null || \ + echo '[ -f "$HOME/.bash_motd" ] && cat "$HOME/.bash_motd"' >> "$HOME/.bashrc" + +# ttyd sert sous le préfixe /console (Traefik y route). Auth assurée en amont (basicAuth). +exec ttyd -p 7681 -W -b /console -t titleFixed='FinLab — console' \ + -t 'theme={"background":"#0b0e14"}' bash -l diff --git a/tools/finlab/entrypoint-seed.sh b/tools/finlab/entrypoint-seed.sh new file mode 100644 index 0000000..43c1656 --- /dev/null +++ b/tools/finlab/entrypoint-seed.sh @@ -0,0 +1,19 @@ +#!/usr/bin/env bash +# initContainer : prépare le workspace persistant (PVC) AVANT le démarrage des conteneurs. +# Tourne en uid 1000. Robuste au piège NFS : si le point de montage / le dossier workspace a +# été créé root et non-inscriptible (fsGroup non honoré par le provisioner NFS), on le recrée +# (le parent /home/finlab est world-writable côté provisioner). +set -euo pipefail + +WS="${WORKSPACE:-/home/finlab/workspace}" + +if [ -d "$WS" ] && [ ! -w "$WS" ]; then + echo "seed: workspace présent mais non inscriptible → recréation" + rmdir "$WS" 2>/dev/null || true +fi +mkdir -p "$WS" + +# cp no-clobber : pose les fichiers manquants (nouvelle image), préserve les edits existants. +cp -rn /opt/seed/. "$WS"/ 2>/dev/null || true + +echo "seed: OK — $(ls -A "$WS" 2>/dev/null | wc -l) entrées dans $WS" diff --git a/tools/finlab/entrypoint.sh b/tools/finlab/entrypoint.sh deleted file mode 100644 index f6b765f..0000000 --- a/tools/finlab/entrypoint.sh +++ /dev/null @@ -1,31 +0,0 @@ -#!/usr/bin/env bash -# Prépare le workspace persistant puis lance la console web (ttyd → Claude Code). -set -euo pipefail - -WS="${WORKSPACE:-$HOME/workspace}" -mkdir -p "$WS" - -# Seed du workspace depuis l'image, SANS écraser ce que l'utilisateur (ou Claude) a modifié. -# `cp -rn` = no-clobber : pose les fichiers manquants (nouvelle version d'image → nouveaux -# fichiers), préserve portfolio.yaml/edits/.cache existants sur le PVC. -cp -rn /opt/seed/. "$WS"/ 2>/dev/null || true - -cd "$WS" - -# Petit mot d'accueil dans le shell de la console. -cat > "$HOME/.bash_motd" <<'MOTD' -──────────────────────────────────────────────────────────── - FinLab — console Claude Code (analyste de marché) - • Lance l'assistant : claude - • 1ʳᵉ fois : dans claude, fais /login (abonnement, persisté) - • Outils finlab en MCP + CLI : python -m finlab.cli digest - • Ton portefeuille : portfolio.yaml (éditable ici) - ⚠ Analyse uniquement — aucun ordre réel. -──────────────────────────────────────────────────────────── -MOTD -grep -q bash_motd "$HOME/.bashrc" 2>/dev/null || echo '[ -f "$HOME/.bash_motd" ] && cat "$HOME/.bash_motd"' >> "$HOME/.bashrc" - -# ttyd : terminal web (port 7681), écriture activée. L'auth est assurée en amont par le -# middleware basicAuth de Traefik (cf. k8s/apps/finlab/middleware.yaml) — ttyd reste interne. -exec ttyd -p 7681 -W -t titleFixed='FinLab — console' -t 'theme={"background":"#0b0e14"}' \ - bash -l diff --git a/tools/finlab/finlab/alerts.py b/tools/finlab/finlab/alerts.py index 1fd6383..ea7ec42 100644 --- a/tools/finlab/finlab/alerts.py +++ b/tools/finlab/finlab/alerts.py @@ -5,6 +5,7 @@ que ce qui vient de basculer (croisement MACD, cassure MM50, survente...). """ from __future__ import annotations +import os from pathlib import Path import pandas as pd @@ -12,7 +13,8 @@ import yaml from . import data, indicators as ind, scanner -ALERTS_FILE = Path(__file__).resolve().parent.parent / "alerts.yaml" +# Config lue depuis FINLAB_HOME (workspace persistant) si défini — cf. data.ROOT. +ALERTS_FILE = Path(os.environ.get("FINLAB_HOME") or Path(__file__).resolve().parent.parent) / "alerts.yaml" def _events(symbol: str) -> dict: diff --git a/tools/finlab/finlab/data.py b/tools/finlab/finlab/data.py index 1168807..b61bd0d 100644 --- a/tools/finlab/finlab/data.py +++ b/tools/finlab/finlab/data.py @@ -6,6 +6,7 @@ pour que tous les outils partagent la même source. from __future__ import annotations import functools +import os import pickle import time from pathlib import Path @@ -14,7 +15,10 @@ import pandas as pd import yaml import yfinance as yf -ROOT = Path(__file__).resolve().parent.parent +# FINLAB_HOME découple le CODE (package) de la DONNÉE (configs éditables + cache). En +# déploiement, le code vit dans l'image (/opt/app) et les configs dans le workspace persistant +# (PVC). Non défini → repli sur le dossier parent du package (dév local inchangé). +ROOT = Path(os.environ.get("FINLAB_HOME") or Path(__file__).resolve().parent.parent) PORTFOLIO_FILE = ROOT / "portfolio.yaml" CACHE_DIR = ROOT / ".cache" diff --git a/tools/finlab/finlab/digest.py b/tools/finlab/finlab/digest.py index cc9284e..42e15e3 100644 --- a/tools/finlab/finlab/digest.py +++ b/tools/finlab/finlab/digest.py @@ -7,11 +7,13 @@ alertes déclenchées. Écrit dans reports/ et renvoyé comme texte court. from __future__ import annotations import datetime as dt +import os from pathlib import Path from . import alerts, data, scanner, tracker -REPORTS_DIR = Path(__file__).resolve().parent.parent / "reports" +# Écrit dans FINLAB_HOME/reports si défini (workspace inscriptible) — cf. data.ROOT. +REPORTS_DIR = Path(os.environ.get("FINLAB_HOME") or Path(__file__).resolve().parent.parent) / "reports" def build(theme: str = "all", target_pct: float = 5.0, top: int = 6) -> str: diff --git a/tools/finlab/finlab/scanner.py b/tools/finlab/finlab/scanner.py index d4274eb..1800e3e 100644 --- a/tools/finlab/finlab/scanner.py +++ b/tools/finlab/finlab/scanner.py @@ -10,6 +10,7 @@ faire +8% est tout aussi capable de faire -8%. from __future__ import annotations import math +import os from pathlib import Path import pandas as pd @@ -17,7 +18,8 @@ import yaml from . import data, indicators as ind -WATCHLISTS_FILE = Path(__file__).resolve().parent.parent / "watchlists.yaml" +# Config lue depuis FINLAB_HOME (workspace persistant) si défini — cf. data.ROOT. +WATCHLISTS_FILE = Path(os.environ.get("FINLAB_HOME") or Path(__file__).resolve().parent.parent) / "watchlists.yaml" def load_theme(theme: str | None = None) -> list[str]: diff --git a/tools/finlab/workspace/.mcp.json b/tools/finlab/workspace/.mcp.json index e38103a..1da2edc 100644 --- a/tools/finlab/workspace/.mcp.json +++ b/tools/finlab/workspace/.mcp.json @@ -4,7 +4,8 @@ "command": "/opt/venv/bin/python", "args": ["-m", "finlab.mcp_server"], "env": { - "PYTHONPATH": "/home/finlab/workspace" + "PYTHONPATH": "/opt/app", + "FINLAB_HOME": "/home/finlab/workspace" } } }