This commit is contained in:
hemangjoshi37a 2026-02-11 01:37:36 +11:00
parent 473478a32d
commit b79179cea2
54 changed files with 1855 additions and 606 deletions

568
README.md
View File

@ -1,302 +1,289 @@
<p align="center">
<img src="assets/TauricResearch.png" style="width: 60%; height: auto;">
</p>
<div align="center" style="line-height: 1;">
<a href="https://arxiv.org/abs/2412.20138" target="_blank"><img alt="arXiv" src="https://img.shields.io/badge/arXiv-2412.20138-B31B1B?logo=arxiv"/></a>
<a href="https://discord.com/invite/hk9PGKShPK" target="_blank"><img alt="Discord" src="https://img.shields.io/badge/Discord-TradingResearch-7289da?logo=discord&logoColor=white&color=7289da"/></a>
<a href="./assets/wechat.png" target="_blank"><img alt="WeChat" src="https://img.shields.io/badge/WeChat-TauricResearch-brightgreen?logo=wechat&logoColor=white"/></a>
<a href="https://x.com/TauricResearch" target="_blank"><img alt="X Follow" src="https://img.shields.io/badge/X-TauricResearch-white?logo=x&logoColor=white"/></a>
<br>
<a href="https://github.com/TauricResearch/" target="_blank"><img alt="Community" src="https://img.shields.io/badge/Join_GitHub_Community-TauricResearch-14C290?logo=discourse"/></a>
</div>
<div align="center">
<!-- Keep these links. Translations will automatically update with the README. -->
<a href="https://www.readme-i18n.com/TauricResearch/TradingAgents?lang=de">Deutsch</a> |
<a href="https://www.readme-i18n.com/TauricResearch/TradingAgents?lang=es">Español</a> |
<a href="https://www.readme-i18n.com/TauricResearch/TradingAgents?lang=fr">français</a> |
<a href="https://www.readme-i18n.com/TauricResearch/TradingAgents?lang=ja">日本語</a> |
<a href="https://www.readme-i18n.com/TauricResearch/TradingAgents?lang=ko">한국어</a> |
<a href="https://www.readme-i18n.com/TauricResearch/TradingAgents?lang=pt">Português</a> |
<a href="https://www.readme-i18n.com/TauricResearch/TradingAgents?lang=ru">Русский</a> |
<a href="https://www.readme-i18n.com/TauricResearch/TradingAgents?lang=zh">中文</a>
<img src="assets/schema.png" width="120" alt="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/)
<br />
An open-source framework that deploys **specialized AI agents** &mdash; analysts, researchers, traders, and risk managers &mdash; to collaboratively analyze markets and generate investment recommendations through structured debate.
<br />
[Getting Started](#-getting-started) &nbsp;&bull;&nbsp; [Web Dashboard](#-nifty50-ai-web-dashboard) &nbsp;&bull;&nbsp; [Python API](#-python-api) &nbsp;&bull;&nbsp; [Architecture](#-architecture) &nbsp;&bull;&nbsp; [Contributing](#-contributing)
<br />
</div>
---
# 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!
<table>
<tr>
<td width="50%">
<div align="center">
<a href="https://www.star-history.com/#TauricResearch/TradingAgents&Date">
<picture>
<source media="(prefers-color-scheme: dark)" srcset="https://api.star-history.com/svg?repos=TauricResearch/TradingAgents&type=Date&theme=dark" />
<source media="(prefers-color-scheme: light)" srcset="https://api.star-history.com/svg?repos=TauricResearch/TradingAgents&type=Date" />
<img alt="TradingAgents Star History" src="https://api.star-history.com/svg?repos=TauricResearch/TradingAgents&type=Date" style="width: 80%; height: auto;" />
</picture>
</a>
</div>
**Multi-Agent Collaboration** &mdash; Specialized AI agents (Technical, Fundamental, Sentiment, Risk) work together, each bringing domain expertise to stock analysis.
<div align="center">
**Structured Debate System** &mdash; 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)
</td>
<td width="50%">
</div>
**Real-Time Web Dashboard** &mdash; Production-grade React frontend with live analysis pipeline visualization, backtesting, and portfolio simulation.
## TradingAgents Framework
**Configurable & Extensible** &mdash; 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.
<p align="center">
<img src="assets/schema.png" style="width: 100%; height: auto;">
</p>
> 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.
<p align="center">
<img src="assets/analyst.png" width="100%" style="display: inline-block; margin: 0 2%;">
</p>
### 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.
<p align="center">
<img src="assets/researcher.png" width="70%" style="display: inline-block; margin: 0 2%;">
</p>
### 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.
<p align="center">
<img src="assets/trader.png" width="70%" style="display: inline-block; margin: 0 2%;">
</p>
### 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.
<p align="center">
<img src="assets/risk.png" width="70%" style="display: inline-block; margin: 0 2%;">
</p>
## 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 Vantages 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.
<p align="center">
<img src="assets/cli/cli_init.png" width="100%" style="display: inline-block; margin: 0 2%;">
</p>
An interface will appear showing results as they load, letting you track the agent's progress as it runs.
<p align="center">
<img src="assets/cli/cli_news.png" width="100%" style="display: inline-block; margin: 0 2%;">
</p>
<p align="center">
<img src="assets/cli/cli_transaction.png" width="100%" style="display: inline-block; margin: 0 2%;">
</p>
</td>
</tr>
</table>
---
## 🌐 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
<details open>
<summary><b>Dashboard &mdash; AI Recommendations at a Glance</b></summary>
<br />
<p align="center">
<img src="frontend/docs/screenshots/01-dashboard.png" width="100%" style="display: inline-block;">
</p>
#### 🌙 Dark Mode Support
Full dark mode with automatic system theme detection for comfortable viewing.
<p align="center">
<img src="frontend/docs/screenshots/08-dashboard-dark-mode.png" width="100%" style="display: inline-block;">
</p>
#### ⚙️ 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)
<p align="center">
<img src="frontend/docs/screenshots/02-settings-modal.png" width="60%" style="display: inline-block;">
</p>
#### 📊 Stock Detail View
Detailed analysis for each stock with interactive price charts, recommendation history, and AI analysis summaries.
<p align="center">
<img src="frontend/docs/screenshots/03-stock-detail-overview.png" width="100%" style="display: inline-block;">
</p>
#### 🔬 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
<p align="center">
<img src="frontend/docs/screenshots/04-analysis-pipeline.png" width="100%" style="display: inline-block;">
</p>
#### 💬 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
<p align="center">
<img src="frontend/docs/screenshots/05-debates-tab.png" width="100%" style="display: inline-block;">
</p>
<details>
<summary><b>📜 View Full Debate Example (Click to expand)</b></summary>
<p align="center">
<img src="frontend/docs/screenshots/06-investment-debate-expanded.png" width="100%" style="display: inline-block;">
<img src="frontend/docs/screenshots/01-dashboard.png" width="100%" alt="TradingAgents Dashboard showing all 50 Nifty stocks with AI recommendations, rank badges, and decision filters" />
</p>
</details>
#### 📈 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
<details>
<summary><b>History &mdash; Backtesting & Portfolio Simulation</b></summary>
<br />
<p align="center">
<img src="frontend/docs/screenshots/10-history-page.png" width="100%" alt="Historical analysis page with prediction accuracy, risk metrics, portfolio simulator, and AI vs Nifty50 comparison" />
</p>
</details>
<details>
<summary><b>Stock Detail &mdash; Deep Analysis View</b></summary>
<br />
<p align="center">
<img src="frontend/docs/screenshots/03-stock-detail-overview.png" width="100%" alt="Stock detail page showing SBIN analysis with rank badge, recommendation history, and prediction accuracy" />
</p>
</details>
<details>
<summary><b>Historical Date View &mdash; Ranked Stock Lists</b></summary>
<br />
<p align="center">
<img src="frontend/docs/screenshots/11-history-stocks-expanded.png" width="100%" alt="History page with date cards showing buy/sell/hold breakdown and expanded ranked stock list" />
</p>
</details>
<details>
<summary><b>How It Works &mdash; Multi-Agent AI System</b></summary>
<br />
<p align="center">
<img src="frontend/docs/screenshots/09-how-it-works.png" width="100%" alt="Educational page explaining the multi-agent AI system with agent cards and debate process" />
</p>
</details>
<details>
<summary><b>Settings &mdash; Configurable AI Models</b></summary>
<br />
<p align="center">
<img src="frontend/docs/screenshots/02-settings-modal.png" width="60%" alt="Settings panel for configuring LLM provider, model selection, and analysis parameters" />
</p>
</details>
<details>
<summary><b>Dark Mode</b></summary>
<br />
<p align="center">
<img src="frontend/docs/screenshots/08-dashboard-dark-mode.png" width="100%" alt="Dashboard in dark mode with glassmorphic cards and premium styling" />
</p>
</details>
---
## 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 │
└─────────────────────────────────────┘
```
<details>
<summary><b>Agent Details</b></summary>
| 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 |
</details>
---
## 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.
<p align="center">
<img src="frontend/docs/screenshots/10-history-page.png" width="100%" style="display: inline-block;">
<img src="assets/cli/cli_init.png" width="100%" alt="TradingAgents CLI interface" />
</p>
#### 📚 How It Works
Educational content explaining the multi-agent AI system and decision process.
---
<p align="center">
<img src="frontend/docs/screenshots/09-how-it-works.png" width="100%" style="display: inline-block;">
</p>
## 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 &mdash; 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},
}
```
---
<div align="center">
Built and maintained by **[hjlabs.in](https://hjlabs.in)**
<sub>Made with AI agents that actually debate before deciding.</sub>
</div>

BIN
dashboard-polished-full.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 249 KiB

BIN
dashboard-polished.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 64 KiB

BIN
dashboard-ranking-grid.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 70 KiB

BIN
dashboard-ranking.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 68 KiB

View File

@ -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,

View File

@ -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("""

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 256 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 321 KiB

After

Width:  |  Height:  |  Size: 249 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 81 KiB

After

Width:  |  Height:  |  Size: 72 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 149 KiB

After

Width:  |  Height:  |  Size: 181 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 319 KiB

After

Width:  |  Height:  |  Size: 64 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 400 KiB

After

Width:  |  Height:  |  Size: 381 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 226 KiB

After

Width:  |  Height:  |  Size: 264 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 68 KiB

View File

@ -42,7 +42,7 @@
<!-- Google Fonts -->
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&family=Lexend:wght@400;500;600;700&display=swap" rel="stylesheet">
<link href="https://fonts.googleapis.com/css2?family=DM+Sans:ital,opsz,wght@0,9..40,300;0,9..40,400;0,9..40,500;0,9..40,600;0,9..40,700;1,9..40,400&family=Plus+Jakarta+Sans:wght@500;600;700;800&display=swap" rel="stylesheet">
<!-- Structured Data (JSON-LD) -->
<script type="application/ld+json">

View File

@ -35,7 +35,7 @@ export default function AccuracyTrendChart({ height = 200, className = '', data:
return (
<div className={className} style={{ height }}>
<ResponsiveContainer width="100%" height="100%">
<ResponsiveContainer width="100%" height="100%" minWidth={0} minHeight={0}>
<LineChart data={formattedData} margin={{ top: 5, right: 10, bottom: 5, left: 0 }}>
<CartesianGrid strokeDasharray="3 3" className="stroke-gray-200 dark:stroke-slate-700" />
<XAxis

View File

@ -40,7 +40,7 @@ export default function BackgroundSparkline({
return (
<div className={`w-full h-full ${className}`} style={{ filter: 'blur(1px)' }}>
<ResponsiveContainer width="100%" height="100%">
<ResponsiveContainer width="100%" height="100%" minWidth={0} minHeight={0}>
<AreaChart data={normalizedData} margin={{ top: 0, right: 0, bottom: 0, left: 0 }}>
<YAxis domain={[minPrice - padding, maxPrice + padding]} hide />
<defs>

View File

@ -21,7 +21,7 @@ export function SummaryPieChart({ buy, sell, hold }: SummaryChartProps) {
return (
<div style={{ width: '100%', height: '256px' }}>
<ResponsiveContainer width="100%" height={256}>
<ResponsiveContainer width="100%" height={256} minWidth={0} minHeight={0}>
<PieChart>
<Pie
data={data}
@ -77,7 +77,7 @@ export function HistoricalBarChart({ data }: HistoricalChartProps) {
return (
<div className="h-72">
<ResponsiveContainer width="100%" height="100%">
<ResponsiveContainer width="100%" height="100%" minWidth={0} minHeight={0}>
<BarChart data={formattedData} margin={{ top: 20, right: 30, left: 20, bottom: 5 }}>
<CartesianGrid strokeDasharray="3 3" stroke="#f0f0f0" />
<XAxis

View File

@ -31,7 +31,7 @@ export default function CumulativeReturnChart({ height = 160, className = '', da
return (
<div className={className} style={{ height }}>
<ResponsiveContainer width="100%" height="100%">
<ResponsiveContainer width="100%" height="100%" minWidth={0} minHeight={0}>
<AreaChart data={formattedData} margin={{ top: 5, right: 10, bottom: 5, left: 0 }}>
<defs>
<linearGradient id="cumulativeGradient" x1="0" y1="0" x2="0" y2="1">

View File

@ -3,42 +3,40 @@ import { Link } from 'react-router-dom';
export default function Footer() {
return (
<footer className="bg-white dark:bg-slate-900 border-t border-gray-200 dark:border-slate-700 mt-auto transition-colors">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-4">
{/* Compact single-row layout */}
<div className="flex flex-col sm:flex-row items-center justify-between gap-3">
<footer className="mt-auto border-t border-gray-200/50 dark:border-slate-700/30 bg-white/50 dark:bg-slate-900/50 transition-colors" style={{ backdropFilter: 'blur(8px)' }}>
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-5">
<div className="flex flex-col sm:flex-row items-center justify-between gap-4">
{/* Brand */}
<div className="flex items-center gap-2">
<div className="w-7 h-7 bg-gradient-to-br from-nifty-500 to-nifty-700 rounded-lg flex items-center justify-center">
<TrendingUp className="w-4 h-4 text-white" />
<div className="flex items-center gap-2.5">
<div className="w-7 h-7 rounded-lg flex items-center justify-center" style={{ background: 'linear-gradient(135deg, #0ea5e9, #0369a1)' }}>
<TrendingUp className="w-3.5 h-3.5 text-white" />
</div>
<span className="font-display font-bold text-sm gradient-text">Nifty50 AI</span>
</div>
{/* Links */}
<div className="flex items-center gap-4 text-xs text-gray-500 dark:text-gray-400">
<Link to="/" className="hover:text-nifty-600 dark:hover:text-nifty-400">Dashboard</Link>
<Link to="/history" className="hover:text-nifty-600 dark:hover:text-nifty-400">History</Link>
<Link to="/about" className="hover:text-nifty-600 dark:hover:text-nifty-400">How It Works</Link>
<span className="text-gray-300 dark:text-gray-600">|</span>
<a href="#" className="hover:text-nifty-600 dark:hover:text-nifty-400">Disclaimer</a>
<a href="#" className="hover:text-nifty-600 dark:hover:text-nifty-400">Privacy</a>
<div className="flex items-center gap-5 text-xs text-gray-500 dark:text-gray-400">
<Link to="/" className="hover:text-gray-900 dark:hover:text-gray-200 transition-colors">Dashboard</Link>
<Link to="/history" className="hover:text-gray-900 dark:hover:text-gray-200 transition-colors">History</Link>
<Link to="/about" className="hover:text-gray-900 dark:hover:text-gray-200 transition-colors">How It Works</Link>
<span className="text-gray-200 dark:text-gray-700">|</span>
<a href="#" className="hover:text-gray-900 dark:hover:text-gray-200 transition-colors">Disclaimer</a>
<a href="#" className="hover:text-gray-900 dark:hover:text-gray-200 transition-colors">Privacy</a>
</div>
{/* Social & Copyright */}
<div className="flex items-center gap-3">
<a href="#" className="text-gray-400 dark:text-gray-500 hover:text-gray-600 dark:hover:text-gray-300">
<a href="#" className="text-gray-400 dark:text-gray-500 hover:text-gray-600 dark:hover:text-gray-300 transition-colors">
<Github className="w-4 h-4" />
</a>
<a href="#" className="text-gray-400 dark:text-gray-500 hover:text-gray-600 dark:hover:text-gray-300">
<a href="#" className="text-gray-400 dark:text-gray-500 hover:text-gray-600 dark:hover:text-gray-300 transition-colors">
<Twitter className="w-4 h-4" />
</a>
<span className="text-xs text-gray-400 dark:text-gray-500">© {new Date().getFullYear()}</span>
<span className="text-xs text-gray-400 dark:text-gray-500">&copy; {new Date().getFullYear()}</span>
</div>
</div>
{/* Compact Disclaimer */}
<p className="text-[10px] text-gray-400 dark:text-gray-500 text-center mt-3 leading-relaxed">
<p className="text-[11px] text-gray-400 dark:text-gray-500 text-center mt-4 leading-relaxed">
AI-generated recommendations for educational purposes only. Not financial advice. Do your own research.
</p>
</div>

View File

@ -18,42 +18,41 @@ export default function Header() {
const isActive = (path: string) => location.pathname === path;
return (
<header className="bg-white dark:bg-slate-900 border-b border-gray-200 dark:border-slate-700 sticky top-0 z-50 transition-colors">
<header className="sticky top-0 z-50 transition-colors border-b border-gray-200/50 dark:border-slate-700/50 bg-white/70 dark:bg-slate-900/80" style={{ backdropFilter: 'blur(16px) saturate(180%)' }}>
<div className="max-w-7xl mx-auto px-3 sm:px-4 lg:px-6">
<div className="flex justify-between items-center h-12">
<div className="flex justify-between items-center h-14">
{/* Logo */}
<Link to="/" className="flex items-center gap-2">
<div className="w-8 h-8 bg-gradient-to-br from-nifty-500 to-nifty-700 rounded-lg flex items-center justify-center">
<Link to="/" className="flex items-center gap-2.5 group">
<div className="w-8 h-8 rounded-lg flex items-center justify-center transition-transform group-hover:scale-105" style={{ background: 'linear-gradient(135deg, #0ea5e9, #0369a1)' }}>
<TrendingUp className="w-4 h-4 text-white" />
</div>
<span className="font-display font-bold gradient-text text-sm">Nifty50 AI</span>
<span className="font-display font-bold gradient-text text-base tracking-tight">Nifty50 AI</span>
</Link>
{/* Desktop Navigation */}
<nav className="hidden md:flex items-center gap-0.5" aria-label="Main navigation">
<nav className="hidden md:flex items-center gap-1" aria-label="Main navigation">
{navItems.map(({ path, label, icon: Icon }) => (
<Link
key={path}
to={path}
aria-current={isActive(path) ? 'page' : undefined}
className={`flex items-center gap-1.5 px-3 py-1.5 rounded-md text-xs font-medium transition-colors focus:outline-none focus:ring-2 focus:ring-nifty-500 ${
className={`relative flex items-center gap-1.5 px-3 py-2 rounded-lg text-sm font-medium transition-all ${
isActive(path)
? 'bg-nifty-50 dark:bg-nifty-900/30 text-nifty-700 dark:text-nifty-400'
: 'text-gray-600 dark:text-gray-300 hover:bg-gray-50 dark:hover:bg-slate-800 hover:text-gray-900 dark:hover:text-gray-100'
? 'text-nifty-700 dark:text-nifty-400 bg-nifty-50/80 dark:bg-nifty-900/20'
: 'text-gray-500 dark:text-gray-400 hover:text-gray-900 dark:hover:text-gray-100 hover:bg-gray-50 dark:hover:bg-slate-800/50'
}`}
>
<Icon className="w-3.5 h-3.5" aria-hidden="true" />
<Icon className="w-4 h-4" aria-hidden="true" />
{label}
</Link>
))}
</nav>
{/* Settings, Theme Toggle & Mobile Menu */}
<div className="flex items-center gap-2">
{/* Settings Button */}
<div className="flex items-center gap-1.5">
<button
onClick={openSettings}
className="p-2 rounded-lg hover:bg-gray-100 dark:hover:bg-slate-800 transition-colors text-gray-600 dark:text-gray-300"
className="p-2 rounded-lg hover:bg-gray-100/80 dark:hover:bg-slate-800/50 transition-colors text-gray-500 dark:text-gray-400 hover:text-gray-700 dark:hover:text-gray-200"
aria-label="Open settings"
title="Settings"
>
@ -66,7 +65,7 @@ export default function Header() {
<ThemeToggle compact />
</div>
<button
className="md:hidden p-1.5 rounded-md hover:bg-gray-100 dark:hover:bg-slate-800 focus:outline-none focus:ring-2 focus:ring-nifty-500 transition-colors text-gray-600 dark:text-gray-300"
className="md:hidden p-2 rounded-lg hover:bg-gray-100/80 dark:hover:bg-slate-800/50 transition-colors text-gray-500 dark:text-gray-400"
onClick={() => setMobileMenuOpen(!mobileMenuOpen)}
aria-label={mobileMenuOpen ? 'Close menu' : 'Open menu'}
aria-expanded={mobileMenuOpen}
@ -79,17 +78,17 @@ export default function Header() {
{/* Mobile Navigation */}
{mobileMenuOpen && (
<nav id="mobile-menu" className="md:hidden py-2 border-t border-gray-100 dark:border-slate-700 animate-in slide-in-from-top-2 duration-200" aria-label="Mobile navigation">
<nav id="mobile-menu" className="md:hidden py-2 border-t border-gray-100 dark:border-slate-700/50 animate-in slide-in-from-top-2 duration-200" aria-label="Mobile navigation">
{navItems.map(({ path, label, icon: Icon }) => (
<Link
key={path}
to={path}
onClick={() => setMobileMenuOpen(false)}
aria-current={isActive(path) ? 'page' : undefined}
className={`flex items-center gap-2 px-3 py-2 rounded-md text-sm font-medium transition-colors ${
className={`flex items-center gap-2.5 px-3 py-2.5 rounded-lg text-sm font-medium transition-colors ${
isActive(path)
? 'bg-nifty-50 dark:bg-nifty-900/30 text-nifty-700 dark:text-nifty-400'
: 'text-gray-600 dark:text-gray-300 hover:bg-gray-50 dark:hover:bg-slate-800'
? 'bg-nifty-50/80 dark:bg-nifty-900/20 text-nifty-700 dark:text-nifty-400'
: 'text-gray-600 dark:text-gray-300 hover:bg-gray-50 dark:hover:bg-slate-800/50'
}`}
>
<Icon className="w-4 h-4" aria-hidden="true" />

View File

@ -64,7 +64,7 @@ export default function IndexComparisonChart({ height = 220, className = '', dat
{/* Chart */}
<div style={{ height }}>
<ResponsiveContainer width="100%" height="100%">
<ResponsiveContainer width="100%" height="100%" minWidth={0} minHeight={0}>
<LineChart data={formattedData} margin={{ top: 5, right: 10, bottom: 5, left: 0 }}>
<CartesianGrid strokeDasharray="3 3" className="stroke-gray-200 dark:stroke-slate-700" />
<XAxis

View File

@ -525,7 +525,7 @@ export default function PortfolioSimulator({
{/* Chart with Nifty Comparison - Fixed Y-axis */}
{portfolioData.length > 0 && (
<div className="h-48 mb-4">
<ResponsiveContainer width="100%" height="100%">
<ResponsiveContainer width="100%" height="100%" minWidth={0} minHeight={0}>
<LineChart data={portfolioData} margin={{ top: 5, right: 10, bottom: 5, left: 0 }}>
<CartesianGrid strokeDasharray="3 3" className="stroke-gray-200 dark:stroke-slate-700" />
<XAxis
@ -604,7 +604,7 @@ export default function PortfolioSimulator({
</div>
<div className="h-64 overflow-y-auto">
<div style={{ height: Math.max(200, stats.trades.length * 28) }}>
<ResponsiveContainer width="100%" height="100%">
<ResponsiveContainer width="100%" height="100%" minWidth={0} minHeight={0}>
<BarChart
data={stats.trades.map((t, i) => ({
...t,

View File

@ -43,7 +43,7 @@ export default function ReturnDistributionChart({ height = 200, className = '',
return (
<div className={className}>
<div style={{ height }}>
<ResponsiveContainer width="100%" height="100%">
<ResponsiveContainer width="100%" height="100%" minWidth={0} minHeight={0}>
<BarChart data={data} margin={{ top: 5, right: 10, bottom: 5, left: 0 }}>
<CartesianGrid strokeDasharray="3 3" className="stroke-gray-200 dark:stroke-slate-700" />
<XAxis

View File

@ -13,32 +13,35 @@ export function DecisionBadge({ decision, size = 'default' }: { decision: Decisi
const config = {
BUY: {
bg: 'bg-green-100 dark:bg-green-900/30',
text: 'text-green-800 dark:text-green-400',
bg: 'bg-emerald-100 dark:bg-emerald-900/25',
text: 'text-emerald-700 dark:text-emerald-400',
border: 'border border-emerald-200/60 dark:border-emerald-800/40',
icon: TrendingUp,
},
SELL: {
bg: 'bg-red-100 dark:bg-red-900/30',
text: 'text-red-800 dark:text-red-400',
bg: 'bg-red-100 dark:bg-red-900/25',
text: 'text-red-700 dark:text-red-400',
border: 'border border-red-200/60 dark:border-red-800/40',
icon: TrendingDown,
},
HOLD: {
bg: 'bg-amber-100 dark:bg-amber-900/30',
text: 'text-amber-800 dark:text-amber-400',
bg: 'bg-amber-100 dark:bg-amber-900/25',
text: 'text-amber-700 dark:text-amber-400',
border: 'border border-amber-200/60 dark:border-amber-800/40',
icon: Minus,
},
};
const entry = config[decision];
if (!entry) return null;
const { bg, text, icon: Icon } = entry;
const { bg, text, border, icon: Icon } = entry;
const sizeClasses = size === 'small'
? 'px-2 py-0.5 text-xs gap-1'
? 'px-2 py-0.5 text-[11px] gap-1'
: 'px-2.5 py-0.5 text-xs gap-1';
const iconSize = size === 'small' ? 'w-3 h-3' : 'w-3.5 h-3.5';
return (
<span className={`inline-flex items-center rounded-full font-semibold ${bg} ${text} ${sizeClasses}`}>
<span className={`inline-flex items-center rounded-full font-semibold tracking-wide ${bg} ${text} ${border} ${sizeClasses}`}>
<Icon className={iconSize} />
{decision}
</span>
@ -49,13 +52,13 @@ export function ConfidenceBadge({ confidence }: { confidence?: string }) {
if (!confidence) return null;
const colors = {
HIGH: 'bg-green-50 dark:bg-green-900/20 text-green-700 dark:text-green-400 border-green-200 dark:border-green-800',
MEDIUM: 'bg-amber-50 dark:bg-amber-900/20 text-amber-700 dark:text-amber-400 border-amber-200 dark:border-amber-800',
LOW: 'bg-gray-50 dark:bg-gray-800 text-gray-700 dark:text-gray-300 border-gray-200 dark:border-gray-600',
HIGH: 'bg-emerald-50 dark:bg-emerald-900/15 text-emerald-700 dark:text-emerald-400 border-emerald-200/60 dark:border-emerald-800/40',
MEDIUM: 'bg-amber-50 dark:bg-amber-900/15 text-amber-700 dark:text-amber-400 border-amber-200/60 dark:border-amber-800/40',
LOW: 'bg-gray-50 dark:bg-gray-800/50 text-gray-600 dark:text-gray-400 border-gray-200/60 dark:border-gray-700/40',
};
return (
<span className={`text-xs px-2 py-0.5 rounded border ${colors[confidence as keyof typeof colors] || colors.MEDIUM}`}>
<span className={`text-[11px] font-medium px-2 py-0.5 rounded-md border ${colors[confidence as keyof typeof colors] || colors.MEDIUM}`}>
{confidence} Confidence
</span>
);
@ -67,11 +70,11 @@ export function RiskBadge({ risk }: { risk?: string }) {
const colors = {
HIGH: 'text-red-600 dark:text-red-400',
MEDIUM: 'text-amber-600 dark:text-amber-400',
LOW: 'text-green-600 dark:text-green-400',
LOW: 'text-emerald-600 dark:text-emerald-400',
};
return (
<span className={`text-xs ${colors[risk as keyof typeof colors] || colors.MEDIUM}`}>
<span className={`text-[11px] font-medium ${colors[risk as keyof typeof colors] || colors.MEDIUM}`}>
{risk} Risk
</span>
);
@ -83,29 +86,71 @@ export function HoldDaysBadge({ holdDays, decision }: { holdDays?: number | null
const label = holdDays === 1 ? '1 day' : `${holdDays}d`;
return (
<span className="inline-flex items-center gap-1 text-xs px-2 py-0.5 rounded border bg-blue-50 dark:bg-blue-900/20 text-blue-700 dark:text-blue-400 border-blue-200 dark:border-blue-800">
<span className="inline-flex items-center gap-1 text-[11px] font-medium px-2 py-0.5 rounded-md border bg-blue-50 dark:bg-blue-900/15 text-blue-700 dark:text-blue-400 border-blue-200/60 dark:border-blue-800/40">
<Clock className="w-3 h-3" />
Hold {label}
</span>
);
}
export function RankBadge({ rank, size = 'default' }: { rank?: number | null; size?: 'small' | 'default' }) {
if (!rank) return null;
let style: React.CSSProperties;
let textClass: string;
if (rank <= 10) {
style = {
background: 'linear-gradient(135deg, #fbbf24, #f59e0b)',
boxShadow: '0 1px 3px rgba(245, 158, 11, 0.25)',
};
textClass = 'text-amber-900';
} else if (rank <= 30) {
style = {
background: 'rgba(148, 163, 184, 0.15)',
border: '1px solid rgba(148, 163, 184, 0.25)',
};
textClass = 'text-gray-600 dark:text-gray-300';
} else {
style = {
background: 'rgba(239, 68, 68, 0.1)',
border: '1px solid rgba(239, 68, 68, 0.2)',
};
textClass = 'text-red-600 dark:text-red-400';
}
const sizeClasses = size === 'small'
? 'w-5 h-5 text-[10px]'
: 'w-6 h-6 text-xs';
return (
<span
className={`inline-flex items-center justify-center rounded-full font-bold ${textClass} ${sizeClasses} flex-shrink-0 tabular-nums`}
style={style}
title={`Rank #${rank} of analyzed stocks`}
>
{rank}
</span>
);
}
export default function StockCard({ stock, showDetails = true, compact = false }: StockCardProps) {
if (compact) {
return (
<Link
to={`/stock/${stock.symbol}`}
className="flex items-center justify-between px-3 py-2 hover:bg-gray-50 dark:hover:bg-slate-700/50 transition-colors group focus:outline-none focus:bg-nifty-50 dark:focus:bg-nifty-900/30"
className="flex items-center justify-between px-3 py-2.5 hover:bg-gray-50/80 dark:hover:bg-slate-700/30 transition-all group focus:outline-none focus:bg-nifty-50 dark:focus:bg-nifty-900/30 rounded-lg"
role="listitem"
aria-label={`${stock.symbol} - ${stock.company_name} - ${stock.decision} recommendation`}
>
<div className="flex items-center gap-3 flex-1 min-w-0">
<RankBadge rank={stock.rank} size="small" />
<div className={`w-1.5 h-1.5 rounded-full flex-shrink-0 ${
stock.decision === 'BUY' ? 'bg-green-500' :
stock.decision === 'BUY' ? 'bg-emerald-500' :
stock.decision === 'SELL' ? 'bg-red-500' : 'bg-amber-500'
}`} aria-hidden="true" />
<span className="font-medium text-gray-900 dark:text-gray-100 text-sm">{stock.symbol}</span>
<span className="text-gray-400 dark:text-gray-500 text-xs hidden sm:inline" aria-hidden="true">·</span>
<span className="font-semibold text-gray-900 dark:text-gray-100 text-sm">{stock.symbol}</span>
<span className="text-gray-300 dark:text-gray-600 text-xs hidden sm:inline" aria-hidden="true">&middot;</span>
<span className="text-xs text-gray-500 dark:text-gray-400 truncate hidden sm:inline">{stock.company_name}</span>
</div>
<div className="flex items-center gap-2">
@ -123,6 +168,7 @@ export default function StockCard({ stock, showDetails = true, compact = false }
>
<div className="flex-1 min-w-0">
<div className="flex items-center gap-2 mb-0.5">
<RankBadge rank={stock.rank} size="small" />
<h3 className="font-semibold text-gray-900 dark:text-gray-100 truncate text-sm">{stock.symbol}</h3>
<DecisionBadge decision={stock.decision} />
</div>
@ -144,16 +190,17 @@ export function StockCardCompact({ stock }: { stock: StockAnalysis }) {
return (
<Link
to={`/stock/${stock.symbol}`}
className="flex items-center justify-between p-3 rounded-lg hover:bg-gray-50 dark:hover:bg-slate-700/50 transition-colors"
className="flex items-center justify-between p-3 rounded-lg hover:bg-gray-50/80 dark:hover:bg-slate-700/30 transition-all"
>
<div className="flex items-center gap-3">
<RankBadge rank={stock.rank} size="small" />
<div className={`w-2 h-2 rounded-full ${
stock.decision === 'BUY' ? 'bg-green-500' :
stock.decision === 'BUY' ? 'bg-emerald-500' :
stock.decision === 'SELL' ? 'bg-red-500' : 'bg-amber-500'
}`} />
<div>
<span className="font-medium text-gray-900 dark:text-gray-100">{stock.symbol}</span>
<span className="text-gray-400 dark:text-gray-500 mx-2">·</span>
<span className="font-semibold text-gray-900 dark:text-gray-100">{stock.symbol}</span>
<span className="text-gray-300 dark:text-gray-600 mx-2">&middot;</span>
<span className="text-sm text-gray-500 dark:text-gray-400">{stock.company_name}</span>
</div>
</div>

View File

@ -1,7 +1,8 @@
import { Link } from 'react-router-dom';
import { Trophy, AlertTriangle, TrendingUp, TrendingDown } from 'lucide-react';
import { Trophy, AlertTriangle, TrendingUp, TrendingDown, ChevronRight } from 'lucide-react';
import type { TopPick, StockToAvoid } from '../types';
import BackgroundSparkline from './BackgroundSparkline';
import { RankBadge } from './StockCard';
import { getBacktestResult } from '../data/recommendations';
interface TopPicksProps {
@ -9,14 +10,16 @@ interface TopPicksProps {
}
export default function TopPicks({ picks }: TopPicksProps) {
const medals = ['🥇', '🥈', '🥉'];
return (
<div className="card p-4">
<div className="flex items-center gap-2 mb-3">
<Trophy className="w-5 h-5 text-amber-500" />
<h2 className="font-semibold text-gray-900 dark:text-gray-100">Top Picks</h2>
<span className="text-xs text-gray-500 dark:text-gray-400">({picks.length})</span>
<div className="flex items-center gap-2.5 mb-3">
<div className="w-7 h-7 rounded-lg flex items-center justify-center" style={{ background: 'linear-gradient(135deg, #fbbf24, #f59e0b)', boxShadow: '0 2px 6px rgba(245,158,11,0.25)' }}>
<Trophy className="w-3.5 h-3.5 text-amber-900" />
</div>
<div>
<h2 className="font-display font-bold text-gray-900 dark:text-gray-100 text-sm tracking-tight">Top Picks</h2>
<p className="text-[11px] text-gray-500 dark:text-gray-400">Best ranked stocks today</p>
</div>
</div>
<div className="grid grid-cols-1 sm:grid-cols-3 gap-2">
@ -26,40 +29,39 @@ export default function TopPicks({ picks }: TopPicksProps) {
<Link
key={pick.symbol}
to={`/stock/${pick.symbol}`}
className="card-hover p-3 group bg-gradient-to-br from-green-50 to-emerald-50 dark:from-green-900/20 dark:to-emerald-900/20 border-green-200 dark:border-green-800 relative overflow-hidden"
className="group relative overflow-hidden rounded-xl border border-emerald-200/50 dark:border-emerald-800/30 p-3 transition-all hover:border-emerald-300 dark:hover:border-emerald-700/50"
style={{
background: index === 0
? 'linear-gradient(135deg, rgba(16,185,129,0.06), rgba(5,150,105,0.03))'
: 'linear-gradient(135deg, rgba(16,185,129,0.04), rgba(5,150,105,0.01))',
}}
>
{/* Background Chart */}
{backtest && (
<div className="absolute inset-0 opacity-[0.08]">
<BackgroundSparkline
data={backtest.price_history}
trend="up"
/>
<div className="absolute inset-0 opacity-[0.06]">
<BackgroundSparkline data={backtest.price_history} trend="up" />
</div>
)}
{/* Content */}
<div className="relative z-10">
<div className="flex items-center justify-between mb-2">
<div className="flex items-center gap-2">
<span className="text-lg">{medals[index]}</span>
<span className="font-bold text-gray-900 dark:text-gray-100">{pick.symbol}</span>
<RankBadge rank={pick.rank} size="small" />
<span className="font-bold text-gray-900 dark:text-gray-100 text-sm">{pick.symbol}</span>
</div>
<div className="flex items-center gap-1 px-2 py-0.5 rounded-full bg-green-500 text-white text-xs font-medium">
<span className="inline-flex items-center gap-1 px-2 py-0.5 rounded-full text-[11px] font-semibold text-white" style={{ background: 'linear-gradient(135deg, #10b981, #059669)' }}>
<TrendingUp className="w-3 h-3" />
BUY
</div>
</span>
</div>
<p className="text-xs text-gray-600 dark:text-gray-400 line-clamp-2 mb-2">{pick.reason}</p>
<p className="text-[11px] text-gray-600 dark:text-gray-400 line-clamp-2 mb-2 leading-relaxed">{pick.reason}</p>
<div className="flex items-center justify-between">
<span className={`text-xs px-2 py-0.5 rounded-full font-medium ${
pick.risk_level === 'LOW' ? 'bg-green-100 dark:bg-green-900/40 text-green-700 dark:text-green-400' :
pick.risk_level === 'HIGH' ? 'bg-red-100 dark:bg-red-900/40 text-red-700 dark:text-red-400' :
'bg-amber-100 dark:bg-amber-900/40 text-amber-700 dark:text-amber-400'
<span className={`text-[11px] px-2 py-0.5 rounded-md font-medium border ${
pick.risk_level === 'LOW' ? 'bg-emerald-50 dark:bg-emerald-900/20 text-emerald-700 dark:text-emerald-400 border-emerald-200/50 dark:border-emerald-800/30' :
pick.risk_level === 'HIGH' ? 'bg-red-50 dark:bg-red-900/20 text-red-700 dark:text-red-400 border-red-200/50 dark:border-red-800/30' :
'bg-amber-50 dark:bg-amber-900/20 text-amber-700 dark:text-amber-400 border-amber-200/50 dark:border-amber-800/30'
}`}>
{pick.risk_level} Risk
</span>
<span className="text-xs text-green-600 dark:text-green-400 font-medium group-hover:underline">View </span>
<ChevronRight className="w-3.5 h-3.5 text-gray-400 dark:text-gray-500 group-hover:text-emerald-600 dark:group-hover:text-emerald-400 transition-colors" />
</div>
</div>
</Link>
@ -77,10 +79,14 @@ interface StocksToAvoidProps {
export function StocksToAvoid({ stocks }: StocksToAvoidProps) {
return (
<div className="card p-4">
<div className="flex items-center gap-2 mb-3">
<AlertTriangle className="w-5 h-5 text-red-500" />
<h2 className="font-semibold text-gray-900 dark:text-gray-100">Stocks to Avoid</h2>
<span className="text-xs text-gray-500 dark:text-gray-400">({stocks.length})</span>
<div className="flex items-center gap-2.5 mb-3">
<div className="w-7 h-7 rounded-lg flex items-center justify-center bg-red-100 dark:bg-red-900/25">
<AlertTriangle className="w-3.5 h-3.5 text-red-600 dark:text-red-400" />
</div>
<div>
<h2 className="font-display font-bold text-gray-900 dark:text-gray-100 text-sm tracking-tight">Stocks to Avoid</h2>
<p className="text-[11px] text-gray-500 dark:text-gray-400">Lowest ranked stocks today</p>
</div>
</div>
<div className="grid grid-cols-1 sm:grid-cols-2 gap-2">
@ -90,29 +96,24 @@ export function StocksToAvoid({ stocks }: StocksToAvoidProps) {
<Link
key={stock.symbol}
to={`/stock/${stock.symbol}`}
className="card-hover p-3 group bg-gradient-to-br from-red-50 to-rose-50 dark:from-red-900/20 dark:to-rose-900/20 border-red-200 dark:border-red-800 relative overflow-hidden"
className="group relative overflow-hidden rounded-xl border border-red-200/40 dark:border-red-800/25 p-3 transition-all hover:border-red-300 dark:hover:border-red-700/40"
style={{ background: 'linear-gradient(135deg, rgba(239,68,68,0.04), rgba(220,38,38,0.01))' }}
>
{/* Background Chart */}
{backtest && (
<div className="absolute inset-0 opacity-[0.08]">
<BackgroundSparkline
data={backtest.price_history}
trend="down"
/>
<div className="absolute inset-0 opacity-[0.06]">
<BackgroundSparkline data={backtest.price_history} trend="down" />
</div>
)}
{/* Content */}
<div className="relative z-10">
<div className="flex items-center justify-between mb-2">
<span className="font-bold text-gray-900 dark:text-gray-100">{stock.symbol}</span>
<div className="flex items-center gap-1 px-2 py-0.5 rounded-full bg-red-500 text-white text-xs font-medium">
<span className="font-bold text-gray-900 dark:text-gray-100 text-sm">{stock.symbol}</span>
<span className="inline-flex items-center gap-1 px-2 py-0.5 rounded-full text-[11px] font-semibold text-white" style={{ background: 'linear-gradient(135deg, #ef4444, #dc2626)' }}>
<TrendingDown className="w-3 h-3" />
SELL
</div>
</span>
</div>
<p className="text-xs text-gray-600 dark:text-gray-400 line-clamp-2 mb-2">{stock.reason}</p>
<span className="text-xs text-red-600 dark:text-red-400 font-medium group-hover:underline">View </span>
<p className="text-[11px] text-gray-600 dark:text-gray-400 line-clamp-2 mb-2 leading-relaxed">{stock.reason}</p>
<ChevronRight className="w-3.5 h-3.5 text-gray-400 dark:text-gray-500 group-hover:text-red-600 dark:group-hover:text-red-400 transition-colors" />
</div>
</Link>
);

View File

@ -26,13 +26,15 @@
--color-hold-dark: #b45309;
/* Custom fonts */
--font-sans: 'Inter', system-ui, sans-serif;
--font-display: 'Lexend', system-ui, sans-serif;
--font-sans: 'DM Sans', system-ui, -apple-system, sans-serif;
--font-display: 'Plus Jakarta Sans', system-ui, sans-serif;
}
@layer base {
html {
scroll-behavior: smooth;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
html.dark {
@ -40,30 +42,99 @@
}
body {
@apply font-sans antialiased bg-gray-50 text-gray-900;
@apply font-sans antialiased text-gray-900;
margin: 0;
background: linear-gradient(135deg, #f8fafc 0%, #f0f4f8 50%, #e8eef4 100%);
min-height: 100vh;
}
html.dark body {
@apply bg-slate-900 text-gray-100;
@apply text-gray-100;
background: linear-gradient(135deg, #0b1120 0%, #0f172a 40%, #131c31 100%);
}
/* Subtle dot grid background pattern */
body::before {
content: '';
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-image: radial-gradient(circle at 1px 1px, rgba(148, 163, 184, 0.08) 1px, transparent 0);
background-size: 32px 32px;
pointer-events: none;
z-index: 0;
}
html.dark body::before {
background-image: radial-gradient(circle at 1px 1px, rgba(148, 163, 184, 0.04) 1px, transparent 0);
}
#root {
position: relative;
z-index: 1;
}
/* Better default focus styles */
:focus-visible {
outline: 2px solid var(--color-nifty-500);
outline-offset: 2px;
border-radius: 4px;
}
}
@layer components {
.card {
@apply bg-white rounded-xl shadow-sm border border-gray-100 overflow-hidden;
@apply bg-white/80 rounded-xl border border-gray-200/60 overflow-hidden;
backdrop-filter: blur(12px);
box-shadow:
0 1px 2px rgba(0, 0, 0, 0.04),
0 2px 8px rgba(0, 0, 0, 0.02);
}
html.dark .card {
@apply bg-slate-800 border-slate-700;
@apply border-slate-700/60;
background: rgba(30, 41, 59, 0.7);
box-shadow:
0 1px 2px rgba(0, 0, 0, 0.2),
0 0 0 1px rgba(148, 163, 184, 0.04);
}
.card-hover {
@apply bg-white rounded-xl shadow-sm border border-gray-100 overflow-hidden transition-all duration-200 hover:shadow-md hover:border-gray-200 focus-within:ring-2 focus-within:ring-nifty-500 focus-within:ring-offset-1;
@apply bg-white/80 rounded-xl border border-gray-200/60 overflow-hidden;
backdrop-filter: blur(12px);
box-shadow:
0 1px 2px rgba(0, 0, 0, 0.04),
0 2px 8px rgba(0, 0, 0, 0.02);
transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1);
}
.card-hover:hover {
@apply border-gray-300/80;
box-shadow:
0 4px 12px rgba(0, 0, 0, 0.06),
0 1px 3px rgba(0, 0, 0, 0.04);
transform: translateY(-1px);
}
.card-hover:focus-within {
@apply ring-2 ring-nifty-500 ring-offset-1;
}
html.dark .card-hover {
@apply bg-slate-800 border-slate-700 hover:border-slate-600;
@apply border-slate-700/60;
background: rgba(30, 41, 59, 0.7);
box-shadow:
0 1px 2px rgba(0, 0, 0, 0.2),
0 0 0 1px rgba(148, 163, 184, 0.04);
}
html.dark .card-hover:hover {
@apply border-slate-600/80;
box-shadow:
0 4px 16px rgba(0, 0, 0, 0.3),
0 0 0 1px rgba(148, 163, 184, 0.08);
}
html.dark .card-hover:focus-within {
@ -71,11 +142,25 @@
}
.btn {
@apply inline-flex items-center justify-center px-4 py-2 rounded-lg font-medium transition-all duration-200;
@apply inline-flex items-center justify-center px-4 py-2 rounded-lg font-medium;
transition: all 0.15s cubic-bezier(0.4, 0, 0.2, 1);
}
.btn-primary {
@apply inline-flex items-center justify-center px-4 py-2 rounded-lg font-medium transition-all duration-200 bg-nifty-600 text-white hover:bg-nifty-700 focus:ring-2 focus:ring-nifty-500 focus:ring-offset-2;
@apply inline-flex items-center justify-center px-4 py-2 rounded-lg font-semibold text-white;
background: linear-gradient(135deg, var(--color-nifty-600), var(--color-nifty-700));
box-shadow: 0 1px 3px rgba(2, 132, 199, 0.3), inset 0 1px 0 rgba(255, 255, 255, 0.1);
transition: all 0.15s cubic-bezier(0.4, 0, 0.2, 1);
}
.btn-primary:hover {
background: linear-gradient(135deg, var(--color-nifty-700), var(--color-nifty-800));
box-shadow: 0 2px 8px rgba(2, 132, 199, 0.4), inset 0 1px 0 rgba(255, 255, 255, 0.1);
transform: translateY(-0.5px);
}
.btn-primary:active {
transform: translateY(0);
}
html.dark .btn-primary {
@ -83,7 +168,8 @@
}
.btn-secondary {
@apply inline-flex items-center justify-center px-4 py-2 rounded-lg font-medium transition-all duration-200 bg-gray-100 text-gray-700 hover:bg-gray-200;
@apply inline-flex items-center justify-center px-4 py-2 rounded-lg font-medium bg-gray-100 text-gray-700 hover:bg-gray-200;
transition: all 0.15s cubic-bezier(0.4, 0, 0.2, 1);
}
html.dark .btn-secondary {
@ -91,11 +177,12 @@
}
.badge {
@apply inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium;
@apply inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-semibold;
letter-spacing: 0.01em;
}
.badge-buy {
@apply inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-bull-light text-bull-dark;
@apply inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-semibold bg-bull-light text-bull-dark;
}
html.dark .badge-buy {
@ -103,7 +190,7 @@
}
.badge-sell {
@apply inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-bear-light text-bear-dark;
@apply inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-semibold bg-bear-light text-bear-dark;
}
html.dark .badge-sell {
@ -111,7 +198,7 @@
}
.badge-hold {
@apply inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-hold-light text-hold-dark;
@apply inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-semibold bg-hold-light text-hold-dark;
}
html.dark .badge-hold {
@ -119,15 +206,17 @@
}
.gradient-text {
@apply bg-gradient-to-r from-nifty-600 to-nifty-800 bg-clip-text text-transparent;
@apply bg-clip-text text-transparent;
background-image: linear-gradient(135deg, var(--color-nifty-500), var(--color-nifty-700));
}
html.dark .gradient-text {
@apply from-nifty-400 to-nifty-600;
background-image: linear-gradient(135deg, var(--color-nifty-400), var(--color-nifty-500));
}
.section-title {
@apply text-2xl font-display font-semibold text-gray-900;
@apply text-2xl font-display font-bold text-gray-900;
letter-spacing: -0.02em;
}
html.dark .section-title {
@ -146,9 +235,9 @@
min-width: 44px;
}
/* Animation utilities */
/* Entrance animation */
.animate-in {
animation: animate-in 0.2s ease-out;
animation: animate-in 0.3s cubic-bezier(0.16, 1, 0.3, 1);
}
.slide-in-from-top-2 {
@ -166,11 +255,27 @@
}
}
/* Staggered fade-in for lists */
.fade-in-up {
animation: fadeInUp 0.4s cubic-bezier(0.16, 1, 0.3, 1) both;
}
@keyframes fadeInUp {
from {
opacity: 0;
transform: translateY(8px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
/* Shimmer animation for stocks being analyzed */
.shimmer-effect {
background: linear-gradient(90deg, transparent 0%, rgba(59, 130, 246, 0.08) 50%, transparent 100%);
background: linear-gradient(90deg, transparent 0%, rgba(59, 130, 246, 0.06) 50%, transparent 100%);
background-size: 200% 100%;
animation: shimmer 1.5s ease-in-out infinite;
animation: shimmer 2s ease-in-out infinite;
}
@keyframes shimmer {
@ -178,6 +283,16 @@
100% { background-position: -200% 0; }
}
/* Subtle pulse glow for active elements */
.pulse-glow {
animation: pulseGlow 2s ease-in-out infinite;
}
@keyframes pulseGlow {
0%, 100% { box-shadow: 0 0 0 0 rgba(14, 165, 233, 0); }
50% { box-shadow: 0 0 12px 2px rgba(14, 165, 233, 0.15); }
}
/* Smooth scrollbar styling */
.scroll-smooth {
scroll-behavior: smooth;
@ -189,9 +304,9 @@
background-color: transparent !important;
}
/* Custom scrollbar for stock lists */
/* Premium scrollbar */
.overflow-y-auto::-webkit-scrollbar {
width: 6px;
width: 5px;
}
.overflow-y-auto::-webkit-scrollbar-track {
@ -199,19 +314,24 @@
}
.overflow-y-auto::-webkit-scrollbar-thumb {
background: #d1d5db;
border-radius: 3px;
background: rgba(156, 163, 175, 0.4);
border-radius: 100px;
}
.overflow-y-auto::-webkit-scrollbar-thumb:hover {
background: #9ca3af;
background: rgba(156, 163, 175, 0.6);
}
html.dark .overflow-y-auto::-webkit-scrollbar-thumb {
background: #475569;
background: rgba(71, 85, 105, 0.5);
}
html.dark .overflow-y-auto::-webkit-scrollbar-thumb:hover {
background: #64748b;
background: rgba(100, 116, 139, 0.6);
}
/* Number tabular for data alignment */
.tabular-nums {
font-variant-numeric: tabular-nums;
}
}

View File

@ -2,7 +2,7 @@ import { useState, useMemo, useEffect, useCallback, useRef } from 'react';
import { Link } from 'react-router-dom';
import { Calendar, RefreshCw, Filter, ChevronRight, TrendingUp, TrendingDown, Minus, History, Search, X, Play, Loader2, Square, AlertCircle, Terminal } from 'lucide-react';
import TopPicks, { StocksToAvoid } from '../components/TopPicks';
import { DecisionBadge, HoldDaysBadge } from '../components/StockCard';
import { DecisionBadge, HoldDaysBadge, RankBadge } from '../components/StockCard';
import TerminalModal from '../components/TerminalModal';
import HowItWorks from '../components/HowItWorks';
import BackgroundSparkline from '../components/BackgroundSparkline';
@ -52,6 +52,7 @@ export default function Dashboard() {
}, [fetchRecommendation]);
const [filter, setFilter] = useState<FilterType>('ALL');
const [searchQuery, setSearchQuery] = useState('');
const [sortBy, setSortBy] = useState<'rank' | 'symbol'>('rank');
const { settings } = useSettings();
const { addNotification } = useNotification();
@ -314,7 +315,7 @@ export default function Dashboard() {
});
}, [isAnalyzing, analysisProgress, recommendation]);
// Filter grid items based on filter and search query
// Filter grid items based on filter and search query, then sort
const filteredItems = useMemo(() => {
let result = stockGridItems;
if (filter !== 'ALL') {
@ -330,8 +331,16 @@ export default function Dashboard() {
item.company_name.toLowerCase().includes(query)
);
}
if (sortBy === 'rank') {
result = [...result].sort((a, b) => {
const aRank = a.analysis?.rank ?? Infinity;
const bRank = b.analysis?.rank ?? Infinity;
if (aRank !== bRank) return aRank - bRank;
return a.symbol.localeCompare(b.symbol);
});
}
return result;
}, [stockGridItems, filter, searchQuery]);
}, [stockGridItems, filter, searchQuery, sortBy]);
// Show loading state — but also include analysis progress banner if running
if (isLoadingData || !recommendation) {
@ -401,13 +410,13 @@ export default function Dashboard() {
return (
<div className="space-y-4">
{/* Compact Header with Stats */}
<section className="card p-4">
<section className="card p-5">
<div className="flex flex-col lg:flex-row lg:items-center justify-between gap-4">
<div>
<h1 className="text-2xl font-display font-bold text-gray-900 dark:text-gray-100">
<h1 className="text-2xl font-display font-bold text-gray-900 dark:text-gray-100 tracking-tight">
Nifty 50 <span className="gradient-text">AI Recommendations</span>
</h1>
<div className="flex items-center gap-2 mt-1 text-sm text-gray-500 dark:text-gray-400">
<div className="flex items-center gap-2 mt-1.5 text-sm text-gray-500 dark:text-gray-400">
<Calendar className="w-3.5 h-3.5" />
<span>{new Date(recommendation.date).toLocaleDateString('en-IN', {
weekday: 'short',
@ -476,11 +485,11 @@ export default function Dashboard() {
</div>
{/* Progress bar */}
<div className="mt-3">
<div className="flex h-2 rounded-full overflow-hidden bg-gray-100 dark:bg-slate-700">
<div className="bg-green-500 transition-all" style={{ width: `${buyPct}%` }} />
<div className="bg-amber-500 transition-all" style={{ width: `${holdPct}%` }} />
<div className="bg-red-500 transition-all" style={{ width: `${sellPct}%` }} />
<div className="mt-4">
<div className="flex h-1.5 rounded-full overflow-hidden bg-gray-100 dark:bg-slate-700/50">
<div className="transition-all duration-500 rounded-l-full" style={{ width: `${buyPct}%`, background: 'linear-gradient(90deg, #10b981, #059669)' }} />
<div className="transition-all duration-500" style={{ width: `${holdPct}%`, background: 'linear-gradient(90deg, #f59e0b, #d97706)' }} />
<div className="transition-all duration-500 rounded-r-full" style={{ width: `${sellPct}%`, background: 'linear-gradient(90deg, #ef4444, #dc2626)' }} />
</div>
</div>
@ -560,12 +569,12 @@ export default function Dashboard() {
{/* All Stocks Section with Integrated Filter */}
<section className="card">
<div className="p-3 border-b border-gray-100 dark:border-slate-700 bg-gray-50/50 dark:bg-slate-700/50">
<div className="p-4 border-b border-gray-100/80 dark:border-slate-700/40">
<div className="flex flex-col gap-3">
<div className="flex flex-col sm:flex-row sm:items-center justify-between gap-3">
<div className="flex items-center gap-2">
<Filter className="w-4 h-4 text-gray-400 dark:text-gray-500" />
<h2 className="font-semibold text-gray-900 dark:text-gray-100">
<h2 className="font-display font-bold text-gray-900 dark:text-gray-100 text-sm tracking-tight">
{isAnalyzing ? `All 50 Stocks (${analysisProgress?.completed || 0} analyzed)` : `All ${total} Stocks`}
</h2>
</div>
@ -614,6 +623,17 @@ export default function Dashboard() {
>
Sell ({sell})
</button>
<span className="mx-0.5 text-gray-300 dark:text-gray-600">|</span>
<button
onClick={() => setSortBy(sortBy === 'rank' ? 'symbol' : 'rank')}
className={`px-2.5 py-1 text-xs font-medium rounded-md transition-all focus:outline-none focus:ring-2 focus:ring-nifty-500 focus:ring-offset-1 dark:focus:ring-offset-slate-800 ${
sortBy === 'rank'
? 'bg-amber-100 dark:bg-amber-900/30 text-amber-700 dark:text-amber-400'
: 'bg-gray-100 dark:bg-slate-600 text-gray-700 dark:text-gray-200 hover:bg-gray-200 dark:hover:bg-slate-500'
}`}
>
{sortBy === 'rank' ? '#Rank' : 'A-Z'}
</button>
</div>
</div>
{/* Search Bar */}
@ -658,6 +678,7 @@ export default function Dashboard() {
)}
<div className="relative z-10">
<div className="flex items-center gap-1.5 mb-0.5">
<RankBadge rank={item.analysis.rank} size="small" />
<span className="font-semibold text-sm text-gray-900 dark:text-gray-100">{item.symbol}</span>
<DecisionBadge decision={item.analysis.decision} size="small" />
</div>
@ -762,14 +783,18 @@ export default function Dashboard() {
{/* Compact CTA */}
<Link
to="/history"
className="card flex items-center justify-between p-4 bg-gradient-to-r from-nifty-600 to-nifty-700 text-white hover:from-nifty-700 hover:to-nifty-800 transition-all group focus:outline-none focus:ring-2 focus:ring-nifty-500 focus:ring-offset-2"
className="flex items-center justify-between p-4 rounded-xl text-white group focus:outline-none focus:ring-2 focus:ring-nifty-500 focus:ring-offset-2 transition-all hover:shadow-lg"
style={{
background: 'linear-gradient(135deg, #0284c7, #0369a1)',
boxShadow: '0 2px 8px rgba(2, 132, 199, 0.25)',
}}
aria-label="View historical stock recommendations"
>
<div className="flex items-center gap-3">
<History className="w-5 h-5" aria-hidden="true" />
<span className="font-semibold">View Historical Recommendations</span>
<History className="w-5 h-5 opacity-80" aria-hidden="true" />
<span className="font-display font-bold tracking-tight">View Historical Recommendations</span>
</div>
<ChevronRight className="w-5 h-5 group-hover:translate-x-1 transition-transform" aria-hidden="true" />
<ChevronRight className="w-5 h-5 group-hover:translate-x-1 transition-transform opacity-80" aria-hidden="true" />
</Link>
{/* Terminal Modal */}

View File

@ -3,7 +3,7 @@ import { Link } from 'react-router-dom';
import { Calendar, TrendingUp, TrendingDown, Minus, ChevronRight, BarChart3, Target, HelpCircle, Activity, Calculator, LineChart, PieChart, Shield, Filter, Loader2, AlertCircle } from 'lucide-react';
import { sampleRecommendations, getBacktestResult as getStaticBacktestResult, calculateAccuracyMetrics as calculateStaticAccuracyMetrics, getDateStats as getStaticDateStats, getOverallStats as getStaticOverallStats, getReturnBreakdown as getStaticReturnBreakdown } from '../data/recommendations';
import type { ReturnBreakdown } from '../data/recommendations';
import { DecisionBadge, HoldDaysBadge } from '../components/StockCard';
import { DecisionBadge, HoldDaysBadge, RankBadge } from '../components/StockCard';
import Sparkline from '../components/Sparkline';
import AccuracyBadge from '../components/AccuracyBadge';
import AccuracyExplainModal from '../components/AccuracyExplainModal';
@ -281,6 +281,9 @@ export default function History() {
const totalPredictions = totalBuy + totalSell + totalHold;
const totalCorrect = correctBuy + correctSell + correctHold;
// Only include dates with enough data points for meaningful accuracy
if (totalPredictions < 3) continue;
trendData.push({
date,
overall: totalPredictions > 0 ? Math.round((totalCorrect / totalPredictions) * 100) : 0,
@ -831,10 +834,18 @@ export default function History() {
const rec = getRecommendation(date);
if (!rec) return [];
let stocks: StockAnalysis[];
if (dateFilterMode === 'topPicks') {
return rec.top_picks.map(pick => rec.analysis[pick.symbol]).filter(Boolean);
stocks = rec.top_picks.map(pick => rec.analysis[pick.symbol]).filter(Boolean);
} else {
stocks = Object.values(rec.analysis);
}
return Object.values(rec.analysis);
// Sort by rank when available
return [...stocks].sort((a, b) => {
const aRank = a.rank ?? Infinity;
const bRank = b.rank ?? Infinity;
return aRank - bRank;
});
};
// Build ReturnBreakdown from real batch backtest data for the modal
@ -1280,6 +1291,7 @@ export default function History() {
className="flex items-center justify-between px-3 py-2 hover:bg-gray-50 dark:hover:bg-slate-700/50 transition-colors group"
>
<div className="flex items-center gap-3 flex-1 min-w-0">
<RankBadge rank={stock.rank} size="small" />
<span className="font-medium text-gray-900 dark:text-gray-100 text-sm">{stock.symbol}</span>
<span className="text-xs text-gray-500 dark:text-gray-400 hidden sm:inline truncate">{stock.company_name}</span>
{realData && (

View File

@ -8,7 +8,7 @@ import {
import { NIFTY_50_STOCKS } from '../types';
import type { DailyRecommendation, StockAnalysis } from '../types';
import { sampleRecommendations, getStockHistory as getStaticStockHistory, getRawAnalysis } from '../data/recommendations';
import { DecisionBadge, ConfidenceBadge, RiskBadge, HoldDaysBadge } from '../components/StockCard';
import { DecisionBadge, ConfidenceBadge, RiskBadge, HoldDaysBadge, RankBadge } from '../components/StockCard';
import AIAnalysisPanel from '../components/AIAnalysisPanel';
import StockPriceChart from '../components/StockPriceChart';
import {
@ -31,6 +31,7 @@ interface BacktestResult {
holdDays: number | null;
primaryReturn: number | null; // return_at_hold ?? return_1d
predictionCorrect: boolean | null;
rank?: number | null;
isLoading?: boolean;
}
@ -68,7 +69,7 @@ export default function StockDetail() {
// API-first loading for recommendation data
const [latestRecommendation, setLatestRecommendation] = useState<DailyRecommendation | null>(null);
const [analysis, setAnalysis] = useState<StockAnalysis | undefined>(undefined);
const [history, setHistory] = useState<Array<{ date: string; decision: string; confidence?: string; risk?: string }>>([]);
const [history, setHistory] = useState<Array<{ date: string; decision: string; confidence?: string; risk?: string; hold_days?: number | null; rank?: number | null }>>([]);
// Fetch recommendation and stock history from API
useEffect(() => {
const fetchData = async () => {
@ -147,6 +148,7 @@ export default function StockDetail() {
holdDays: backtest.hold_days ?? null,
primaryReturn,
predictionCorrect,
rank: entry.rank,
});
} else {
// No backtest data available for this date
@ -159,6 +161,7 @@ export default function StockDetail() {
holdDays: null,
primaryReturn: null,
predictionCorrect: null,
rank: entry.rank,
});
}
} catch (err) {
@ -172,6 +175,7 @@ export default function StockDetail() {
holdDays: null,
primaryReturn: null,
predictionCorrect: null,
rank: entry.rank,
});
}
}
@ -585,6 +589,11 @@ export default function StockDetail() {
<div className="flex items-center gap-3">
<div>
<div className="flex items-center gap-2">
{analysis?.rank && (
<span className="inline-flex items-center justify-center w-7 h-7 rounded-full text-sm font-bold bg-white/20 text-white" title={`Rank #${analysis.rank}`}>
#{analysis.rank}
</span>
)}
<h1 className="text-2xl font-display font-bold">{stock.symbol}</h1>
{analysis?.decision && (
<span className="inline-flex items-center gap-1 px-2 py-0.5 rounded-full text-xs font-semibold bg-white/20">
@ -633,6 +642,12 @@ export default function StockDetail() {
<HoldDaysBadge holdDays={analysis.hold_days} decision={analysis.decision} />
</div>
)}
{analysis.rank && (
<div className="flex items-center gap-1.5 sm:gap-2">
<span className="text-[10px] sm:text-xs text-gray-500 dark:text-gray-400">Rank:</span>
<RankBadge rank={analysis.rank} />
</div>
)}
</div>
)}
</section>
@ -953,8 +968,9 @@ export default function StockDetail() {
</div>
</div>
{/* Decision Badge + Hold Days */}
{/* Rank + Decision Badge + Hold Days */}
<div className="flex items-center gap-1.5 flex-shrink-0">
<RankBadge rank={entry.rank} size="small" />
<DecisionBadge decision={entry.decision as 'BUY' | 'SELL' | 'HOLD'} size="small" />
{entry.holdDays && entry.decision !== 'SELL' && (
<span className="text-[10px] text-blue-600 dark:text-blue-400 font-medium">{entry.holdDays}d</span>

View File

@ -58,6 +58,7 @@ export interface StockAnalysis {
risk?: Risk;
raw_analysis?: string;
hold_days?: number | null;
rank?: number | null;
error?: string | null;
}
@ -105,6 +106,7 @@ export interface HistoricalEntry {
confidence?: Confidence;
risk?: Risk;
hold_days?: number | null;
rank?: number | null;
}
export interface StockHistory {

View File

@ -36,8 +36,8 @@ export default {
}
},
fontFamily: {
sans: ['Inter', 'system-ui', 'sans-serif'],
display: ['Lexend', 'system-ui', 'sans-serif'],
sans: ['DM Sans', 'system-ui', '-apple-system', 'sans-serif'],
display: ['Plus Jakarta Sans', 'system-ui', 'sans-serif'],
},
},
},

BIN
history-accuracy-fix.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 57 KiB

BIN
history-bottom.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 42 KiB

BIN
history-datecards.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 51 KiB

BIN
history-dates.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 42 KiB

BIN
history-polished.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 57 KiB

BIN
history-ranking.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 58 KiB

BIN
stockdetail-polished.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 63 KiB

BIN
stockdetail-ranking.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 53 KiB

View File

@ -1,18 +1,40 @@
from langchain_core.prompts import ChatPromptTemplate, MessagesPlaceholder
import time
import json
from tradingagents.agents.utils.agent_utils import get_fundamentals, get_balance_sheet, get_cashflow, get_income_statement, get_insider_sentiment, get_insider_transactions, execute_text_tool_calls, needs_followup_call, execute_default_tools, generate_analysis_from_data
from tradingagents.agents.utils.agent_utils import get_fundamentals, get_balance_sheet, get_cashflow, get_income_statement, get_insider_sentiment, get_insider_transactions, get_analyst_recommendations, get_earnings_data, get_institutional_holders, execute_text_tool_calls, needs_followup_call, execute_default_tools, generate_analysis_from_data
from tradingagents.dataflows.config import get_config
from tradingagents.log_utils import add_log, step_timer, symbol_progress
ANALYST_RESPONSE_FORMAT = """
RESPONSE FORMAT RULES:
- Keep your analysis concise: maximum 3000 characters total
- Use a compact markdown table to organize key findings
- Do NOT repeat raw data values verbatim summarize trends and insights
- Complete your ENTIRE analysis in a SINGLE response do not split across multiple messages"""
RESPONSE FORMAT (follow this structure exactly):
## EXECUTIVE SUMMARY
2-3 sentences: Key fundamental finding and directional bias (BULLISH / BEARISH / NEUTRAL).
## KEY DATA POINTS
- Bullet list of the 5 most significant fundamental metrics with specific numbers
- Include valuation, profitability, institutional positioning, analyst consensus
## SIGNAL ASSESSMENT
Your overall reading: BULLISH / BEARISH / NEUTRAL
1-2 sentences explaining why, referencing specific financial data.
## RISK FACTORS
2-3 specific fundamental risks (debt, margins, earnings misses, etc.).
## CONFIDENCE: HIGH / MEDIUM / LOW
1 sentence justifying your confidence level.
| Metric | Value | Signal | Significance |
|--------|-------|--------|-------------|
| (fill with key fundamental metrics) |
RULES:
- Maximum 3000 characters total
- Do NOT repeat raw data verbatim summarize trends and insights
- Complete your ENTIRE analysis in a SINGLE response"""
def create_fundamentals_analyst(llm):
@ -26,12 +48,20 @@ def create_fundamentals_analyst(llm):
get_balance_sheet,
get_cashflow,
get_income_statement,
get_analyst_recommendations,
get_earnings_data,
get_institutional_holders,
]
system_message = (
"You are a researcher tasked with analyzing fundamental information over the past week about a company. Please write a comprehensive report of the company's fundamental information such as financial documents, company profile, basic company financials, and company financial history to gain a full view of the company's fundamental information to inform traders. Make sure to include as much detail as possible. Do not simply state the trends are mixed, provide detailed and finegrained analysis and insights that may help traders make decisions."
+ " Make sure to append a Markdown table at the end of the report to organize key points in the report, organized and easy to read."
+ " Use the available tools: `get_fundamentals` for comprehensive company analysis, `get_balance_sheet`, `get_cashflow`, and `get_income_statement` for specific financial statements."
"You are a fundamentals analyst tasked with analyzing a company's financial health, valuation, and institutional positioning. "
"Use ALL available tools to build a comprehensive fundamental picture:\n"
"- `get_fundamentals`: Company overview, valuation ratios, profitability metrics\n"
"- `get_balance_sheet`, `get_cashflow`, `get_income_statement`: Financial statements\n"
"- `get_analyst_recommendations`: Wall Street analyst consensus and recent rating changes\n"
"- `get_earnings_data`: Earnings dates, EPS estimates vs actuals, earnings surprises\n"
"- `get_institutional_holders`: Top institutional holders, insider vs institutional ownership\n\n"
"Provide specific numbers and quantitative evidence. Do not simply state trends are mixed."
+ ANALYST_RESPONSE_FORMAT,
)

View File

@ -6,14 +6,36 @@ from tradingagents.dataflows.config import get_config
from tradingagents.log_utils import add_log, step_timer, symbol_progress
# Verbosity format appended to analyst prompts
# Structured response format for explainability
ANALYST_RESPONSE_FORMAT = """
RESPONSE FORMAT RULES:
- Keep your analysis concise: maximum 3000 characters total
- Use a compact markdown table to organize key findings
- Do NOT repeat raw data values verbatim summarize trends and insights
- Complete your ENTIRE analysis in a SINGLE response do not split across multiple messages"""
RESPONSE FORMAT (follow this structure exactly):
## EXECUTIVE SUMMARY
2-3 sentences: Key finding and directional bias (BULLISH / BEARISH / NEUTRAL).
## KEY DATA POINTS
- Bullet list of the 5 most significant data points with specific numbers
- Each point should include the metric name, value, and what it signals
## SIGNAL ASSESSMENT
Your overall reading: BULLISH / BEARISH / NEUTRAL
1-2 sentences explaining why, referencing specific data.
## RISK FACTORS
2-3 specific risks that could invalidate your assessment.
## CONFIDENCE: HIGH / MEDIUM / LOW
1 sentence justifying your confidence level.
| Metric | Value | Signal | Significance |
|--------|-------|--------|-------------|
| (fill with key metrics from your analysis) |
RULES:
- Maximum 3000 characters total
- Do NOT repeat raw data verbatim summarize trends and insights
- Complete your ENTIRE analysis in a SINGLE response"""
def create_market_analyst(llm):
@ -29,32 +51,39 @@ def create_market_analyst(llm):
]
system_message = (
"""You are a trading assistant tasked with analyzing financial markets. Your role is to select the **most relevant indicators** for a given market condition or trading strategy from the following list. The goal is to choose up to **8 indicators** that provide complementary insights without redundancy. Categories and each category's indicators are:
"""You are a market/technical analyst tasked with analyzing financial markets. Select up to **8 of the most relevant indicators** for the current market condition. Available indicators by category:
Moving Averages:
- close_50_sma: 50 SMA: A medium-term trend indicator. Usage: Identify trend direction and serve as dynamic support/resistance. Tips: It lags price; combine with faster indicators for timely signals.
- close_200_sma: 200 SMA: A long-term trend benchmark. Usage: Confirm overall market trend and identify golden/death cross setups. Tips: It reacts slowly; best for strategic trend confirmation rather than frequent trading entries.
- close_10_ema: 10 EMA: A responsive short-term average. Usage: Capture quick shifts in momentum and potential entry points. Tips: Prone to noise in choppy markets; use alongside longer averages for filtering false signals.
- close_5_ema: 5 EMA ultra-responsive short-term momentum
- close_10_ema: 10 EMA responsive short-term average
- close_20_sma: 20 SMA short-term trend (Bollinger baseline)
- close_50_sma: 50 SMA medium-term trend direction
- close_200_sma: 200 SMA long-term trend benchmark, golden/death cross
MACD Related:
- macd: MACD: Computes momentum via differences of EMAs. Usage: Look for crossovers and divergence as signals of trend changes. Tips: Confirm with other indicators in low-volatility or sideways markets.
- macds: MACD Signal: An EMA smoothing of the MACD line. Usage: Use crossovers with the MACD line to trigger trades. Tips: Should be part of a broader strategy to avoid false positives.
- macdh: MACD Histogram: Shows the gap between the MACD line and its signal. Usage: Visualize momentum strength and spot divergence early. Tips: Can be volatile; complement with additional filters in fast-moving markets.
- macd: MACD line momentum via EMA differences
- macds: MACD Signal smoothed MACD for crossover triggers
- macdh: MACD Histogram momentum strength visualization
Momentum Indicators:
- rsi: RSI: Measures momentum to flag overbought/oversold conditions. Usage: Apply 70/30 thresholds and watch for divergence to signal reversals. Tips: In strong trends, RSI may remain extreme; always cross-check with trend analysis.
Momentum & Oscillators:
- rsi: RSI overbought(>70)/oversold(<30) momentum
- kdjk: Stochastic %K momentum oscillator, overbought(>80)/oversold(<20)
- cci: CCI price deviation from mean, overbought(>100)/oversold(<-100)
Volatility Indicators:
- boll: Bollinger Middle: A 20 SMA serving as the basis for Bollinger Bands. Usage: Acts as a dynamic benchmark for price movement. Tips: Combine with the upper and lower bands to effectively spot breakouts or reversals.
- boll_ub: Bollinger Upper Band: Typically 2 standard deviations above the middle line. Usage: Signals potential overbought conditions and breakout zones. Tips: Confirm signals with other tools; prices may ride the band in strong trends.
- boll_lb: Bollinger Lower Band: Typically 2 standard deviations below the middle line. Usage: Indicates potential oversold conditions. Tips: Use additional analysis to avoid false reversal signals.
- atr: ATR: Averages true range to measure volatility. Usage: Set stop-loss levels and adjust position sizes based on current market volatility. Tips: It's a reactive measure, so use it as part of a broader risk management strategy.
Trend Strength:
- adx: ADX trend strength regardless of direction (>25 = strong trend, <20 = ranging)
Volume-Based Indicators:
- vwma: VWMA: A moving average weighted by volume. Usage: Confirm trends by integrating price action with volume data. Tips: Watch for skewed results from volume spikes; use in combination with other volume analyses.
Volatility:
- boll: Bollinger Middle (20 SMA) dynamic price benchmark
- boll_ub: Bollinger Upper overbought/breakout zone
- boll_lb: Bollinger Lower oversold/support zone
- atr: ATR volatility for stop-loss and position sizing
- Select indicators that provide diverse and complementary information. Avoid redundancy (e.g., do not select both rsi and stochrsi). Also briefly explain why they are suitable for the given market context. When you tool call, please use the exact name of the indicators provided above as they are defined parameters, otherwise your call will fail. Please make sure to call get_stock_data first to retrieve the CSV that is needed to generate indicators. Then use get_indicators with the specific indicator names. Write a very detailed and nuanced report of the trends you observe. Do not simply state the trends are mixed, provide detailed and finegrained analysis and insights that may help traders make decisions."""
+ """ Make sure to append a Markdown table at the end of the report to organize key points in the report, organized and easy to read."""
Volume-Based:
- vwma: VWMA volume-weighted moving average for trend confirmation
- mfi: MFI money flow index combining price and volume
Strategy: Call `get_stock_data` first, then `get_indicators` with specific indicator names. Select indicators that provide diverse, complementary information avoid redundancy. Provide specific numbers and quantitative reasoning, not generic statements."""
+ ANALYST_RESPONSE_FORMAT
)

View File

@ -1,18 +1,40 @@
from langchain_core.prompts import ChatPromptTemplate, MessagesPlaceholder
import time
import json
from tradingagents.agents.utils.agent_utils import get_news, get_global_news, execute_text_tool_calls, needs_followup_call, execute_default_tools, generate_analysis_from_data
from tradingagents.agents.utils.agent_utils import get_news, get_global_news, get_earnings_calendar, execute_text_tool_calls, needs_followup_call, execute_default_tools, generate_analysis_from_data
from tradingagents.dataflows.config import get_config
from tradingagents.log_utils import add_log, step_timer, symbol_progress
ANALYST_RESPONSE_FORMAT = """
RESPONSE FORMAT RULES:
- Keep your analysis concise: maximum 3000 characters total
- Use a compact markdown table to organize key findings
- Do NOT repeat raw data values verbatim summarize trends and insights
- Complete your ENTIRE analysis in a SINGLE response do not split across multiple messages"""
RESPONSE FORMAT (follow this structure exactly):
## EXECUTIVE SUMMARY
2-3 sentences: Key news finding and directional bias (BULLISH / BEARISH / NEUTRAL).
## KEY DATA POINTS
- Bullet list of the 5 most significant news items with specific details
- Include company-specific news, macro factors, upcoming catalysts
## SIGNAL ASSESSMENT
Your overall reading: BULLISH / BEARISH / NEUTRAL
1-2 sentences explaining why, referencing specific news events.
## RISK FACTORS
2-3 specific risks from the news landscape.
## CONFIDENCE: HIGH / MEDIUM / LOW
1 sentence justifying your confidence level.
| News Item | Impact | Direction | Timing |
|-----------|--------|-----------|--------|
| (fill with key news events and their expected impact) |
RULES:
- Maximum 3000 characters total
- Do NOT repeat raw data verbatim summarize trends and insights
- Complete your ENTIRE analysis in a SINGLE response"""
def create_news_analyst(llm):
@ -23,11 +45,17 @@ def create_news_analyst(llm):
tools = [
get_news,
get_global_news,
get_earnings_calendar,
]
system_message = (
"You are a news researcher tasked with analyzing recent news and trends over the past week. Please write a comprehensive report of the current state of the world that is relevant for trading and macroeconomics. Use the available tools: get_news(query, start_date, end_date) for company-specific or targeted news searches, and get_global_news(curr_date, look_back_days, limit) for broader macroeconomic news. Do not simply state the trends are mixed, provide detailed and finegrained analysis and insights that may help traders make decisions."
+ """ Make sure to append a Markdown table at the end of the report to organize key points in the report, organized and easy to read."""
"You are a news and macro analyst tasked with analyzing recent news, global trends, and upcoming catalysts. "
"Use ALL available tools:\n"
"- `get_news(ticker, start_date, end_date)`: Company-specific news from Google News\n"
"- `get_global_news(curr_date, look_back_days, limit)`: Broader macroeconomic and market news\n"
"- `get_earnings_calendar(ticker, curr_date)`: Upcoming earnings dates, ex-dividend dates, and dividend info\n\n"
"Focus on: (1) company-specific catalysts, (2) macro headwinds/tailwinds, (3) upcoming events that could move the stock. "
"Quantify impact where possible. Do not simply state trends are mixed — provide specific, actionable insights."
+ ANALYST_RESPONSE_FORMAT
)

View File

@ -1,18 +1,40 @@
from langchain_core.prompts import ChatPromptTemplate, MessagesPlaceholder
import time
import json
from tradingagents.agents.utils.agent_utils import get_news, execute_text_tool_calls, needs_followup_call, execute_default_tools, generate_analysis_from_data
from tradingagents.agents.utils.agent_utils import get_yfinance_news, get_analyst_sentiment, get_sector_performance, execute_text_tool_calls, needs_followup_call, execute_default_tools, generate_analysis_from_data
from tradingagents.dataflows.config import get_config
from tradingagents.log_utils import add_log, step_timer, symbol_progress
ANALYST_RESPONSE_FORMAT = """
RESPONSE FORMAT RULES:
- Keep your analysis concise: maximum 3000 characters total
- Use a compact markdown table to organize key findings
- Do NOT repeat raw data values verbatim summarize trends and insights
- Complete your ENTIRE analysis in a SINGLE response do not split across multiple messages"""
RESPONSE FORMAT (follow this structure exactly):
## EXECUTIVE SUMMARY
2-3 sentences: Key sentiment finding and directional bias (BULLISH / BEARISH / NEUTRAL).
## KEY DATA POINTS
- Bullet list of the 5 most significant sentiment signals with specific numbers
- Include analyst consensus, price target implied upside, sector positioning
## SIGNAL ASSESSMENT
Your overall sentiment reading: BULLISH / BEARISH / NEUTRAL
1-2 sentences explaining why, referencing specific data.
## RISK FACTORS
2-3 specific risks or sentiment divergences.
## CONFIDENCE: HIGH / MEDIUM / LOW
1 sentence justifying your confidence level.
| Signal Source | Finding | Sentiment | Weight |
|--------------|---------|-----------|--------|
| (fill with key sentiment signals) |
RULES:
- Maximum 3000 characters total
- Do NOT repeat raw data verbatim summarize trends and insights
- Complete your ENTIRE analysis in a SINGLE response"""
def create_social_media_analyst(llm):
@ -22,12 +44,20 @@ def create_social_media_analyst(llm):
company_name = state["company_of_interest"]
tools = [
get_news,
get_yfinance_news,
get_analyst_sentiment,
get_sector_performance,
]
system_message = (
"You are a social media and company specific news researcher/analyst tasked with analyzing social media posts, recent company news, and public sentiment for a specific company over the past week. You will be given a company's name your objective is to write a comprehensive long report detailing your analysis, insights, and implications for traders and investors on this company's current state after looking at social media and what people are saying about that company, analyzing sentiment data of what people feel each day about the company, and looking at recent company news. Use the get_news(query, start_date, end_date) tool to search for company-specific news and social media discussions. Try to look at all sources possible from social media to sentiment to news. Do not simply state the trends are mixed, provide detailed and finegrained analysis and insights that may help traders make decisions."
+ """ Make sure to append a Markdown table at the end of the report to organize key points in the report, organized and easy to read."""
"You are a sentiment and market perception analyst. Your job is to assess the overall market sentiment around a company "
"by synthesizing multiple signal sources:\n"
"- `get_yfinance_news`: Curated news from Yahoo Finance (multiple publishers) — analyze headlines, publishers, recency, and tone\n"
"- `get_analyst_sentiment`: Wall Street consensus — price targets, buy/sell/hold distribution, implied upside/downside\n"
"- `get_sector_performance`: Sector context — how the stock is positioned vs moving averages, 52-week range, beta, and index\n\n"
"Synthesize these into a unified sentiment assessment. Quantify sentiment where possible (e.g., '70% of analysts rate Buy', "
"'trading at 85% of 52-week range', 'implied upside of 15%'). Identify sentiment divergences (e.g., analysts bullish but "
"price below moving averages). Do not simply state trends are mixed — provide specific, actionable insights."
+ ANALYST_RESPONSE_FORMAT,
)

View File

@ -37,13 +37,22 @@ from tradingagents.agents.utils.fundamental_data_tools import (
get_fundamentals,
get_balance_sheet,
get_cashflow,
get_income_statement
get_income_statement,
get_analyst_recommendations,
get_earnings_data,
get_institutional_holders,
)
from tradingagents.agents.utils.news_data_tools import (
get_news,
get_insider_sentiment,
get_insider_transactions,
get_global_news
get_global_news,
get_earnings_calendar,
)
from tradingagents.agents.utils.social_sentiment_tools import (
get_yfinance_news,
get_analyst_sentiment,
get_sector_performance,
)
def strip_tool_call_lines(text):
@ -85,6 +94,13 @@ def execute_default_tools(tools, ticker, current_date):
"get_balance_sheet": {"ticker": ticker, "curr_date": current_date},
"get_cashflow": {"ticker": ticker, "curr_date": current_date},
"get_income_statement": {"ticker": ticker, "curr_date": current_date},
"get_analyst_recommendations": {"ticker": ticker, "curr_date": current_date},
"get_earnings_data": {"ticker": ticker, "curr_date": current_date},
"get_institutional_holders": {"ticker": ticker, "curr_date": current_date},
"get_yfinance_news": {"ticker": ticker, "curr_date": current_date},
"get_analyst_sentiment": {"ticker": ticker, "curr_date": current_date},
"get_sector_performance": {"ticker": ticker, "curr_date": current_date},
"get_earnings_calendar": {"ticker": ticker, "curr_date": current_date},
}
results = []

View File

@ -74,4 +74,55 @@ def get_income_statement(
Returns:
str: A formatted report containing income statement data
"""
return route_to_vendor("get_income_statement", ticker, freq, curr_date)
return route_to_vendor("get_income_statement", ticker, freq, curr_date)
@tool
def get_analyst_recommendations(
ticker: Annotated[str, "ticker symbol"],
curr_date: Annotated[str, "current date you are trading at, yyyy-mm-dd"] = None,
) -> str:
"""
Retrieve analyst recommendations and recent upgrades/downgrades for a ticker.
Returns analyst buy/sell/hold consensus counts and recent rating changes with firm names.
Args:
ticker (str): Ticker symbol of the company
curr_date (str): Current date you are trading at, yyyy-mm-dd
Returns:
str: A formatted report of analyst recommendations and rating changes
"""
return route_to_vendor("get_analyst_recommendations", ticker, curr_date)
@tool
def get_earnings_data(
ticker: Annotated[str, "ticker symbol"],
curr_date: Annotated[str, "current date you are trading at, yyyy-mm-dd"] = None,
) -> str:
"""
Retrieve earnings dates and historical EPS data (estimates vs actuals).
Returns upcoming earnings dates and EPS surprise history.
Args:
ticker (str): Ticker symbol of the company
curr_date (str): Current date you are trading at, yyyy-mm-dd
Returns:
str: A formatted report of earnings dates and EPS history
"""
return route_to_vendor("get_earnings_data", ticker, curr_date)
@tool
def get_institutional_holders(
ticker: Annotated[str, "ticker symbol"],
curr_date: Annotated[str, "current date you are trading at, yyyy-mm-dd"] = None,
) -> str:
"""
Retrieve institutional holders and ownership breakdown for a ticker.
Returns top institutional holders, insider vs institutional ownership percentages.
Args:
ticker (str): Ticker symbol of the company
curr_date (str): Current date you are trading at, yyyy-mm-dd
Returns:
str: A formatted report of institutional ownership data
"""
return route_to_vendor("get_institutional_holders", ticker, curr_date)

View File

@ -76,3 +76,20 @@ def get_insider_transactions(
str: A report of insider transaction data
"""
return route_to_vendor("get_insider_transactions", ticker, curr_date)
@tool
def get_earnings_calendar(
ticker: Annotated[str, "ticker symbol"],
curr_date: Annotated[str, "current date you are trading at, yyyy-mm-dd"] = None,
) -> str:
"""
Retrieve upcoming earnings and dividend calendar for a company.
Returns next earnings date, ex-dividend date, and dividend information.
Args:
ticker (str): Ticker symbol of the company
curr_date (str): Current date you are trading at, yyyy-mm-dd
Returns:
str: A formatted report of upcoming earnings and dividend events
"""
return route_to_vendor("get_earnings_calendar", ticker, curr_date)

View File

@ -0,0 +1,57 @@
from langchain_core.tools import tool
from typing import Annotated
from tradingagents.dataflows.interface import route_to_vendor
@tool
def get_yfinance_news(
ticker: Annotated[str, "ticker symbol"],
curr_date: Annotated[str, "current date you are trading at, yyyy-mm-dd"] = None,
) -> str:
"""
Retrieve aggregated news from Yahoo Finance's curated feed for a ticker.
Returns titles, publishers, publish times, and related tickers from multiple news sources.
Different from get_news (Google News) this provides Yahoo Finance's own aggregated feed.
Args:
ticker (str): Ticker symbol of the company
curr_date (str): Current date you are trading at, yyyy-mm-dd
Returns:
str: A formatted report of curated news articles
"""
return route_to_vendor("get_yfinance_news", ticker, curr_date)
@tool
def get_analyst_sentiment(
ticker: Annotated[str, "ticker symbol"],
curr_date: Annotated[str, "current date you are trading at, yyyy-mm-dd"] = None,
) -> str:
"""
Retrieve analyst sentiment: price targets and recommendation distribution.
Returns mean/median/low/high price targets, buy/sell/hold rating counts,
and implied upside/downside vs current price.
Args:
ticker (str): Ticker symbol of the company
curr_date (str): Current date you are trading at, yyyy-mm-dd
Returns:
str: A formatted report of analyst sentiment and price targets
"""
return route_to_vendor("get_analyst_sentiment", ticker, curr_date)
@tool
def get_sector_performance(
ticker: Annotated[str, "ticker symbol"],
curr_date: Annotated[str, "current date you are trading at, yyyy-mm-dd"] = None,
) -> str:
"""
Retrieve sector performance context for a ticker.
Returns the stock's sector, its position vs moving averages, 52-week range,
beta, and relative performance vs Nifty50 index (for Indian stocks).
Args:
ticker (str): Ticker symbol of the company
curr_date (str): Current date you are trading at, yyyy-mm-dd
Returns:
str: A formatted report of sector-level performance context
"""
return route_to_vendor("get_sector_performance", ticker, curr_date)

View File

@ -42,7 +42,7 @@ def clear_request_cache():
# Import from vendor-specific modules
from .local import get_YFin_data, get_finnhub_news, get_finnhub_company_insider_sentiment, get_finnhub_company_insider_transactions, get_simfin_balance_sheet, get_simfin_cashflow, get_simfin_income_statements, get_reddit_global_news, get_reddit_company_news
from .y_finance import get_YFin_data_online, get_stock_stats_indicators_window, get_balance_sheet as get_yfinance_balance_sheet, get_cashflow as get_yfinance_cashflow, get_income_statement as get_yfinance_income_statement, get_insider_transactions as get_yfinance_insider_transactions, get_fundamentals as get_yfinance_fundamentals
from .y_finance import get_YFin_data_online, get_stock_stats_indicators_window, get_balance_sheet as get_yfinance_balance_sheet, get_cashflow as get_yfinance_cashflow, get_income_statement as get_yfinance_income_statement, get_insider_transactions as get_yfinance_insider_transactions, get_fundamentals as get_yfinance_fundamentals, get_analyst_recommendations as get_yfinance_analyst_recommendations, get_earnings_data as get_yfinance_earnings_data, get_institutional_holders as get_yfinance_institutional_holders, get_yfinance_news as get_yfinance_news_feed, get_analyst_sentiment as get_yfinance_analyst_sentiment, get_sector_performance as get_yfinance_sector_performance, get_earnings_calendar as get_yfinance_earnings_calendar
from .google import get_google_news, get_google_global_news
from .openai import get_stock_news_openai, get_global_news_openai, get_fundamentals_openai
from .alpha_vantage import (
@ -82,7 +82,10 @@ TOOLS_CATEGORIES = {
"get_fundamentals",
"get_balance_sheet",
"get_cashflow",
"get_income_statement"
"get_income_statement",
"get_analyst_recommendations",
"get_earnings_data",
"get_institutional_holders",
]
},
"news_data": {
@ -92,6 +95,15 @@ TOOLS_CATEGORIES = {
"get_global_news",
"get_insider_sentiment",
"get_insider_transactions",
"get_earnings_calendar",
]
},
"social_sentiment_data": {
"description": "Social sentiment and market perception",
"tools": [
"get_yfinance_news",
"get_analyst_sentiment",
"get_sector_performance",
]
}
}
@ -161,6 +173,30 @@ VENDOR_METHODS = {
"yfinance": get_yfinance_insider_transactions,
"local": get_finnhub_company_insider_transactions,
},
# New fundamental tools
"get_analyst_recommendations": {
"yfinance": get_yfinance_analyst_recommendations,
},
"get_earnings_data": {
"yfinance": get_yfinance_earnings_data,
},
"get_institutional_holders": {
"yfinance": get_yfinance_institutional_holders,
},
# New social sentiment tools
"get_yfinance_news": {
"yfinance": get_yfinance_news_feed,
},
"get_analyst_sentiment": {
"yfinance": get_yfinance_analyst_sentiment,
},
"get_sector_performance": {
"yfinance": get_yfinance_sector_performance,
},
# New news tool
"get_earnings_calendar": {
"yfinance": get_yfinance_earnings_calendar,
},
}
def get_category_for_method(method: str) -> str:

View File

@ -130,6 +130,35 @@ def get_stock_stats_indicators_window(
"Usage: Identify overbought (>80) or oversold (<20) conditions and confirm the strength of trends or reversals. "
"Tips: Use alongside RSI or MACD to confirm signals; divergence between price and MFI can indicate potential reversals."
),
# Short-term Moving Average
"close_20_sma": (
"20 SMA: A short-term trend indicator and Bollinger Band baseline. "
"Usage: Identify short-term trend direction and mean-reversion levels. "
"Tips: More responsive than 50 SMA; works well for swing trading setups."
),
"close_5_ema": (
"5 EMA: An ultra-responsive short-term average. "
"Usage: Capture very short-term momentum shifts and identify immediate trend direction. "
"Tips: Highly sensitive to noise; best used as a trigger in conjunction with slower averages."
),
# Trend Strength
"adx": (
"ADX: Average Directional Index measures trend strength regardless of direction. "
"Usage: Values above 25 suggest a strong trend; below 20 suggests ranging/consolidation. "
"Tips: Combine with +DI/-DI for directional bias; ADX alone doesn't indicate direction."
),
# Mean Reversion
"cci": (
"CCI: Commodity Channel Index measures price deviation from its statistical mean. "
"Usage: Values above +100 signal overbought; below -100 signal oversold. "
"Tips: Effective for identifying cyclical turns; combine with trend indicators to avoid false reversals."
),
# Stochastic Oscillator
"kdjk": (
"Stochastic %K: Compares closing price to the price range over a period. "
"Usage: Values above 80 suggest overbought; below 20 suggest oversold conditions. "
"Tips: Complements RSI with a different calculation method; crossovers with %D provide entry signals."
),
}
if indicator not in best_ind_params:
@ -518,6 +547,385 @@ def get_fundamentals(
return f"Error retrieving fundamentals for {ticker}: {str(e)}"
def get_analyst_recommendations(
ticker: Annotated[str, "ticker symbol of the company"],
curr_date: Annotated[str, "current date for reference"] = None,
) -> str:
"""Get analyst recommendations summary and recent upgrades/downgrades from yfinance."""
try:
normalized_ticker = normalize_symbol(ticker, target="yfinance")
ticker_obj = yf.Ticker(normalized_ticker)
sections = [f"# Analyst Recommendations for {normalized_ticker}"]
if curr_date:
sections.append(f"# As of: {curr_date}")
sections.append("")
# Recommendations summary (buy/sell/hold counts)
try:
rec_summary = ticker_obj.recommendations_summary
if rec_summary is not None and not rec_summary.empty:
sections.append("## Analyst Consensus")
csv_string = rec_summary.to_csv(index=True)
sections.append(csv_string)
sections.append("")
except Exception:
sections.append("## Analyst Consensus\nNo recommendations summary available\n")
# Recent upgrades/downgrades
try:
upgrades = ticker_obj.upgrades_downgrades
if upgrades is not None and not upgrades.empty:
# Limit to most recent 15 entries
recent = upgrades.head(15)
sections.append("## Recent Upgrades/Downgrades")
csv_string = recent.to_csv(index=True)
sections.append(csv_string)
sections.append("")
except Exception:
sections.append("## Recent Upgrades/Downgrades\nNo upgrade/downgrade data available\n")
return "\n".join(sections)
except Exception as e:
return f"Error retrieving analyst recommendations for {ticker}: {str(e)}"
def get_earnings_data(
ticker: Annotated[str, "ticker symbol of the company"],
curr_date: Annotated[str, "current date for reference"] = None,
) -> str:
"""Get earnings dates and historical EPS data from yfinance."""
try:
normalized_ticker = normalize_symbol(ticker, target="yfinance")
ticker_obj = yf.Ticker(normalized_ticker)
sections = [f"# Earnings Data for {normalized_ticker}"]
if curr_date:
sections.append(f"# As of: {curr_date}")
sections.append("")
# Earnings dates (upcoming and recent)
try:
earnings_dates = ticker_obj.earnings_dates
if earnings_dates is not None and not earnings_dates.empty:
sections.append("## Earnings Dates (Upcoming & Recent)")
# Show up to 8 entries
csv_string = earnings_dates.head(8).to_csv(index=True)
sections.append(csv_string)
sections.append("")
except Exception:
sections.append("## Earnings Dates\nNo earnings dates available\n")
# Earnings history (EPS estimates vs actuals)
try:
earnings_hist = ticker_obj.earnings_history
if earnings_hist is not None and not earnings_hist.empty:
sections.append("## Earnings History (EPS Estimates vs Actuals)")
csv_string = earnings_hist.to_csv(index=True)
sections.append(csv_string)
sections.append("")
except Exception:
sections.append("## Earnings History\nNo earnings history available\n")
return "\n".join(sections)
except Exception as e:
return f"Error retrieving earnings data for {ticker}: {str(e)}"
def get_institutional_holders(
ticker: Annotated[str, "ticker symbol of the company"],
curr_date: Annotated[str, "current date for reference"] = None,
) -> str:
"""Get institutional holders and major holders breakdown from yfinance."""
try:
normalized_ticker = normalize_symbol(ticker, target="yfinance")
ticker_obj = yf.Ticker(normalized_ticker)
sections = [f"# Institutional Holders for {normalized_ticker}"]
if curr_date:
sections.append(f"# As of: {curr_date}")
sections.append("")
# Major holders (% breakdown)
try:
major = ticker_obj.major_holders
if major is not None and not major.empty:
sections.append("## Major Holders Breakdown")
csv_string = major.to_csv(index=True)
sections.append(csv_string)
sections.append("")
except Exception:
sections.append("## Major Holders Breakdown\nNo major holders data available\n")
# Top institutional holders
try:
inst = ticker_obj.institutional_holders
if inst is not None and not inst.empty:
sections.append("## Top Institutional Holders")
csv_string = inst.head(10).to_csv(index=False)
sections.append(csv_string)
sections.append("")
except Exception:
sections.append("## Top Institutional Holders\nNo institutional holders data available\n")
return "\n".join(sections)
except Exception as e:
return f"Error retrieving institutional holders for {ticker}: {str(e)}"
def get_yfinance_news(
ticker: Annotated[str, "ticker symbol of the company"],
curr_date: Annotated[str, "current date for reference"] = None,
) -> str:
"""Get aggregated news for a ticker from Yahoo Finance's curated feed."""
try:
normalized_ticker = normalize_symbol(ticker, target="yfinance")
ticker_obj = yf.Ticker(normalized_ticker)
sections = [f"# Yahoo Finance News for {normalized_ticker}"]
if curr_date:
sections.append(f"# As of: {curr_date}")
sections.append("")
try:
news = ticker_obj.news
if news and len(news) > 0:
for i, article in enumerate(news[:10]):
title = article.get("title", "No title")
publisher = article.get("publisher", "Unknown")
publish_time = article.get("providerPublishTime", "")
if publish_time:
try:
from datetime import datetime as _dt
publish_time = _dt.fromtimestamp(publish_time).strftime("%Y-%m-%d %H:%M")
except Exception:
pass
related = article.get("relatedTickers", [])
related_str = ", ".join(related) if related else "N/A"
sections.append(f"## Article {i+1}: {title}")
sections.append(f" Publisher: {publisher}")
sections.append(f" Published: {publish_time}")
sections.append(f" Related Tickers: {related_str}")
sections.append("")
else:
sections.append("No news articles available from Yahoo Finance.\n")
except Exception:
sections.append("Unable to fetch Yahoo Finance news feed.\n")
return "\n".join(sections)
except Exception as e:
return f"Error retrieving Yahoo Finance news for {ticker}: {str(e)}"
def get_analyst_sentiment(
ticker: Annotated[str, "ticker symbol of the company"],
curr_date: Annotated[str, "current date for reference"] = None,
) -> str:
"""Get analyst sentiment: price targets + recommendation distribution from yfinance."""
try:
normalized_ticker = normalize_symbol(ticker, target="yfinance")
ticker_obj = yf.Ticker(normalized_ticker)
sections = [f"# Analyst Sentiment for {normalized_ticker}"]
if curr_date:
sections.append(f"# As of: {curr_date}")
sections.append("")
# Analyst price targets
try:
targets = ticker_obj.analyst_price_targets
if targets is not None:
sections.append("## Analyst Price Targets")
if isinstance(targets, dict):
for k, v in targets.items():
sections.append(f" {k}: {v}")
else:
sections.append(str(targets))
sections.append("")
except Exception:
sections.append("## Analyst Price Targets\nNo price target data available\n")
# Recommendations summary for sentiment distribution
try:
rec_summary = ticker_obj.recommendations_summary
if rec_summary is not None and not rec_summary.empty:
sections.append("## Analyst Rating Distribution")
csv_string = rec_summary.to_csv(index=True)
sections.append(csv_string)
sections.append("")
except Exception:
sections.append("## Analyst Rating Distribution\nNo rating distribution data available\n")
# Current price vs targets for sentiment gauge
try:
info = ticker_obj.info
current_price = info.get("currentPrice")
target_mean = info.get("targetMeanPrice")
if current_price and target_mean:
upside = ((target_mean - current_price) / current_price) * 100
sections.append("## Price vs Target Analysis")
sections.append(f" Current Price: {current_price}")
sections.append(f" Mean Target: {target_mean}")
sections.append(f" Implied Upside: {upside:.1f}%")
sentiment = "BULLISH" if upside > 10 else "BEARISH" if upside < -10 else "NEUTRAL"
sections.append(f" Analyst Sentiment: {sentiment}")
sections.append("")
except Exception:
pass
return "\n".join(sections)
except Exception as e:
return f"Error retrieving analyst sentiment for {ticker}: {str(e)}"
def get_sector_performance(
ticker: Annotated[str, "ticker symbol of the company"],
curr_date: Annotated[str, "current date for reference"] = None,
) -> str:
"""Get sector performance context — how is this stock's sector performing vs the market."""
try:
normalized_ticker = normalize_symbol(ticker, target="yfinance")
ticker_obj = yf.Ticker(normalized_ticker)
sections = [f"# Sector Performance Context for {normalized_ticker}"]
if curr_date:
sections.append(f"# As of: {curr_date}")
sections.append("")
info = ticker_obj.info
sector = info.get("sector", "Unknown")
industry = info.get("industry", "Unknown")
sections.append(f"## Stock Sector: {sector}")
sections.append(f"## Industry: {industry}")
sections.append("")
# Get stock's own performance metrics
beta = info.get("beta")
fifty_day_avg = info.get("fiftyDayAverage")
two_hundred_day_avg = info.get("twoHundredDayAverage")
current_price = info.get("currentPrice")
fifty_two_high = info.get("fiftyTwoWeekHigh")
fifty_two_low = info.get("fiftyTwoWeekLow")
sections.append("## Stock vs Moving Averages")
if current_price and fifty_day_avg:
pct_vs_50d = ((current_price - fifty_day_avg) / fifty_day_avg) * 100
sections.append(f" Current Price: {current_price}")
sections.append(f" 50-Day Avg: {fifty_day_avg} ({pct_vs_50d:+.1f}%)")
if current_price and two_hundred_day_avg:
pct_vs_200d = ((current_price - two_hundred_day_avg) / two_hundred_day_avg) * 100
sections.append(f" 200-Day Avg: {two_hundred_day_avg} ({pct_vs_200d:+.1f}%)")
if current_price and fifty_two_high and fifty_two_low:
range_pct = ((current_price - fifty_two_low) / (fifty_two_high - fifty_two_low)) * 100 if fifty_two_high != fifty_two_low else 50
sections.append(f" 52-Week Range: {fifty_two_low} - {fifty_two_high} (currently at {range_pct:.0f}% of range)")
if beta:
sections.append(f" Beta: {beta}")
sections.append("")
# Nifty50 index comparison (for Indian stocks)
if is_nifty_50_stock(ticker):
try:
end_date = curr_date or datetime.now().strftime("%Y-%m-%d")
from dateutil.relativedelta import relativedelta as _rd
start_date_dt = datetime.strptime(end_date, "%Y-%m-%d") - _rd(days=30)
start_date = start_date_dt.strftime("%Y-%m-%d")
nifty = yf.Ticker("^NSEI")
nifty_hist = nifty.history(start=start_date, end=end_date)
if not nifty_hist.empty:
nifty_return = ((nifty_hist['Close'].iloc[-1] - nifty_hist['Close'].iloc[0]) / nifty_hist['Close'].iloc[0]) * 100
sections.append("## Nifty50 Index (30-day)")
sections.append(f" Nifty50 Return: {nifty_return:.1f}%")
stock_hist = ticker_obj.history(start=start_date, end=end_date)
if not stock_hist.empty:
stock_return = ((stock_hist['Close'].iloc[-1] - stock_hist['Close'].iloc[0]) / stock_hist['Close'].iloc[0]) * 100
sections.append(f" {normalized_ticker} Return: {stock_return:.1f}%")
alpha = stock_return - nifty_return
sections.append(f" Alpha vs Nifty: {alpha:+.1f}%")
sections.append("")
except Exception:
sections.append("## Nifty50 Comparison\nUnable to fetch index data\n")
return "\n".join(sections)
except Exception as e:
return f"Error retrieving sector performance for {ticker}: {str(e)}"
def get_earnings_calendar(
ticker: Annotated[str, "ticker symbol of the company"],
curr_date: Annotated[str, "current date for reference"] = None,
) -> str:
"""Get upcoming earnings and dividend calendar from yfinance."""
try:
normalized_ticker = normalize_symbol(ticker, target="yfinance")
ticker_obj = yf.Ticker(normalized_ticker)
sections = [f"# Earnings & Dividend Calendar for {normalized_ticker}"]
if curr_date:
sections.append(f"# As of: {curr_date}")
sections.append("")
# Calendar data
try:
calendar = ticker_obj.calendar
if calendar is not None:
if isinstance(calendar, dict):
for k, v in calendar.items():
sections.append(f" {k}: {v}")
else:
sections.append(str(calendar))
sections.append("")
except Exception:
sections.append("No calendar data available\n")
# Earnings dates for more detail
try:
earnings_dates = ticker_obj.earnings_dates
if earnings_dates is not None and not earnings_dates.empty:
sections.append("## Upcoming & Recent Earnings Dates")
csv_string = earnings_dates.head(4).to_csv(index=True)
sections.append(csv_string)
sections.append("")
except Exception:
pass
# Dividend info
try:
info = ticker_obj.info
div_rate = info.get("dividendRate")
div_yield = info.get("dividendYield")
ex_div_date = info.get("exDividendDate")
if div_rate or div_yield:
sections.append("## Dividend Information")
if div_rate:
sections.append(f" Dividend Rate: {div_rate}")
if div_yield:
sections.append(f" Dividend Yield: {div_yield * 100:.2f}%")
if ex_div_date:
try:
from datetime import datetime as _dt
ex_date_str = _dt.fromtimestamp(ex_div_date).strftime("%Y-%m-%d")
sections.append(f" Ex-Dividend Date: {ex_date_str}")
except Exception:
sections.append(f" Ex-Dividend Date: {ex_div_date}")
sections.append("")
except Exception:
pass
return "\n".join(sections)
except Exception as e:
return f"Error retrieving earnings calendar for {ticker}: {str(e)}"
def get_insider_transactions(
ticker: Annotated[str, "ticker symbol of the company"]
):

View File

@ -25,22 +25,31 @@ class SignalProcessor:
messages = [
(
"system",
"You are an efficient assistant designed to analyze paragraphs or financial reports "
"You are an efficient assistant designed to analyze financial reports "
"provided by a group of analysts. Extract the following information:\n"
"1. The investment decision: SELL, BUY, or HOLD\n"
"2. The recommended holding period in trading days (only for BUY or HOLD decisions)\n"
"3. The confidence level of the decision: HIGH, MEDIUM, or LOW\n"
"4. The risk level of the investment: HIGH, MEDIUM, or LOW\n\n"
"Respond in exactly this format (nothing else):\n"
"4. The risk level of the investment: HIGH, MEDIUM, or LOW\n"
"5. A brief rationale explaining the decision\n"
"6. Key supporting evidence (top 3 data points)\n"
"7. Key opposing evidence (top 2 data points arguing against the decision)\n\n"
"Respond in exactly this format:\n"
"DECISION: <BUY|SELL|HOLD>\n"
"HOLD_DAYS: <number|N/A>\n"
"CONFIDENCE: <HIGH|MEDIUM|LOW>\n"
"RISK_LEVEL: <HIGH|MEDIUM|LOW>\n\n"
"RISK_LEVEL: <HIGH|MEDIUM|LOW>\n"
"RATIONALE: <2-3 sentence explanation of WHY this decision>\n"
"SUPPORTING: <top 3 data points, semicolon-separated>\n"
"OPPOSING: <top 2 counter-arguments, semicolon-separated>\n\n"
"For SELL decisions, always use HOLD_DAYS: N/A\n"
"For BUY or HOLD decisions, extract the EXACT number of days mentioned in the report. "
"Look for phrases like 'N-day hold', 'N trading days', 'hold for N days', "
"'N-day horizon', 'over N days'. If no specific number is mentioned, use 5.\n"
"For CONFIDENCE and RISK_LEVEL, infer from the tone and content of the report. Default to MEDIUM if unclear.",
"For CONFIDENCE and RISK_LEVEL, infer from the tone and content of the report. Default to MEDIUM if unclear.\n"
"For RATIONALE, summarize the core reasoning in 2-3 sentences.\n"
"For SUPPORTING, list the 3 strongest data points that support the decision.\n"
"For OPPOSING, list the 2 strongest counter-arguments or risks.",
),
("human", full_signal),
]
@ -99,11 +108,14 @@ class SignalProcessor:
return max(set(candidates), key=candidates.count)
def _parse_signal_response(self, response: str) -> dict:
"""Parse the structured LLM response into decision, hold_days, confidence, risk."""
"""Parse the structured LLM response into decision, hold_days, confidence, risk, and explainability fields."""
decision = "HOLD"
hold_days = None
confidence = "MEDIUM"
risk = "MEDIUM"
rationale = ""
supporting = ""
opposing = ""
for line in response.strip().split("\n"):
line = line.strip()
@ -134,6 +146,12 @@ class SignalProcessor:
raw = raw.replace("*", "").strip()
if raw in ("HIGH", "MEDIUM", "LOW"):
risk = raw
elif upper.startswith("RATIONALE:"):
rationale = line.split(":", 1)[1].strip()
elif upper.startswith("SUPPORTING:"):
supporting = line.split(":", 1)[1].strip()
elif upper.startswith("OPPOSING:"):
opposing = line.split(":", 1)[1].strip()
# Enforce: SELL never has hold_days; BUY/HOLD default to 5 if missing
if decision == "SELL":
@ -141,4 +159,11 @@ class SignalProcessor:
elif hold_days is None:
hold_days = 5 # Default hold period
return {"decision": decision, "hold_days": hold_days, "confidence": confidence, "risk": risk}
result = {"decision": decision, "hold_days": hold_days, "confidence": confidence, "risk": risk}
if rationale:
result["rationale"] = rationale
if supporting:
result["supporting"] = supporting
if opposing:
result["opposing"] = opposing
return result

View File

@ -43,7 +43,14 @@ from tradingagents.agents.utils.agent_utils import (
get_news,
get_insider_sentiment,
get_insider_transactions,
get_global_news
get_global_news,
get_analyst_recommendations,
get_earnings_data,
get_institutional_holders,
get_yfinance_news,
get_analyst_sentiment,
get_sector_performance,
get_earnings_calendar,
)
from .conditional_logic import ConditionalLogic
@ -152,23 +159,26 @@ class TradingAgentsGraph:
[
# Core stock data tools
get_stock_data,
# Technical indicators
# Technical indicators (18 indicators available)
get_indicators,
]
),
"social": ToolNode(
[
# News tools for social media analysis
get_news,
# Sentiment and market perception tools
get_yfinance_news,
get_analyst_sentiment,
get_sector_performance,
]
),
"news": ToolNode(
[
# News and insider information
# News, insider information, and upcoming catalysts
get_news,
get_global_news,
get_insider_sentiment,
get_insider_transactions,
get_earnings_calendar,
]
),
"fundamentals": ToolNode(
@ -178,6 +188,9 @@ class TradingAgentsGraph:
get_balance_sheet,
get_cashflow,
get_income_statement,
get_analyst_recommendations,
get_earnings_data,
get_institutional_holders,
]
),
}
@ -424,9 +437,16 @@ class TradingAgentsGraph:
"get_balance_sheet": ("fundamentals", "Balance Sheet"),
"get_income_statement": ("fundamentals", "Income Statement"),
"get_cashflow": ("fundamentals", "Cash Flow"),
"get_analyst_recommendations": ("fundamentals", "Analyst Recommendations"),
"get_earnings_data": ("fundamentals", "Earnings Data"),
"get_institutional_holders": ("fundamentals", "Institutional Holders"),
"get_news": ("news", "Google News"),
"get_global_news": ("news", "Global News"),
"get_earnings_calendar": ("news", "Earnings Calendar"),
"get_reddit_posts": ("social_media", "Reddit"),
"get_yfinance_news": ("social_media", "Yahoo Finance News"),
"get_analyst_sentiment": ("social_media", "Analyst Sentiment"),
"get_sector_performance": ("social_media", "Sector Performance"),
}
raw_entries = raw_data_store.get_entries()