diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..5c2dd22 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,260 @@ +# AGENTS.md — TradingAgentsWeb + +This file describes the repository structure, architecture, build/run commands, coding conventions, and guidelines for AI agents (Copilot, Codex, etc.) working in this codebase. + +--- + +## Repository Overview + +**TradingAgentsWeb** is a full-stack web application that wraps the TradingAgents multi-agent quantitative analysis framework. It extends the original CLI-only framework into a modern web platform supporting US, Hong Kong, and A-share markets. + +- **Backend**: FastAPI + SQLAlchemy + JWT authentication (Python 3.10+) +- **Frontend**: Next.js 15 (App Router) + React 19 + Tailwind CSS +- **AI Core**: LangChain / LangGraph multi-agent trading analysis graph +- **Data Sources**: akshare, yfinance, baostock, tushare, alpha_vantage, EODHD, Finnhub +- **Database**: SQLite (default) or MySQL/PostgreSQL (production) +- **Deployment**: Docker + docker-compose (Nginx reverse proxy for frontend → backend `/api`) + +--- + +## Directory Structure + +``` +TradingAgentsWeb/ +├── tradingagents/ # Core AI framework (multi-agent graph engine) +│ ├── agents/ # Agent implementations +│ │ ├── analysts/ # Market, fundamentals, news, social media analysts +│ │ ├── managers/ # Research & risk managers +│ │ ├── researchers/ # Bull & bear researchers +│ │ ├── risk_mgmt/ # Conservative / neutral / aggressive debaters +│ │ └── trader/ # Trader agent +│ ├── dataflows/ # Data source adapters (akshare, yfinance, etc.) +│ ├── graph/ # LangGraph trading graph (trading_graph.py, etc.) +│ └── default_config.py # Market/tool/data vendor priority config +│ +├── web/ +│ ├── backend/ # FastAPI application +│ │ ├── app.py # Main entrypoint (lifespan, CORS, middleware, routes) +│ │ ├── models.py # SQLAlchemy ORM models +│ │ ├── database.py # DB session / init_db +│ │ ├── schemas.py # Pydantic request/response schemas +│ │ ├── auth.py / auth_routes.py # JWT auth helpers and routes +│ │ ├── routes/ # Route modules (analysis, config, task, websocket, export, …) +│ │ ├── services/ # Business logic (task_executor, llm_config_resolver, …) +│ │ ├── migrations/ # DB migration scripts +│ │ └── tests/ # Backend integration tests +│ │ +│ └── frontend/ # Next.js 15 App Router application +│ ├── src/app/ # Pages & route groups (analysis, auth, history, profile, …) +│ ├── src/components/ # React components (analysis, auth, common, profile, ui, …) +│ ├── src/hooks/ # Custom React hooks +│ ├── src/lib/ # API client, type definitions, utilities +│ └── src/types/ # TypeScript type definitions +│ +├── tests/ # Root-level test scripts (integration / demo / verify) +├── docs/ # Documentation and supplementary guides +├── db/ # Database files (SQLite, migrations) +├── devops/ # CI/CD, deployment scripts +├── .env.example # Environment variable template +├── docker-compose.yml # Production compose file +├── Makefile # Docker management shortcuts +├── pyproject.toml # Python project metadata and dependencies +└── requirements.txt # Python package list +``` + +--- + +## Getting Started + +### Prerequisites + +| Tool | Version | +|------|---------| +| Python | 3.10+ | +| Node.js | 18+ | +| npm / pnpm / yarn | any | +| Docker & docker-compose | optional, for container mode | + +### 1. Clone & configure + +```bash +git clone https://github.com/BSTester/TradingAgentsWeb.git +cd TradingAgentsWeb +cp .env.example .env +# Edit .env and fill in API keys as needed +``` + +### 2. Backend setup + +```bash +python -m venv .venv +source .venv/bin/activate # Windows: .\.venv\Scripts\activate +pip install -r requirements.txt +pip install -e . +``` + +### 3. Frontend setup + +```bash +cd web/frontend +npm install +``` + +### 4. Run in development mode + +```bash +# Terminal 1 — backend (port 8000) +python web/backend/app.py + +# Terminal 2 — frontend (port 3000) +cd web/frontend && npm run dev +``` + +### 5. Docker (production) + +```bash +make init # copies .env.example → .env +make build # docker-compose build +make up # docker-compose up -d +# Frontend: http://localhost:8000 +# Backend API: http://localhost:8080 +``` + +--- + +## Environment Variables + +Key variables from `.env.example`: + +| Variable | Description | Default | +|----------|-------------|---------| +| `DATABASE_URL` | SQLite or MySQL connection string | `sqlite+aiosqlite:///./db/tradingagents.db` | +| `SECRET_KEY` | JWT signing key | *(required)* | +| `LLM_PROVIDER` | Default LLM provider (`openai`, `anthropic`, etc.) | `openai` | +| `OPENAI_API_KEY` | OpenAI API key | — | +| `OPENAI_BASE_URL` | OpenAI-compatible base URL | `https://api.openai.com/v1` | +| `DEEP_THINK_LLM` | Model for deep reasoning agents | — | +| `QUICK_THINK_LLM` | Model for fast/utility agents | — | +| `ALPHA_VANTAGE_API_KEY` | Alpha Vantage market data key | — | +| `XUEQIU_TOKEN` | Xueqiu cookie token (A/HK/US stock data) | — | +| `SMTP_HOST` / `SMTP_*` | Email notification settings | — | +| `TASK_MONITOR_LEADER_PORT` | Leader-election port for multi-process mode | `8001` | + +--- + +## Build & Test Commands + +### Backend (Python) + +```bash +# Run all backend tests +cd tests && python -m pytest . + +# Run a specific test file +python tests/test_llm_config_resolver.py + +# Type-check (optional) +mypy web/backend/ +``` + +### Frontend (Next.js) + +```bash +cd web/frontend + +npm run dev # Development server (port 3000) +npm run build # Production build +npm run start # Start production server +npm run lint # ESLint +npm run typecheck # TypeScript type-check (tsc --noEmit) +npm run test # Run Vitest tests (watch mode) +npm run test:run # Run Vitest tests (CI mode, single pass) +``` + +--- + +## Architecture Notes + +### Multi-Agent Graph (`tradingagents/`) + +The AI core is a **LangGraph-based directed graph** that orchestrates agents in sequence: + +1. **Analysts**: Market, Fundamentals, News, Social Media analysts collect and process data +2. **Researchers**: Bull and Bear researchers form opposing hypotheses +3. **Risk Management**: Conservative / Neutral / Aggressive debaters assess risk +4. **Trader**: Synthesizes recommendations into a final trading decision + +Market detection is automatic from the ticker symbol: +- 6-digit codes → A-share (CN) — primary: akshare, fallback: baostock, yfinance +- 4–5 digit codes / `.HK` suffix → HK stock — primary: akshare, fallback: yfinance +- Letter codes (e.g. `AAPL`) → US stock — primary: akshare, fallback: yfinance, alpha_vantage + +### Backend (`web/backend/`) + +- `app.py` — FastAPI app with lifespan hooks, CORS, logging middleware, and route registration +- `TaskManager` — thread-pool task queue with per-user queuing, stall detection (60 s), and WebSocket real-time log streaming +- Authentication — JWT (access + refresh tokens); first registered user becomes admin +- Routes registered under `/api/*`; WebSocket at `/ws/{task_id}` +- Database initializes automatically on startup (`init_db()` in lifespan) + +### Frontend (`web/frontend/`) + +- **Next.js 15 App Router** with route groups: `(auth)`, `analysis`, `history`, `profile`, `admin`, `scheduled-tasks` +- API calls go through `src/lib/api.ts` which wraps `fetch` with JWT token injection +- Real-time progress uses a WebSocket hook in `src/hooks/` +- Result export supports PDF, Markdown, and JSON + +--- + +## Coding Conventions + +### Python + +- Python 3.10+ type hints on all public functions +- Async SQLAlchemy sessions (`AsyncSession`) for database access +- Route handlers in `web/backend/routes/`, business logic in `web/backend/services/` +- Pydantic v2 schemas in `schemas.py` for request/response validation +- Do not store secrets or keys in source code; use environment variables + +### TypeScript / React + +- Strict TypeScript — always run `npm run typecheck` before committing frontend changes +- React Server Components where possible; use `"use client"` only when interactivity is required +- Tailwind CSS for all styling; avoid inline styles +- API response types defined in `src/lib/types.ts` and `src/types/` +- List endpoints that return paginated data follow the `{ data: T[], meta: { total, page, … } }` shape + +### Git + +- Branch naming: `feature/`, `fix/`, `agent/`, `perf/` +- Commit messages: follow Conventional Commits (`feat:`, `fix:`, `chore:`, `docs:`, etc.) +- Keep PRs focused; one feature or fix per PR + +--- + +## Key API Endpoints + +| Method | Path | Description | +|--------|------|-------------| +| POST | `/api/auth/register` | Register new user | +| POST | `/api/auth/login` | Login (returns JWT) | +| GET | `/api/auth/me` | Current user info | +| POST | `/api/auth/refresh` | Refresh access token | +| POST | `/api/analyze` | Start a new analysis (protected) | +| GET | `/api/analysis/{id}/status` | Poll analysis status | +| GET | `/api/analysis/{id}/results` | Fetch analysis results | +| GET | `/api/analyses` | List current user's analyses | +| GET | `/api/config` | Available models, analysts, depths | +| WS | `/ws/{task_id}` | Real-time log/progress stream | +| GET | `/api/export/{id}/{format}` | Export results (pdf/md/json) | + +--- + +## Common Tasks for AI Agents + +- **Add a new analyst type**: Create a new file in `tradingagents/agents/analysts/`, register it in `tradingagents/graph/trading_graph.py`, and expose a toggle via `GET /api/config`. +- **Add a new data vendor**: Implement the adapter in `tradingagents/dataflows/`, register it in `tradingagents/default_config.py` under `data_vendors` / `market_vendors`. +- **Add a new API route**: Create a route file in `web/backend/routes/`, import and register it in `app.py`. +- **Add a new frontend page**: Create a folder under `web/frontend/src/app/`, following the Next.js App Router convention. +- **Change the DB schema**: Add a new model or field in `web/backend/models.py`, then add a migration script in `web/backend/migrations/`. +- **Update environment config**: Edit `.env.example` and document the new variable in this file and in `README.md`. diff --git a/tests/test_llm_provider_bootstrap.py b/tests/test_llm_provider_bootstrap.py new file mode 100644 index 0000000..7861a6d --- /dev/null +++ b/tests/test_llm_provider_bootstrap.py @@ -0,0 +1,69 @@ +"""Regression coverage for fresh-database LLM provider bootstrapping.""" + +import os +import subprocess +import sys +import tempfile +import unittest +from pathlib import Path + + +PROJECT_ROOT = Path(__file__).resolve().parents[1] + + +class LLMProviderBootstrapTests(unittest.TestCase): + def test_empty_sqlite_bootstrap_seeds_providers_with_is_default(self): + """Startup migrations must seed a fresh ORM-created SQLite database.""" + with tempfile.TemporaryDirectory() as tmpdir: + database_path = Path(tmpdir) / "bootstrap.db" + environment = os.environ.copy() + environment["DATABASE_URL"] = f"sqlite+aiosqlite:///{database_path}" + script = """ +from sqlalchemy import create_engine, text + +from web.backend.database import init_db_sync +from web.backend.migrations.auto_migrate import auto_migrate + +init_db_sync() +_, failed, _ = auto_migrate(verbose=False) +assert failed == 0, f\"fresh bootstrap migration failures: {{failed}}\" + +engine = create_engine("sqlite:///{database_path}") +with engine.connect() as connection: + providers = connection.execute( + text("SELECT provider_name, is_active, is_default FROM llm_providers ORDER BY provider_name") + ).mappings().all() + openai_model_types = connection.execute( + text( + "SELECT model_type FROM llm_models " + "JOIN llm_providers ON llm_providers.id = llm_models.provider_id " + "WHERE llm_providers.provider_name = 'openai'" + ) + ).scalars().all() + +assert {{provider["provider_name"] for provider in providers}} == {{ + "anthropic", "custom", "deepseek", "openai" +}} +assert next(provider for provider in providers if provider["provider_name"] == "openai")["is_active"] in (1, True) +assert all(provider["is_default"] in (0, False) for provider in providers) +assert {{"shallow_thinker", "deep_thinker"}} <= set(openai_model_types) +""".format(database_path=database_path) + + result = subprocess.run( + [sys.executable, "-c", script], + cwd=PROJECT_ROOT, + env=environment, + text=True, + capture_output=True, + timeout=60, + ) + + self.assertEqual( + result.returncode, + 0, + msg=f"stdout:\n{result.stdout}\nstderr:\n{result.stderr}", + ) + + +if __name__ == "__main__": + unittest.main() diff --git a/web/backend/migrations/add_llm_providers_models.py b/web/backend/migrations/add_llm_providers_models.py index 44a6ef9..86712fc 100644 --- a/web/backend/migrations/add_llm_providers_models.py +++ b/web/backend/migrations/add_llm_providers_models.py @@ -35,6 +35,7 @@ def upgrade(): Column('base_url', String(500), nullable=True, comment='API基础URL'), Column('description', Text, nullable=True, comment='供应商描述'), Column('is_active', Boolean, default=True, nullable=False, index=True, comment='是否启用'), + Column('is_default', Boolean, default=False, nullable=False, index=True, comment='是否为系统默认供应商'), Column('config_json', JSON, nullable=True, comment='额外配置参数(JSON格式)'), Column('created_at', DateTime(timezone=True), server_default=func.now(), nullable=False), Column('updated_at', DateTime(timezone=True), server_default=func.now(), onupdate=func.now(), nullable=False), @@ -93,6 +94,7 @@ def insert_default_data(): 'base_url': 'https://api.openai.com/v1', 'description': 'OneInfinity OpenAI兼容模型', 'is_active': True, + 'is_default': False, }, { 'provider_name': 'anthropic', @@ -100,6 +102,7 @@ def insert_default_data(): 'base_url': 'https://api.anthropic.com/v1', 'description': 'Anthropic Claude系列模型', 'is_active': True, + 'is_default': False, }, { 'provider_name': 'deepseek', @@ -107,6 +110,7 @@ def insert_default_data(): 'base_url': 'https://api.deepseek.com/v1', 'description': 'DeepSeek系列模型', 'is_active': True, + 'is_default': False, }, { 'provider_name': 'custom', @@ -114,6 +118,7 @@ def insert_default_data(): 'base_url': '', 'description': '自定义LLM服务供应商', 'is_active': True, + 'is_default': False, } ] @@ -122,8 +127,8 @@ def insert_default_data(): result = conn.execute( text(""" INSERT INTO llm_providers - (provider_name, display_name, base_url, description, is_active) - VALUES (:provider_name, :display_name, :base_url, :description, :is_active) + (provider_name, display_name, base_url, description, is_active, is_default) + VALUES (:provider_name, :display_name, :base_url, :description, :is_active, :is_default) """), provider ) diff --git a/web/backend/routes/scheduled_task_routes.py b/web/backend/routes/scheduled_task_routes.py index 30cdd17..250a620 100644 --- a/web/backend/routes/scheduled_task_routes.py +++ b/web/backend/routes/scheduled_task_routes.py @@ -66,12 +66,15 @@ async def _task_payload(db: AsyncSession, task: ScheduledTask) -> dict: "ticker": task.ticker, "market": task.market, "is_enabled": task.is_enabled, + "status": task.status, "execution_cycle": _cycle_for_contract(task.execution_cycle, task.interval_days), "execution_time": task.execution_time, "interval_days": task.interval_days, + "day_of_week": task.day_of_week, "end_date": task.end_date.date().isoformat() if task.end_date else None, "next_run": _iso(task.next_run_time), "last_run": _iso(task.last_run_time), + "total_executions": task.total_executions, "last_report": await _last_report(db, task), "analysts": task.analysts, "research_depth": task.research_depth, @@ -225,6 +228,10 @@ async def scheduled_task_stats( AnalysisRecord.user_id == current_user.id, AnalysisRecord.status.in_(["error", "interrupted"]), )) + completed_result = await db.execute(select(func.count(ScheduledTask.id)).where( + ScheduledTask.user_id == current_user.id, + ScheduledTask.status == "completed", + )) today = datetime.utcnow().date().isoformat() today_result = await db.execute(select(func.count(ScheduledTask.id)).where( ScheduledTask.user_id == current_user.id, @@ -236,6 +243,7 @@ async def scheduled_task_stats( "paused": paused_result.scalar() or 0, "scheduled_today": today_result.scalar() or 0, "failed": failed_result.scalar() or 0, + "completed": completed_result.scalar() or 0, } } diff --git a/web/frontend/issues/WS-86/CHANGES.md b/web/frontend/issues/WS-86/CHANGES.md new file mode 100644 index 0000000..39058f6 --- /dev/null +++ b/web/frontend/issues/WS-86/CHANGES.md @@ -0,0 +1,37 @@ +# WS-86 改动清单 + +分支:`perf/frontend-first-screen`(基于 `main`)。只动前端 `web/frontend/`,不涉及后端 / API 契约 / 框架 / SSR。 + +## 任务 1:Font Awesome 非阻塞化(自托管) + +- `web/frontend/src/app/layout.tsx` — 删除 `` 里指向 `cdnjs.cloudflare.com` 的渲染阻塞外链 ``,改为自托管 `/lib/font-awesome/css/all.min.css`,用 `media="print"` + `onload` 切 `all` 的内联脚本非阻塞加载(`