diff --git a/README.md b/README.md index a3f7f112..f9774110 100644 --- a/README.md +++ b/README.md @@ -1,302 +1,289 @@ -

- -

- -
- arXiv - Discord - WeChat - X Follow -
- Community -
-
- - Deutsch | - Español | - français | - 日本語 | - 한국어 | - Português | - Русский | - 中文 + +TradingAgents Logo + +# TradingAgents + +### Multi-Agent LLM Financial Trading Framework + +[![arXiv](https://img.shields.io/badge/arXiv-2412.20138-B31B1B?logo=arxiv)](https://arxiv.org/abs/2412.20138) +[![Python 3.13+](https://img.shields.io/badge/Python-3.13+-3776AB?logo=python&logoColor=white)](https://www.python.org/) +[![License](https://img.shields.io/badge/License-MIT-green.svg)](LICENSE) +[![React](https://img.shields.io/badge/React_18-TypeScript-61DAFB?logo=react&logoColor=white)](https://react.dev/) +[![FastAPI](https://img.shields.io/badge/FastAPI-Backend-009688?logo=fastapi&logoColor=white)](https://fastapi.tiangolo.com/) +[![Tailwind CSS](https://img.shields.io/badge/Tailwind_CSS-4.0-06B6D4?logo=tailwindcss&logoColor=white)](https://tailwindcss.com/) + +
+ +An open-source framework that deploys **specialized AI agents** — analysts, researchers, traders, and risk managers — to collaboratively analyze markets and generate investment recommendations through structured debate. + +
+ +[Getting Started](#-getting-started)  •  [Web Dashboard](#-nifty50-ai-web-dashboard)  •  [Python API](#-python-api)  •  [Architecture](#-architecture)  •  [Contributing](#-contributing) + +
+
--- -# TradingAgents: Multi-Agents LLM Financial Trading Framework +## Highlights -> 🎉 **TradingAgents** officially released! We have received numerous inquiries about the work, and we would like to express our thanks for the enthusiasm in our community. -> -> So we decided to fully open-source the framework. Looking forward to building impactful projects with you! + + + + + +
-
- - - - - TradingAgents Star History - - -
+**Multi-Agent Collaboration** — Specialized AI agents (Technical, Fundamental, Sentiment, Risk) work together, each bringing domain expertise to stock analysis. -
+**Structured Debate System** — Bull and bear researchers debate findings, challenge assumptions, and reach consensus through reasoned discussion. -🚀 [TradingAgents](#tradingagents-framework) | ⚡ [Installation & CLI](#installation-and-cli) | 🎬 [Demo](https://www.youtube.com/watch?v=90gr5lwjIho) | 📦 [Package Usage](#tradingagents-package) | 🤝 [Contributing](#contributing) | 📄 [Citation](#citation) +
- +**Real-Time Web Dashboard** — Production-grade React frontend with live analysis pipeline visualization, backtesting, and portfolio simulation. -## TradingAgents Framework +**Configurable & Extensible** — Swap LLM providers (OpenAI, Anthropic Claude), adjust debate rounds, configure data sources, and extend with custom agents. -TradingAgents is a multi-agent trading framework that mirrors the dynamics of real-world trading firms. By deploying specialized LLM-powered agents: from fundamental analysts, sentiment experts, and technical analysts, to trader, risk management team, the platform collaboratively evaluates market conditions and informs trading decisions. Moreover, these agents engage in dynamic discussions to pinpoint the optimal strategy. - -

- -

- -> TradingAgents framework is designed for research purposes. Trading performance may vary based on many factors, including the chosen backbone language models, model temperature, trading periods, the quality of data, and other non-deterministic factors. [It is not intended as financial, investment, or trading advice.](https://tauric.ai/disclaimer/) - -Our framework decomposes complex trading tasks into specialized roles. This ensures the system achieves a robust, scalable approach to market analysis and decision-making. - -### Analyst Team -- Fundamentals Analyst: Evaluates company financials and performance metrics, identifying intrinsic values and potential red flags. -- Sentiment Analyst: Analyzes social media and public sentiment using sentiment scoring algorithms to gauge short-term market mood. -- News Analyst: Monitors global news and macroeconomic indicators, interpreting the impact of events on market conditions. -- Technical Analyst: Utilizes technical indicators (like MACD and RSI) to detect trading patterns and forecast price movements. - -

- -

- -### Researcher Team -- Comprises both bullish and bearish researchers who critically assess the insights provided by the Analyst Team. Through structured debates, they balance potential gains against inherent risks. - -

- -

- -### Trader Agent -- Composes reports from the analysts and researchers to make informed trading decisions. It determines the timing and magnitude of trades based on comprehensive market insights. - -

- -

- -### Risk Management and Portfolio Manager -- Continuously evaluates portfolio risk by assessing market volatility, liquidity, and other risk factors. The risk management team evaluates and adjusts trading strategies, providing assessment reports to the Portfolio Manager for final decision. -- The Portfolio Manager approves/rejects the transaction proposal. If approved, the order will be sent to the simulated exchange and executed. - -

- -

- -## Installation and CLI - -### Installation - -Clone TradingAgents: -```bash -git clone https://github.com/TauricResearch/TradingAgents.git -cd TradingAgents -``` - -Create a virtual environment in any of your favorite environment managers: -```bash -conda create -n tradingagents python=3.13 -conda activate tradingagents -``` - -Install dependencies: -```bash -pip install -r requirements.txt -``` - -### Required APIs - -You will need the OpenAI API for all the agents, and [Alpha Vantage API](https://www.alphavantage.co/support/#api-key) for fundamental and news data (default configuration). - -```bash -export OPENAI_API_KEY=$YOUR_OPENAI_API_KEY -export ALPHA_VANTAGE_API_KEY=$YOUR_ALPHA_VANTAGE_API_KEY -``` - -Alternatively, you can create a `.env` file in the project root with your API keys (see `.env.example` for reference): -```bash -cp .env.example .env -# Edit .env with your actual API keys -``` - -**Note:** We are happy to partner with Alpha Vantage to provide robust API support for TradingAgents. You can get a free AlphaVantage API [here](https://www.alphavantage.co/support/#api-key), TradingAgents-sourced requests also have increased rate limits to 60 requests per minute with no daily limits. Typically the quota is sufficient for performing complex tasks with TradingAgents thanks to Alpha Vantage’s open-source support program. If you prefer to use OpenAI for these data sources instead, you can modify the data vendor settings in `tradingagents/default_config.py`. - -### CLI Usage - -You can also try out the CLI directly by running: -```bash -python -m cli.main -``` -You will see a screen where you can select your desired tickers, date, LLMs, research depth, etc. - -

- -

- -An interface will appear showing results as they load, letting you track the agent's progress as it runs. - -

- -

- -

- -

+
--- -## 🌐 Nifty50 AI Trading Dashboard (Web Frontend) - -A modern, feature-rich web dashboard for TradingAgents, specifically built for **Indian Nifty 50 stocks**. This dashboard provides a complete visual interface for AI-powered stock analysis with full transparency into the multi-agent decision process. - -### 🚀 Quick Start - -```bash -# Start the backend server -cd frontend/backend -pip install -r requirements.txt -python server.py # Runs on http://localhost:8001 - -# Start the frontend (in a new terminal) -cd frontend -npm install -npm run dev # Runs on http://localhost:5173 -``` - -### ✨ Key Features - -#### Dashboard - AI Recommendations at a Glance -View all 50 Nifty stocks with AI recommendations, top picks, stocks to avoid, and one-click bulk analysis. +## Screenshots +
+Dashboard — AI Recommendations at a Glance +

- -

- -#### 🌙 Dark Mode Support -Full dark mode with automatic system theme detection for comfortable viewing. - -

- -

- -#### ⚙️ Configurable Settings Panel -Configure your AI analysis directly from the browser: -- **LLM Provider**: Claude Subscription or Anthropic API -- **Model Selection**: Choose Deep Think (Opus) and Quick Think (Sonnet/Haiku) models -- **API Key Management**: Securely stored in browser localStorage -- **Debate Rounds**: Adjust thoroughness (1-5 rounds) - -

- -

- -#### 📊 Stock Detail View -Detailed analysis for each stock with interactive price charts, recommendation history, and AI analysis summaries. - -

- -

- -#### 🔬 Analysis Pipeline Visualization -See exactly how the AI reached its decision with a 9-step pipeline showing: -- Data collection progress -- Individual agent reports (Market, News, Social Media, Fundamentals) -- Real-time status tracking - -

- -

- -#### 💬 Investment Debates (Bull vs Bear) -Watch AI agents debate investment decisions with full transparency: -- **Bull Analyst**: Makes the case for buying -- **Bear Analyst**: Presents risks and concerns -- **Research Manager**: Weighs both sides and decides - -

- -

- -
-📜 View Full Debate Example (Click to expand) -

- + TradingAgents Dashboard showing all 50 Nifty stocks with AI recommendations, rank badges, and decision filters

-#### 📈 Historical Analysis & Backtesting -Track AI performance over time with comprehensive analytics: -- Prediction accuracy metrics (Buy/Sell/Hold) -- Risk metrics (Sharpe ratio, max drawdown, win rate) -- Portfolio simulator with customizable starting amounts -- AI Strategy vs Nifty50 Index comparison +
+History — Backtesting & Portfolio Simulation +
+

+ Historical analysis page with prediction accuracy, risk metrics, portfolio simulator, and AI vs Nifty50 comparison +

+
+ +
+Stock Detail — Deep Analysis View +
+

+ Stock detail page showing SBIN analysis with rank badge, recommendation history, and prediction accuracy +

+
+ +
+Historical Date View — Ranked Stock Lists +
+

+ History page with date cards showing buy/sell/hold breakdown and expanded ranked stock list +

+
+ +
+How It Works — Multi-Agent AI System +
+

+ Educational page explaining the multi-agent AI system with agent cards and debate process +

+
+ +
+Settings — Configurable AI Models +
+

+ Settings panel for configuring LLM provider, model selection, and analysis parameters +

+
+ +
+Dark Mode +
+

+ Dashboard in dark mode with glassmorphic cards and premium styling +

+
+ +--- + +## Architecture + +TradingAgents mirrors the structure of real-world trading firms by decomposing complex trading tasks into specialized roles: + +``` + ┌─────────────────────────────────────┐ + │ Data Collection │ + │ (Market, News, Social, Financials) │ + └──────────────┬──────────────────────┘ + │ + ┌──────────────▼──────────────────────┐ + │ Analyst Team │ + │ Technical ┃ Fundamental ┃ Sentiment │ + │ ┃ News ┃ │ + └──────────────┬──────────────────────┘ + │ + ┌──────────────▼──────────────────────┐ + │ Researcher Team │ + │ Bull Researcher ⚔ Bear Researcher │ + │ (Structured AI Debate) │ + └──────────────┬──────────────────────┘ + │ + ┌──────────────▼──────────────────────┐ + │ Trader Agent │ + │ Synthesizes reports → Decision │ + └──────────────┬──────────────────────┘ + │ + ┌──────────────▼──────────────────────┐ + │ Risk Management & Portfolio Mgr │ + │ Evaluates risk → Approves/Rejects │ + └─────────────────────────────────────┘ +``` + +
+Agent Details + +| Agent | Role | Key Capabilities | +|-------|------|------------------| +| **Technical Analyst** | Chart & indicator analysis | RSI, MACD, Bollinger Bands, moving averages, volume patterns | +| **Fundamental Analyst** | Financial evaluation | P/E ratios, earnings, debt analysis, intrinsic value | +| **Sentiment Analyst** | Market mood assessment | Social media trends, analyst ratings, market psychology | +| **News Analyst** | Event impact analysis | Macro indicators, breaking news, sector developments | +| **Bull Researcher** | Bullish case builder | Identifies growth catalysts, upside potential | +| **Bear Researcher** | Risk challenger | Highlights risks, valuation concerns, downside scenarios | +| **Trader Agent** | Decision synthesis | Combines all reports into actionable BUY/SELL/HOLD | +| **Risk Manager** | Portfolio protection | Volatility assessment, position sizing, drawdown limits | + +
+ +--- + +## Getting Started + +### Prerequisites + +- Python 3.13+ +- Node.js 18+ (for web dashboard) +- API keys: OpenAI or Anthropic Claude, [Alpha Vantage](https://www.alphavantage.co/support/#api-key) (free) + +### Installation + +```bash +# Clone the repository +git clone https://github.com/hjlabs/TradingAgents.git +cd TradingAgents + +# Create virtual environment +conda create -n tradingagents python=3.13 +conda activate tradingagents + +# Install dependencies +pip install -r requirements.txt +``` + +### API Keys + +```bash +export OPENAI_API_KEY=your_openai_key +export ALPHA_VANTAGE_API_KEY=your_alpha_vantage_key +``` + +Or create a `.env` file from the template: +```bash +cp .env.example .env +``` + +> **Note:** Alpha Vantage provides a free API key with 60 requests/minute for TradingAgents-sourced requests. For offline experimentation, a local data vendor option is also available. + +### CLI Usage + +```bash +python -m cli.main +``` + +Select your tickers, date, LLMs, and research depth from the interactive interface.

- + TradingAgents CLI interface

-#### 📚 How It Works -Educational content explaining the multi-agent AI system and decision process. +--- -

- -

+## Nifty50 AI Web Dashboard -### 🛠️ Frontend Tech Stack +A production-grade web dashboard built for **Indian Nifty 50 stocks** with full transparency into the multi-agent decision process. -| Technology | Purpose | -|------------|---------| -| React 18 + TypeScript | Core framework | -| Vite | Build tool & dev server | -| Tailwind CSS | Styling with dark mode | -| Recharts | Interactive charts | -| Lucide React | Icons | -| FastAPI (Python) | Backend API | -| SQLite | Data persistence | +### Quick Start -### 📁 Frontend Project Structure +```bash +# Terminal 1: Start the backend +cd frontend/backend +pip install -r requirements.txt +python server.py # http://localhost:8001 + +# Terminal 2: Start the frontend +cd frontend +npm install +npm run dev # http://localhost:5173 +``` + +### Features + +| Feature | Description | +|---------|-------------| +| **AI Recommendations** | BUY/SELL/HOLD decisions for all 50 Nifty stocks with confidence levels and risk ratings | +| **Stock Ranking (1-50)** | Composite scoring algorithm ranks stocks from best to worst investment opportunity | +| **Analysis Pipeline** | 12-step visualization showing data collection, agent analysis, debate, and decision | +| **Investment Debates** | Full bull vs bear debate transcripts with research manager synthesis | +| **Backtesting** | Prediction accuracy tracking, risk metrics (Sharpe, drawdown), win/loss ratios | +| **Portfolio Simulator** | Paper trading simulation with Zerodha-accurate brokerage charges and Nifty50 benchmarking | +| **Settings Panel** | Configure LLM provider (Claude/OpenAI), model tiers, debate rounds, parallel workers | +| **Dark Mode** | Automatic system theme detection with manual toggle | + +### Tech Stack + +| Layer | Technology | +|-------|-----------| +| Frontend | React 18 + TypeScript, Vite, Tailwind CSS | +| Charts | Recharts | +| Icons | Lucide React | +| Backend | FastAPI (Python) | +| Database | SQLite | +| Fonts | DM Sans + Plus Jakarta Sans | + +### Project Structure ``` frontend/ ├── src/ -│ ├── components/ -│ │ ├── pipeline/ # Pipeline visualization -│ │ ├── SettingsModal.tsx # Settings UI -│ │ └── Header.tsx -│ ├── contexts/ -│ │ └── SettingsContext.tsx +│ ├── components/ # Reusable UI components +│ │ ├── pipeline/ # Analysis pipeline visualization +│ │ ├── StockCard.tsx # Stock cards with rank badges +│ │ ├── TopPicks.tsx # Top picks & stocks to avoid +│ │ └── Header.tsx # Navigation header +│ ├── contexts/ # React contexts (Settings, Theme) │ ├── pages/ -│ │ ├── Dashboard.tsx -│ │ ├── StockDetail.tsx -│ │ ├── History.tsx -│ │ └── About.tsx -│ └── services/ -│ └── api.ts +│ │ ├── Dashboard.tsx # Main stock grid with filters +│ │ ├── StockDetail.tsx # Individual stock analysis +│ │ ├── History.tsx # Backtesting & portfolio sim +│ │ └── About.tsx # How it works +│ ├── services/api.ts # API client +│ └── types/index.ts # TypeScript type definitions ├── backend/ -│ ├── server.py -│ └── database.py -└── docs/screenshots/ +│ ├── server.py # FastAPI server +│ ├── database.py # SQLite operations & ranking +│ └── backtest_service.py # Backtesting engine +└── docs/screenshots/ # Documentation screenshots ``` --- -## TradingAgents Package +## Python API -### Implementation Details - -We built TradingAgents with LangGraph to ensure flexibility and modularity. We utilize `o1-preview` and `gpt-4o` as our deep thinking and fast thinking LLMs for our experiments. However, for testing purposes, we recommend you use `o4-mini` and `gpt-4.1-mini` to save on costs as our framework makes **lots of** API calls. - -### Python Usage - -To use TradingAgents inside your code, you can import the `tradingagents` module and initialize a `TradingAgentsGraph()` object. The `.propagate()` function will return a decision. You can run `main.py`, here's also a quick example: +Use TradingAgents programmatically in your own projects: ```python from tradingagents.graph.trading_graph import TradingAgentsGraph @@ -304,59 +291,76 @@ from tradingagents.default_config import DEFAULT_CONFIG ta = TradingAgentsGraph(debug=True, config=DEFAULT_CONFIG.copy()) -# forward propagate +# Analyze a stock _, decision = ta.propagate("NVDA", "2024-05-10") print(decision) ``` -You can also adjust the default configuration to set your own choice of LLMs, debate rounds, etc. +### Custom Configuration ```python from tradingagents.graph.trading_graph import TradingAgentsGraph from tradingagents.default_config import DEFAULT_CONFIG -# Create a custom config config = DEFAULT_CONFIG.copy() -config["deep_think_llm"] = "gpt-4.1-nano" # Use a different model -config["quick_think_llm"] = "gpt-4.1-nano" # Use a different model -config["max_debate_rounds"] = 1 # Increase debate rounds +config["deep_think_llm"] = "gpt-4.1-nano" +config["quick_think_llm"] = "gpt-4.1-nano" +config["max_debate_rounds"] = 3 -# Configure data vendors (default uses yfinance and Alpha Vantage) config["data_vendors"] = { - "core_stock_apis": "yfinance", # Options: yfinance, alpha_vantage, local - "technical_indicators": "yfinance", # Options: yfinance, alpha_vantage, local - "fundamental_data": "alpha_vantage", # Options: openai, alpha_vantage, local - "news_data": "alpha_vantage", # Options: openai, alpha_vantage, google, local + "core_stock_apis": "yfinance", + "technical_indicators": "yfinance", + "fundamental_data": "alpha_vantage", + "news_data": "alpha_vantage", } -# Initialize with custom config ta = TradingAgentsGraph(debug=True, config=config) - -# forward propagate _, decision = ta.propagate("NVDA", "2024-05-10") -print(decision) ``` -> The default configuration uses yfinance for stock price and technical data, and Alpha Vantage for fundamental and news data. For production use or if you encounter rate limits, consider upgrading to [Alpha Vantage Premium](https://www.alphavantage.co/premium/) for more stable and reliable data access. For offline experimentation, there's a local data vendor option that uses our **Tauric TradingDB**, a curated dataset for backtesting, though this is still in development. We're currently refining this dataset and plan to release it soon alongside our upcoming projects. Stay tuned! +See `tradingagents/default_config.py` for the full list of configuration options. -You can view the full list of configurations in `tradingagents/default_config.py`. +--- ## Contributing -We welcome contributions from the community! Whether it's fixing a bug, improving documentation, or suggesting a new feature, your input helps make this project better. If you are interested in this line of research, please consider joining our open-source financial AI research community [Tauric Research](https://tauric.ai/). +We welcome contributions! Whether it's fixing a bug, improving documentation, or suggesting a new feature — your input helps make this project better. + +1. Fork the repository +2. Create a feature branch (`git checkout -b feature/your-feature`) +3. Commit your changes +4. Push to the branch and open a Pull Request + +--- + +## Disclaimer + +TradingAgents is designed for **research and educational purposes only**. Trading performance varies based on LLM selection, model temperature, data quality, and other non-deterministic factors. This software is **not financial, investment, or trading advice**. Always do your own research and consult a qualified financial advisor. + +--- ## Citation -Please reference our work if you find *TradingAgents* provides you with some help :) +If you find TradingAgents useful in your research, please cite: -``` +```bibtex @misc{xiao2025tradingagentsmultiagentsllmfinancial, - title={TradingAgents: Multi-Agents LLM Financial Trading Framework}, + title={TradingAgents: Multi-Agents LLM Financial Trading Framework}, author={Yijia Xiao and Edward Sun and Di Luo and Wei Wang}, year={2025}, eprint={2412.20138}, archivePrefix={arXiv}, primaryClass={q-fin.TR}, - url={https://arxiv.org/abs/2412.20138}, + url={https://arxiv.org/abs/2412.20138}, } ``` + +--- + +
+ +Built and maintained by **[hjlabs.in](https://hjlabs.in)** + +Made with AI agents that actually debate before deciding. + +
diff --git a/dashboard-polished-full.png b/dashboard-polished-full.png new file mode 100644 index 00000000..4bb213b5 Binary files /dev/null and b/dashboard-polished-full.png differ diff --git a/dashboard-polished.png b/dashboard-polished.png new file mode 100644 index 00000000..9ba2e382 Binary files /dev/null and b/dashboard-polished.png differ diff --git a/dashboard-ranking-grid.png b/dashboard-ranking-grid.png new file mode 100644 index 00000000..367d9ce3 Binary files /dev/null and b/dashboard-ranking-grid.png differ diff --git a/dashboard-ranking.png b/dashboard-ranking.png new file mode 100644 index 00000000..d2b29c48 Binary files /dev/null and b/dashboard-ranking.png differ diff --git a/frontend/backend/backtest_service.py b/frontend/backend/backtest_service.py index b3c373a8..9a1a5cb1 100644 --- a/frontend/backend/backtest_service.py +++ b/frontend/backend/backtest_service.py @@ -7,7 +7,7 @@ import database as db def get_trading_day_price(ticker: yf.Ticker, target_date: datetime, - direction: str = 'forward', max_days: int = 7) -> Optional[float]: + direction: str = 'forward', max_days: int = 7) -> tuple[Optional[float], Optional[datetime]]: """ Get the closing price for a trading day near the target date. @@ -18,7 +18,7 @@ def get_trading_day_price(ticker: yf.Ticker, target_date: datetime, max_days: Maximum days to search Returns: - Closing price or None if not found + Tuple of (closing_price, actual_date) or (None, None) if not found """ for i in range(max_days): if direction == 'forward': @@ -32,9 +32,9 @@ def get_trading_day_price(ticker: yf.Ticker, target_date: datetime, hist = ticker.history(start=start.strftime('%Y-%m-%d'), end=end.strftime('%Y-%m-%d')) if not hist.empty: - return hist['Close'].iloc[0] + return hist['Close'].iloc[0], check_date - return None + return None, None def calculate_backtest_for_recommendation(date: str, symbol: str, decision: str, @@ -61,7 +61,7 @@ def calculate_backtest_for_recommendation(date: str, symbol: str, decision: str, ticker = yf.Ticker(yf_symbol) # Get price at prediction date (or next trading day) - price_at_pred = get_trading_day_price(ticker, pred_date, 'forward') + price_at_pred, actual_pred_date = get_trading_day_price(ticker, pred_date, 'forward') if price_at_pred is None: return None @@ -70,11 +70,16 @@ def calculate_backtest_for_recommendation(date: str, symbol: str, decision: str, date_1w = pred_date + timedelta(weeks=1) date_1m = pred_date + timedelta(days=30) - price_1d = get_trading_day_price(ticker, date_1d, 'forward') - price_1w = get_trading_day_price(ticker, date_1w, 'forward') - price_1m = get_trading_day_price(ticker, date_1m, 'forward') + price_1d, actual_1d_date = get_trading_day_price(ticker, date_1d, 'forward') + price_1w, actual_1w_date = get_trading_day_price(ticker, date_1w, 'forward') + price_1m, actual_1m_date = get_trading_day_price(ticker, date_1m, 'forward') - # Calculate returns + # Detect same-day resolution: if pred and 1d resolved to the same trading day, + # the 0% return is meaningless — treat as no data + if price_1d and actual_pred_date and actual_1d_date and actual_pred_date == actual_1d_date: + price_1d = None + + # Calculate returns (only when we have a genuinely different trading day) return_1d = ((price_1d - price_at_pred) / price_at_pred * 100) if price_1d else None return_1w = ((price_1w - price_at_pred) / price_at_pred * 100) if price_1w else None return_1m = ((price_1m - price_at_pred) / price_at_pred * 100) if price_1m else None @@ -83,24 +88,34 @@ def calculate_backtest_for_recommendation(date: str, symbol: str, decision: str, return_at_hold = None if hold_days and hold_days > 0: date_hold = pred_date + timedelta(days=hold_days) - price_at_hold = get_trading_day_price(ticker, date_hold, 'forward') - if price_at_hold: + price_at_hold, actual_hold_date = get_trading_day_price(ticker, date_hold, 'forward') + # Only count if we found a different day than the prediction date + if price_at_hold and actual_hold_date and actual_hold_date != actual_pred_date: return_at_hold = round(((price_at_hold - price_at_pred) / price_at_pred * 100), 2) + # Skip if we have no usable return data at all + if return_1d is None and return_1w is None and return_at_hold is None: + return None + # Determine if prediction was correct - # Use hold_days return when available, fall back to 1-week return + # Use hold_days return when available, fall back to 1-day return prediction_correct = None - check_return = return_at_hold if return_at_hold is not None else return_1w + check_return = return_at_hold if return_at_hold is not None else return_1d if check_return is not None: if decision == 'BUY' or decision == 'HOLD': prediction_correct = check_return > 0 elif decision == 'SELL': prediction_correct = check_return < 0 + # Sanitize the decision value before storing + clean_decision = decision.strip().upper() + if clean_decision not in ('BUY', 'SELL', 'HOLD'): + clean_decision = 'HOLD' + return { 'date': date, 'symbol': symbol, - 'decision': decision, + 'decision': clean_decision, 'price_at_prediction': round(price_at_pred, 2), 'price_1d_later': round(price_1d, 2) if price_1d else None, 'price_1w_later': round(price_1w, 2) if price_1w else None, diff --git a/frontend/backend/database.py b/frontend/backend/database.py index 7d611437..98ab787d 100644 --- a/frontend/backend/database.py +++ b/frontend/backend/database.py @@ -1,6 +1,7 @@ """SQLite database module for storing stock recommendations.""" import sqlite3 import json +import re from pathlib import Path from datetime import datetime from typing import Optional @@ -8,6 +9,40 @@ from typing import Optional DB_PATH = Path(__file__).parent / "recommendations.db" +def sanitize_decision(raw: str) -> str: + """Extract BUY/SELL/HOLD from potentially noisy LLM output. + + Handles: 'BUY', '**SELL**', 'HOLD\n\n---\nHOWEVER...', 'The decision is: **BUY**', etc. + Returns 'HOLD' as fallback. + """ + if not raw: + return 'HOLD' + text = raw.strip() + + # Quick exact match (most common case) + upper = text.upper() + if upper in ('BUY', 'SELL', 'HOLD'): + return upper + + # Strip markdown bold/italic: **SELL** → SELL, *BUY* → BUY + stripped = re.sub(r'[*_]+', '', text).strip().upper() + if stripped in ('BUY', 'SELL', 'HOLD'): + return stripped + + # First word after stripping markdown + first_word = stripped.split()[0] if stripped else '' + if first_word in ('BUY', 'SELL', 'HOLD'): + return first_word + + # Search for decision keyword in the text (prioritize earlier occurrences) + # Look for standalone BUY/SELL/HOLD words (not part of longer words) + for keyword in ('BUY', 'SELL', 'HOLD'): + if re.search(r'\b' + keyword + r'\b', upper): + return keyword + + return 'HOLD' + + def get_connection(): """Get SQLite database connection.""" conn = sqlite3.connect(DB_PATH) @@ -180,6 +215,12 @@ def init_db(): except sqlite3.OperationalError: pass # Column already exists + # Add rank column if it doesn't exist (migration for existing DBs) + try: + cursor.execute("ALTER TABLE stock_analysis ADD COLUMN rank INTEGER") + except sqlite3.OperationalError: + pass # Column already exists + # Create indexes for new tables cursor.execute(""" CREATE INDEX IF NOT EXISTS idx_agent_reports_date_symbol ON agent_reports(date, symbol) @@ -200,9 +241,10 @@ def init_db(): conn.commit() conn.close() - # Re-extract hold_days from raw_analysis for rows that have the default value (5) - # This fixes data where the signal processor LLM failed to extract the actual hold period + # Fix data quality issues at startup _fix_default_hold_days() + _fix_garbage_decisions() + _cleanup_bad_backtest_data() def _fix_default_hold_days(): @@ -275,6 +317,133 @@ def _fix_default_hold_days(): conn.close() +def _fix_garbage_decisions(): + """Fix stock_analysis rows where the decision field contains garbage LLM output. + + Uses sanitize_decision() to extract the real BUY/SELL/HOLD from the text, + then updates the DB rows and rebuilds daily_recommendations summaries. + """ + conn = get_connection() + cursor = conn.cursor() + + try: + # Find rows where decision is not a clean BUY/SELL/HOLD + cursor.execute( + "SELECT id, date, symbol, decision FROM stock_analysis " + "WHERE decision NOT IN ('BUY', 'SELL', 'HOLD') AND decision IS NOT NULL" + ) + rows = cursor.fetchall() + if not rows: + return + + fixed = 0 + affected_dates = set() + for row in rows: + clean = sanitize_decision(row['decision']) + if clean != row['decision']: + cursor.execute( + "UPDATE stock_analysis SET decision = ? WHERE id = ?", + (clean, row['id']) + ) + fixed += 1 + affected_dates.add(row['date']) + old_preview = row['decision'][:40].replace('\n', ' ') + print(f" Fixed decision for {row['symbol']} ({row['date']}): '{old_preview}...' -> {clean}") + + if fixed > 0: + conn.commit() + print(f"Fixed {fixed} stock(s) with garbage decision values.") + + # Rebuild daily_recommendations summaries for affected dates + for date in affected_dates: + cursor.execute( + "SELECT decision FROM stock_analysis WHERE date = ?", (date,) + ) + decisions = [sanitize_decision(r['decision']) for r in cursor.fetchall()] + buy_count = decisions.count('BUY') + sell_count = decisions.count('SELL') + hold_count = decisions.count('HOLD') + cursor.execute( + "UPDATE daily_recommendations SET summary_buy=?, summary_sell=?, summary_hold=?, summary_total=? WHERE date=?", + (buy_count, sell_count, hold_count, len(decisions), date) + ) + conn.commit() + print(f"Rebuilt summaries for {len(affected_dates)} date(s).") + + # Clear backtest results that may have wrong decisions stored + cursor.execute("DELETE FROM backtest_results WHERE decision NOT IN ('BUY', 'SELL', 'HOLD')") + conn.commit() + finally: + conn.close() + + +def _cleanup_bad_backtest_data(): + """Remove backtest results that have invalid data. + + Deletes rows where: + - return_1d is exactly 0.0 AND return_1w is also 0.0 or NULL (indicates same-day resolution) + - return_1d is NULL and return_1w is NULL and return_at_hold is NULL (no usable data) + """ + conn = get_connection() + cursor = conn.cursor() + + try: + # Delete rows where return_1d=0 and no other useful return data + # This typically means pred_date and next-day resolved to the same trading day + cursor.execute( + "DELETE FROM backtest_results " + "WHERE return_1d = 0.0 AND (return_1w IS NULL OR return_1w = 0.0) " + "AND (return_at_hold IS NULL OR return_at_hold = 0.0)" + ) + deleted_zero = cursor.rowcount + + # Delete rows where all returns are NULL (no price data available) + cursor.execute( + "DELETE FROM backtest_results " + "WHERE return_1d IS NULL AND return_1w IS NULL AND return_at_hold IS NULL" + ) + deleted_null = cursor.rowcount + + if deleted_zero + deleted_null > 0: + conn.commit() + print(f"Cleaned up backtest data: removed {deleted_zero} zero-return rows, {deleted_null} null-return rows.") + + # Fix rows where prediction_correct is NULL but we have return data + # Cross-reference with stock_analysis for the correct decision + cursor.execute(""" + SELECT br.id, br.date, br.symbol, br.return_1d, br.return_at_hold, + sa.decision as sa_decision + FROM backtest_results br + JOIN stock_analysis sa ON br.date = sa.date AND br.symbol = sa.symbol + WHERE br.prediction_correct IS NULL + AND (br.return_1d IS NOT NULL OR br.return_at_hold IS NOT NULL) + """) + null_correct_rows = cursor.fetchall() + fixed_count = 0 + for row in null_correct_rows: + decision = sanitize_decision(row['sa_decision']) + primary_return = row['return_at_hold'] if row['return_at_hold'] is not None else row['return_1d'] + if primary_return is None: + continue + if decision in ('BUY', 'HOLD'): + is_correct = 1 if primary_return > 0 else 0 + elif decision == 'SELL': + is_correct = 1 if primary_return < 0 else 0 + else: + continue + cursor.execute( + "UPDATE backtest_results SET prediction_correct = ?, decision = ? WHERE id = ?", + (is_correct, decision, row['id']) + ) + fixed_count += 1 + + if fixed_count > 0: + conn.commit() + print(f"Fixed prediction_correct for {fixed_count} backtest rows.") + finally: + conn.close() + + def save_recommendation(date: str, analysis_data: dict, summary: dict, top_picks: list, stocks_to_avoid: list): """Save a daily recommendation to the database.""" @@ -389,9 +558,7 @@ def get_recommendation_by_date(date: str) -> Optional[dict]: analysis = {} for a in analysis_rows: - decision = (a['decision'] or '').strip().upper() - if decision not in ('BUY', 'SELL', 'HOLD'): - decision = 'HOLD' + decision = sanitize_decision(a['decision']) analysis[a['symbol']] = { 'symbol': a['symbol'], 'company_name': a['company_name'], @@ -399,7 +566,8 @@ def get_recommendation_by_date(date: str) -> Optional[dict]: 'confidence': a['confidence'] or 'MEDIUM', 'risk': a['risk'] or 'MEDIUM', 'raw_analysis': a['raw_analysis'], - 'hold_days': a['hold_days'] if 'hold_days' in a.keys() else None + 'hold_days': a['hold_days'] if 'hold_days' in a.keys() else None, + 'rank': a['rank'] if 'rank' in a.keys() else None } if row: @@ -480,7 +648,7 @@ def get_stock_history(symbol: str) -> list: try: cursor.execute(""" - SELECT date, decision, confidence, risk, hold_days + SELECT date, decision, confidence, risk, hold_days, rank FROM stock_analysis WHERE symbol = ? ORDER BY date DESC @@ -488,16 +656,14 @@ def get_stock_history(symbol: str) -> list: results = [] for row in cursor.fetchall(): - decision = (row['decision'] or '').strip().upper() - # Sanitize: only allow BUY/SELL/HOLD - if decision not in ('BUY', 'SELL', 'HOLD'): - decision = 'HOLD' + decision = sanitize_decision(row['decision']) results.append({ 'date': row['date'], 'decision': decision, 'confidence': row['confidence'] or 'MEDIUM', 'risk': row['risk'] or 'MEDIUM', - 'hold_days': row['hold_days'] if 'hold_days' in row.keys() else None + 'hold_days': row['hold_days'] if 'hold_days' in row.keys() else None, + 'rank': row['rank'] if 'rank' in row.keys() else None }) return results finally: @@ -1089,72 +1255,167 @@ def get_all_backtest_results() -> list: def calculate_accuracy_metrics() -> dict: - """Calculate overall backtest accuracy metrics.""" - results = get_all_backtest_results() + """Calculate overall backtest accuracy metrics. - if not results: - return { - 'overall_accuracy': 0, - 'total_predictions': 0, - 'correct_predictions': 0, - 'by_decision': {'BUY': {'accuracy': 0, 'total': 0}, 'SELL': {'accuracy': 0, 'total': 0}, 'HOLD': {'accuracy': 0, 'total': 0}}, - 'by_confidence': {'HIGH': {'accuracy': 0, 'total': 0}, 'MEDIUM': {'accuracy': 0, 'total': 0}, 'LOW': {'accuracy': 0, 'total': 0}} - } - - total = len(results) - correct = sum(1 for r in results if r['prediction_correct']) - - # By decision type - by_decision = {} - for decision in ['BUY', 'SELL', 'HOLD']: - decision_results = [r for r in results if r['decision'] == decision] - if decision_results: - decision_correct = sum(1 for r in decision_results if r['prediction_correct']) - by_decision[decision] = { - 'accuracy': round(decision_correct / len(decision_results) * 100, 1), - 'total': len(decision_results), - 'correct': decision_correct - } - else: - by_decision[decision] = {'accuracy': 0, 'total': 0, 'correct': 0} - - # By confidence level - by_confidence = {} - for conf in ['HIGH', 'MEDIUM', 'LOW']: - conf_results = [r for r in results if r.get('confidence') == conf] - if conf_results: - conf_correct = sum(1 for r in conf_results if r['prediction_correct']) - by_confidence[conf] = { - 'accuracy': round(conf_correct / len(conf_results) * 100, 1), - 'total': len(conf_results), - 'correct': conf_correct - } - else: - by_confidence[conf] = {'accuracy': 0, 'total': 0, 'correct': 0} - - return { - 'overall_accuracy': round(correct / total * 100, 1) if total > 0 else 0, - 'total_predictions': total, - 'correct_predictions': correct, - 'by_decision': by_decision, - 'by_confidence': by_confidence + Cross-references backtest_results with stock_analysis to use the correct + (sanitized) decision values and compute prediction correctness accurately. + """ + conn = get_connection() + cursor = conn.cursor() + empty = { + 'overall_accuracy': 0, + 'total_predictions': 0, + 'correct_predictions': 0, + 'by_decision': {'BUY': {'accuracy': 0, 'total': 0, 'correct': 0}, + 'SELL': {'accuracy': 0, 'total': 0, 'correct': 0}, + 'HOLD': {'accuracy': 0, 'total': 0, 'correct': 0}}, + 'by_confidence': {} } + try: + # Join backtest_results with stock_analysis to get the correct decision + cursor.execute(""" + SELECT br.date, br.symbol, br.return_1d, br.return_1w, br.return_at_hold, + sa.decision as sa_decision, sa.confidence + FROM backtest_results br + JOIN stock_analysis sa ON br.date = sa.date AND br.symbol = sa.symbol + WHERE br.return_1d IS NOT NULL OR br.return_at_hold IS NOT NULL + """) + rows = cursor.fetchall() + + if not rows: + return empty + + # Compute accuracy using sanitized decisions and primaryReturn logic + total = 0 + correct = 0 + by_decision = {'BUY': {'total': 0, 'correct': 0}, 'SELL': {'total': 0, 'correct': 0}, 'HOLD': {'total': 0, 'correct': 0}} + + for row in rows: + decision = sanitize_decision(row['sa_decision']) + primary_return = row['return_at_hold'] if row['return_at_hold'] is not None else row['return_1d'] + if primary_return is None: + continue + + total += 1 + if decision in by_decision: + by_decision[decision]['total'] += 1 + + if decision in ('BUY', 'HOLD'): + is_correct = primary_return > 0 + elif decision == 'SELL': + is_correct = primary_return < 0 + else: + continue + + if is_correct: + correct += 1 + if decision in by_decision: + by_decision[decision]['correct'] += 1 + + # Build response + for d in by_decision: + t = by_decision[d]['total'] + c = by_decision[d]['correct'] + by_decision[d]['accuracy'] = round(c / t * 100, 1) if t > 0 else 0 + + return { + 'overall_accuracy': round(correct / total * 100, 1) if total > 0 else 0, + 'total_predictions': total, + 'correct_predictions': correct, + 'by_decision': by_decision, + 'by_confidence': {} + } + finally: + conn.close() + + +def compute_stock_rankings(date: str): + """Compute and store rank (1..N) for all stocks analyzed on a given date. + + Uses a deterministic composite score: + decision: BUY=30, HOLD=15, SELL=0 + confidence: HIGH=20, MEDIUM=10, LOW=0 + risk (inv): LOW=15, MEDIUM=8, HIGH=0 + hold bonus: BUY with short hold gets up to +5 + + Score range: 0-70. Sorted descending; ties broken alphabetically. + """ + DECISION_W = {'BUY': 30, 'HOLD': 15, 'SELL': 0} + CONFIDENCE_W = {'HIGH': 20, 'MEDIUM': 10, 'LOW': 0} + RISK_W = {'LOW': 15, 'MEDIUM': 8, 'HIGH': 0} + + conn = get_connection() + cursor = conn.cursor() + + try: + cursor.execute(""" + SELECT id, symbol, decision, confidence, risk, hold_days + FROM stock_analysis WHERE date = ? + """, (date,)) + rows = cursor.fetchall() + + if not rows: + return + + scored = [] + for row in rows: + decision = sanitize_decision(row['decision']) + confidence = (row['confidence'] or 'MEDIUM').upper() + risk = (row['risk'] or 'MEDIUM').upper() + hold_days = row['hold_days'] + + score = DECISION_W.get(decision, 0) + score += CONFIDENCE_W.get(confidence, 0) + score += RISK_W.get(risk, 0) + + # Hold days bonus: BUY with shorter hold = more immediate opportunity + if decision == 'BUY' and hold_days and hold_days > 0: + if hold_days <= 5: + score += 5 + elif hold_days <= 10: + score += 4 + elif hold_days <= 15: + score += 3 + elif hold_days <= 20: + score += 2 + else: + score += 1 + + scored.append((row['id'], row['symbol'], score)) + + # Sort by score descending, then symbol ascending for ties + scored.sort(key=lambda x: (-x[2], x[1])) + + for rank, (row_id, _symbol, _score) in enumerate(scored, start=1): + cursor.execute( + "UPDATE stock_analysis SET rank = ? WHERE id = ?", + (rank, row_id) + ) + + conn.commit() + finally: + conn.close() + def update_daily_recommendation_summary(date: str): """Auto-create/update daily_recommendations from stock_analysis for a date. - Counts BUY/SELL/HOLD decisions, generates top_picks and stocks_to_avoid, - and upserts the daily_recommendations row. + Computes rankings first, then counts BUY/SELL/HOLD decisions, generates + rank-ordered top_picks and stocks_to_avoid, and upserts the row. """ + # Compute rankings first so top_picks/stocks_to_avoid use rank order + compute_stock_rankings(date) + conn = get_connection() cursor = conn.cursor() try: - # Get all stock analyses for this date + # Get all stock analyses ordered by rank cursor.execute(""" - SELECT symbol, company_name, decision, confidence, risk, raw_analysis + SELECT symbol, company_name, decision, confidence, risk, raw_analysis, rank FROM stock_analysis WHERE date = ? + ORDER BY rank ASC NULLS LAST """, (date,)) rows = cursor.fetchall() @@ -1168,42 +1429,44 @@ def update_daily_recommendation_summary(date: str): sell_stocks = [] for row in rows: - decision = (row['decision'] or '').upper() + decision = sanitize_decision(row['decision']) if decision == 'BUY': buy_count += 1 buy_stocks.append({ 'symbol': row['symbol'], 'company_name': row['company_name'] or row['symbol'], - 'decision': 'BUY', 'confidence': row['confidence'] or 'MEDIUM', - 'reason': (row['raw_analysis'] or '')[:200] + 'reason': (row['raw_analysis'] or '')[:200], + 'rank': row['rank'] }) elif decision == 'SELL': sell_count += 1 sell_stocks.append({ 'symbol': row['symbol'], 'company_name': row['company_name'] or row['symbol'], - 'decision': 'SELL', 'confidence': row['confidence'] or 'MEDIUM', - 'reason': (row['raw_analysis'] or '')[:200] + 'reason': (row['raw_analysis'] or '')[:200], + 'rank': row['rank'] }) else: hold_count += 1 total = buy_count + sell_count + hold_count - # Top picks: up to 5 BUY stocks + # Top picks: top 5 BUY stocks by rank (already rank-sorted) top_picks = [ {'symbol': s['symbol'], 'company_name': s['company_name'], - 'confidence': s['confidence'], 'reason': s['reason']} + 'confidence': s['confidence'], 'reason': s['reason'], + 'rank': s['rank']} for s in buy_stocks[:5] ] - # Stocks to avoid: up to 5 SELL stocks + # Stocks to avoid: bottom-ranked SELL stocks (last 5) stocks_to_avoid = [ {'symbol': s['symbol'], 'company_name': s['company_name'], - 'confidence': s['confidence'], 'reason': s['reason']} - for s in sell_stocks[:5] + 'confidence': s['confidence'], 'reason': s['reason'], + 'rank': s['rank']} + for s in sell_stocks[-5:] ] cursor.execute(""" diff --git a/frontend/backend/recommendations.db b/frontend/backend/recommendations.db index afaa6600..d2152884 100644 Binary files a/frontend/backend/recommendations.db and b/frontend/backend/recommendations.db differ diff --git a/frontend/docs/screenshots/01-dashboard-light.png b/frontend/docs/screenshots/01-dashboard-light.png new file mode 100644 index 00000000..e1e95fdf Binary files /dev/null and b/frontend/docs/screenshots/01-dashboard-light.png differ diff --git a/frontend/docs/screenshots/01-dashboard.png b/frontend/docs/screenshots/01-dashboard.png index 43f80757..4bb213b5 100644 Binary files a/frontend/docs/screenshots/01-dashboard.png and b/frontend/docs/screenshots/01-dashboard.png differ diff --git a/frontend/docs/screenshots/02-settings-modal.png b/frontend/docs/screenshots/02-settings-modal.png index 3b67507e..539c50f2 100644 Binary files a/frontend/docs/screenshots/02-settings-modal.png and b/frontend/docs/screenshots/02-settings-modal.png differ diff --git a/frontend/docs/screenshots/03-stock-detail-overview.png b/frontend/docs/screenshots/03-stock-detail-overview.png index 07f7fb7f..185bff8f 100644 Binary files a/frontend/docs/screenshots/03-stock-detail-overview.png and b/frontend/docs/screenshots/03-stock-detail-overview.png differ diff --git a/frontend/docs/screenshots/08-dashboard-dark-mode.png b/frontend/docs/screenshots/08-dashboard-dark-mode.png index 36680cd3..9ba2e382 100644 Binary files a/frontend/docs/screenshots/08-dashboard-dark-mode.png and b/frontend/docs/screenshots/08-dashboard-dark-mode.png differ diff --git a/frontend/docs/screenshots/09-how-it-works.png b/frontend/docs/screenshots/09-how-it-works.png index 95140dae..b9fe7e96 100644 Binary files a/frontend/docs/screenshots/09-how-it-works.png and b/frontend/docs/screenshots/09-how-it-works.png differ diff --git a/frontend/docs/screenshots/10-history-page.png b/frontend/docs/screenshots/10-history-page.png index 0a5d01c4..b60e1b48 100644 Binary files a/frontend/docs/screenshots/10-history-page.png and b/frontend/docs/screenshots/10-history-page.png differ diff --git a/frontend/docs/screenshots/11-history-stocks-expanded.png b/frontend/docs/screenshots/11-history-stocks-expanded.png new file mode 100644 index 00000000..03fb1b75 Binary files /dev/null and b/frontend/docs/screenshots/11-history-stocks-expanded.png differ diff --git a/frontend/index.html b/frontend/index.html index 5a738c33..27aaa0ce 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -42,7 +42,7 @@ - +