mirror of
https://github.com/Alkatrazz24/Funk-lab.git
synced 2026-07-08 21:54:42 +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>
149 lines
7.1 KiB
Python
149 lines
7.1 KiB
Python
"""FinLab Web — chat Claude (Opus) spécialisé finance, branché sur les outils finlab.
|
|
|
|
Modèle : claude-opus-4-8 (API Anthropic), thinking adaptatif, streaming, tool-use.
|
|
Connexion : ANTHROPIC_API_KEY en direct, ou ANTHROPIC_BASE_URL pour passer par
|
|
le proxy LiteLLM du lab (ex: http://192.168.10.1:4000).
|
|
|
|
Lancement : .venv/bin/python -m webapp.server (puis http://localhost:8800)
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
import json
|
|
import sys
|
|
from pathlib import Path
|
|
|
|
import anthropic
|
|
from fastapi import FastAPI
|
|
from fastapi.responses import FileResponse, StreamingResponse
|
|
from fastapi.staticfiles import StaticFiles
|
|
from pydantic import BaseModel
|
|
|
|
ROOT = Path(__file__).resolve().parent.parent
|
|
sys.path.insert(0, str(ROOT)) # rend le package finlab importable
|
|
|
|
from finlab import alerts, data, fundamental, plan as plan_mod, scanner, technical, tracker # noqa: E402
|
|
|
|
MODEL = "claude-opus-4-8"
|
|
SOUL = (Path(__file__).resolve().parent / "finance_soul.md").read_text(encoding="utf-8")
|
|
client = anthropic.Anthropic() # lit ANTHROPIC_API_KEY / ANTHROPIC_BASE_URL de l'env
|
|
|
|
# ── Outils exposés à Claude (mappés sur finlab) ───────────────────────────────
|
|
TOOLS = [
|
|
{"name": "digest", "description": "Rapport compact tout-en-un : portefeuille + opportunités haussières filtrées + alertes du jour. À privilégier pour une vue d'ensemble.",
|
|
"input_schema": {"type": "object", "properties": {
|
|
"theme": {"type": "string", "description": "thème de watchlist, 'all' ou 'portfolio'"},
|
|
"target_pct": {"type": "number", "description": "cible de perf hebdo en %"}}}},
|
|
{"name": "portfolio_summary", "description": "Valorisation du portefeuille réel : valeur, P&L, poids, exposition sectorielle, concentration.",
|
|
"input_schema": {"type": "object", "properties": {}}},
|
|
{"name": "opportunities", "description": "Scanner d'opportunités d'un thème : capacité de mouvement (volatilité) + biais directionnel. Thèmes : energy_power, chips, cables_optical_network, software_cloud, datacenter_infra, 'all', 'portfolio'.",
|
|
"input_schema": {"type": "object", "properties": {
|
|
"theme": {"type": "string"}, "target_pct": {"type": "number"},
|
|
"bullish_only": {"type": "boolean"}}}},
|
|
{"name": "technical_analysis", "description": "Indicateurs techniques (RSI, MACD, MM50/200, Bollinger, ATR) + signaux pour une liste de tickers.",
|
|
"input_schema": {"type": "object", "properties": {
|
|
"tickers": {"type": "array", "items": {"type": "string"}},
|
|
"period": {"type": "string", "description": "ex: 6mo, 1y"}}, "required": ["tickers"]}},
|
|
{"name": "fundamentals", "description": "Ratios fondamentaux (PER, PEG, marges, ROE, croissance, dette, cible analystes) pour une liste de tickers.",
|
|
"input_schema": {"type": "object", "properties": {
|
|
"tickers": {"type": "array", "items": {"type": "string"}}}, "required": ["tickers"]}},
|
|
{"name": "trade_plan", "description": "Plan de trade chiffré : entrée, stop ATR, objectifs +5/+10%, taille de position et ratio risque/rendement (R:R). Le verdict R:R est central.",
|
|
"input_schema": {"type": "object", "properties": {
|
|
"ticker": {"type": "string"}, "capital": {"type": "number", "description": "capital à déployer (devise du titre)"}},
|
|
"required": ["ticker"]}},
|
|
{"name": "alerts_today", "description": "Événements du jour : croisements MACD, cassures MM50, survente. watch = thème, 'all' ou 'portfolio'.",
|
|
"input_schema": {"type": "object", "properties": {"watch": {"type": "string"}}}},
|
|
{"name": "price", "description": "Dernier cours d'un titre dans sa devise native.",
|
|
"input_schema": {"type": "object", "properties": {"ticker": {"type": "string"}}, "required": ["ticker"]}},
|
|
]
|
|
|
|
|
|
def execute(name: str, args: dict) -> str:
|
|
"""Exécute un outil finlab et renvoie du texte (jamais d'exception remontée à Claude)."""
|
|
try:
|
|
if name == "digest":
|
|
from finlab import digest as digest_mod
|
|
return digest_mod.build(args.get("theme", "all"), args.get("target_pct", 5.0))
|
|
if name == "portfolio_summary":
|
|
return tracker.report()
|
|
if name == "opportunities":
|
|
df = scanner.scan_theme(args.get("theme", "all"), args.get("target_pct", 5.0),
|
|
bullish_only=args.get("bullish_only", False))
|
|
return df.to_string(index=False)
|
|
if name == "technical_analysis":
|
|
return technical.scan(args["tickers"], args.get("period", "1y")).to_string(index=False)
|
|
if name == "fundamentals":
|
|
return fundamental.compare(args["tickers"]).to_string(index=False)
|
|
if name == "trade_plan":
|
|
return plan_mod.render(plan_mod.plan(args["ticker"], args.get("capital", 1427.0)))
|
|
if name == "alerts_today":
|
|
return alerts.render(alerts.run(args.get("watch")))
|
|
if name == "price":
|
|
p, cur = data.last_price(args["ticker"])
|
|
return f"{args['ticker']}: {p:.2f} {cur}"
|
|
return f"Outil inconnu: {name}"
|
|
except Exception as e:
|
|
return f"Erreur outil {name}: {e}"
|
|
|
|
|
|
# ── API ───────────────────────────────────────────────────────────────────────
|
|
app = FastAPI(title="FinLab")
|
|
STATIC = Path(__file__).resolve().parent / "static"
|
|
|
|
|
|
class ChatRequest(BaseModel):
|
|
messages: list # [{role, content}]
|
|
|
|
|
|
def _sse(event: str, data) -> str:
|
|
return f"data: {json.dumps({'event': event, 'data': data})}\n\n"
|
|
|
|
|
|
def agent_stream(messages: list):
|
|
"""Boucle agentique : streame le texte, exécute les outils, recommence si besoin."""
|
|
system = [{"type": "text", "text": SOUL, "cache_control": {"type": "ephemeral"}}]
|
|
while True:
|
|
with client.messages.stream(
|
|
model=MODEL,
|
|
max_tokens=16000,
|
|
thinking={"type": "adaptive"},
|
|
output_config={"effort": "high"},
|
|
system=system,
|
|
tools=TOOLS,
|
|
messages=messages,
|
|
) as stream:
|
|
for text in stream.text_stream:
|
|
yield _sse("delta", text)
|
|
final = stream.get_final_message()
|
|
|
|
messages.append({"role": "assistant", "content": final.content})
|
|
if final.stop_reason != "tool_use":
|
|
break
|
|
|
|
results = []
|
|
for block in final.content:
|
|
if block.type == "tool_use":
|
|
yield _sse("tool", block.name)
|
|
results.append({"type": "tool_result", "tool_use_id": block.id,
|
|
"content": execute(block.name, block.input)})
|
|
messages.append({"role": "user", "content": results})
|
|
|
|
yield _sse("done", None)
|
|
|
|
|
|
@app.post("/api/chat")
|
|
def chat(req: ChatRequest):
|
|
return StreamingResponse(agent_stream(req.messages), media_type="text/event-stream")
|
|
|
|
|
|
@app.get("/")
|
|
def index():
|
|
return FileResponse(STATIC / "index.html")
|
|
|
|
|
|
app.mount("/static", StaticFiles(directory=STATIC), name="static")
|
|
|
|
|
|
if __name__ == "__main__":
|
|
import uvicorn
|
|
print("FinLab → http://localhost:8800")
|
|
uvicorn.run(app, host="0.0.0.0", port=8800)
|