mirror of
https://github.com/Alkatrazz24/Funk-lab.git
synced 2026-07-10 18:34: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>
This commit is contained in:
parent
35ad1deb64
commit
b649afd059
39 changed files with 2110 additions and 2 deletions
149
tools/finlab/webapp/server.py
Normal file
149
tools/finlab/webapp/server.py
Normal file
|
|
@ -0,0 +1,149 @@
|
|||
"""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)
|
||||
Loading…
Add table
Add a link
Reference in a new issue