205 lines
7.0 KiB
Python
205 lines
7.0 KiB
Python
"""
|
|
TradingAgents Web API Server
|
|
FastAPI wrapper for the TradingAgents multi-agent trading framework.
|
|
"""
|
|
import os
|
|
import json
|
|
import datetime
|
|
import threading
|
|
from pathlib import Path
|
|
|
|
from dotenv import load_dotenv
|
|
load_dotenv()
|
|
|
|
from fastapi import FastAPI, HTTPException, BackgroundTasks, Request, Response, Depends
|
|
from fastapi.middleware.cors import CORSMiddleware
|
|
from pydantic import BaseModel
|
|
from typing import Optional
|
|
import uvicorn
|
|
|
|
from jose import jwt, JWTError
|
|
|
|
from tradingagents.graph.trading_graph import TradingAgentsGraph
|
|
from tradingagents.default_config import DEFAULT_CONFIG
|
|
|
|
# ── SSO 配置(与 SSO 中心共用)──────────────────
|
|
SECRET_KEY = "your-secret-key-change-in-production"
|
|
ALGORITHM = "HS256"
|
|
SSO_CENTER_URL = "http://129.211.173.87:9000"
|
|
COOKIE_DOMAIN = "129.211.173.87"
|
|
|
|
app = FastAPI(
|
|
title="TradingAgents API",
|
|
description="Multi-Agent LLM Financial Trading Framework",
|
|
version="1.0.0",
|
|
)
|
|
|
|
app.add_middleware(
|
|
CORSMiddleware,
|
|
allow_origins=["*"],
|
|
allow_credentials=True,
|
|
allow_methods=["*"],
|
|
allow_headers=["*"],
|
|
)
|
|
|
|
# ── 全局任务存储 ──────────────────────────────────
|
|
tasks_lock = threading.Lock()
|
|
tasks_store = {} # task_id -> {status, result, error, ...}
|
|
TASKS_DIR = Path(__file__).parent / "logs" / "tasks"
|
|
TASKS_DIR.mkdir(parents=True, exist_ok=True)
|
|
|
|
# 启动时从磁盘加载已有任务
|
|
def _load_tasks_from_disk():
|
|
for f in sorted(TASKS_DIR.glob("*.json")):
|
|
try:
|
|
data = json.loads(f.read_text())
|
|
tid = f.stem
|
|
# 服务重启后,running/pending 任务已丢失后台线程,标记为失败
|
|
if data.get("status") in ("running", "pending"):
|
|
data["status"] = "failed"
|
|
data["error"] = "服务重启,分析中断,请重新提交"
|
|
f.write_text(json.dumps(data, ensure_ascii=False, indent=2))
|
|
tasks_store[tid] = data
|
|
except Exception:
|
|
pass
|
|
|
|
_load_tasks_from_disk()
|
|
|
|
|
|
def _save_task_to_disk(task_id: str):
|
|
"""将任务写入磁盘 JSON 文件"""
|
|
with tasks_lock:
|
|
data = tasks_store.get(task_id)
|
|
if data:
|
|
(TASKS_DIR / f"{task_id}.json").write_text(json.dumps(data, ensure_ascii=False, indent=2))
|
|
|
|
|
|
# ── SSO 认证 ─────────────────────────────────────
|
|
async def require_sso(request: Request) -> str:
|
|
"""从 cookie 中验证 JWT,返回用户名"""
|
|
token = request.cookies.get("access_token")
|
|
if not token:
|
|
raise HTTPException(401, detail="未登录")
|
|
try:
|
|
payload = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM])
|
|
username = payload.get("sub")
|
|
if not username:
|
|
raise HTTPException(401, detail="无效令牌")
|
|
return username
|
|
except JWTError:
|
|
raise HTTPException(401, detail="令牌已过期或无效")
|
|
|
|
|
|
# ── 请求/响应模型 ──────────────────────────────────
|
|
class AnalysisRequest(BaseModel):
|
|
ticker: str
|
|
date: Optional[str] = None # YYYY-MM-DD, defaults to today
|
|
analysts: Optional[list[str]] = ["market", "news"]
|
|
max_debate_rounds: Optional[int] = 1
|
|
output_language: Optional[str] = "Chinese" # "Chinese" or "English"
|
|
|
|
|
|
# ── 后台分析任务 ──────────────────────────────────
|
|
def run_analysis_task(task_id: str, ticker: str, date: str, analysts: list, max_debate_rounds: int, output_language: str = "English"):
|
|
config = DEFAULT_CONFIG.copy()
|
|
config["max_debate_rounds"] = max_debate_rounds
|
|
config["output_language"] = output_language
|
|
|
|
try:
|
|
with tasks_lock:
|
|
tasks_store[task_id]["status"] = "running"
|
|
_save_task_to_disk(task_id)
|
|
|
|
graph = TradingAgentsGraph(selected_analysts=analysts, debug=False, config=config)
|
|
state, decision = graph.propagate(ticker, date)
|
|
|
|
with tasks_lock:
|
|
tasks_store[task_id]["status"] = "completed"
|
|
tasks_store[task_id]["result"] = {
|
|
"ticker": ticker,
|
|
"date": date,
|
|
"decision": decision,
|
|
"reports": {
|
|
k: v for k, v in state.items()
|
|
if k.endswith("_report") or k in ("investment_plan", "trader_investment_plan", "final_trade_decision")
|
|
},
|
|
}
|
|
except Exception as e:
|
|
with tasks_lock:
|
|
tasks_store[task_id]["status"] = "failed"
|
|
tasks_store[task_id]["error"] = str(e)
|
|
finally:
|
|
_save_task_to_disk(task_id)
|
|
|
|
|
|
# ── Web 页面 ──────────────────────────────────────
|
|
WEB_HTML = Path(__file__).parent / "web.html"
|
|
|
|
|
|
@app.get("/")
|
|
def index():
|
|
from fastapi.responses import HTMLResponse
|
|
return HTMLResponse(WEB_HTML.read_text("utf-8"))
|
|
|
|
|
|
@app.get("/api/health")
|
|
def health():
|
|
return {"status": "ok", "time": datetime.datetime.now().isoformat()}
|
|
|
|
|
|
@app.post("/api/analyze")
|
|
def analyze(req: AnalysisRequest, background_tasks: BackgroundTasks, username: str = Depends(require_sso)):
|
|
date = req.date or datetime.date.today().isoformat()
|
|
task_id = f"{req.ticker}_{datetime.datetime.now().strftime('%Y%m%d%H%M%S')}"
|
|
|
|
with tasks_lock:
|
|
tasks_store[task_id] = {
|
|
"task_id": task_id,
|
|
"status": "pending",
|
|
"ticker": req.ticker,
|
|
"date": date,
|
|
"result": None,
|
|
"error": None,
|
|
"created_at": datetime.datetime.now().isoformat(),
|
|
}
|
|
_save_task_to_disk(task_id)
|
|
|
|
background_tasks.add_task(
|
|
run_analysis_task, task_id, req.ticker, date, req.analysts, req.max_debate_rounds, req.output_language
|
|
)
|
|
return {"task_id": task_id, "status": "pending", "message": "Analysis started"}
|
|
|
|
|
|
@app.get("/api/task/{task_id}")
|
|
def get_task(task_id: str):
|
|
with tasks_lock:
|
|
task = tasks_store.get(task_id)
|
|
if task:
|
|
return task
|
|
# 回退到磁盘(服务重启后内存丢失,但磁盘还在)
|
|
disk_file = TASKS_DIR / f"{task_id}.json"
|
|
if disk_file.exists():
|
|
try:
|
|
return json.loads(disk_file.read_text())
|
|
except Exception:
|
|
pass
|
|
raise HTTPException(status_code=404, detail="Task not found")
|
|
|
|
|
|
# ── SSO 用户接口 ──────────────────────────────────
|
|
@app.get("/api/user")
|
|
async def api_user(username: str = Depends(require_sso)):
|
|
"""获取当前登录用户信息"""
|
|
return {"username": username, "logged_in": True}
|
|
|
|
|
|
@app.post("/api/logout")
|
|
def api_logout(response: Response):
|
|
"""清除登录 cookie"""
|
|
response.delete_cookie("access_token", domain=COOKIE_DOMAIN, path="/")
|
|
return {"success": True}
|
|
|
|
|
|
if __name__ == "__main__":
|
|
uvicorn.run(app, host="0.0.0.0", port=8500)
|